@aron98/load-lock 1.6.0 → 1.6.1
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/README.md +2 -1
- package/dist/mcp/server.js +1 -1
- package/dist/test/ace-mcp.test.js +57 -8
- package/dist/test/mcp.test.js +17 -0
- package/package.json +1 -1
- package/skills/load-lock/SKILL.md +2 -2
package/README.md
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
<p align="center">
|
|
9
9
|
<a href="https://github.com/aron98/load-lock/actions/workflows/ci.yml"><img src="https://github.com/aron98/load-lock/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
10
10
|
<a href="https://www.npmjs.com/package/@aron98/load-lock"><img src="https://img.shields.io/npm/v/@aron98/load-lock?color=%233fb950" alt="npm version"></a>
|
|
11
|
-
<a href="https://www.npmjs.com/package/@aron98/load-lock"><img src="https://
|
|
11
|
+
<a href="https://www.npmjs.com/package/@aron98/load-lock"><img src="https://badgen.net/npm/dm/@aron98/load-lock?color=3fb950" alt="npm downloads"></a>
|
|
12
|
+
<a href="https://skills.sh/aron98/load-lock"><img src="https://skills.sh/b/aron98/load-lock" alt="skills.sh"></a>
|
|
12
13
|
<a href="https://github.com/aron98/load-lock/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="license MIT"></a>
|
|
13
14
|
<img src="https://img.shields.io/badge/node-%E2%89%A520-green" alt="node >= 20">
|
|
14
15
|
</p>
|
package/dist/mcp/server.js
CHANGED
|
@@ -47,7 +47,7 @@ export function buildServer(opts) {
|
|
|
47
47
|
const drift = opts.drift !== undefined ? opts.drift : detectCliDrift(packageVersion());
|
|
48
48
|
if (drift)
|
|
49
49
|
process.stderr.write(drift.message + "\n");
|
|
50
|
-
const server = new McpServer({ name: opts.serverName ?? "load-lock", version:
|
|
50
|
+
const server = new McpServer({ name: opts.serverName ?? "load-lock", version: packageVersion() }, { instructions: POLICY_BRIEF + "\n\n" + SETUP_HINT + (drift ? "\n\n" + drift.message : "") });
|
|
51
51
|
const json = (v) => ({
|
|
52
52
|
content: [{ type: "text", text: JSON.stringify(v, null, 2) }],
|
|
53
53
|
});
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
* Rules from the same revision: backend/app/domain/kinds/MCP.md (inline
|
|
10
10
|
* secrets), compliance_mcp_rule_checks_service.py (`_first_non_flag`,
|
|
11
11
|
* companion-file roots), artifact_import_zip_safety_service.py (forbidden
|
|
12
|
-
* names in a non-plugin bundle).
|
|
12
|
+
* names in a non-plugin bundle), and artifact_import_entry_points_service.py
|
|
13
|
+
* (`_split_mcp_bundle_members`, `_require_companion_files_match` — the MCP
|
|
14
|
+
* bundle layout rules mirrored below; issue #98).
|
|
13
15
|
*/
|
|
14
16
|
import test from "node:test";
|
|
15
17
|
import assert from "node:assert/strict";
|
|
@@ -141,14 +143,61 @@ test("ace mcp.json: local npx server running `load-lock mcp` at the package.json
|
|
|
141
143
|
assert.match(String(v), /\$\{[^}]+\}/, `env ${k} must be a \${VAR} placeholder`);
|
|
142
144
|
}
|
|
143
145
|
});
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Every file in the ace/mcp bundle, as `member` paths relative to the bundle
|
|
148
|
+
* root (POSIX separators), mirroring what ACE's `parse_mcp_zip` sees.
|
|
149
|
+
*/
|
|
150
|
+
function listBundleMembers(dir, rel = "") {
|
|
151
|
+
const members = [];
|
|
152
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
153
|
+
const relPath = rel ? `${rel}/${e.name}` : e.name;
|
|
154
|
+
if (e.isDirectory())
|
|
155
|
+
members.push(...listBundleMembers(join(dir, e.name), relPath));
|
|
156
|
+
else
|
|
157
|
+
members.push(relPath);
|
|
158
|
+
}
|
|
159
|
+
return members.sort();
|
|
160
|
+
}
|
|
161
|
+
test("ace/mcp bundle: _split_mcp_bundle_members — only mcp.json at the root", () => {
|
|
162
|
+
// ACE's artifact_import_entry_points_service.py `_split_mcp_bundle_members`:
|
|
163
|
+
// the bundle root may contain ONLY `mcp.json`; every other member must live
|
|
164
|
+
// under one of bin/ | scripts/ | config/ | references/. A root-level file
|
|
165
|
+
// like the old README.md gets the whole bundle rejected (issue #98).
|
|
166
|
+
const members = listBundleMembers(ACE_MCP_DIR);
|
|
167
|
+
assert.ok(members.includes("mcp.json"), "mcp.json must exist at the bundle root");
|
|
168
|
+
const rootFiles = members.filter((m) => !m.includes("/"));
|
|
169
|
+
assert.deepEqual(rootFiles, ["mcp.json"], "only mcp.json at the bundle root");
|
|
170
|
+
const allowedDirs = ["bin", "scripts", "config", "references"];
|
|
171
|
+
for (const entry of readdirSync(ACE_MCP_DIR)) {
|
|
172
|
+
if (entry === "mcp.json")
|
|
173
|
+
continue;
|
|
174
|
+
assert.ok(allowedDirs.includes(entry), `root member ${entry} must be a directory under ${allowedDirs.join("|")}/`);
|
|
175
|
+
}
|
|
176
|
+
for (const m of members) {
|
|
177
|
+
if (m === "mcp.json")
|
|
178
|
+
continue;
|
|
179
|
+
assert.ok(allowedDirs.includes(m.split("/")[0]), `member ${m} must be under ${allowedDirs.join("|")}/`);
|
|
180
|
+
}
|
|
181
|
+
// Names forbidden in a non-plugin bundle (artifact_import_zip_safety_service.py).
|
|
182
|
+
for (const bad of [".claude-plugin", ".mcp.json", ".lsp.json", ".opencode"]) {
|
|
183
|
+
assert.ok(!members.includes(bad), bad);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
test("ace/mcp bundle: _require_companion_files_match — bin|scripts|config == companion_files", () => {
|
|
187
|
+
// ACE's artifact_import_entry_points_service.py
|
|
188
|
+
// `_require_companion_files_match`: every file under bin/, scripts/ or
|
|
189
|
+
// config/ must be listed in the manifest's `companion_files`, and every
|
|
190
|
+
// `companion_files` entry must exist — exactly. Files under references/
|
|
191
|
+
// are exempt (issue #98 moved README.md there).
|
|
192
|
+
const members = listBundleMembers(ACE_MCP_DIR);
|
|
193
|
+
const companionRoots = ["bin", "scripts", "config"];
|
|
194
|
+
const declared = new Set((manifest.companion_files ?? []));
|
|
195
|
+
const present = members.filter((m) => companionRoots.includes(m.split("/")[0]));
|
|
196
|
+
assert.deepEqual([...declared].sort(), present, "companion_files and the files under bin|scripts|config must match exactly");
|
|
197
|
+
// references/ members must NOT need a companion_files entry.
|
|
198
|
+
for (const m of members.filter((m2) => m2.startsWith("references/"))) {
|
|
199
|
+
assert.ok(!declared.has(m), `references/ member ${m} is exempt from companion_files`);
|
|
150
200
|
}
|
|
151
|
-
assert.equal(manifest.companion_files, undefined);
|
|
152
201
|
});
|
|
153
202
|
test("ace mcp.json: pin is declared in scripts/check-pins.mjs", async () => {
|
|
154
203
|
const pins = (await import(pathToFileURL(join(REPO_ROOT, "scripts", "check-pins.mjs")).href));
|
package/dist/test/mcp.test.js
CHANGED
|
@@ -50,6 +50,23 @@ test("mcp: `load-lock mcp` serves the same MCP server as load-lock-mcp (#65)", a
|
|
|
50
50
|
await d.stop();
|
|
51
51
|
}
|
|
52
52
|
});
|
|
53
|
+
test("mcp: initialize reports this package's version from package.json (#97)", async () => {
|
|
54
|
+
const d = await startDaemon();
|
|
55
|
+
const viaBin = await connectMcp(d); // load-lock-mcp entry point
|
|
56
|
+
const viaCli = await connectMcp(d, {}, [cliMainPath, "mcp"]); // `load-lock mcp`
|
|
57
|
+
try {
|
|
58
|
+
const pkgVersion = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json"), "utf8")).version;
|
|
59
|
+
assert.ok(pkgVersion, "package.json has a version");
|
|
60
|
+
assert.equal(viaBin.getServerVersion()?.version, pkgVersion, "load-lock-mcp serverInfo.version matches package.json");
|
|
61
|
+
assert.equal(viaCli.getServerVersion()?.version, pkgVersion, "`load-lock mcp` serverInfo.version matches package.json");
|
|
62
|
+
assert.notEqual(pkgVersion, "0.1.0", "guards against the old hard-coded constant");
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
await viaCli.close();
|
|
66
|
+
await viaBin.close();
|
|
67
|
+
await d.stop();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
53
70
|
test("mcp: tool listing + acquire/release/configure round trip", async () => {
|
|
54
71
|
const d = await startDaemon();
|
|
55
72
|
const c = await connectMcp(d);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aron98/load-lock",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "Machine-wide counting semaphore coordinating resource-intensive work across parallel coding agents (OpenCode, Codex CLI, Claude Code, ...)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -7,7 +7,7 @@ description: >-
|
|
|
7
7
|
queued FIFO, sleep honestly with bounded waits instead of polling, and
|
|
8
8
|
release the permit when done. Use when several agents on one machine risk
|
|
9
9
|
overloading it by starting heavy work in the same minute. If load-lock is
|
|
10
|
-
not installed, this skill runs it via `npx -y @aron98/load-lock@1.6.
|
|
10
|
+
not installed, this skill runs it via `npx -y @aron98/load-lock@1.6.1`
|
|
11
11
|
(downloads the pinned npm package), and load-lock keeps its shared state in
|
|
12
12
|
~/.local/state/load-lock (a small per-user daemon started on first use).
|
|
13
13
|
---
|
|
@@ -26,7 +26,7 @@ Use the first of these that works; they all talk to the same daemon:
|
|
|
26
26
|
1. **load-lock MCP tools** (`acquire`, `wait_for_turn`, `run_with_permit`,
|
|
27
27
|
...), if your harness lists them. The rest of this skill names them.
|
|
28
28
|
2. **`load-lock` on PATH**: check with `load-lock version`.
|
|
29
|
-
3. **Pinned npx**: `npx -y @aron98/load-lock@1.6.
|
|
29
|
+
3. **Pinned npx**: `npx -y @aron98/load-lock@1.6.1 <command>` (Node >= 20).
|
|
30
30
|
It downloads that exact npm package once into the npx cache. Below,
|
|
31
31
|
read `load-lock` as this whole prefix. `npm install -g @aron98/load-lock`
|
|
32
32
|
makes it permanent.
|