@kaydash9999/banker-plugins 0.20.1 → 0.20.5
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 +195 -1
- package/PRIVACY.md +13 -3
- package/README.md +27 -4
- package/bin/banker.js +84 -40
- package/bin/lib/telemetry-config.mjs +31 -16
- package/codex/manifest.json +1 -1
- package/codex/transform-matrix.md +1 -1
- package/commands/setup.md +23 -0
- package/hooks/progress.mjs +143 -38
- package/hooks/register.mjs +13 -3
- package/hooks/update-checkin.mjs +15 -7
- package/hooks/update-fetch.mjs +15 -9
- package/hooks/update-notify.mjs +2 -9
- 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/audit-mock/SKILL.md +1 -1
- package/skills/audit-security/SKILL.md +64 -2
- package/skills/deep-research/SKILL.md +5 -0
- package/skills/docs-setup/SKILL.md +37 -7
- package/skills/lineage/SKILL.md +12 -12
- package/skills/lineage/lineage.py +399 -300
- 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 +1 -1
- package/skills/obsidizer/obsidize.mjs +8 -1
- package/skills/omc-patch/SKILL.md +80 -23
- package/skills/omc-patch/scripts/omc-patch.mjs +460 -116
- package/skills/payload-mon/SKILL.md +5 -2
- package/skills/payload-mon/scripts/payload-mon.mjs +137 -30
- 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/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 +13 -5
- package/skills/setup-omc-hud/assets/apply-hud.mjs +381 -27
- 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/tone-compact/SKILL.md +2 -1
- package/skills/tone-compact/scripts/instruction-toggle.mjs +105 -20
- package/skills/ultra-chain/SKILL.md +4 -3
- 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 +1 -0
- 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,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* seat-spawn: how the ralph-qa runner (seat-run.mjs) finds a seat CLI and runs it inside its
|
|
3
|
+
* time limit, with every process the seat started ended when the run returns.
|
|
4
|
+
*
|
|
5
|
+
* which(name) the CLI on PATH, from absolute PATH folders only. libuv on Windows looks in the
|
|
6
|
+
* child's working folder before PATH for a bare name, so the runner always starts
|
|
7
|
+
* a CLI by the absolute path this gives. Windows tries .exe, then .cmd and .bat
|
|
8
|
+
* in each folder, never an extensionless file.
|
|
9
|
+
* cmdLine(exe, args) the cmd.exe line for a .cmd or .bat seat (Node refuses to start those
|
|
10
|
+
* without a shell since CVE-2024-27980). Every word is quoted by the MSVC rules;
|
|
11
|
+
* a word holding what cmd.exe reads itself (% ! ^ & | < >, a line break, NUL,
|
|
12
|
+
* or a parenthesis beside a quote, which cmd's quote toggling over \" can bare)
|
|
13
|
+
* makes it null: the seat does not start.
|
|
14
|
+
* runLimited(opts) starts the seat with stdin, stdout and stderr on files and resolves with
|
|
15
|
+
* { exit, signal, timedOut, stopped, secs, error, leftover }. On POSIX the seat
|
|
16
|
+
* gets its own process group: the limit sends the group TERM and, after the grace,
|
|
17
|
+
* KILL; a seat that exits on its own has its group sent KILL at once, so nothing it
|
|
18
|
+
* left (a late writer, an MCP server) outlives the run. On Windows the limit ends
|
|
19
|
+
* the tree with System32's taskkill /T /F; a process left after the seat exits on
|
|
20
|
+
* its own is not reached (Windows keeps no group).
|
|
21
|
+
* stopAll() ends every seat still running (TERM, then KILL 2 s later; taskkill on Windows).
|
|
22
|
+
*
|
|
23
|
+
* No shell and no timeout program: the limit is a timer here, so Git Bash's or System32's
|
|
24
|
+
* timeout never comes into it.
|
|
25
|
+
*/
|
|
26
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
27
|
+
import { accessSync, closeSync, constants, openSync, statSync } from "node:fs";
|
|
28
|
+
import { posix, win32 } from "node:path";
|
|
29
|
+
|
|
30
|
+
const running = new Set();
|
|
31
|
+
const STOP_GRACE_MS = 2000;
|
|
32
|
+
// How long a run waits for a seat to go after its KILL before it gives up on it (`leftover`).
|
|
33
|
+
const BACKSTOP_MS = 10000;
|
|
34
|
+
|
|
35
|
+
const system32 = (file) => win32.join(process.env.SystemRoot || "C:\\Windows", "System32", file);
|
|
36
|
+
|
|
37
|
+
function pathOf(env, platform) {
|
|
38
|
+
if (platform !== "win32") return env.PATH || "";
|
|
39
|
+
const key = Object.keys(env).find((k) => k.toUpperCase() === "PATH");
|
|
40
|
+
return key ? env[key] || "" : "";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const runnable = (platform) => (file) => {
|
|
44
|
+
try {
|
|
45
|
+
if (!statSync(file).isFile()) return false;
|
|
46
|
+
if (platform !== "win32") accessSync(file, constants.X_OK);
|
|
47
|
+
return true;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export function which(name, opts = {}) {
|
|
54
|
+
const platform = opts.platform || process.platform;
|
|
55
|
+
const paths = platform === "win32" ? win32 : posix;
|
|
56
|
+
const exts = platform === "win32" ? [".exe", ".cmd", ".bat"] : [""];
|
|
57
|
+
const isFile = opts.isFile || runnable(platform);
|
|
58
|
+
const dirs = pathOf(opts.env || process.env, platform).split(paths.delimiter).filter((dir) => dir && paths.isAbsolute(dir));
|
|
59
|
+
for (const dir of dirs) {
|
|
60
|
+
for (const ext of exts) {
|
|
61
|
+
const file = paths.join(dir, name + ext);
|
|
62
|
+
if (isFile(file)) return file;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const CMD_META = /[%!^&|<>\r\n\0]/;
|
|
69
|
+
const cmdSafe = (w) => !CMD_META.test(w) && !(/[()]/.test(w) && w.includes('"'));
|
|
70
|
+
const quoteWord = (w) => `"${w.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, "$1$1")}"`;
|
|
71
|
+
|
|
72
|
+
export function cmdLine(exe, args) {
|
|
73
|
+
const words = [exe, ...args].map(String);
|
|
74
|
+
return words.every(cmdSafe) ? words.map(quoteWord).join(" ") : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The files the seat reads and writes, opened for it; "ignore" is passed through.
|
|
78
|
+
function openStdio({ stdin, stdout, stderr }) {
|
|
79
|
+
const fds = [];
|
|
80
|
+
const open = (path, flags) => {
|
|
81
|
+
if (path === undefined || path === "ignore") return "ignore";
|
|
82
|
+
const fd = openSync(path, flags, 0o600);
|
|
83
|
+
fds.push(fd);
|
|
84
|
+
return fd;
|
|
85
|
+
};
|
|
86
|
+
try {
|
|
87
|
+
return { stdio: [open(stdin, "r"), open(stdout, "w"), open(stderr, "w")], fds };
|
|
88
|
+
} catch (e) {
|
|
89
|
+
for (const fd of fds) closeSync(fd);
|
|
90
|
+
throw e;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// The child process for the seat: a .cmd or .bat through System32's cmd.exe, anything else as is.
|
|
95
|
+
function launch({ exe, args, cwd, env, platform }, stdio) {
|
|
96
|
+
const viaCmd = platform === "win32" && /\.(cmd|bat)$/i.test(exe);
|
|
97
|
+
if (!viaCmd) return spawn(exe, args, { cwd, env, stdio, windowsHide: true, detached: platform !== "win32" });
|
|
98
|
+
const line = cmdLine(exe, args);
|
|
99
|
+
if (line === null) throw Object.assign(new Error("a word cmd.exe would read"), { code: "unsafe-word" });
|
|
100
|
+
return spawn(system32("cmd.exe"), ["/d", "/v:off", "/s", "/c", `"${line}"`],
|
|
101
|
+
{ cwd, env, stdio, windowsHide: true, windowsVerbatimArguments: true });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const signalGroup = (pid, sig) => {
|
|
105
|
+
try {
|
|
106
|
+
process.kill(-pid, sig);
|
|
107
|
+
} catch {
|
|
108
|
+
/* the group is gone */
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const taskkill = (pid) => spawnSync(system32("taskkill.exe"), ["/T", "/F", "/PID", String(pid)], { stdio: "ignore", windowsHide: true });
|
|
112
|
+
|
|
113
|
+
// Ends a seat: TERM to its group and KILL after `grace` (POSIX), or taskkill on its tree (Windows).
|
|
114
|
+
function terminate(seat, why, grace) {
|
|
115
|
+
if (seat.ending) return;
|
|
116
|
+
seat.ending = why;
|
|
117
|
+
if (seat.platform === "win32") {
|
|
118
|
+
taskkill(seat.child.pid);
|
|
119
|
+
seat.backstop = setTimeout(() => seat.finish(true), BACKSTOP_MS);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
signalGroup(seat.child.pid, "SIGTERM");
|
|
123
|
+
seat.killTimer = setTimeout(() => {
|
|
124
|
+
seat.killed = true;
|
|
125
|
+
signalGroup(seat.child.pid, "SIGKILL");
|
|
126
|
+
if (seat.exited) seat.finish(false);
|
|
127
|
+
else seat.backstop = setTimeout(() => seat.finish(true), BACKSTOP_MS);
|
|
128
|
+
}, grace);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// The seat's own exit: on POSIX its group is sent KILL now, or when the grace ends if it is being
|
|
132
|
+
// ended, so nothing it left keeps running.
|
|
133
|
+
function onExit(seat, code, signal) {
|
|
134
|
+
if (seat.child.pid === undefined) return undefined; // a seat that never started: its error ends the run
|
|
135
|
+
seat.exited = { code, signal };
|
|
136
|
+
clearTimeout(seat.limitTimer);
|
|
137
|
+
if (seat.platform === "win32") return seat.finish(false);
|
|
138
|
+
if (seat.ending && !seat.killed) return undefined;
|
|
139
|
+
signalGroup(seat.child.pid, "SIGKILL");
|
|
140
|
+
return seat.finish(false);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function watch(seat, opts) {
|
|
144
|
+
const { child } = seat;
|
|
145
|
+
const grace = opts.graceMs ?? Math.min(10000, opts.limitMs / 2);
|
|
146
|
+
seat.limitTimer = setTimeout(() => terminate(seat, "limit", grace), opts.limitMs);
|
|
147
|
+
child.on("exit", (code, signal) => onExit(seat, code, signal));
|
|
148
|
+
child.on("error", (e) => {
|
|
149
|
+
if (child.pid === undefined) seat.finish(false, e.code || "spawn");
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function result(seat, t0, leftover, error) {
|
|
154
|
+
const { code = null, signal = null } = seat.exited || {};
|
|
155
|
+
return {
|
|
156
|
+
exit: error ? null : code, signal: error ? null : signal, timedOut: seat.ending === "limit", stopped: seat.ending === "stop",
|
|
157
|
+
secs: Math.round((Date.now() - t0) / 100) / 10, error: error || null, leftover,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function runLimited(opts) {
|
|
162
|
+
const t0 = Date.now();
|
|
163
|
+
const platform = opts.platform || process.platform;
|
|
164
|
+
return new Promise((done) => {
|
|
165
|
+
let io;
|
|
166
|
+
try {
|
|
167
|
+
if (!statSync(opts.exe).isFile()) throw Object.assign(new Error("not a file"), { code: "ENOENT" });
|
|
168
|
+
io = openStdio(opts);
|
|
169
|
+
} catch (e) {
|
|
170
|
+
done(result({}, t0, false, e.code || "spawn"));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const seat = { platform };
|
|
174
|
+
seat.finish = (leftover, error) => {
|
|
175
|
+
if (seat.done) return;
|
|
176
|
+
seat.done = true;
|
|
177
|
+
for (const t of [seat.limitTimer, seat.killTimer, seat.backstop]) clearTimeout(t);
|
|
178
|
+
running.delete(seat);
|
|
179
|
+
done(result(seat, t0, leftover, error));
|
|
180
|
+
};
|
|
181
|
+
try {
|
|
182
|
+
seat.child = launch({ ...opts, args: opts.args || [], platform }, io.stdio);
|
|
183
|
+
} catch (e) {
|
|
184
|
+
seat.finish(false, e.code || "spawn");
|
|
185
|
+
return;
|
|
186
|
+
} finally {
|
|
187
|
+
for (const fd of io.fds) closeSync(fd);
|
|
188
|
+
}
|
|
189
|
+
running.add(seat);
|
|
190
|
+
watch(seat, opts);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function stopAll() {
|
|
195
|
+
const seats = [...running];
|
|
196
|
+
const ends = seats.map((seat) => new Promise((resolve) => {
|
|
197
|
+
const finish = seat.finish;
|
|
198
|
+
seat.finish = (...a) => {
|
|
199
|
+
finish(...a);
|
|
200
|
+
resolve();
|
|
201
|
+
};
|
|
202
|
+
}));
|
|
203
|
+
for (const seat of seats) terminate(seat, "stop", STOP_GRACE_MS);
|
|
204
|
+
return Promise.all(ends);
|
|
205
|
+
}
|
|
@@ -205,6 +205,13 @@ const STRING_VALUE = /^(?:"((?:[^"\\]|\\.)*)"|'([^']*)')\s*(?:#.*)?$/s;
|
|
|
205
205
|
|
|
206
206
|
// A dotted key's segments, quotes taken off (escapes kept as written).
|
|
207
207
|
const keyPath = (text) => [...text.matchAll(SEG_RE)].map((m) => m[1] ?? m[2] ?? m[3]);
|
|
208
|
+
// True when a "basic" key segment holds an escape: codex reads the key the escape spells
|
|
209
|
+
// ("sandbox\u005Fmode" is sandbox_mode), and the probe keeps it as written.
|
|
210
|
+
const escapedKey = (text) => [...text.matchAll(SEG_RE)].some((m) => m[1]?.includes("\\"));
|
|
211
|
+
// Such a key leaves the read not whole: what codex reads there is not what the probe placed.
|
|
212
|
+
function markEscape(state, keyText) {
|
|
213
|
+
if (escapedKey(keyText)) state.unplaced = true;
|
|
214
|
+
}
|
|
208
215
|
|
|
209
216
|
// The walk's state: where it is (open string, nesting depth, table, array being gathered) and
|
|
210
217
|
// what it found so far.
|
|
@@ -305,10 +312,14 @@ function assignment(state, path, value) {
|
|
|
305
312
|
if (str) state.values[path[0]] = str[1] ?? str[2];
|
|
306
313
|
}
|
|
307
314
|
|
|
308
|
-
// A table header: the table later keys go in, and the MCP server a [mcp_servers.<name>...] names.
|
|
315
|
+
// A table header: the table later keys go in, and the MCP server a [mcp_servers.<name>...] names. An array of tables
|
|
316
|
+
// under mcp_servers ([[mcp_servers.<name>]], quoted or not) is a server list the probe cannot read: codex refuses it
|
|
317
|
+
// ("invalid type: sequence", 0.144.5), so a seat on it would fail later for no reason the probe gave.
|
|
309
318
|
function enterTable(state, header) {
|
|
310
|
-
|
|
311
|
-
state.
|
|
319
|
+
const path = keyPath(header[2]);
|
|
320
|
+
state.roots.add(path[0]);
|
|
321
|
+
state.table = header[1] ? ["[[]]"] : path;
|
|
322
|
+
if (header[1] && path[0] === "mcp_servers") state.mcpUnread = true;
|
|
312
323
|
if (state.table[0] === "mcp_servers" && state.table.length > 1) state.mcpServers.add(state.table[1]);
|
|
313
324
|
}
|
|
314
325
|
|
|
@@ -324,9 +335,15 @@ function tomlLine(state, line) {
|
|
|
324
335
|
return;
|
|
325
336
|
}
|
|
326
337
|
const header = HEADER_RE.exec(line);
|
|
327
|
-
if (header)
|
|
338
|
+
if (header) {
|
|
339
|
+
markEscape(state, header[2]);
|
|
340
|
+
return enterTable(state, header);
|
|
341
|
+
}
|
|
328
342
|
const assign = ASSIGN_RE.exec(line);
|
|
329
|
-
if (assign)
|
|
343
|
+
if (assign) {
|
|
344
|
+
markEscape(state, assign[1]);
|
|
345
|
+
return assignment(state, keyPath(assign[1]), assign[2]);
|
|
346
|
+
}
|
|
330
347
|
if (!/^\s*(?:#|$)/.test(line)) state.unplaced = true;
|
|
331
348
|
}
|
|
332
349
|
|
|
@@ -361,13 +378,25 @@ const rawMcpNames = (lines) => lines.filter((l) => !/^\s*#/.test(l))
|
|
|
361
378
|
// defined (table headers, under [mcp_servers], dotted keys), whether servers were written in a
|
|
362
379
|
// form it does not list or that the text names beyond what it listed (`mcpUnread`), whether any
|
|
363
380
|
// table sets a model (`modelKeys`), whether a multi-line string, array or inline table was left
|
|
364
|
-
// open or a line could not be placed (`broken`
|
|
381
|
+
// open or a line could not be placed (`broken`, with `brokenAt`: the first line it could not place
|
|
382
|
+
// or the line that opened what is still open), and the strings of each top-level array
|
|
365
383
|
// (`arrays`). Enough for those keys; not a TOML parser.
|
|
366
384
|
export function readToml(text) {
|
|
367
385
|
const state = tomlState();
|
|
368
386
|
const lines = String(text ?? "").split(/\r?\n/);
|
|
369
|
-
|
|
387
|
+
const isOpen = () => state.open !== null || state.depth > 0;
|
|
388
|
+
// the first line it could not place, and the line that opened what is still open at the end
|
|
389
|
+
let badAt = 0;
|
|
390
|
+
let openAt = 0;
|
|
391
|
+
lines.forEach((line, i) => {
|
|
392
|
+
const wasOpen = isOpen();
|
|
393
|
+
const wasBad = state.unplaced;
|
|
394
|
+
tomlLine(state, line);
|
|
395
|
+
if (!wasBad && state.unplaced) badAt = i + 1;
|
|
396
|
+
if (!wasOpen && isOpen()) openAt = i + 1;
|
|
397
|
+
});
|
|
370
398
|
const unlisted = rawMcpNames(lines).filter((n) => !state.mcpServers.has(n));
|
|
399
|
+
const broken = isOpen() || state.unplaced;
|
|
371
400
|
return {
|
|
372
401
|
values: state.values,
|
|
373
402
|
roots: [...state.roots],
|
|
@@ -375,7 +404,8 @@ export function readToml(text) {
|
|
|
375
404
|
mcpServers: [...state.mcpServers],
|
|
376
405
|
mcpUnread: state.mcpUnread || unlisted.length > 0,
|
|
377
406
|
modelKeys: state.modelKeys,
|
|
378
|
-
broken
|
|
407
|
+
broken,
|
|
408
|
+
...(broken ? { brokenAt: Math.min(...[badAt, isOpen() ? openAt : 0].filter(Boolean)) } : {}),
|
|
379
409
|
};
|
|
380
410
|
}
|
|
381
411
|
|
|
@@ -491,7 +521,8 @@ function codexConfigFiles(world) {
|
|
|
491
521
|
|
|
492
522
|
// Those layers as one: the first layer with a value wins, MCP servers add up.
|
|
493
523
|
function codexConfig(world) {
|
|
494
|
-
const
|
|
524
|
+
const files = codexConfigFiles(world);
|
|
525
|
+
const layers = files.map((f) => readToml(world.readFile(f)));
|
|
495
526
|
const pick = (key) => layers.map((l) => l.values[key]).find((v) => v !== undefined);
|
|
496
527
|
return {
|
|
497
528
|
cfg: { model: pick("model"), model_provider: pick("model_provider"), profile: pick("profile") },
|
|
@@ -499,6 +530,7 @@ function codexConfig(world) {
|
|
|
499
530
|
mcpUnread: layers.some((l) => l.mcpUnread),
|
|
500
531
|
modelKeys: layers.some((l) => l.modelKeys),
|
|
501
532
|
broken: layers.some((l) => l.broken),
|
|
533
|
+
brokenAt: layers.map((l, k) => (l.broken ? `${files[k]} line ${l.brokenAt}` : null)).filter(Boolean),
|
|
502
534
|
};
|
|
503
535
|
}
|
|
504
536
|
|
|
@@ -512,8 +544,8 @@ function mcpBlockedSeat(seat, conf, notes) {
|
|
|
512
544
|
const bad = conf.mcpServers.filter((n) => !MCP_NAME.test(n)).length;
|
|
513
545
|
if (!bad && !conf.mcpUnread && !conf.broken) return null;
|
|
514
546
|
if (bad) notes.push(`${bad} MCP server name(s) hold characters \`-c mcp_servers.<name>.enabled=false\` cannot address: rename them to letters, digits, _ and -`);
|
|
515
|
-
if (conf.mcpUnread) notes.push("the probe could not list every MCP server the codex config defines (one inline mcp_servers table, or a name its walk did not place): write each server as its own [mcp_servers.<name>] table");
|
|
516
|
-
if (conf.broken) notes.push(
|
|
547
|
+
if (conf.mcpUnread) notes.push("the probe could not list every MCP server the codex config defines (one inline mcp_servers table, an array of tables under mcp_servers, or a name its walk did not place): write each server as its own [mcp_servers.<name>] table");
|
|
548
|
+
if (conf.broken) notes.push(`the probe could not read the codex config to its end at ${conf.brokenAt.join(", ")} (a multi-line string, array or inline table left open, or a line it could not place, such as a quoted key with a \\ escape that codex reads as another key), so its list of MCP servers may be short`);
|
|
517
549
|
return { ...seat, decision: "unseated", reason: "cli-call-failed" };
|
|
518
550
|
}
|
|
519
551
|
|
|
@@ -530,7 +562,7 @@ function managedBlockedSeat(seat, world, notes) {
|
|
|
530
562
|
const read = readToml(world.readFile(CODEX_MANAGED));
|
|
531
563
|
const keys = read.roots.filter((k) => SEAT_KEYS.includes(k));
|
|
532
564
|
if (!keys.length && !read.broken) return null;
|
|
533
|
-
notes.push(`${CODEX_MANAGED} ${keys.length ? `sets ${keys.join(", ")}` :
|
|
565
|
+
notes.push(`${CODEX_MANAGED} ${keys.length ? `sets ${keys.join(", ")}` : `could not be read to its end (line ${read.brokenAt})`}, which codex applies over the seat's -c flags: the seat cannot hold its guards`);
|
|
534
566
|
return { ...seat, decision: "unseated", reason: "cli-call-failed" };
|
|
535
567
|
}
|
|
536
568
|
|
|
@@ -551,7 +583,7 @@ function requirementsBlockedSeat(seat, world, notes) {
|
|
|
551
583
|
const allows = (k) => SEAT_ALLOWS[k] !== undefined && (read.arrays[k] ?? []).includes(SEAT_ALLOWS[k]);
|
|
552
584
|
const keys = read.roots.filter((k) => !GUARD_FREE.includes(k) && !allows(k));
|
|
553
585
|
if (!keys.length && !read.broken) return null;
|
|
554
|
-
notes.push(`${CODEX_REQUIREMENTS} ${keys.length ? `sets ${keys.join(", ")}` :
|
|
586
|
+
notes.push(`${CODEX_REQUIREMENTS} ${keys.length ? `sets ${keys.join(", ")}` : `could not be read to its end (line ${read.brokenAt})`}, which codex holds over the seat's -c, --disable and -s flags: the seat cannot hold its guards`);
|
|
555
587
|
return { ...seat, decision: "unseated", reason: "cli-call-failed" };
|
|
556
588
|
}
|
|
557
589
|
|
|
@@ -797,9 +829,11 @@ function opencodeModels(cfg) {
|
|
|
797
829
|
return { configured: own, models: [own, ...listed] };
|
|
798
830
|
}
|
|
799
831
|
|
|
832
|
+
const opencodeCache = (world) => join(world.env.XDG_CACHE_HOME || join(world.home, ".cache"), "opencode");
|
|
833
|
+
|
|
800
834
|
// The providers opencode's local copy of the models.dev list knows; null when it cannot be read.
|
|
801
835
|
function catalogProviders(world) {
|
|
802
|
-
const file = join(
|
|
836
|
+
const file = join(opencodeCache(world), "models.json");
|
|
803
837
|
const read = readJsonc(world.readFile(file));
|
|
804
838
|
return read.value ? new Set(Object.keys(read.value)) : null;
|
|
805
839
|
}
|
|
@@ -835,6 +869,10 @@ function opencodeSeat(flag, { world, authorFamilies }) {
|
|
|
835
869
|
notes.push(...cfg.unreadable.map((f) => `config not readable: ${f}`));
|
|
836
870
|
if (world.env.OPENCODE_CONFIG_CONTENT) notes.push("$OPENCODE_CONFIG_CONTENT is not read: the seat command replaces it with its own reviewer agent");
|
|
837
871
|
const catalog = catalogProviders(world);
|
|
872
|
+
// opencode empties its cache on a start after an update; with the list refresh off it fetches nothing
|
|
873
|
+
if (!catalog && world.readFile(join(opencodeCache(world), "version")) != null) {
|
|
874
|
+
notes.push("opencode's models.dev copy is missing or unreadable while its cache version file is there: an opencode start with the list refresh off (the seat and cleanup commands) empties the cache after an update; start opencode once, online and without OPENCODE_DISABLE_MODELS_FETCH, to fetch it again; offline it cannot come back: where the copy let the seat be adopted (single-candidate), each run asks instead (no-catalog), and a run that cannot ask seats the fallback");
|
|
875
|
+
}
|
|
838
876
|
const closed = closedSet(cfg, catalog);
|
|
839
877
|
const out = flagged(seat, flag, authorFamilies) ?? opencodeDecision(seat, { configured, unsure: cfg.unreadable.length > 0, closed, catalog });
|
|
840
878
|
return withNotes(out, notes);
|
|
@@ -933,13 +971,16 @@ function realWorld() {
|
|
|
933
971
|
const exe = onPath(cmd);
|
|
934
972
|
if (!exe) return { status: 127, stdout: "" };
|
|
935
973
|
// Windows runs .cmd and .bat shims through cmd.exe; the path is quoted and the arguments are fixed words.
|
|
974
|
+
// System32's cmd.exe (a bare name is looked for in the current folder first), told not to look there for the
|
|
975
|
+
// bare `node` an npm shim runs.
|
|
936
976
|
const viaCmd = process.platform === "win32" && /\.(cmd|bat)$/i.test(exe);
|
|
977
|
+
const shell = viaCmd ? join(process.env.SystemRoot || "C:\\Windows", "System32", "cmd.exe") : false;
|
|
937
978
|
const r = spawnSync(viaCmd ? `"${exe}"` : exe, args, {
|
|
938
979
|
encoding: "utf8",
|
|
939
980
|
timeout: 15000,
|
|
940
981
|
maxBuffer: 64 * 1024 * 1024,
|
|
941
|
-
shell
|
|
942
|
-
env: { ...process.env, ...env },
|
|
982
|
+
shell,
|
|
983
|
+
env: { ...process.env, ...(viaCmd ? { NoDefaultCurrentDirectoryInExePath: "1" } : {}), ...env },
|
|
943
984
|
});
|
|
944
985
|
return { status: r.status, stdout: r.stdout ?? "" };
|
|
945
986
|
},
|
|
@@ -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 스킵 등).
|