wicked-garden 12.60.0 → 12.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +9 -1
- package/package.json +1 -1
- package/scripts/editor/install.py +11 -3
- package/scripts/mcp/install.py +110 -24
- package/skills/mcp-scaffold/SKILL.md +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name": "wicked-garden", "owner": {"name": "Mike Parcewski"}, "plugins": [{"name": "wicked-garden", "source": "./", "description": "Your coding agent already plans and swarms \u2014 Wicked Garden is the curated toolkit for what it can't: proof-not-claims gates, code links grep can't see, deterministic refactor, cross-session memory, multi-model second opinions (fallback seats labelled). Don't fight the harness; fill its gaps. The gate needs wicked-vault (a self-contained infra peer, installed directly) and re-derives through garden's built-in engine; estate/bus are opt-in layers (wicked-testing and wicked-brain are retired \u2014 QE ships in-catalog as the qe domain, memory lives in wicked-estate).", "version": "12.
|
|
1
|
+
{"name": "wicked-garden", "owner": {"name": "Mike Parcewski"}, "plugins": [{"name": "wicked-garden", "source": "./", "description": "Your coding agent already plans and swarms \u2014 Wicked Garden is the curated toolkit for what it can't: proof-not-claims gates, code links grep can't see, deterministic refactor, cross-session memory, multi-model second opinions (fallback seats labelled). Don't fight the harness; fill its gaps. The gate needs wicked-vault (a self-contained infra peer, installed directly) and re-derives through garden's built-in engine; estate/bus are opt-in layers (wicked-testing and wicked-brain are retired \u2014 QE ships in-catalog as the qe domain, memory lives in wicked-estate).", "version": "12.61.0"}], "version": "12.61.0"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wicked-garden",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.61.0",
|
|
4
4
|
"description": "Your coding agent's harness already plans and swarms; Wicked Garden is the curated toolkit for what it can't do alone. Proof, not claims \u2014 gates re-derive 'done' through the internal loom engine (fail-closed, independently attested via wicked-vault), so a self-asserted pass can't lie its way green. Relationships grep can't see \u2014 the wicked-estate code graph + injected bus/dispatch/capability/archetype edges power blast-radius & lineage. Plus deterministic multi-file refactor (wicked-patch), cross-session memory + knowledge (the mem domain over wicked-estate), multi-model second opinions (fallback seats labelled) (jam:council), portable expertise as on-demand skill-refs, and evidence-gated testing (the in-catalog qe domain). It reads the shape of your work to apply the right rigor \u2014 steering, not a pipeline \u2014 then gets out of the way. The compiler stamps a self-contained evidence gate into any repo that runs with no wicked-garden installed. Don't fight the harness; fill its gaps. The evidence gate requires wicked-vault, a self-contained infra peer wicked-garden installs directly; wicked-bus is an opt-in layer \u2014 install what you'll use, the toolkit works without the rest.",
|
|
5
5
|
"wicked_vault_version": ">=0.8.0",
|
|
6
6
|
"wicked_ledger_version": "^0.6.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wicked-garden",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.61.0",
|
|
4
4
|
"description": "The skills your coding agent can't build alone: \"done\" re-checked from evidence, graph-aware search and multi-file refactor, 40 QE specialists, review panels from other models, and cross-session memory. Works in Claude Code, Codex, Antigravity, OpenCode and Pi, in CI, or in a wicked-crew job.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"coding-agent",
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [12.61.0] — 2026-10-26
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **`install.py --from-run` finds the pack in a real run (editor and MCP) (#1351).** The engine runs the install-plan and install Tool phases in the run's worktree and sets `WICKED_TREE` to that worktree's git tree id (wicked-core#776), not a path. Both install scripts read it as a directory, found nothing, and every editor-plugin run failed at install-plan with "found 0 (none)", so the install consent gate was never reached. `--from-run` now searches the worktree (the cwd) and uses `WICKED_TREE` only when it names a directory.
|
|
9
|
+
- **A credential-free MCP server installs (`scripts/mcp/install.py`) (#1351).** The installer assumed every server reads a `<SERVER>_TOKEN` secret. A local-computation server has no upstream, and its config names no `baseUrl` and no auth scheme (`auth` absent, null or `{"scheme": "none"}`). The installer still sent `auth: env:<SERVER>_TOKEN`, crew answered `secret_missing`, and the server was never registered, so a design that correctly invented no credential could not be installed. Such a server now smokes and registers with no secret variable. A server with an upstream, and `--from-npm` installs, are unchanged.
|
|
10
|
+
- **An MCP server delivered without a `package-lock.json` installs (`scripts/mcp/install.py`) (#1352).** A container seat can't reach the npm registry, so the server a `/workflow-mcp-server` run delivers has no lockfile. `npm ci` refuses such a tree, so the install failed after the operator had consented, with only npm's debug-log path as the reason. The installer now copies the server to a transient `<root>/<key>/resolve/`, resolves the dependencies there once with `npm install --package-lock=true --ignore-scripts`, and builds there. The staged copy installs from that lock, and the delivered tree is never written. An npm that writes no lock (`package-lock=false`) fails before anything is built. The plan and the install record say `"lockfile": "resolved at install"`, because those versions were picked at install time and the run never reviewed them. A delivered lock still goes through `npm ci`, and the output says `"lockfile": "delivered"`. A failing npm step now reports npm's own reason instead of its debug-log path. That is the `npm error code` line and the first line after it that isn't metadata, plus the script's own last line (tsc's error) when `npm run build` fails.
|
|
11
|
+
|
|
5
12
|
## [12.60.0] — 2026-10-26
|
|
6
13
|
|
|
7
14
|
### Fixed
|
|
@@ -1212,7 +1219,8 @@ through `v9.2.18`. Major themes across that span:
|
|
|
1212
1219
|
|
|
1213
1220
|
For each of those releases, see the corresponding tag in git history.
|
|
1214
1221
|
|
|
1215
|
-
[Unreleased]: https://github.com/mikeparcewski/wicked-garden/compare/v12.
|
|
1222
|
+
[Unreleased]: https://github.com/mikeparcewski/wicked-garden/compare/v12.61.0...HEAD
|
|
1223
|
+
[12.61.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.60.0...v12.61.0
|
|
1216
1224
|
[12.60.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.59.0...v12.60.0
|
|
1217
1225
|
[12.59.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.58.0...v12.59.0
|
|
1218
1226
|
[12.58.0]: https://github.com/mikeparcewski/wicked-garden/compare/v12.57.0...v12.58.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wicked-garden",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.61.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The skills your coding agent can't build alone: \"done\" re-checked from evidence, graph-aware search and multi-file refactor, 40 QE specialists, review panels from other models, and cross-session memory. Works in Claude Code, Codex, Antigravity, OpenCode and Pi, in CI, or in a wicked-crew job.",
|
|
6
6
|
"keywords": [
|
|
@@ -9,7 +9,7 @@ An editor plugin is a ``wicked-pack.json`` spec-2 pack with ``editors[]`` (scaff
|
|
|
9
9
|
``scripts/editor/scaffold.py``). Install, idempotent by the pack's ``name``:
|
|
10
10
|
|
|
11
11
|
1. **Locate**: ``--from-run`` finds the one spec-2 ``wicked-pack.json`` with editors under
|
|
12
|
-
``$WICKED_TREE``
|
|
12
|
+
the run's worktree (the cwd; ``$WICKED_TREE`` when it names a directory), outside node_modules/.git/dist; ``--dir`` names it.
|
|
13
13
|
2. **Stage** into ``<root>/<name>/next/`` (root: ``--install-root``, ``$WICKED_EDITOR_INSTALL_ROOT``,
|
|
14
14
|
else ``~/.wicked/editors``, a program-owned place): ``wicked-pack.json``, every editor's entry
|
|
15
15
|
file at its manifest path, and the README. Nothing else of the tree is copied (an editor is ONE
|
|
@@ -127,11 +127,19 @@ def _is_editor_pack(manifest_path: Path) -> bool:
|
|
|
127
127
|
return False
|
|
128
128
|
|
|
129
129
|
|
|
130
|
+
def run_tree() -> Path:
|
|
131
|
+
"""The run's worktree for ``--from-run``. The engine runs an install Tool phase IN the worktree and hands
|
|
132
|
+
``$WICKED_TREE`` as that worktree's git tree id (wicked-core#776), never a path; reading the id as a path found
|
|
133
|
+
no pack in any real run. A ``$WICKED_TREE`` that names a directory is still honoured, else the cwd."""
|
|
134
|
+
named = os.environ.get("WICKED_TREE")
|
|
135
|
+
return Path(named) if named and Path(named).is_dir() else Path.cwd()
|
|
136
|
+
|
|
137
|
+
|
|
130
138
|
def locate(args) -> tuple[Path, dict]:
|
|
131
139
|
if args.dir is not None:
|
|
132
140
|
pack = args.dir
|
|
133
141
|
elif args.from_run:
|
|
134
|
-
tree =
|
|
142
|
+
tree = run_tree()
|
|
135
143
|
found = []
|
|
136
144
|
for dirpath, dirnames, filenames in os.walk(tree):
|
|
137
145
|
dirnames[:] = sorted(d for d in dirnames if d not in SKIP_DIRS)
|
|
@@ -372,7 +380,7 @@ def consent_refusal(args, env) -> str | None:
|
|
|
372
380
|
def main(argv: list[str] | None = None) -> int:
|
|
373
381
|
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
374
382
|
where = parser.add_mutually_exclusive_group()
|
|
375
|
-
where.add_argument("--from-run", action="store_true", help="find the one editor pack
|
|
383
|
+
where.add_argument("--from-run", action="store_true", help="find the one editor pack in the run's worktree (the cwd, or $WICKED_TREE when it names a directory)")
|
|
376
384
|
where.add_argument("--dir", type=Path, help="the pack directory")
|
|
377
385
|
where.add_argument("--uninstall", metavar="NAME", help="unregister and remove an installed editor pack")
|
|
378
386
|
parser.add_argument("--target", type=parse_target, default=("daemon", None),
|
package/scripts/mcp/install.py
CHANGED
|
@@ -21,12 +21,18 @@ bound URL for every governed unit). There is no port default: a guessed ``:7701`
|
|
|
21
21
|
daemon's registry. Neither set exits 2 naming the remedy.
|
|
22
22
|
|
|
23
23
|
1. **Locate** the server: ``--from-run`` finds the one ``mcp-server.config.json`` under
|
|
24
|
-
``$WICKED_TREE``
|
|
24
|
+
the run's worktree (the cwd; ``$WICKED_TREE`` when it names a directory), outside node_modules/dist; ``--dir`` names it. The key and
|
|
25
25
|
version come from that config.
|
|
26
26
|
2. **Stage** into ``<root>/<key>/next/`` (root: ``--install-root``, ``$WICKED_MCP_INSTALL_ROOT``,
|
|
27
27
|
else ``~/.wicked/mcp-servers``): ``npm ci --ignore-scripts && npm run build`` in the server
|
|
28
28
|
dir, copy dist/ + package.json + package-lock.json + tools.json + mcp-server.config.json,
|
|
29
29
|
``npm ci --omit=dev --ignore-scripts`` there. ``--from-npm`` installs the package instead.
|
|
30
|
+
A server delivered WITHOUT a package-lock.json (a seat with no npm registry cannot write one) is
|
|
31
|
+
copied to a transient ``<root>/<key>/resolve/`` (removed afterwards, like ``next/``), resolved
|
|
32
|
+
there with ``npm install --package-lock=true --ignore-scripts`` and built there; the staged copy
|
|
33
|
+
installs from that lock. The delivered tree is not written. The plan and the record say so
|
|
34
|
+
(``"lockfile": "resolved at install"``): the versions were chosen at install time, not reviewed
|
|
35
|
+
in the run.
|
|
30
36
|
3. **Smoke** with ``probe.py``. A failure naming ``<SERVER>_TOKEN`` is a pending credential
|
|
31
37
|
(disclosed, not failed); any other failure exits 1 with ``next/`` removed and nothing else
|
|
32
38
|
touched.
|
|
@@ -94,6 +100,17 @@ def env_name_for(key: str) -> str:
|
|
|
94
100
|
return f"{env_prefix(key)}_TOKEN"
|
|
95
101
|
|
|
96
102
|
|
|
103
|
+
def credential_free(config: dict) -> bool:
|
|
104
|
+
"""A server with no upstream has nothing to authenticate to: its committed config names no ``baseUrl`` and no
|
|
105
|
+
auth scheme (``auth`` absent, null, or ``{"scheme": "none"}``), a local-computation server. It smokes and
|
|
106
|
+
registers with no secret variable. Any other server, and an --from-npm install (no config to read), reads the
|
|
107
|
+
one ``<SERVER>_TOKEN`` variable."""
|
|
108
|
+
if not config or "baseUrl" in config:
|
|
109
|
+
return False
|
|
110
|
+
auth = config.get("auth")
|
|
111
|
+
return auth is None or (isinstance(auth, dict) and auth.get("scheme") == "none")
|
|
112
|
+
|
|
113
|
+
|
|
97
114
|
def crew_origin(flag: str | None) -> str:
|
|
98
115
|
"""The daemon THIS run belongs to — never a guessed port (garden#1258)."""
|
|
99
116
|
origin = flag or os.environ.get("WICKED_CREW_URL")
|
|
@@ -152,14 +169,40 @@ def _now() -> str:
|
|
|
152
169
|
return datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds")
|
|
153
170
|
|
|
154
171
|
|
|
172
|
+
NPM_META = ("npm error path ", "npm error syscall ", "npm error errno ", "npm error command ", "npm error workspace ",
|
|
173
|
+
"npm error location ", "npm error signal ", "npm error cwd ")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _lines(output: str) -> list[str]:
|
|
177
|
+
return [ln.strip() for ln in (output or "").splitlines()
|
|
178
|
+
if ln.strip() not in ("", "npm error") and "A complete log of this run" not in ln]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _why(stderr: str, stdout: str = "") -> str:
|
|
182
|
+
"""What went wrong, in one line. npm ends with a pointer at its debug log; its reason is the ``npm error code``
|
|
183
|
+
line and the first line after it that is not metadata (path, syscall, errno, command), e.g. "npm error code
|
|
184
|
+
EUSAGE npm error The `npm ci` command can only install with an existing package-lock.json...". A script that
|
|
185
|
+
failed (``npm error code 1``: npm run build) printed its own reason, usually on stdout (tsc): its last line
|
|
186
|
+
rides along. With nothing useful on stderr, the last line of stdout."""
|
|
187
|
+
err, out = _lines(stderr), _lines(stdout)
|
|
188
|
+
codes = [i for i, ln in enumerate(err) if ln.startswith("npm error code")]
|
|
189
|
+
if codes:
|
|
190
|
+
head = err[codes[0]]
|
|
191
|
+
detail = next((ln for ln in err[codes[0] + 1:] if not ln.startswith(NPM_META)), None)
|
|
192
|
+
parts = [head, *([detail] if detail else [])]
|
|
193
|
+
if head == "npm error code 1" and out:
|
|
194
|
+
parts.append(out[-1])
|
|
195
|
+
return " ".join(parts)
|
|
196
|
+
return (err or out or [""])[-1]
|
|
197
|
+
|
|
198
|
+
|
|
155
199
|
def _run(argv: list[str], cwd: Path, what: str, timeout: float = NPM_TIMEOUT_S) -> None:
|
|
156
200
|
try:
|
|
157
201
|
done = subprocess.run(argv, cwd=cwd, capture_output=True, text=True, timeout=timeout)
|
|
158
202
|
except (OSError, subprocess.TimeoutExpired) as err:
|
|
159
203
|
raise Failure(1, f"{what} failed: {err}") from err
|
|
160
204
|
if done.returncode != 0:
|
|
161
|
-
|
|
162
|
-
raise Failure(1, f"{what} exited {done.returncode}: {tail[0][:300]}")
|
|
205
|
+
raise Failure(1, f"{what} exited {done.returncode}: {_why(done.stderr, done.stdout)[:300]}")
|
|
163
206
|
|
|
164
207
|
|
|
165
208
|
def _npm() -> str:
|
|
@@ -224,6 +267,14 @@ def find_configs(tree: Path) -> list[Path]:
|
|
|
224
267
|
return found
|
|
225
268
|
|
|
226
269
|
|
|
270
|
+
def run_tree() -> Path:
|
|
271
|
+
"""The run's worktree for ``--from-run``. The engine runs an install Tool phase IN the worktree and hands
|
|
272
|
+
``$WICKED_TREE`` as that worktree's git tree id (wicked-core#776), never a path; reading the id as a path found
|
|
273
|
+
no server config in any real run. A ``$WICKED_TREE`` that names a directory is still honoured, else the cwd."""
|
|
274
|
+
named = os.environ.get("WICKED_TREE")
|
|
275
|
+
return Path(named) if named and Path(named).is_dir() else Path.cwd()
|
|
276
|
+
|
|
277
|
+
|
|
227
278
|
def locate(args) -> tuple[Path | None, dict]:
|
|
228
279
|
if args.dir is not None:
|
|
229
280
|
config_path = args.dir / CONFIG_NAME
|
|
@@ -232,7 +283,7 @@ def locate(args) -> tuple[Path | None, dict]:
|
|
|
232
283
|
return None, {}
|
|
233
284
|
raise Failure(2, f"{args.dir} has no {CONFIG_NAME}; scaffold the server first")
|
|
234
285
|
elif args.from_run:
|
|
235
|
-
tree =
|
|
286
|
+
tree = run_tree()
|
|
236
287
|
found = find_configs(tree)
|
|
237
288
|
if len(found) != 1:
|
|
238
289
|
names = ", ".join(str(p) for p in found) or "none"
|
|
@@ -252,8 +303,9 @@ def locate(args) -> tuple[Path | None, dict]:
|
|
|
252
303
|
|
|
253
304
|
# ── 3. smoke ───────────────────────────────────────────────────────────────────────────────
|
|
254
305
|
|
|
255
|
-
def smoke(server_js: Path, var: str) -> tuple[str, dict]:
|
|
256
|
-
|
|
306
|
+
def smoke(server_js: Path, var: str | None) -> tuple[str, dict]:
|
|
307
|
+
env_args = ["--env", var] if var else []
|
|
308
|
+
argv = [sys.executable, str(PROBE), "--timeout", str(PROBE_TIMEOUT_S), *env_args, "--",
|
|
257
309
|
"node", str(server_js)]
|
|
258
310
|
try:
|
|
259
311
|
done = subprocess.run(argv, capture_output=True, text=True, timeout=PROBE_TIMEOUT_S + 30)
|
|
@@ -262,7 +314,7 @@ def smoke(server_js: Path, var: str) -> tuple[str, dict]:
|
|
|
262
314
|
return "failed", {"ok": False, "error": f"probe did not run: {err}"}
|
|
263
315
|
if report.get("ok") is True:
|
|
264
316
|
return "ok", report
|
|
265
|
-
if var in (report.get("stderrNames") or []):
|
|
317
|
+
if var and var in (report.get("stderrNames") or []):
|
|
266
318
|
return "pending_credential", report
|
|
267
319
|
return "failed", report
|
|
268
320
|
|
|
@@ -311,9 +363,10 @@ def check_key(origin: str, key: str, server_js: Path) -> dict | None:
|
|
|
311
363
|
|
|
312
364
|
# ── 5. register ────────────────────────────────────────────────────────────────────────────
|
|
313
365
|
|
|
314
|
-
def register(origin: str, key: str, server_js: Path, var: str) -> dict:
|
|
315
|
-
body = {"name": key, "kind": "mcp-stdio", "command": "node", "args": [str(server_js)]
|
|
316
|
-
|
|
366
|
+
def register(origin: str, key: str, server_js: Path, var: str | None) -> dict:
|
|
367
|
+
body: dict = {"name": key, "kind": "mcp-stdio", "command": "node", "args": [str(server_js)]}
|
|
368
|
+
if var:
|
|
369
|
+
body["auth"] = {"ref": f"env:{var}", "env": var}
|
|
317
370
|
try:
|
|
318
371
|
status, preview = http_json("POST", f"{origin}/api/v1/mcp/servers/preview", body)
|
|
319
372
|
except (urllib.error.URLError, OSError):
|
|
@@ -396,6 +449,19 @@ def _source(server_dir: Path | None, npm_spec: str | None) -> dict:
|
|
|
396
449
|
"dir": str(server_dir)}
|
|
397
450
|
|
|
398
451
|
|
|
452
|
+
def lockfile_state(server_dir: Path | None, npm_spec: str | None) -> str | None:
|
|
453
|
+
"""Whether the server's dependency versions were delivered with it (``"delivered"``) or are chosen when it
|
|
454
|
+
installs (``"resolved at install"``: no package-lock.json in the server dir). ``None`` for --from-npm."""
|
|
455
|
+
if npm_spec or server_dir is None:
|
|
456
|
+
return None
|
|
457
|
+
return "delivered" if (server_dir / "package-lock.json").is_file() else "resolved at install"
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def resolve_dir(next_dir: Path) -> Path:
|
|
461
|
+
"""Where a server delivered without a lock is copied, resolved and built: beside next/, and as transient."""
|
|
462
|
+
return next_dir.with_name("resolve")
|
|
463
|
+
|
|
464
|
+
|
|
399
465
|
def stage(server_dir: Path | None, npm_spec: str | None, next_dir: Path) -> Path:
|
|
400
466
|
"""Build + copy into next/; the server.js path inside next/."""
|
|
401
467
|
npm = _npm()
|
|
@@ -407,15 +473,29 @@ def stage(server_dir: Path | None, npm_spec: str | None, next_dir: Path) -> Path
|
|
|
407
473
|
server_js = next_dir / "node_modules" / Path(*pkg.split("/")) / "dist" / "server.js"
|
|
408
474
|
else:
|
|
409
475
|
assert server_dir is not None
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
476
|
+
if lockfile_state(server_dir, None) == "delivered":
|
|
477
|
+
build_dir = server_dir
|
|
478
|
+
_run([npm, "ci", "--ignore-scripts"], build_dir, "npm ci (server dir)")
|
|
479
|
+
else:
|
|
480
|
+
# `npm ci` refuses a tree with no lock, and every server a container seat builds has none (the seat
|
|
481
|
+
# cannot reach the registry; j6-cert, unit-convert, 2026-10-26). The lock is resolved in a throwaway
|
|
482
|
+
# copy beside next/, never in the delivered tree: a lock left there would read as "delivered" on the
|
|
483
|
+
# next plan, and it is a write the consent plan does not list.
|
|
484
|
+
build_dir = resolve_dir(next_dir)
|
|
485
|
+
shutil.copytree(server_dir, build_dir, ignore=shutil.ignore_patterns("node_modules", "dist", ".git"))
|
|
486
|
+
_run([npm, "install", "--package-lock=true", "--ignore-scripts", "--no-audit", "--no-fund"], build_dir,
|
|
487
|
+
"npm install (no package-lock.json delivered)")
|
|
488
|
+
if not (build_dir / "package-lock.json").is_file():
|
|
489
|
+
raise Failure(1, "npm install wrote no package-lock.json, so the staged copy cannot be installed "
|
|
490
|
+
"from a lock; nothing was installed")
|
|
491
|
+
_run([npm, "run", "build"], build_dir, "npm run build")
|
|
492
|
+
if not (build_dir / "dist").is_dir():
|
|
413
493
|
raise Failure(1, f"npm run build left no dist/ in {server_dir}")
|
|
414
494
|
next_dir.mkdir(parents=True)
|
|
415
|
-
shutil.copytree(
|
|
495
|
+
shutil.copytree(build_dir / "dist", next_dir / "dist")
|
|
416
496
|
for name in STAGE_FILES:
|
|
417
|
-
if (
|
|
418
|
-
shutil.copy2(
|
|
497
|
+
if (build_dir / name).is_file():
|
|
498
|
+
shutil.copy2(build_dir / name, next_dir / name)
|
|
419
499
|
if not (next_dir / "package-lock.json").is_file():
|
|
420
500
|
raise Failure(1, f"{server_dir} has no package-lock.json (npm ci needs it)")
|
|
421
501
|
_run([npm, "ci", "--omit=dev", "--ignore-scripts"], next_dir, "npm ci --omit=dev (staged copy)")
|
|
@@ -451,18 +531,20 @@ def do_install(args) -> tuple[int, dict]:
|
|
|
451
531
|
version = config.get("version")
|
|
452
532
|
if args.from_npm:
|
|
453
533
|
version = NPM_SPEC_RE.match(args.from_npm).group("version")
|
|
454
|
-
var = env_name_for(key)
|
|
534
|
+
var = None if credential_free(config) else env_name_for(key)
|
|
455
535
|
root = install_root(args.install_root)
|
|
456
536
|
key_root = root / key
|
|
457
537
|
next_dir = key_root / "next"
|
|
458
538
|
origin = crew_origin(args.crew_url)
|
|
459
539
|
check_key(origin, key, key_root / "current" / installed_rel(args.from_npm))
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
540
|
+
for stale in (next_dir, resolve_dir(next_dir)): # a half-staged copy from an interrupted run
|
|
541
|
+
if stale.is_symlink():
|
|
542
|
+
stale.unlink()
|
|
543
|
+
elif stale.exists():
|
|
544
|
+
shutil.rmtree(stale)
|
|
464
545
|
key_root.mkdir(parents=True, exist_ok=True)
|
|
465
546
|
|
|
547
|
+
lock = lockfile_state(server_dir, args.from_npm) # before staging: an `npm install` there writes one
|
|
466
548
|
try:
|
|
467
549
|
staged_js = stage(server_dir if not args.from_npm else None, args.from_npm, next_dir)
|
|
468
550
|
smoke_state, probe_report = smoke(staged_js, var)
|
|
@@ -471,11 +553,14 @@ def do_install(args) -> tuple[int, dict]:
|
|
|
471
553
|
except Failure:
|
|
472
554
|
shutil.rmtree(next_dir, ignore_errors=True)
|
|
473
555
|
raise
|
|
556
|
+
finally:
|
|
557
|
+
shutil.rmtree(resolve_dir(next_dir), ignore_errors=True)
|
|
474
558
|
swap(key_root)
|
|
475
559
|
current_js = key_root / "current" / staged_js.relative_to(next_dir)
|
|
476
560
|
|
|
477
561
|
record = {"key": key, "version": version, "source": _source(server_dir, args.from_npm),
|
|
478
|
-
"
|
|
562
|
+
**({"lockfile": lock} if lock else {}),
|
|
563
|
+
"command": "node", "args": [str(current_js)], "envNames": [var] if var else [],
|
|
479
564
|
"installedAt": _now()}
|
|
480
565
|
code = 0
|
|
481
566
|
try:
|
|
@@ -552,7 +637,8 @@ def do_plan(args) -> dict:
|
|
|
552
637
|
_cli_rows(installer([*upsert, *worker_home_args()], download=False), root, worker_writes, skipped)
|
|
553
638
|
operator_writes = list(worker_writes)
|
|
554
639
|
_cli_rows(installer(upsert, download=False), root, operator_writes, skipped)
|
|
555
|
-
|
|
640
|
+
lock = lockfile_state(server_dir, args.from_npm)
|
|
641
|
+
return {"dry_run": True, "key": key, **({"lockfile": lock} if lock else {}), "choices": [
|
|
556
642
|
{"id": "worker", "label": CHOICE_LABELS["worker"], "default": True, "writes": worker_writes},
|
|
557
643
|
{"id": "operator", "label": CHOICE_LABELS["operator"], "writes": operator_writes}],
|
|
558
644
|
"skipped": skipped}
|
|
@@ -596,7 +682,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
596
682
|
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
597
683
|
where = parser.add_mutually_exclusive_group()
|
|
598
684
|
where.add_argument("--from-run", action="store_true",
|
|
599
|
-
help=f"find the one {CONFIG_NAME}
|
|
685
|
+
help=f"find the one {CONFIG_NAME} in the run's worktree (the cwd, or $WICKED_TREE when it names a directory)")
|
|
600
686
|
where.add_argument("--dir", type=Path, help="the server directory")
|
|
601
687
|
where.add_argument("--uninstall", metavar="KEY", help="unregister and remove an installed server")
|
|
602
688
|
parser.add_argument("--key", help="the server key (must equal the config's key)")
|
|
@@ -203,8 +203,8 @@ wicked-garden run scripts/mcp/install.py --uninstall <key> [--target worker|oper
|
|
|
203
203
|
Installs the built TypeScript server — or updates the installed copy — idempotent by the
|
|
204
204
|
config's `key`:
|
|
205
205
|
|
|
206
|
-
1. **Locate**: `--from-run` finds the one `mcp-server.config.json`
|
|
207
|
-
|
|
206
|
+
1. **Locate**: `--from-run` finds the one `mcp-server.config.json` in the run's worktree (the
|
|
207
|
+
cwd; `$WICKED_TREE` is the worktree's tree id, used only when it names a directory), outside `node_modules`/`dist`; more or fewer than one exits 2 naming the
|
|
208
208
|
candidates. `--dir` names it. The version is the config's `version`.
|
|
209
209
|
2. **Stage**: `npm ci --ignore-scripts && npm run build` in the server dir, then `dist/`,
|
|
210
210
|
`package.json`, `package-lock.json`, `tools.json` and `mcp-server.config.json` are copied
|