mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
7bd017a796
## Summary Adds an opt-in **committable / repo-local** install mode so a team can version Argent _with the repo_. `argent init --local` installs `@swmansion/argent` as a project **devDependency** and writes MCP configs that launch the project-local copy — so after `git clone && npm install`, every teammate has the same Argent version and MCP wiring with **no global install and no per-developer `argent init`**. **Global mode stays the default** and is unchanged; everything here is additive and backward-compatible. Motivation: the workflow some users already do by hand (#207). Supersedes the earlier, since-closed #232 — this version is Windows-safe (`node`, not the `.bin` shim), handles Yarn PnP, and closes the cross-install tool-server hazards rather than leaving them. ## What changed **`init`** - `--local` / `--global` flags + an interactive "How should argent be installed?" prompt. Mutually exclusive; non-interactive (`--yes`) defaults to **global**. - Local mode installs the devDependency with the package manager detected from the project's **lockfile** (run with `cwd = projectRoot`). - MCP entry points at the local copy: `node node_modules/@swmansion/argent/dist/cli.js mcp` (the bin path is derived from the installed `package.json` `bin` and existence-checked — forward slashes, committable, Windows-safe). Yarn PnP → `yarn argent mcp`; an `npx --no-install argent mcp` fallback is used only when the bin can't be resolved. - Records the mode in a committed `<root>/.argent/install.json`. - Drops global-only adapters (Windsurf/Hermes) in local mode; prints team-setup / commit guidance. **`update`** — detects the project's mode; in local mode reads the installed version from `node_modules` (not the global binary / npx cache) and bumps the **devDependency + lockfile**, then re-emits the local MCP command. **`uninstall`** — scoped to the project's mode: a local-mode uninstall removes the **local devDependency only and never the shared global install**, and prunes `.argent/install.json`. Global-mode behavior is unchanged. **Cross-install hardening (the gate that makes per-project pinning safe)** - `launcher`: a tool-server is reused only when its `bundlePath` matches the caller's — a repo-local pin never silently runs against (or kills) a different version's server. - `postinstall`: only stops a tool-server spawned from *this package's own* bundle, so a devDependency install can't tear down an unrelated editor session. **Telemetry** — adds an `install_mode` dimension to init/update/uninstall events. ## Testing - New unit tests: install-mode helpers + record (`utils`), mode-aware `getMcpEntry`/`resolveLocalCommandMode` incl. the opencode array form (`mcp-configs`), local-only uninstall guard (`uninstall`), and bundlePath-gated tool-server reuse (`launcher`). - All existing suites green (installer 346, tools-client 115, telemetry 191, registry 83, argent 13, tool-server 1621). Build + lint clean. - **Live end-to-end** in an isolated sandbox: `init --local` → real MCP stdio handshake serves **69 tools from `node_modules`**; `update --local` reads the local version; `uninstall --local` removes the local install and **leaves the global install intact**; `init --global` unchanged; flag conflict rejected. ## Notes / known limitations - Recommended on mainstream platforms (macOS, Linux x64, Windows x64) with prebuilt native binaries; Linux arm64 / Windows arm need a C/C++ toolchain to build `tree-sitter` on `npm install` (documented in the README). - Concurrent sessions of two *different* Argent versions on one machine each spawn their own tool-server; an orphaned one idle-times-out (no session is ever broken). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
84 lines
3.4 KiB
TypeScript
84 lines
3.4 KiB
TypeScript
import * as fs from "node:fs";
|
|
import * as os from "node:os";
|
|
import * as path from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import { findStablePackageDir } from "../src/bundled-paths.js";
|
|
|
|
// findStablePackageDir resolves the version-STABLE node_modules symlink for the
|
|
// running package — the one that survives a pnpm store prune. The bug it fixes:
|
|
// in a pnpm workspace the symlink lives in the DECLARING member's node_modules,
|
|
// a level below the workspace root where the .pnpm store (the realpath target)
|
|
// lives, so a probe that only checks the root misses it and falls back to the
|
|
// version-pinned store path.
|
|
|
|
const PKG = "@swmansion/argent";
|
|
|
|
let tmpDir: string;
|
|
|
|
// Creates a real "store" dir standing in for the resolved package, and a
|
|
// node_modules/<pkg> SYMLINK to it under `linkParent`.
|
|
function linkTo(linkParent: string, realPkgDir: string): string {
|
|
const nm = path.join(linkParent, "node_modules", "@swmansion");
|
|
fs.mkdirSync(nm, { recursive: true });
|
|
const link = path.join(nm, "argent");
|
|
fs.symlinkSync(realPkgDir, link);
|
|
return path.join(linkParent, "node_modules", PKG);
|
|
}
|
|
|
|
beforeEach(() => {
|
|
tmpDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "argent-stable-path-")));
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe("findStablePackageDir", () => {
|
|
it("finds the symlink at cwd for a single-package layout", () => {
|
|
const store = path.join(tmpDir, "store", "argent");
|
|
fs.mkdirSync(store, { recursive: true });
|
|
const proj = path.join(tmpDir, "proj");
|
|
const link = linkTo(proj, store);
|
|
|
|
expect(findStablePackageDir(proj, store)).toBe(link);
|
|
});
|
|
|
|
it("scans up to find the DECLARING member's symlink in a pnpm workspace", () => {
|
|
// The version-pinned store lives at the workspace root; the stable symlink
|
|
// is only in the member's node_modules, a level below where the store sits.
|
|
const store = path.join(tmpDir, "root", "node_modules", ".pnpm", "argent");
|
|
fs.mkdirSync(store, { recursive: true });
|
|
const member = path.join(tmpDir, "root", "packages", "app");
|
|
const memberLink = linkTo(member, store);
|
|
// Root has a .pnpm store but NO top-level @swmansion/argent symlink.
|
|
|
|
// cwd is the member root.
|
|
expect(findStablePackageDir(member, store)).toBe(memberLink);
|
|
// ...and the returned path is NOT the version-pinned store dir.
|
|
expect(findStablePackageDir(member, store)).not.toContain(".pnpm");
|
|
});
|
|
|
|
it("prefers the deepest (closest-to-cwd) alias when several resolve to the package", () => {
|
|
const store = path.join(tmpDir, "store", "argent");
|
|
fs.mkdirSync(store, { recursive: true });
|
|
const root = path.join(tmpDir, "root");
|
|
const member = path.join(root, "packages", "app");
|
|
linkTo(root, store);
|
|
const memberLink = linkTo(member, store);
|
|
|
|
expect(findStablePackageDir(member, store)).toBe(memberLink);
|
|
});
|
|
|
|
it("returns undefined when no symlink resolves to the running package", () => {
|
|
const store = path.join(tmpDir, "store", "argent");
|
|
fs.mkdirSync(store, { recursive: true });
|
|
const proj = path.join(tmpDir, "proj");
|
|
// A node_modules/<pkg> that points somewhere ELSE must not be accepted.
|
|
const other = path.join(tmpDir, "other", "argent");
|
|
fs.mkdirSync(other, { recursive: true });
|
|
linkTo(proj, other);
|
|
|
|
expect(findStablePackageDir(proj, store)).toBeUndefined();
|
|
});
|
|
});
|