@kaydash9999/banker-plugins 0.22.0 → 0.23.2
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 +68 -0
- package/PRIVACY.md +1 -0
- package/README.md +39 -13
- package/bin/banker.js +118 -22
- package/bin/lib/codex-hooks.mjs +116 -18
- package/bin/lib/codex-names.mjs +4 -0
- package/bin/lib/codex-routes.mjs +93 -1
- package/bin/lib/command-skill.cjs +37 -6
- package/codex/command-skills/front-qa/SKILL.md +1 -1
- package/codex/command-skills/setup/SKILL.md +4 -4
- package/codex/hooks.json +5 -5
- package/codex/manifest.json +18 -15
- package/codex/skills/progress/scripts/progress.mjs +44 -14
- package/codex/skills/teamwork/SKILL.md +143 -0
- package/codex/skills/teamwork/scripts/teamwork.mjs +1007 -0
- package/codex/transform-matrix.md +5 -3
- package/hooks/obsidize-hook.mjs +75 -32
- package/hooks/telemetry-count-codex.mjs +238 -57
- package/hooks/update-fetch.mjs +2 -1
- package/hooks/update-notify.mjs +82 -20
- package/package.json +3 -1
- package/skills/3d-intro-build/SKILL.md +101 -20
- package/skills/3d-intro-build/references/NOTICE.md +9 -2
- package/skills/3d-intro-build/references/assemble.mjs +113 -22
- package/skills/3d-intro-build/references/azure-adapter.mjs +110 -24
- package/skills/3d-intro-build/references/clip-post.mjs +443 -0
- package/skills/3d-intro-build/references/dive-mode.mjs +299 -0
- package/skills/3d-intro-build/references/intro-fixes.css +11 -0
- package/skills/3d-intro-build/references/prompts.md +51 -4
- package/skills/3d-intro-build/references/scrub-engine.js +1 -0
- package/skills/3d-intro-build/references/step-nav.css +29 -0
- package/skills/3d-intro-build/references/step-nav.js +284 -106
- package/skills/3d-intro-build/references/verify-intro.mjs +373 -32
- package/skills/3d-intro-build/references/verify.md +6 -0
- package/skills/3d-intro-build/references/video-pool.mjs +142 -12
- package/skills/3d-intro-setup/SKILL.md +20 -7
- package/skills/3d-intro-setup/references/azure-adapter.mjs +110 -24
- package/skills/3d-intro-setup/references/video-pool.mjs +142 -12
- package/skills/all-in-one/SKILL.md +125 -86
- package/skills/omc-patch/SKILL.md +1 -1
- package/skills/payload-mon/SKILL.md +1 -1
- package/skills/play-qa/SKILL.md +6 -4
- package/skills/progress-mon/README.md +164 -0
- package/skills/progress-mon/SKILL.md +165 -0
- package/skills/progress-mon/assets/fixture/README.md +1 -0
- package/skills/progress-mon/assets/fixture/arch.json +33 -0
- package/skills/progress-mon/assets/fixture/commits.json +7 -0
- package/skills/progress-mon/assets/fixture/config.json +30 -0
- package/skills/progress-mon/assets/fixture/events.jsonl +7 -0
- package/skills/progress-mon/assets/fixture/flow.json +44 -0
- package/skills/progress-mon/assets/fixture/items.json +14 -0
- package/skills/progress-mon/assets/fixture/req-spec.json +31 -0
- package/skills/progress-mon/assets/fixture/reqs.json +11 -0
- package/skills/progress-mon/assets/fixture/variants/arch-auto/arch.json +58 -0
- package/skills/progress-mon/assets/fixture/variants/check-auto/arch.json +15 -0
- package/skills/progress-mon/assets/fixture/variants/check-overlap/arch.json +14 -0
- package/skills/progress-mon/assets/fixture/variants/history-manual/config.json +13 -0
- package/skills/progress-mon/assets/fixture/variants/history-manual/items.json +7 -0
- package/skills/progress-mon/assets/fixture/variants/workflow-wide/flow.json +35 -0
- package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a01.jsonl +1 -0
- package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a02.jsonl +1 -0
- package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a03.jsonl +1 -0
- package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a04.jsonl +1 -0
- package/skills/progress-mon/assets/fixture/workflows/wf_ex1/journal.jsonl +7 -0
- package/skills/progress-mon/references/arch.md +280 -0
- package/skills/progress-mon/references/authoring.md +199 -0
- package/skills/progress-mon/references/config.md +115 -0
- package/skills/progress-mon/references/flow.md +75 -0
- package/skills/progress-mon/references/inventory.md +58 -0
- package/skills/progress-mon/references/items.md +74 -0
- package/skills/progress-mon/references/req-spec.md +74 -0
- package/skills/progress-mon/references/reqs.md +30 -0
- package/skills/progress-mon/scripts/index.js +31 -0
- package/skills/progress-mon/scripts/pm-check.mjs +549 -0
- package/skills/progress-mon/scripts/pm-config.mjs +343 -0
- package/skills/progress-mon/scripts/pm-data.mjs +394 -0
- package/skills/progress-mon/scripts/pm-html.mjs +77 -0
- package/skills/progress-mon/scripts/pm-memory.mjs +333 -0
- package/skills/progress-mon/scripts/pm-render.mjs +1644 -0
- package/skills/progress-mon/scripts/pm-server.mjs +781 -0
- package/skills/progress-mon/scripts/pm-sources.mjs +501 -0
- package/skills/progress-mon/scripts/progress-mon.mjs +167 -0
- package/skills/ralph-qa/SKILL.md +107 -17
- package/skills/ralph-qa/references/gate-evidence.mjs +457 -0
- package/skills/ralph-qa/references/payload-scan.mjs +19 -2
- package/skills/ralph-qa/references/run-folder.mjs +299 -0
- package/skills/ralph-qa/references/seat-payload.mjs +60 -13
- package/skills/ralph-qa/references/seat-run.mjs +200 -159
- package/skills/remains/SKILL.md +2 -0
- package/skills/remains/scripts/boxes.mjs +6 -2
- package/skills/setup-bypass-permissions/scripts/codex-config.mjs +19 -8
- package/skills/setup-mcp/SKILL.md +1 -1
- package/skills/setup-omc-hud/SKILL.md +2 -1
- package/skills/setup-omc-hud/assets/codex-config.mjs +19 -8
- package/skills/ultra-chain/SKILL.md +32 -14
- package/skills/ultra-chain/references/chain-ledger.mjs +111 -3
- package/skills/ultra-init/SKILL.md +30 -22
- package/skills/ultraqa/NOTICE.md +34 -0
- package/skills/ultraqa/SKILL.md +197 -0
- package/skills/ultraqa/references/gate-evidence.mjs +457 -0
- package/skills/ultraqa/references/run-folder.mjs +299 -0
- package/skills/ultraqa/references/tmux-drive.md +52 -0
- package/skills/ultraqa/references/uqa-run.mjs +339 -0
- package/skills/update-banker/SKILL.md +1 -1
|
@@ -19,10 +19,11 @@
|
|
|
19
19
|
* (the DRM of a closed network encrypts files by their extension, and .md is one it leaves readable).
|
|
20
20
|
*/
|
|
21
21
|
import { fork } from "node:child_process";
|
|
22
|
-
import {
|
|
23
|
-
import { constants as osConstants
|
|
24
|
-
import { basename, dirname,
|
|
22
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, utimesSync, writeFileSync } from "node:fs";
|
|
23
|
+
import { constants as osConstants } from "node:os";
|
|
24
|
+
import { basename, dirname, join } from "node:path";
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
|
+
import { Stop as RunStop, bases, isFile, listDir, lstatOr, makeRunFolder, norm, realOr, removeMarked, runFolder } from "./run-folder.mjs";
|
|
26
27
|
import { POLICY, cleanSeat, policyIntact, strayEnvFiles, sweepBase } from "./gemini-seat.mjs";
|
|
27
28
|
import { scan } from "./payload-scan.mjs";
|
|
28
29
|
import { MAX_BYTES, Stop, buildPayload, checkPayload, gitAt, markerCount, readRun, splitPayload, untracked } from "./seat-payload.mjs";
|
|
@@ -31,7 +32,7 @@ import { CLI_DEFAULT, probe as localProbe, safeModel } from "./verifier-probe.mj
|
|
|
31
32
|
|
|
32
33
|
// The stops a command may end with, as `stop` in its JSON; the skill's table of what to do holds the same set.
|
|
33
34
|
export const STOP_CODES = ["index-missing", "no-diff", "list", "binary-diff", "range-source", "range-link", "range-outside", "file", "markers",
|
|
34
|
-
"spec", "not-a-repo", "base-invalid", "git-failed", "git-timeout", "scan", "too-large", "sent", "sandboxed", "base-not-private",
|
|
35
|
+
"spec", "not-a-repo", "base-invalid", "git-failed", "git-timeout", "scan", "too-large", "sent", "sent-mark", "sandboxed", "base-not-private",
|
|
35
36
|
"base-not-writable", "inside-repo", "busy", "not-prepared", "not-a-run-folder", "split", "cleanup-left"];
|
|
36
37
|
const EXIT_OF = { scan: 1, "too-large": 4, busy: 5 };
|
|
37
38
|
|
|
@@ -42,6 +43,12 @@ const RUN_NAME = /^ralph-qa\.[A-Za-z0-9]+$/;
|
|
|
42
43
|
const ANY_RUN = /^ralph-qa(-out)?\.[A-Za-z0-9]+$/;
|
|
43
44
|
// The lock of a send that runs: one name, made with "wx", holding the pid and the time.
|
|
44
45
|
const LOCK_NAME = ".running.md";
|
|
46
|
+
const TAKING_NAME = ".taking.md";
|
|
47
|
+
// The "sent" mark, written once under the lock before any seat work (send's markOrStop). It stands apart from
|
|
48
|
+
// result-send.md (written after every seat ends): a send that holds the lock in the A2 window finds this mark and
|
|
49
|
+
// does not send the payload again, even if that first send died before it wrote result-send.md.
|
|
50
|
+
const SENT_NAME = ".sent.md";
|
|
51
|
+
const STALE_TAKING_MS = 60 * 1000;
|
|
45
52
|
|
|
46
53
|
class Usage extends Error {}
|
|
47
54
|
const USAGE = "usage: node seat-run.mjs init | untracked --repo <repo> [--folder <f>] | prepare --dir <d> --repo <repo> --base <base|auto> --id <id>"
|
|
@@ -93,98 +100,30 @@ export function fromMsys(p, platform) {
|
|
|
93
100
|
return m ? `${m[1].toUpperCase()}:\\${(m[2] || "").replace(/\//g, "\\")}` : p;
|
|
94
101
|
}
|
|
95
102
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
const isFile = (p) => Boolean(lstatOr(p)?.isFile());
|
|
104
|
-
const listDir = (dir) => {
|
|
105
|
-
try {
|
|
106
|
-
return readdirSync(dir);
|
|
107
|
-
} catch {
|
|
108
|
-
return [];
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
// A folder init made: named ralph-qa.<letters and digits>, a folder and no link, with its marker. Its answer folder
|
|
113
|
-
// is the sibling ralph-qa-out.<the same>.
|
|
114
|
-
export function runFolder(dir) {
|
|
115
|
-
const d = resolve(String(dir));
|
|
116
|
-
if (!RUN_NAME.test(basename(d)) || !lstatOr(d)?.isDirectory() || !isFile(join(d, ".ralph-qa"))) {
|
|
117
|
-
throw new Stop("not-a-run-folder", `init 이 만든 폴더가 아니다: ${d}`);
|
|
118
|
-
}
|
|
103
|
+
// A folder init made (run-folder.mjs: its name, its marker, right under a known private base) and its answer folder,
|
|
104
|
+
// the sibling ralph-qa-out.<the same>.
|
|
105
|
+
function runPair(dir, ctx) {
|
|
106
|
+
const { d } = runFolder(dir, ctx, ["ralph-qa"]);
|
|
119
107
|
const o = join(dirname(d), basename(d).replace(/^ralph-qa\./, "ralph-qa-out."));
|
|
120
108
|
const answers = lstatOr(o);
|
|
121
109
|
return { d, o: answers?.isDirectory() && isFile(join(o, ".ralph-qa")) ? o : null };
|
|
122
110
|
}
|
|
123
111
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const realOr = (p) => {
|
|
127
|
-
try {
|
|
128
|
-
return realpathSync.native(p);
|
|
129
|
-
} catch {
|
|
130
|
-
return p;
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
// A folder only this account may write in (POSIX: its own, no group or other write bit).
|
|
135
|
-
function privateDir(p) {
|
|
136
|
-
try {
|
|
137
|
-
const st = statSync(p);
|
|
138
|
-
return st.isDirectory() && st.uid === process.getuid() && (st.mode & 0o022) === 0;
|
|
139
|
-
} catch {
|
|
140
|
-
return false;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
const insideHome = (ctx, p) => norm(ctx, realOr(p)).startsWith(norm(ctx, realOr(homeOf(ctx))) + sep);
|
|
144
|
-
|
|
145
|
-
// The base for this run's folders, and every base a run of any version may have used, for the day rule. Linux: an
|
|
146
|
-
// XDG_RUNTIME_DIR of this account's own, else ~/.cache; macOS: $TMPDIR (its own per account); Windows: %TEMP% when it
|
|
147
|
-
// is inside the home, else ~/.cache (the old Git Bash runs used ~/.cache). Never /tmp: gemini reads a .env it finds
|
|
148
|
-
// walking up from its folder, and another account may put one there.
|
|
149
|
-
export function bases(ctx) {
|
|
150
|
-
const cache = join(homeOf(ctx), ".cache");
|
|
151
|
-
const base = baseOn(ctx, cache);
|
|
152
|
-
const xdg = ctx.platform === "linux" && ctx.env.XDG_RUNTIME_DIR ? [resolve(ctx.env.XDG_RUNTIME_DIR)] : [];
|
|
153
|
-
return { base, known: [...new Set([base, cache, ...xdg])] };
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function baseOn(ctx, cache) {
|
|
157
|
-
const { env } = ctx;
|
|
158
|
-
if (ctx.platform === "win32") return env.TEMP && insideHome(ctx, env.TEMP) ? resolve(env.TEMP) : cache;
|
|
159
|
-
if (ctx.platform === "darwin") return env.TMPDIR ? resolve(env.TMPDIR) : cache;
|
|
160
|
-
const xdg = env.XDG_RUNTIME_DIR;
|
|
161
|
-
return xdg && isAbsolute(xdg) && privateDir(xdg) ? resolve(xdg) : cache;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function readyBase(ctx, base) {
|
|
165
|
-
try {
|
|
166
|
-
mkdirSync(base, { recursive: true, mode: 0o700 });
|
|
167
|
-
} catch (e) {
|
|
168
|
-
throw new Stop("base-not-writable", `기준 폴더를 만들 수 없다(${e.code}): ${base}`);
|
|
169
|
-
}
|
|
170
|
-
if (ctx.platform !== "win32" && !privateDir(base)) throw new Stop("base-not-private", `기준 폴더를 다른 계정도 쓸 수 있다: ${base}`);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Two folders with the same six characters: the payload one by mkdtemp, its answer folder beside it.
|
|
174
|
-
function makeFolders(base) {
|
|
112
|
+
// Two folders with the same six characters: the payload one by run-folder.mjs, its answer folder beside it.
|
|
113
|
+
function makeFolders(ctx) {
|
|
175
114
|
for (let tries = 0; tries < 20; tries += 1) {
|
|
176
|
-
const d =
|
|
115
|
+
const { dir: d, base } = makeRunFolder(ctx, "ralph-qa");
|
|
177
116
|
const o = join(base, basename(d).replace(/^ralph-qa\./, "ralph-qa-out."));
|
|
178
117
|
try {
|
|
179
118
|
mkdirSync(o, { mode: 0o700 });
|
|
180
|
-
|
|
181
|
-
return { d, o };
|
|
119
|
+
writeFileSync(join(o, ".ralph-qa"), "", { mode: 0o600 });
|
|
120
|
+
return { d, o, base };
|
|
182
121
|
} catch (e) {
|
|
183
122
|
rmSync(d, { recursive: true, force: true });
|
|
184
|
-
if (e.code !== "EEXIST") throw e;
|
|
123
|
+
if (e.code !== "EEXIST") throw new Stop("base-not-writable", `기준 폴더에 쓸 수 없다(${e.code}): ${base}`);
|
|
185
124
|
}
|
|
186
125
|
}
|
|
187
|
-
throw new Stop("base-not-writable", `기준 폴더에 새 폴더 이름을 정하지 못했다: ${base}`);
|
|
126
|
+
throw new Stop("base-not-writable", `기준 폴더에 새 폴더 이름을 정하지 못했다: ${bases(ctx).base}`);
|
|
188
127
|
}
|
|
189
128
|
|
|
190
129
|
// Whether `dir` is inside a git work tree: a seat would then read the repo through its tools.
|
|
@@ -197,31 +136,51 @@ function insideRepo(ctx, dir) {
|
|
|
197
136
|
}
|
|
198
137
|
|
|
199
138
|
function init(_o, ctx) {
|
|
200
|
-
const
|
|
201
|
-
readyBase(ctx, base);
|
|
202
|
-
let made;
|
|
203
|
-
try {
|
|
204
|
-
made = makeFolders(base);
|
|
205
|
-
} catch (e) {
|
|
206
|
-
if (e instanceof Stop) throw e;
|
|
207
|
-
throw new Stop("base-not-writable", `기준 폴더에 쓸 수 없다(${e.code}): ${base}`);
|
|
208
|
-
}
|
|
139
|
+
const made = makeFolders(ctx);
|
|
209
140
|
if (insideRepo(ctx, made.d)) {
|
|
210
141
|
for (const p of [made.d, made.o]) rmSync(p, { recursive: true, force: true });
|
|
211
|
-
throw new Stop("inside-repo", `기준 폴더가 git 저장소 안이다: ${base}`);
|
|
142
|
+
throw new Stop("inside-repo", `기준 폴더가 git 저장소 안이다: ${made.base}`);
|
|
212
143
|
}
|
|
213
|
-
return [{ ok: true, dir: made.d, out: made.o, base }, 0, made.d];
|
|
144
|
+
return [{ ok: true, dir: made.d, out: made.o, base: made.base }, 0, made.d];
|
|
214
145
|
}
|
|
215
146
|
|
|
216
147
|
function listUntracked(o, ctx) {
|
|
217
148
|
return [{ ok: true, ...untracked({ repo: o.repo, env: ctx.env, folder: o.folder || null }) }, 0, null];
|
|
218
149
|
}
|
|
219
150
|
|
|
220
|
-
|
|
151
|
+
// A folder is sent once either mark is there: result-send.md (a send ran to the end) or .sent.md (a send took the
|
|
152
|
+
// folder to send it, even if it then stopped or died before result-send.md).
|
|
153
|
+
const sent = (d) => existsSync(join(d, "result-send.md")) || existsSync(join(d, SENT_NAME));
|
|
221
154
|
const refuseSent = (d) => {
|
|
222
|
-
if (sent(d)) throw new Stop("sent", `이미 보낸 폴더다.
|
|
155
|
+
if (sent(d)) throw new Stop("sent", `이미 보낸 폴더다. 좌석의 답이 있으면 답 폴더(ralph-qa-out.*)에 있다. 다시 보내지 말고, 다시 보낼지는 사용자에게 묻는다: ${d}`);
|
|
223
156
|
};
|
|
224
157
|
|
|
158
|
+
// Writes .sent.md once (the memo answers again with the first result). EEXIST means another send (the A2 window)
|
|
159
|
+
// already marked it; any other write error is `err`, and a mark left empty by a failed write is taken away, so the
|
|
160
|
+
// next send is not refused for a folder nothing went out of.
|
|
161
|
+
export function makeMarkSent(d, now) {
|
|
162
|
+
let state = null;
|
|
163
|
+
return () => {
|
|
164
|
+
if (state) return state;
|
|
165
|
+
const file = join(d, SENT_NAME);
|
|
166
|
+
try {
|
|
167
|
+
writeFileSync(file, `${process.pid} ${new Date(now()).toISOString()}\n`, { flag: "wx", mode: 0o600 });
|
|
168
|
+
state = { ok: true };
|
|
169
|
+
} catch (e) {
|
|
170
|
+
if (e.code !== "EEXIST") rmSync(file, { force: true });
|
|
171
|
+
state = e.code === "EEXIST" ? { already: true } : { err: e };
|
|
172
|
+
}
|
|
173
|
+
return state;
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Marks the folder sent under the lock, before any seat work, or stops the send (the lock goes back in send's finally).
|
|
178
|
+
function markOrStop(d, now) {
|
|
179
|
+
const mark = makeMarkSent(d, now)();
|
|
180
|
+
if (mark.already) throw new RunStop("sent", `다른 send 가 이미 이 폴더를 보냈다: ${d}`);
|
|
181
|
+
if (mark.err) throw new RunStop("sent-mark", `보냈음 표지(.sent.md)를 쓰지 못했다(${mark.err.code || mark.err.message}). 좌석은 아무것도 보내지 않았다: ${d}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
225
184
|
// prepare and check end with 1 when a line looks like a secret and 4 past 512 KiB; the JSON carries both.
|
|
226
185
|
function verdict(result) {
|
|
227
186
|
if (result.scan.count) {
|
|
@@ -235,15 +194,15 @@ function verdict(result) {
|
|
|
235
194
|
}
|
|
236
195
|
|
|
237
196
|
function prepare(o, ctx) {
|
|
238
|
-
const { d } =
|
|
197
|
+
const { d } = runPair(o.dir, ctx);
|
|
239
198
|
refuseSent(d);
|
|
240
199
|
const built = buildPayload({ dir: d, repo: o.repo, base: o.base, id: o.id, env: ctx.env });
|
|
241
200
|
const [result, code] = verdict({ dir: d, prompt: join(d, "prompt.md"), raw: join(d, "raw.md"), unsentFile: join(d, "unsent.md"), ...built });
|
|
242
201
|
return [result, code, d];
|
|
243
202
|
}
|
|
244
203
|
|
|
245
|
-
function check(o) {
|
|
246
|
-
const { d } =
|
|
204
|
+
function check(o, ctx) {
|
|
205
|
+
const { d } = runPair(o.dir, ctx);
|
|
247
206
|
const id = readRun(d).id;
|
|
248
207
|
const c = checkPayload({ dir: d, id });
|
|
249
208
|
if (c.markers !== 2) throw new Stop("markers", `표지가 두 줄이 아니다(${c.markers}줄). 가리다 표지를 지웠으면 prepare 를 다시 한다`);
|
|
@@ -262,7 +221,7 @@ const readStdin = (ctx) => {
|
|
|
262
221
|
|
|
263
222
|
function put(o, ctx) {
|
|
264
223
|
if (!["short.md", "spec.md"].includes(o.name)) throw new Usage(`--name 은 short.md 나 spec.md 다: ${String(o.name).slice(0, 80)}`);
|
|
265
|
-
const { d } =
|
|
224
|
+
const { d } = runPair(o.dir, ctx);
|
|
266
225
|
refuseSent(d);
|
|
267
226
|
const data = readStdin(ctx);
|
|
268
227
|
writeFileSync(join(d, o.name), data, { mode: 0o600 });
|
|
@@ -294,22 +253,71 @@ export function liveLock(d, now = Date.now()) {
|
|
|
294
253
|
return st && st.isFile() && pid !== process.pid && now - st.mtimeMs < STALE_LOCK_MS && alive(pid) ? pid : null;
|
|
295
254
|
}
|
|
296
255
|
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
|
|
256
|
+
// A lock with no number to read (empty: another send made it and has not written its number yet; or broken) touched
|
|
257
|
+
// in the last 15 minutes: taken over, it would let two sends hold the folder.
|
|
258
|
+
function freshUnread(d, now) {
|
|
259
|
+
const st = lockPid(d) ? null : lstatOr(join(d, LOCK_NAME));
|
|
260
|
+
return Boolean(st && st.isFile() && now - st.mtimeMs < STALE_LOCK_MS);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const justStarted = () => new Stop("busy", "이 폴더의 send 가 방금 시작했다");
|
|
264
|
+
|
|
265
|
+
// Windows makes "wx" fail EPERM (or EACCES, EBUSY), not EEXIST, where another process is deleting the file (delete
|
|
266
|
+
// pending: a lock given back that moment). Such a code is tried again for up to a second; then it is the lock there
|
|
267
|
+
// when the file is seen, else this send's error. Any other error fails at once.
|
|
268
|
+
const PENDING = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
269
|
+
const PENDING_TRIES = 40;
|
|
270
|
+
const PENDING_WAIT_MS = 25;
|
|
271
|
+
const pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
272
|
+
|
|
273
|
+
// The lock (or the take-over mark) made with "wx": one of two sends that make it together has it.
|
|
274
|
+
function makeLock(file, now) {
|
|
275
|
+
for (let tries = 1; ; tries += 1) {
|
|
302
276
|
try {
|
|
303
|
-
writeFileSync(
|
|
304
|
-
return
|
|
277
|
+
writeFileSync(file, `${process.pid} ${new Date(now).toISOString()}\n`, { flag: "wx", mode: 0o600 });
|
|
278
|
+
return true;
|
|
305
279
|
} catch (e) {
|
|
306
|
-
if (e.code
|
|
280
|
+
if (e.code === "EEXIST") return false;
|
|
281
|
+
if (!PENDING.has(e.code)) throw e;
|
|
282
|
+
if (tries >= PENDING_TRIES) {
|
|
283
|
+
if (existsSync(file)) return false;
|
|
284
|
+
throw e;
|
|
285
|
+
}
|
|
307
286
|
}
|
|
308
|
-
|
|
309
|
-
|
|
287
|
+
pause(PENDING_WAIT_MS);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// A stale lock is taken over only under the take-over mark .taking.md, and judged again under it: of two sends that
|
|
292
|
+
// both found it stale, the second finds the first's new lock live (or with no number yet) and is busy; neither removes
|
|
293
|
+
// a lock that is not stale. A mark left by a send that died taking over is cleared once it is a minute old (that send
|
|
294
|
+
// is busy; the next one goes on), so it holds the folder no longer than that.
|
|
295
|
+
export function takeOver(d, lock, now = Date.now()) {
|
|
296
|
+
const taking = join(d, TAKING_NAME);
|
|
297
|
+
if (!makeLock(taking, now)) {
|
|
298
|
+
const st = lstatOr(taking);
|
|
299
|
+
if (st?.isFile() && now - st.mtimeMs >= STALE_TAKING_MS) rmSync(taking, { force: true });
|
|
300
|
+
throw new Stop("busy", "다른 send 가 이 폴더의 낡은 잠금을 넘겨받는 중이다");
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
if (liveLock(d, now) || freshUnread(d, now)) throw justStarted();
|
|
310
304
|
rmSync(lock, { force: true });
|
|
305
|
+
if (!makeLock(lock, now)) throw justStarted();
|
|
306
|
+
return lock;
|
|
307
|
+
} finally {
|
|
308
|
+
rmSync(taking, { force: true });
|
|
311
309
|
}
|
|
312
|
-
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Takes the folder's lock: "wx" lets one of two sends that start together have it; a stale one is taken over under
|
|
313
|
+
// the take-over mark. Gives back the lock's path.
|
|
314
|
+
export function takeLock(d, now = Date.now()) {
|
|
315
|
+
const lock = join(d, LOCK_NAME);
|
|
316
|
+
if (makeLock(lock, now)) return lock;
|
|
317
|
+
const pid = liveLock(d, now);
|
|
318
|
+
if (pid) throw new Stop("busy", `이 폴더의 send 가 아직 돈다(pid ${pid})`);
|
|
319
|
+
if (freshUnread(d, now)) throw justStarted();
|
|
320
|
+
return takeOver(d, lock, now);
|
|
313
321
|
}
|
|
314
322
|
|
|
315
323
|
// Touches the lock every `every` ms while a send runs: a send past 15 minutes (split parts) stays live to cleanup.
|
|
@@ -330,31 +338,22 @@ function giveBack(lock) {
|
|
|
330
338
|
if (lockPid(dirname(lock)) === process.pid) rmSync(lock, { force: true });
|
|
331
339
|
}
|
|
332
340
|
|
|
333
|
-
// The
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
function removeMarked(p) {
|
|
338
|
-
const was = lstatOr(p);
|
|
341
|
+
// The lock, and only then the last look for result-send.md: a send that checked before another gave the lock back
|
|
342
|
+
// finds the folder sent here, not after it sent again. A stop here gives the lock back.
|
|
343
|
+
export function holdFolder(d, now = Date.now()) {
|
|
344
|
+
const lock = takeLock(d, now);
|
|
339
345
|
try {
|
|
340
|
-
|
|
346
|
+
refuseSent(d);
|
|
341
347
|
} catch (e) {
|
|
342
|
-
|
|
348
|
+
giveBack(lock);
|
|
343
349
|
throw e;
|
|
344
350
|
}
|
|
351
|
+
return lock;
|
|
345
352
|
}
|
|
346
353
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if (!isFile(join(p, ".ralph-qa"))) writeFileSync(join(p, ".ralph-qa"), "", { flag: "wx", mode: 0o600 });
|
|
351
|
-
if (was) utimesSync(p, was.atime, was.mtime);
|
|
352
|
-
} catch {
|
|
353
|
-
/* the folder takes no marker or times either */
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// One that cannot be removed (a held handle, a folder this account cannot empty) goes to `left` with why; the rest go on.
|
|
354
|
+
// The day rule: folders of an earlier run, marked, past a day, never a link or a folder without the marker (removed
|
|
355
|
+
// by run-folder.mjs, the marker last). One that cannot be removed (a held handle, a folder this account cannot
|
|
356
|
+
// empty) goes to `left` with why; the rest go on.
|
|
358
357
|
function dayRule(base, now, left) {
|
|
359
358
|
const removed = [];
|
|
360
359
|
for (const name of listDir(base)) {
|
|
@@ -362,7 +361,7 @@ function dayRule(base, now, left) {
|
|
|
362
361
|
const st = ANY_RUN.test(name) ? lstatOr(p) : null;
|
|
363
362
|
if (!st?.isDirectory() || now - st.mtimeMs <= DAY_MS || !isFile(join(p, ".ralph-qa"))) continue;
|
|
364
363
|
try {
|
|
365
|
-
removeMarked(p);
|
|
364
|
+
removeMarked(p, ".ralph-qa");
|
|
366
365
|
removed.push(p);
|
|
367
366
|
} catch (e) {
|
|
368
367
|
left.push({ path: p, why: e.code || e.message });
|
|
@@ -376,7 +375,7 @@ function removeOwn(folders) {
|
|
|
376
375
|
const stayed = [];
|
|
377
376
|
for (const p of folders.filter(Boolean)) {
|
|
378
377
|
try {
|
|
379
|
-
removeMarked(p);
|
|
378
|
+
removeMarked(p, ".ralph-qa");
|
|
380
379
|
} catch (e) {
|
|
381
380
|
stayed.push(`${p} (${e.code || e.message})`);
|
|
382
381
|
}
|
|
@@ -430,7 +429,7 @@ const readIdOr = (d) => {
|
|
|
430
429
|
};
|
|
431
430
|
|
|
432
431
|
async function cleanup(o, ctx) {
|
|
433
|
-
const { d, o: out } =
|
|
432
|
+
const { d, o: out } = runPair(o.dir, ctx);
|
|
434
433
|
const pid = o.force ? null : liveLock(d, ctx.now());
|
|
435
434
|
if (pid) throw new Stop("busy", `이 폴더의 send 가 아직 돈다(pid ${pid}). 끝난 뒤 다시 한다`);
|
|
436
435
|
const { known } = bases(ctx);
|
|
@@ -586,8 +585,8 @@ async function cliChecks(ctx, seat, plan, exe) {
|
|
|
586
585
|
}
|
|
587
586
|
|
|
588
587
|
// The parts a seat gets: the payload, or its split, each with its two markers and no secret-like line.
|
|
589
|
-
function partsOf(payload, n, id, short) {
|
|
590
|
-
const parts = n ? splitPayload(payload, n, id, short) : [payload];
|
|
588
|
+
function partsOf(payload, n, { id, short, shortSha }) {
|
|
589
|
+
const parts = n ? splitPayload(payload, n, id, short, shortSha) : [payload];
|
|
591
590
|
parts.forEach((part, k) => {
|
|
592
591
|
if (markerCount(part, id) !== 2 || scan(part.toString("utf8")).count) throw new Stop("split", `부분 ${k + 1}/${parts.length} 의 표지나 검사가 맞지 않는다`);
|
|
593
592
|
});
|
|
@@ -699,7 +698,7 @@ async function seatWork(ctx, run, seat, g, payload, base) {
|
|
|
699
698
|
if (why) return { ...base, why };
|
|
700
699
|
let parts;
|
|
701
700
|
try {
|
|
702
|
-
parts = partsOf(payload, run.split[seat.cli], run
|
|
701
|
+
parts = partsOf(payload, run.split[seat.cli], run);
|
|
703
702
|
} catch (e) {
|
|
704
703
|
if (e instanceof Stop) return { ...base, why: "split", notes: [e.message] };
|
|
705
704
|
throw e;
|
|
@@ -708,10 +707,37 @@ async function seatWork(ctx, run, seat, g, payload, base) {
|
|
|
708
707
|
return runParts(ctx, run, seat, g, parts, base);
|
|
709
708
|
}
|
|
710
709
|
|
|
710
|
+
// The seat CLI's version, read without starting gemini or opencode: codex's from the probe's `codex --version`, the
|
|
711
|
+
// others' from the package.json of their npm package (beside the entry point, above its real path, or under the
|
|
712
|
+
// prefix's lib/node_modules), opencode's own installer folder holding the plugin package of its version.
|
|
713
|
+
const PACKAGES = { codex: "@openai/codex", gemini: "@google/gemini-cli", opencode: "opencode-ai" };
|
|
714
|
+
const readJson = (p) => {
|
|
715
|
+
try {
|
|
716
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
717
|
+
} catch {
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
function installedVersion(cli, exe) {
|
|
722
|
+
const real = realOr(exe);
|
|
723
|
+
const pkg = PACKAGES[cli].split("/");
|
|
724
|
+
const tries = [join(dirname(exe), "node_modules", ...pkg, "package.json"), join(dirname(dirname(exe)), "lib", "node_modules", ...pkg, "package.json")];
|
|
725
|
+
for (let dir = dirname(real), k = 0; k < 5; k += 1, dir = dirname(dir)) tries.push(join(dir, "package.json"));
|
|
726
|
+
const found = tries.map(readJson).find((j) => j?.name === PACKAGES[cli] && typeof j.version === "string");
|
|
727
|
+
if (found) return found.version;
|
|
728
|
+
const own = cli === "opencode" ? readJson(join(dirname(dirname(real)), "package.json"))?.dependencies?.["@opencode-ai/plugin"] : null;
|
|
729
|
+
return typeof own === "string" ? `${own} (설치 폴더의 @opencode-ai/plugin)` : null;
|
|
730
|
+
}
|
|
731
|
+
export function cliVersion(cli, exe, probed) {
|
|
732
|
+
if (cli === "codex" && probed?.version) return String(probed.version).slice(0, 80);
|
|
733
|
+
return installedVersion(cli, exe) || "알 수 없음";
|
|
734
|
+
}
|
|
735
|
+
|
|
711
736
|
async function sendSeat(ctx, run, seat, payload) {
|
|
712
|
-
const
|
|
737
|
+
const none = { seat: seat.cli, model: seat.model, status: "not-sent", why: null, exit: null, signal: null, secs: 0, answer: null, errors: null, facts: {}, notes: [] };
|
|
713
738
|
const g = guards(ctx, seat, run);
|
|
714
|
-
if (g.why) return { ...
|
|
739
|
+
if (g.why) return { ...none, why: g.why, notes: g.notes || [] };
|
|
740
|
+
const base = { ...none, cliVersion: cliVersion(seat.cli, g.exe, g.probed) };
|
|
715
741
|
let result;
|
|
716
742
|
try {
|
|
717
743
|
mkdirSync(join(run.d, seat.cli), { recursive: true, mode: 0o700 });
|
|
@@ -719,7 +745,7 @@ async function sendSeat(ctx, run, seat, payload) {
|
|
|
719
745
|
} catch (e) {
|
|
720
746
|
// the runner's own error in this seat's work (its copy cannot be written: ENOSPC, EACCES, a Windows lock) is this
|
|
721
747
|
// seat's failure: the other seats go on, and the lock and the stop handlers hold until they end
|
|
722
|
-
if (e instanceof
|
|
748
|
+
if (e instanceof RunStop) throw e;
|
|
723
749
|
result = { ...base, status: "failed", why: "spawn", notes: [`좌석을 준비하지 못했다(${e.code || e.message})`] };
|
|
724
750
|
} finally {
|
|
725
751
|
const notes = await afterSeat(ctx, run, seat);
|
|
@@ -775,29 +801,45 @@ function declared(run, bytes, seats, d) {
|
|
|
775
801
|
}
|
|
776
802
|
|
|
777
803
|
function sendStart(o, ctx, a) {
|
|
778
|
-
const { d, o: out } =
|
|
804
|
+
const { d, o: out } = runPair(o.dir, ctx);
|
|
779
805
|
if (!out) throw new Stop("not-a-run-folder", `답 폴더(ralph-qa-out.*)가 없다: ${d}`);
|
|
780
806
|
refuseSent(d);
|
|
781
807
|
if (ctx.env.CODEX_SANDBOX_NETWORK_DISABLED === "1" && !a.dryRun) throw new Stop("sandboxed", "Codex sandbox 가 네트워크를 막는다. 승인을 받아 sandbox 밖에서 send 를 다시 한다");
|
|
782
|
-
const { id, short } = readRun(d);
|
|
808
|
+
const { id, short, shortsha } = readRun(d);
|
|
783
809
|
const payload = payloadToSend(d, id, Boolean(o["allow-large"]));
|
|
784
|
-
return { d, out, id, payload, short: Number(short) };
|
|
810
|
+
return { d, out, id, payload, short: Number(short), shortSha: shortsha || null };
|
|
785
811
|
}
|
|
786
812
|
|
|
813
|
+
const resultJson = (command, result) => `${JSON.stringify({ command, ...result }, null, 2)}\n`;
|
|
814
|
+
const writeResult = (dir, name, json) => {
|
|
815
|
+
try {
|
|
816
|
+
writeFileSync(join(dir, `result-${name}.md`), json, { mode: 0o600 });
|
|
817
|
+
} catch {
|
|
818
|
+
/* the JSON on stdout is the answer */
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
// A send that went out writes result-send.md before the lock goes back, so the next send to take the lock finds it
|
|
823
|
+
// (report writes it again with the same JSON).
|
|
787
824
|
async function send(o, ctx) {
|
|
788
825
|
const a = sendArgs(o);
|
|
789
|
-
const { d, out, id, payload, short } = sendStart(o, ctx, a);
|
|
790
|
-
const lock =
|
|
826
|
+
const { d, out, id, payload, short, shortSha } = sendStart(o, ctx, a);
|
|
827
|
+
const lock = holdFolder(d, ctx.now());
|
|
791
828
|
const touch = keepFresh(lock);
|
|
792
829
|
const signals = watchSignals();
|
|
793
|
-
const run = { ...a, d, o: out, id, short, signalled: () => signals.code() !== 0 };
|
|
830
|
+
const run = { ...a, d, o: out, id, short, shortSha, signalled: () => signals.code() !== 0 };
|
|
794
831
|
try {
|
|
832
|
+
// The folder is marked sent under the lock, before any seat writes into it: a send that lost the A2 window stops
|
|
833
|
+
// here, before it could overwrite the winner's prompt copies or clean the winner's seat folders.
|
|
834
|
+
if (!a.dryRun) markOrStop(d, ctx.now);
|
|
795
835
|
// every seat settles before the lock goes back and the stop handlers go away
|
|
796
836
|
const settled = await Promise.allSettled(a.seats.map((x) => sendSeat(ctx, run, x, payload)));
|
|
797
837
|
const thrown = settled.find((x) => x.status === "rejected");
|
|
798
838
|
if (thrown) throw thrown.reason;
|
|
799
839
|
const seats = settled.map((x) => x.value);
|
|
840
|
+
if (!a.dryRun) appendFileSync(join(d, "run.md"), seats.filter((x) => x.cliVersion).map((x) => `cli-${x.seat} ${x.cliVersion}\n`).join(""), { mode: 0o600 });
|
|
800
841
|
const result = { ok: true, ...(a.dryRun ? { dryRun: true } : {}), dir: d, out, bytes: payload.length, scan: 0, unsent: unsentCount(d), seats, declared: declared(run, payload.length, seats, d) };
|
|
842
|
+
if (!a.dryRun) writeResult(d, "send", resultJson("send", result));
|
|
801
843
|
return [result, signals.code(), a.dryRun ? null : d];
|
|
802
844
|
} finally {
|
|
803
845
|
clearInterval(touch);
|
|
@@ -810,42 +852,41 @@ const COMMANDS = { init, untracked: listUntracked, prepare, check, send, cleanup
|
|
|
810
852
|
|
|
811
853
|
// The JSON on stdout and in <d>/result-<command>.md (not for cleanup: the folder is gone).
|
|
812
854
|
function report(ctx, command, dir, result, code, name = command) {
|
|
813
|
-
const json =
|
|
855
|
+
const json = resultJson(command, result);
|
|
814
856
|
ctx.stdout.write(json);
|
|
815
857
|
if (result.message) ctx.stderr.write(`${result.message}\n`);
|
|
816
|
-
if (dir)
|
|
817
|
-
try {
|
|
818
|
-
writeFileSync(join(dir, `result-${name}.md`), json, { mode: 0o600 });
|
|
819
|
-
} catch {
|
|
820
|
-
/* the JSON on stdout is the answer */
|
|
821
|
-
}
|
|
822
|
-
}
|
|
858
|
+
if (dir) writeResult(dir, name, json);
|
|
823
859
|
return code;
|
|
824
860
|
}
|
|
825
861
|
|
|
826
862
|
// A stop in the folder it concerns, when that is a run folder.
|
|
827
|
-
const folderOf = (o) => {
|
|
863
|
+
const folderOf = (o, ctx) => {
|
|
828
864
|
try {
|
|
829
|
-
return o.dir ?
|
|
865
|
+
return o.dir ? runPair(o.dir, ctx).d : null;
|
|
830
866
|
} catch {
|
|
831
867
|
return null;
|
|
832
868
|
}
|
|
833
869
|
};
|
|
834
870
|
|
|
835
|
-
// A send that stopped goes to result-send-stop.md
|
|
871
|
+
// A send that stopped goes to result-send-stop.md, not result-send.md (that one says every seat ended). A stopped
|
|
872
|
+
// send may still have carried the payload to some seats: .sent.md, written under the lock before any seat work, is what
|
|
873
|
+
// keeps the next send from sending it again.
|
|
836
874
|
const stopName = (command) => (command === "send" ? "send-stop" : command);
|
|
837
875
|
|
|
876
|
+
// A stop of run-folder.mjs carries no name: this runner's goes before it.
|
|
877
|
+
const named = (message) => (message.startsWith("ralph-qa: ") ? message : `ralph-qa: ${message}`);
|
|
878
|
+
|
|
838
879
|
function failed(ctx, { command, o }, e) {
|
|
839
880
|
if (e instanceof Usage) {
|
|
840
881
|
ctx.stderr.write(`ralph-qa: ${e.message}\n${USAGE}`);
|
|
841
882
|
return 2;
|
|
842
883
|
}
|
|
843
|
-
if (!(e instanceof
|
|
884
|
+
if (!(e instanceof RunStop)) {
|
|
844
885
|
ctx.stderr.write(`ralph-qa: 실행기 내부 오류(${e?.code || e?.name || "error"}): ${String(e?.message || e).split("\n")[0]}\n`);
|
|
845
886
|
return 2;
|
|
846
887
|
}
|
|
847
|
-
const result = { ok: false, stop: e.code, message: e.message, detail: e.detail };
|
|
848
|
-
return report(ctx, command, folderOf(o), result, EXIT_OF[e.code] || 3, stopName(command));
|
|
888
|
+
const result = { ok: false, stop: e.code, message: named(e.message), detail: e.detail };
|
|
889
|
+
return report(ctx, command, folderOf(o, ctx), result, EXIT_OF[e.code] || 3, stopName(command));
|
|
849
890
|
}
|
|
850
891
|
|
|
851
892
|
// ---- the send's own node ----
|
package/skills/remains/SKILL.md
CHANGED
|
@@ -54,6 +54,8 @@ argument-hint: "[기준 커밋] [--no-boxes]"
|
|
|
54
54
|
- `--node-test` 는 `*.test.mjs` 만 돌린다. 다른 종류의 시험(Python, `.test.ts` 등)은 `--cmd` 로 따로 돌리거나 그 OS 에서 미검증으로 적는다.
|
|
55
55
|
- 보내는 것은 추적하는 파일의 커밋된 내용 그대로다(줄 끝 변환 없음). `--ref worktree` 는 커밋하지 않은 추적 파일의 변경까지 보낸다. 추적하지 않는 새 파일은 보내지 않는다(출력의 `untracked`). 그 파일이 시험에 필요하면 미검증으로 적는다.
|
|
56
56
|
- Windows 박스에서는 명령이 `.cmd` 파일 안에서 돈다. 명령이 OS 마다 달라야 하면 `--box <이름>` 으로 나눠 돌린다.
|
|
57
|
+
- 그 `.cmd` 파일은 UTF-8 코드 페이지로 돌아 한글 인자도 그대로 간다. `for` 의 변수는 배치 파일 규칙대로 `%%i` 로 쓰고, `for` 나 `if` 안에서 npm 같은 배치 명령을 부르면 `call npm ...` 으로 쓴다.
|
|
58
|
+
- `for` 반복으로 시험을 여러 번 돌릴 때 종료 코드는 마지막 반복 것이라 앞 반복의 실패가 가려진다(POSIX `run.sh` 도 같다). 각 반복에 `|| exit /b 1`(POSIX `|| exit 1`)을 붙여 한 번이라도 실패하면 비정상 종료로 끝낸다.
|
|
57
59
|
- 오래 걸리면 `--timeout <분>`(기본 20)을 늘린다.
|
|
58
60
|
4. 출력 JSON 의 박스별 `summary`(tests, pass, fail, skipped, failures)와 `log` 파일을 근거로 쓴다. 종료 코드는 다음과 같다.
|
|
59
61
|
- 0: 모든 박스가 통과
|
|
@@ -117,10 +117,14 @@ export const nodeTestCommand = (os, files) =>
|
|
|
117
117
|
|
|
118
118
|
// The script that runs on the box from the top of the sent tree. On Linux and macOS it first leaves its process
|
|
119
119
|
// id beside itself, so a stopped run can end everything it started. On Windows it is a .cmd file: `call` returns
|
|
120
|
-
// from a batch command such as npm, and the command stays out of the ssh and cmd /c quoting.
|
|
120
|
+
// from a batch command such as npm, and the command stays out of the ssh and cmd /c quoting. The file is UTF-8, so
|
|
121
|
+
// it first sets that code page: cmd reads each line in the console's (949 on a Korean Windows), and a Korean
|
|
122
|
+
// argument came to node broken. `call` cannot run for, if or a ( block, so those go as they are.
|
|
123
|
+
const NO_CALL = /^\s*(?:(?:for|if)(?=[\s/]|$)|\()/i;
|
|
121
124
|
export function runScript(b, prefix, command) {
|
|
122
125
|
if (b.os === "win32") {
|
|
123
|
-
const
|
|
126
|
+
const run = NO_CALL.test(command) ? command : `call ${command}`;
|
|
127
|
+
const lines = ["@echo off", "chcp 65001 >nul 2>&1", `cd /d "%~dp0${prefix}" || exit /b 97`, run, "exit /b %ERRORLEVEL%"];
|
|
124
128
|
return { ext: ".cmd", text: lines.map((l) => `${l}\r\n`).join("") };
|
|
125
129
|
}
|
|
126
130
|
const path = b.path?.length ? [`${pathPrefix(b)}export PATH`] : [];
|
|
@@ -166,16 +166,27 @@ export function setKey(text, table, key, rawValue, comment = '') {
|
|
|
166
166
|
return ls.join(nl);
|
|
167
167
|
}
|
|
168
168
|
const range = tableRange(scan(text), table);
|
|
169
|
-
if (!range)
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
169
|
+
if (!range) return withNewTable(ls, table, line).join(nl);
|
|
170
|
+
const [at, after] = placeIn(ls, range);
|
|
171
|
+
ls.splice(at, 0, ...line, ...after);
|
|
172
|
+
return ls.join(nl);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function withNewTable(ls, table, line) {
|
|
176
|
+
while (ls.length && ls[ls.length - 1] === '') ls.pop();
|
|
177
|
+
return [...ls, ...(ls.length ? [''] : []), `[${table}]`, ...line, ''];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 표 안에서 새 키를 넣을 줄 번호와 키 뒤에 더할 줄. 다음 표 머리에 붙은 주석(`# MCP servers` 처럼 머리 바로 위의 주석
|
|
181
|
+
// 줄)과 그 앞 빈 줄은 건너뛴다. 건너뛴 주석이 파일 첫 줄까지 닿으면 파일 머리 주석으로 보고 그 뒤에 넣고, 키와 표 머리
|
|
182
|
+
// 사이에 빈 줄 하나를 둔다(파일 첫 줄부터 표 설명 주석인 파일도 이렇게 되지만 값의 뜻은 같다).
|
|
183
|
+
function placeIn(ls, range) {
|
|
174
184
|
let at = range[1];
|
|
175
|
-
|
|
185
|
+
const beforeHead = range[1] < ls.length;
|
|
186
|
+
if (beforeHead) while (at > range[0] && ls[at - 1].trim().startsWith('#')) at--;
|
|
187
|
+
if (beforeHead && at === 0 && range[1] > 0) return [range[1], ['']];
|
|
176
188
|
while (at > range[0] && ls[at - 1].trim() === '') at--;
|
|
177
|
-
|
|
178
|
-
return ls.join(nl);
|
|
189
|
+
return [at, []];
|
|
179
190
|
}
|
|
180
191
|
|
|
181
192
|
// 키를 뺀다. comment 와 같은 주석 줄이 바로 위에 있으면 그것도 뺀다.
|
|
@@ -6,7 +6,7 @@ description: "범용 MCP 서버(context7, sequential-thinking, filesystem, git,
|
|
|
6
6
|
# setup-mcp: 범용 MCP 서버 설치
|
|
7
7
|
|
|
8
8
|
context7·sequential-thinking·filesystem·git·fetch - 특정 도구에 종속되지 않는 5종 표준 MCP 서버를
|
|
9
|
-
Claude Code·Codex 양쪽에 배선한다. **`setup-omc`(OMC/OMX 프레임워크 자체 설치, 자기 도구를 따로 들고 옴)와도,
|
|
9
|
+
Claude Code·Codex 양쪽에 배선한다. **`setup-omc`(Codex 는 `setup-omx`. OMC/OMX 프레임워크 자체 설치, 자기 도구를 따로 들고 옴)와도,
|
|
10
10
|
`setup-lsp`(`lsp_bridge` 전용)와도 다르다** - 이 스킬은 그 외 범용 standalone 서버만 다룬다. Serena 는
|
|
11
11
|
프로젝트 스코프(`--project` 가 홈 디렉터리면 인덱싱 과부하)라 여기서 제외한다. 설치는 **멱등**(있으면
|
|
12
12
|
remove 후 재-add). 답변은 한글(기술 토큰 영문).
|