lensmcp 1.20.1 → 1.21.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/README.md +54 -3
- package/lib/agent-plugins.d.ts +103 -0
- package/lib/agent-plugins.js +2 -0
- package/lib/cli.js +48 -14
- package/lib/plugin-version.d.ts +7 -0
- package/lib/plugin-version.js +3 -3
- package/lib/upgrade.d.ts +133 -0
- package/lib/upgrade.js +1 -0
- package/package.json +3 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugins/lensmcp/.codex-plugin/plugin.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,8 @@ lensmcp <command> [options]
|
|
|
25
25
|
| `lensmcp mcp` | Launch the LensMCP MCP server that agents connect to. Defaults to **stdio**; set `--transport http` (or `LENSMCP_TRANSPORT=http`) to serve over HTTP on `--port` (default **3000**). |
|
|
26
26
|
| `lensmcp dashboard` | Serve the live web view of the lens — Flows, Cluster, Logs, Resources — on `--port` (default **4321**). |
|
|
27
27
|
| `lensmcp install` | Install LensMCP into the host Nx workspace (delegates to `nx g @lensmcp/nx-plugin:init`). Wires `.mcp.json` so agents auto-discover the server, plus `.gitignore` and config. Idempotent. |
|
|
28
|
+
| `lensmcp plugin` | Install the LensMCP plugin into the coding agents on this machine — **Claude Code and Codex** — from the lensmcp package the workspace depends on. `plugin status` compares installed vs packaged versions. Idempotent; the same command upgrades. |
|
|
29
|
+
| `lensmcp upgrade` | Move **every connected workspace** (`~/.lensmcp/workspaces.json` + the current one) onto one lensmcp version: rewrite `lensmcp`/`@lensmcp/*` in each package.json, run each workspace's package manager, refresh a global install + both agent plugins, then restart the gateway daemon and re-register its guests. |
|
|
28
30
|
| `lensmcp doctor` | Diagnose the install: Node version, workspace + plugin registration, `.mcp.json` entry, MCP bundle, per-project Vite/Nest wiring, `agent-dev` targets, and Chrome for browser capture. |
|
|
29
31
|
| `lensmcp rollout` | Edit the gateway rollout config (`@lensmcp/cluster`) — `add-cohort`, `set-weight`, `promote`, `abort`, `add-rule`, `remove-rule`, `status`. |
|
|
30
32
|
| `lensmcp --version` | Print the installed `lensmcp` version. |
|
|
@@ -65,6 +67,44 @@ lensmcp install [--cwd <dir>] [--skip-format] [--register-host-config <mode>]
|
|
|
65
67
|
|
|
66
68
|
Run from inside an Nx workspace (requires `nx.json` and a local `nx` binary). Delegates to `nx g @lensmcp/nx-plugin:init --no-interactive`. Use `--skip-format` to skip post-generation formatting and `--register-host-config <mode>` to control how the host config is registered.
|
|
67
69
|
|
|
70
|
+
### `lensmcp plugin`
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
lensmcp plugin <install|status> [claude|codex] [--from <dir>] [--scope <user|project|local>] [--cwd <dir>]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Installs the LensMCP plugin into the coding agents on this machine, from the lensmcp
|
|
77
|
+
package the workspace at `--cwd` depends on (walks up to the nearest
|
|
78
|
+
`node_modules/lensmcp`; `--from` overrides). Idempotent, and the **same command
|
|
79
|
+
upgrades**: both agents treat the package directory as a live marketplace source, so a
|
|
80
|
+
re-install re-reads whatever version is installed there. An agent whose CLI is not on
|
|
81
|
+
PATH is skipped. `--scope` (Claude Code only) picks the install scope, defaulting to the
|
|
82
|
+
scope of the existing install.
|
|
83
|
+
|
|
84
|
+
### `lensmcp upgrade`
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
lensmcp upgrade [version] [--dry-run] [--no-install] [--no-plugins] [--no-global] [--no-restart]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
One command that moves the whole machine to one lensmcp version (latest by default), in
|
|
91
|
+
the only order that works:
|
|
92
|
+
|
|
93
|
+
1. rewrite `lensmcp` + `@lensmcp/*` lines in every connected workspace's package.json
|
|
94
|
+
(the gateway registry `~/.lensmcp/workspaces.json` plus the workspace you run it in;
|
|
95
|
+
`^`/`~` operators are kept, `link:`/`workspace:` pins are reported and never touched),
|
|
96
|
+
2. run each workspace's **own** package manager install (`packageManager` field, then lockfile),
|
|
97
|
+
3. move a machine-global `npm i -g lensmcp` when one exists (it shadows every project for a bare `lensmcp`),
|
|
98
|
+
4. re-install the Claude Code + Codex plugins from the fresh package,
|
|
99
|
+
5. restart the gateway daemon and **re-register every workspace it hosted** — the daemon
|
|
100
|
+
keeps the bundle it loaded at start forever, and an owner restart drops its guests, so
|
|
101
|
+
both halves are mandatory. A daemon that *reports* a version (1.21+) is restarted even
|
|
102
|
+
when no package.json moved, if it provably lags the target.
|
|
103
|
+
|
|
104
|
+
Per-workspace failure posture: one failed install never blocks the others, every failure
|
|
105
|
+
names the command that finishes the job by hand, and re-running converges. `--dry-run`
|
|
106
|
+
prints the full plan without changing anything.
|
|
107
|
+
|
|
68
108
|
### `lensmcp doctor`
|
|
69
109
|
|
|
70
110
|
```bash
|
|
@@ -113,10 +153,21 @@ lensmcp mcp --transport http --port 3000 # or expose over HTTP
|
|
|
113
153
|
|
|
114
154
|
Most local agents (Claude Code, Cursor, VS Code) read `.mcp.json` and launch `lensmcp mcp` over stdio automatically after `lensmcp install` — no manual wiring needed.
|
|
115
155
|
|
|
116
|
-
### Codex
|
|
156
|
+
### Agent plugins (Claude Code + Codex)
|
|
157
|
+
|
|
158
|
+
The npm package ships a plugin per agent — Claude Code (`plugin/`) and Codex
|
|
159
|
+
(`plugins/lensmcp/`), each with LensMCP skills and project-scoped MCP wiring. One command
|
|
160
|
+
installs (or upgrades) both, pointing each agent's directory marketplace at this
|
|
161
|
+
workspace's `node_modules/lensmcp` so it moves with the dependency:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
npx -y lensmcp plugin install # both agents; a missing CLI is skipped, not an error
|
|
165
|
+
npx -y lensmcp plugin install codex # just one
|
|
166
|
+
npx -y lensmcp plugin status # installed vs packaged versions
|
|
167
|
+
```
|
|
117
168
|
|
|
118
|
-
|
|
119
|
-
|
|
169
|
+
Under the hood that is the documented marketplace-add + plugin-add pair per agent — run
|
|
170
|
+
them by hand if you prefer:
|
|
120
171
|
|
|
121
172
|
```bash
|
|
122
173
|
codex plugin marketplace add ./node_modules/lensmcp
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/** The marketplace both manifests declare — one name, so `<plugin>@<marketplace>` is agent-agnostic. */
|
|
2
|
+
export declare const MARKETPLACE = "lensmcp-marketplace";
|
|
3
|
+
export declare const PLUGIN = "lensmcp";
|
|
4
|
+
export declare const PLUGIN_ID = "lensmcp@lensmcp-marketplace";
|
|
5
|
+
export type AgentId = 'claude' | 'codex';
|
|
6
|
+
export interface AgentDefinition {
|
|
7
|
+
id: AgentId;
|
|
8
|
+
label: string;
|
|
9
|
+
/** The agent's CLI binary, as invoked. */
|
|
10
|
+
bin: string;
|
|
11
|
+
/** Package-relative marketplace manifest. Its presence PROVES this agent's plugin ships in that version. */
|
|
12
|
+
manifest: string;
|
|
13
|
+
/** Package-relative plugin manifest — read for the version we are about to install. */
|
|
14
|
+
pluginManifest: string;
|
|
15
|
+
/** The lensmcp version that first shipped this agent's plugin (named when the manifest is missing). */
|
|
16
|
+
since: string;
|
|
17
|
+
restartHint: string;
|
|
18
|
+
}
|
|
19
|
+
export declare const AGENTS: readonly AgentDefinition[];
|
|
20
|
+
export declare function agentById(id: string): AgentDefinition | undefined;
|
|
21
|
+
export interface ResolveRootOptions {
|
|
22
|
+
cwd: string;
|
|
23
|
+
/** `--from <dir>` — an explicit package root, taken at face value. */
|
|
24
|
+
from?: string;
|
|
25
|
+
/** The running CLI's own package root (`cliPackageRoot()`), used when the workspace has no install. */
|
|
26
|
+
selfRoot?: string;
|
|
27
|
+
/** Injected for tests. */
|
|
28
|
+
exists?: (path: string) => boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface ResolvedRoot {
|
|
31
|
+
root: string;
|
|
32
|
+
source: 'flag' | 'workspace' | 'self';
|
|
33
|
+
/** The lensmcp version at that root, when readable. */
|
|
34
|
+
version?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The directory an agent's `marketplace add` is pointed at.
|
|
38
|
+
*
|
|
39
|
+
* WORKSPACE FIRST, self last, and the order is load-bearing: the marketplace is a
|
|
40
|
+
* DIRECTORY source, so whatever is on disk there is the version the agent installs
|
|
41
|
+
* and keeps re-reading. Pointing it at the running CLI (often a global or an `npx`
|
|
42
|
+
* cache) would pin the agent to a version the project does not use and cannot
|
|
43
|
+
* upgrade — precisely the plugin/project skew this toolchain already had to grow a
|
|
44
|
+
* detector for. `node_modules/lensmcp` moves with `lensmcp upgrade`; nothing else does.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolvePluginRoot(options: ResolveRootOptions): ResolvedRoot | undefined;
|
|
47
|
+
/** The version stamped into an agent's plugin manifest at publish time (`scripts/prepare-dist.mjs`). */
|
|
48
|
+
export declare function readPluginManifestVersion(root: string, agent: AgentDefinition): string | undefined;
|
|
49
|
+
export interface PlanOptions {
|
|
50
|
+
/** Absolute package root the marketplace points at. */
|
|
51
|
+
root: string;
|
|
52
|
+
/** Claude only — `user` (its default) | `project` | `local`. Omitted = the agent's default. */
|
|
53
|
+
scope?: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The exact commands, in order. Every one of them is idempotent (verified against
|
|
57
|
+
* both CLIs): re-adding a marketplace at the same path is a no-op that exits 0, and
|
|
58
|
+
* re-installing an installed plugin re-reads the directory source — which is how an
|
|
59
|
+
* UPGRADE happens here too. So `plugin install` is the same command whether nothing,
|
|
60
|
+
* an old version, or the current version is installed, and there is no
|
|
61
|
+
* install-vs-update branch to get wrong.
|
|
62
|
+
*/
|
|
63
|
+
export declare function planAgentInstall(agent: AgentDefinition, options: PlanOptions): string[][];
|
|
64
|
+
export type AgentInstallOutcome =
|
|
65
|
+
/** Every command exited 0. */
|
|
66
|
+
'installed'
|
|
67
|
+
/** The agent's CLI is not on PATH — not an error, this machine just has no Codex/Claude. */
|
|
68
|
+
| 'skipped-no-cli'
|
|
69
|
+
/** This lensmcp version ships no plugin for that agent. Actionable: upgrade lensmcp. */
|
|
70
|
+
| 'skipped-not-packaged'
|
|
71
|
+
/** A command failed. Actionable, and never fatal to the other agents. */
|
|
72
|
+
| 'failed';
|
|
73
|
+
export interface AgentInstallResult {
|
|
74
|
+
agent: AgentId;
|
|
75
|
+
label: string;
|
|
76
|
+
outcome: AgentInstallOutcome;
|
|
77
|
+
/** The version the plugin manifest declares (what the agent ends up with). */
|
|
78
|
+
version?: string;
|
|
79
|
+
/** What was run (or what to run, when it could not be). */
|
|
80
|
+
commands: string[];
|
|
81
|
+
detail?: string;
|
|
82
|
+
restartHint?: string;
|
|
83
|
+
}
|
|
84
|
+
export interface InstallDeps {
|
|
85
|
+
/** Run a command; `undefined` means the binary was not found (ENOENT). */
|
|
86
|
+
run: (bin: string, args: string[]) => {
|
|
87
|
+
code: number;
|
|
88
|
+
stdout?: string;
|
|
89
|
+
stderr?: string;
|
|
90
|
+
} | undefined;
|
|
91
|
+
exists?: (path: string) => boolean;
|
|
92
|
+
}
|
|
93
|
+
export declare function installAgentPlugin(agent: AgentDefinition, options: PlanOptions, deps: InstallDeps): AgentInstallResult;
|
|
94
|
+
/** The real runner — bounded, non-interactive, never throws. */
|
|
95
|
+
export declare function spawnAgentRunner(env: NodeJS.ProcessEnv, timeoutMs?: number): InstallDeps['run'];
|
|
96
|
+
/**
|
|
97
|
+
* The version Codex has installed, from `codex plugin list --json`.
|
|
98
|
+
*
|
|
99
|
+
* Unlike Claude Code there is no index file contract to read (its state lives in
|
|
100
|
+
* `~/.codex` with no documented layout), so this one IS a subprocess — acceptable
|
|
101
|
+
* for an explicit `plugin status`, which is not on a hook path.
|
|
102
|
+
*/
|
|
103
|
+
export declare function codexInstalledVersion(run: InstallDeps['run']): string | undefined;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var k=Object.defineProperty;var l=(t,e)=>k(t,"name",{value:e,configurable:!0});var y=Object.defineProperty,c=l((t,e)=>y(t,"name",{value:e,configurable:!0}),"d");import{spawnSync as j}from"node:child_process";import{existsSync as m,readFileSync as v}from"node:fs";import{dirname as A,isAbsolute as P,join as d,resolve as x}from"node:path";export const MARKETPLACE="lensmcp-marketplace",PLUGIN="lensmcp",PLUGIN_ID=`${PLUGIN}@${MARKETPLACE}`,AGENTS=[{id:"claude",label:"Claude Code",bin:"claude",manifest:".claude-plugin/marketplace.json",pluginManifest:"plugin/.claude-plugin/plugin.json",since:"1.9.0",restartHint:"restart Claude Code (or run /plugin) for the new version to load"},{id:"codex",label:"Codex",bin:"codex",manifest:".agents/plugins/marketplace.json",pluginManifest:"plugins/lensmcp/.codex-plugin/plugin.json",since:"1.20.0",restartHint:"start a new Codex thread for the plugin to load"}];export function agentById(t){return AGENTS.find(e=>e.id===t)}l(agentById,"agentById"),c(agentById,"agentById");export function resolvePluginRoot(t){const e=t.exists??m,o=c(r=>{try{const s=JSON.parse(v(d(r,"package.json"),"utf8"));return typeof s.version=="string"?s.version:void 0}catch{return}},"readVersion"),i=c((r,s)=>{const a=o(r);return{root:r,source:s,...a?{version:a}:{}}},"decorate");if(t.from){const r=P(t.from)?t.from:x(t.cwd,t.from);return e(d(r,"package.json"))?i(r,"flag"):void 0}let n=x(t.cwd);for(let r=0;r<8;r++){const s=d(n,"node_modules","lensmcp");if(e(d(s,"package.json")))return i(s,"workspace");const a=A(n);if(a===n)break;n=a}if(t.selfRoot&&e(d(t.selfRoot,"package.json")))return i(t.selfRoot,"self")}l(resolvePluginRoot,"resolvePluginRoot"),c(resolvePluginRoot,"resolvePluginRoot");export function readPluginManifestVersion(t,e){try{const o=JSON.parse(v(d(t,e.pluginManifest),"utf8"));return typeof o.version=="string"?o.version:void 0}catch{return}}l(readPluginManifestVersion,"readPluginManifestVersion"),c(readPluginManifestVersion,"readPluginManifestVersion");export function planAgentInstall(t,e){if(t.id==="codex")return[[t.bin,"plugin","marketplace","add",e.root],[t.bin,"plugin","add",PLUGIN_ID]];const o=e.scope?["--scope",e.scope]:[];return[[t.bin,"plugin","marketplace","add",e.root],[t.bin,"plugin","marketplace","update",MARKETPLACE],[t.bin,"plugin","install",PLUGIN_ID,...o,"--yes"]]}l(planAgentInstall,"planAgentInstall"),c(planAgentInstall,"planAgentInstall");export function installAgentPlugin(t,e,o){const i=o.exists??m,n=planAgentInstall(t,e),r=n.map(p=>p.join(" ")),s=readPluginManifestVersion(e.root,t),a={agent:t.id,label:t.label,commands:r,...s?{version:s}:{}};if(!i(d(e.root,t.manifest)))return{...a,outcome:"skipped-not-packaged",detail:`${e.root} ships no ${t.manifest} \u2014 the ${t.label} plugin arrived in lensmcp ${t.since}. Upgrade first: \`lensmcp upgrade\`.`};for(const p of n){const[g,...b]=p,u=g===void 0?void 0:o.run(g,b);if(!u)return{...a,outcome:"skipped-no-cli",detail:`\`${t.bin}\` is not on PATH`};if(u.code!==0){const f=(u.stderr??u.stdout??"").trim().split(`
|
|
2
|
+
`).filter(Boolean).pop();return{...a,outcome:"failed",detail:f&&f.length>0?f:`\`${p.join(" ")}\` exited ${u.code}`}}}return{...a,outcome:"installed",restartHint:t.restartHint}}l(installAgentPlugin,"installAgentPlugin"),c(installAgentPlugin,"installAgentPlugin");export function spawnAgentRunner(t,e=12e4){return(o,i)=>{try{const n=j(o,i,{encoding:"utf8",timeout:e,env:t,stdio:["ignore","pipe","pipe"]});return n.error&&n.error.code==="ENOENT"?void 0:{code:n.status??1,...n.stdout?{stdout:n.stdout}:{},...n.stderr?{stderr:n.stderr}:{}}}catch{return}}}l(spawnAgentRunner,"spawnAgentRunner"),c(spawnAgentRunner,"spawnAgentRunner");export function codexInstalledVersion(t){const e=t("codex",["plugin","list","--json"]);if(!(!e||e.code!==0||!e.stdout))try{const o=(JSON.parse(e.stdout).installed??[]).find(i=>i.pluginId===PLUGIN_ID||i.name===PLUGIN&&i.marketplaceName===MARKETPLACE)?.version;return typeof o=="string"?o:void 0}catch{return}}l(codexInstalledVersion,"codexInstalledVersion"),c(codexInstalledVersion,"codexInstalledVersion");
|
package/lib/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var Tt=Object.defineProperty;var u=(t,n)=>Tt(t,"name",{value:n,configurable:!0});var Mt=Object.defineProperty,d=u((t,n)=>Mt(t,"name",{value:n,configurable:!0}),"d");import{spawn as Lt,spawnSync as j}from"node:child_process";import{existsSync as b,mkdirSync as U,openSync as Ot,readFileSync as F,readdirSync as de,rmSync as At,statSync as Rt,writeFileSync as K}from"node:fs";import{request as xe}from"node:http";import{homedir as _t,tmpdir as It}from"node:os";import{basename as ne,dirname as R,isAbsolute as Dt,join as w,relative as Se,resolve as P}from"node:path";import{fileURLToPath as Ft}from"node:url";import{ensureLensConfig as q,readLensConfig as Kt}from"./workspace-scope.js";import{remember as Gt,shouldAnnounce as Ht}from"./hook-memory.js";import{isMcpMode as je}from"./mcp-mode.js";import{effectiveMcpMode as Vt,LENS_CONFIG as Wt,probeSharedMcp as Ne,probeSharedMcpSync as Jt,readMcpWiring as le,wireMcpMode as Bt}from"./mcp-wiring.js";import{sweepLensRuntimeFiles as Ut}from"./sweep.js";import{candidateSignals as qt,findingLine as Yt,findingsDigest as zt,formatContext as Xt,hookPayload as Pe,isHookEvent as Ee,select as Zt}from"./status-policy.js";import{DEFAULT_BUDGET_MS as Qt,readStatus as es}from"./status-client.js";import{detectSkew as ts,formatSkewContext as Te,readInstalledPlugin as Me,readInstalledPlugins as ss,readVersions as Le,spawnRunner as ns,syncPlugin as os,updateCommands as rs}from"./plugin-version.js";import{AGENTS as Oe,agentById as is,codexInstalledVersion as as,installAgentPlugin as cs,MARKETPLACE as ds,PLUGIN_ID as oe,readPluginManifestVersion as ls,resolvePluginRoot as us,spawnAgentRunner as Ae}from"./agent-plugins.js";import{detectGlobalInstall as ps,detectPackageManager as gs,planRestart as hs,readConnectedWorkspaces as fs,rewriteLensmcpDeps as ms}from"./upgrade.js";const Re=`lensmcp \u2014 CLI for LensMCP (FrontMCP-based observability for coding agents)
|
|
2
2
|
|
|
3
3
|
Usage:
|
|
4
4
|
lensmcp <command> [options]
|
|
@@ -67,6 +67,36 @@ Commands:
|
|
|
67
67
|
Install LensMCP into the host Nx workspace at --cwd
|
|
68
68
|
(current directory by default). Delegates to
|
|
69
69
|
\`nx g @lensmcp/nx-plugin:init\`. Idempotent.
|
|
70
|
+
plugin <install|status> [claude|codex] [--from <dir>] [--scope <s>] [--cwd <dir>]
|
|
71
|
+
Install the LensMCP plugin into the coding agents on this
|
|
72
|
+
machine \u2014 Claude Code AND Codex \u2014 straight from the lensmcp
|
|
73
|
+
package this workspace depends on (the marketplace-add +
|
|
74
|
+
plugin-add pair per agent, in one idempotent command). The
|
|
75
|
+
SAME command upgrades an already-installed plugin: the
|
|
76
|
+
marketplaces are DIRECTORY sources over node_modules, so a
|
|
77
|
+
re-install re-reads whatever version is there. An agent
|
|
78
|
+
whose CLI is not on PATH is skipped, never an error.
|
|
79
|
+
\`status\` prints installed vs packaged versions per agent.
|
|
80
|
+
--scope (Claude only): user | project | local \u2014 defaults to
|
|
81
|
+
the scope of the existing install. --from points at an
|
|
82
|
+
explicit package root instead of the workspace's
|
|
83
|
+
node_modules.
|
|
84
|
+
upgrade [version] [--cwd <dir>] [--dry-run] [--no-install] [--no-plugins] [--no-global] [--no-restart]
|
|
85
|
+
Move EVERY workspace LensMCP is connected to onto one
|
|
86
|
+
version (latest by default) \u2014 the whole set, in the only
|
|
87
|
+
order that works: rewrite each workspace's package.json
|
|
88
|
+
\`lensmcp\` + \`@lensmcp/*\` lines in place (^/~ kept;
|
|
89
|
+
link:/workspace: pins reported, never touched) \u2192 run each
|
|
90
|
+
workspace's own package manager install \u2192 move a stale
|
|
91
|
+
global \`npm i -g lensmcp\` \u2192 re-install the Claude Code +
|
|
92
|
+
Codex plugins \u2192 RESTART the gateway daemon and re-register
|
|
93
|
+
every workspace it hosted (a daemon keeps its loaded
|
|
94
|
+
bundle forever, and an owner restart drops its guests).
|
|
95
|
+
The connected set is ~/.lensmcp/workspaces.json (every
|
|
96
|
+
gateway registers itself) plus the workspace you run it
|
|
97
|
+
in. Re-runnable: each step converges, and any failure is
|
|
98
|
+
reported with the command to finish by hand. \`--dry-run\`
|
|
99
|
+
prints the full plan without changing anything.
|
|
70
100
|
trust [--cwd <dir>] [--project <p>] [--target <t>]
|
|
71
101
|
Make the HTTPS dev gateway trusted: mint + trust the local
|
|
72
102
|
dev CA, write /etc/hosts (127.0.0.1 + ::1) for every
|
|
@@ -122,28 +152,32 @@ Commands:
|
|
|
122
152
|
listening" link in \`lensmcp logs <service>\`.
|
|
123
153
|
--version Print the lensmcp version.
|
|
124
154
|
--help Show this help.
|
|
125
|
-
`;export async function runCli(t){const n=t.out??(
|
|
155
|
+
`;export async function runCli(t){const n=t.out??(r=>console.log(r)),e=t.err??(r=>console.error(r)),[s,...o]=t.argv;if(!s||s==="--help"||s==="-h")return n(Re),{exitCode:0};if(s==="--version"||s==="-V")return n(ee()),{exitCode:0};switch(s){case"setup":return Qe(t,o,n,e);case"mcp":return _e(t,o,n,e);case"dashboard":return Ie(t,o,n,e);case"gateway":return ue(t,o,n,e);case"bridge":return Ye(t,o,n,e);case"install":return fe(t,o,n,e);case"plugin":case"plugins":return ut(t,o,n,e);case"upgrade":return ht(t,o,n,e);case"trust":return me(t,o,n,e);case"status":return et(t,o,n);case"version-check":return tt(t,o,n);case"doctor":return st(t,o,n);case"rollout":return wt(t,o,e);case"logs":return Nt(t,o,n,e);case"debug":return Pt(t,o,n,e);default:return e(`Unknown command: ${s}`),e(Re),{exitCode:2,message:`Unknown command: ${s}`}}}u(runCli,"runCli"),d(runCli,"runCli");function _e(t,n,e,s){const o=A(n,{string:["--cwd","--transport","--port","--mcp"]}),r=P($(o.flags["--cwd"])??t.cwd),c=q(r),i=$(o.flags["--transport"])??t.env?.LENSMCP_TRANSPORT??"stdio",l=$(o.flags["--port"])??t.env?.LENSMCP_PORT??String(c.ports.mcpHttp),a=Vt(r,$(o.flags["--mcp"]),{...process.env,...t.env??{}})==="shared"&&i==="stdio",p=a?yt(r)??ie(r):ie(r);if(!p)return s(`Could not locate the lensmcp-mcp server bundle.
|
|
126
156
|
Looked in (in order):
|
|
127
157
|
\u2022 <cli-package>/bundled/main.js (published bin)
|
|
128
158
|
\u2022 <workspace>/apps/lensmcp-mcp/dist/main.js (dev build)
|
|
129
|
-
For dev: \`yarn nx build @lensmcp/lensmcp-mcp\`.`),{exitCode:1};
|
|
159
|
+
For dev: \`yarn nx build @lensmcp/lensmcp-mcp\`.`),{exitCode:1};se(r);const g=t.env?.LENSMCP_EVENT_FILE??process.env.LENSMCP_EVENT_FILE??w(r,".lensmcp","events.jsonl"),v={...process.env,...t.env??{},LENSMCP_TRANSPORT:i,LENSMCP_PORT:l,LENSMCP_EVENT_FILE:g},m=a?[p,"--cwd",r]:[p];return{exitCode:j(process.execPath,m,{cwd:r,stdio:"inherit",env:v}).status??1}}u(_e,"Cn"),d(_e,"runMcp");function Ie(t,n,e,s){const o=A(n,{string:["--cwd","--port","--base"]}),r=P($(o.flags["--cwd"])??t.cwd),c=q(r),i=$(o.flags["--port"])??t.env?.LENSMCP_DASHBOARD_PORT??String(c.ports.dashboard),l=$(o.flags["--base"])??t.env?.LENSMCP_DASHBOARD_BASE??"",a=ae(r,"dashboard.js");if(!a)return s(`Could not locate the lensmcp dashboard bundle.
|
|
130
160
|
Looked in (in order):
|
|
131
161
|
\u2022 <cli-package>/bundled/dashboard.js (published bin)
|
|
132
162
|
\u2022 <workspace>/servers/lensmcp-mcp/dist/dashboard.js (dev build)
|
|
133
|
-
For dev: \`yarn nx build @lensmcp/lensmcp-mcp\`.`),{exitCode:1};
|
|
134
|
-
%{http_code}`,"--unix-socket",s,"-X",t,`http://localhost${n}`];e!==void 0&&
|
|
135
|
-
`),i=Number(
|
|
136
|
-
`).trim();l=
|
|
137
|
-
Free it first (stop that process), then \`lensmcp gateway start\`.`),{exitCode:1}}const
|
|
163
|
+
For dev: \`yarn nx build @lensmcp/lensmcp-mcp\`.`),{exitCode:1};se(r);const p=t.env?.LENSMCP_EVENT_FILE??process.env.LENSMCP_EVENT_FILE??w(r,".lensmcp","events.jsonl");return e(`lensmcp dashboard \u2192 http://localhost:${i}${l||"/"} (reading ${p})`),{exitCode:j(process.execPath,[a],{cwd:r,stdio:"inherit",env:{...process.env,...t.env??{},LENSMCP_DASHBOARD_PORT:i,LENSMCP_DASHBOARD_BASE:l,LENSMCP_EVENT_FILE:p}}).status??1}}u(Ie,"Sn"),d(Ie,"runDashboard");function De(){return w(process.env.LENSMCP_HOME||_t(),".lensmcp","control.sock")}u(De,"jn"),d(De,"controlSocketPath");function _(t,n,e){const s=De();if(!b(s))return null;const o=["-s","-o","-","-w",`
|
|
164
|
+
%{http_code}`,"--unix-socket",s,"-X",t,`http://localhost${n}`];e!==void 0&&o.push("-H","content-type: application/json","-d",JSON.stringify(e));const r=j("curl",o,{encoding:"utf8",timeout:5e3});if(r.status!==0||typeof r.stdout!="string"||r.stdout.length===0)return null;const c=r.stdout.split(`
|
|
165
|
+
`),i=Number(c.pop());let l=null;try{const a=c.join(`
|
|
166
|
+
`).trim();l=a?JSON.parse(a):null}catch{}return Number.isFinite(i)?{status:i,json:l}:null}u(_,"_"),d(_,"daemonRequest");function Fe(){const t=_("GET","/status");if(t?.status!==200||!t.json||typeof t.json!="object")return;const n=t.json.daemon;return typeof n?.wsKey=="string"&&n.wsKey.length>0?n.wsKey:void 0}u(Fe,"xn"),d(Fe,"daemonWsKey");function Ke(t){const n={};for(const e of J(t,s=>s==="project.json"))try{const s=JSON.parse(F(e,"utf8")),o=R(e);n[s.name??ne(o)]={root:Se(t,o)||"."}}catch{}return n}u(Ke,"Nn"),d(Ke,"buildProjectsMap");function Ge(t){const n=w(t,"node_modules",".bin",process.platform==="win32"?"nx.cmd":"nx");if(!b(n))return;const e=j(n,["graph","--print"],{cwd:t,encoding:"utf8",timeout:15e3,maxBuffer:32*1024*1024});if(!(e.status!==0||typeof e.stdout!="string"))try{const s=JSON.parse(e.stdout).graph?.dependencies;return s?Object.fromEntries(Object.entries(s).map(([o,r])=>[o,r.flatMap(c=>typeof c.target=="string"?[c.target]:[])])):void 0}catch{return}}u(Ge,"Pn"),d(Ge,"buildProjectDependencies");function re(t,n){const e=Ge(t);return{wsKey:n,root:t,projects:Ke(t),...e?{projectDependencies:e}:{}}}u(re,"ie"),d(re,"gatewayRegistrationBody");function ue(t,n,e,s){const o=A(n,{string:["--cwd","--project","--target"]}),r=(o.positional[0]??"status").toLowerCase(),c=n.includes("--json"),i=P($(o.flags["--cwd"])??t.cwd),l=q(i),a=w(i,".lensmcp","gateway.pid"),p=w(i,".lensmcp","gateway.log"),g=w(i,".lensmcp","registered.json"),v=`https://lensmcp.local${l.dashboardBasePath}/`,m=d(()=>{const{pid:f,healed:C}=X(a,i);if(f!==void 0&&z(f)){const L=classifyAliveGateway({oursOn443:V(i)!==void 0,port443Held:G().length>0,pidFileAgeMs:ge(a)});if(L==="serving")return C&&e(`healed: adopted a running gateway (pid ${f}) the pid file had lost \u2014 no duplicate started.`),e(`gateway already running (pid ${f}).`),e(` dashboard \u2192 ${v}`),{exitCode:0};if(L==="booting")return e(`gateway starting (pid ${f}) \u2014 :443 not bound yet.`),e(` dashboard \u2192 ${v}`),e(` logs \u2192 ${p}`),{exitCode:0};Z(f,"SIGKILL"),W(a),Y(i,t.env,()=>{}),e(`reaped a dead gateway (pid ${f} was alive but nothing served :443 for this workspace).`)}const h=G()[0];if(h!==void 0){const L=_("GET","/list");if(L!==null){if(L.status===200&&L.json&&typeof L.json=="object"&&(L.json.workspaces??[]).some(x=>x.key===l.key))return U(R(g),{recursive:!0}),K(g,JSON.stringify({wsKey:l.key})),e(`already registered into the shared gateway daemon (pid ${h}).`),e(` dashboard \u2192 ${v}`),{exitCode:0};const T=_("POST","/register",re(i,l.key));return T&&T.status===200?(U(R(g),{recursive:!0}),K(g,JSON.stringify({wsKey:l.key})),e(`registered '${l.key}' into the shared gateway daemon (pid ${h}).`),e(` dashboard \u2192 ${v}`),e(" the daemon hosts this workspace's services + dashboard; `lensmcp gateway stop` unregisters."),{exitCode:0}):(s(`failed to register into the gateway daemon: ${T?JSON.stringify(T.json):"no response from the control socket"}`),{exitCode:1})}return s(`port :443 is held by pid ${h}, which is not this workspace's gateway.
|
|
167
|
+
Free it first (stop that process), then \`lensmcp gateway start\`.`),{exitCode:1}}const S=He(i,$(o.flags["--project"]),$(o.flags["--target"]));if(!S)return s(`This workspace ('${l.key}') has no gateway target \u2014 no project declares the`),s("`@lensmcp/cluster:gateway` executor, so there is nothing here to launch on :443."),s(""),s(" \u2022 Joining a shared gateway? Start it in the workspace that OWNS it, then run"),s(" `lensmcp gateway start` here \u2014 this workspace registers into it (no local target needed)."),s(" \u2022 Should this workspace host its own gateway? Scaffold it: `lensmcp install`"),s(" (or `lensmcp setup` for the full install + trust bootstrap)."),s(" \u2022 Already have a differently-named target? `lensmcp gateway start --project <p> --target <t>`."),{exitCode:1};const E=te(i);if(!E)return s("Could not find the `nx` binary in the workspace. Install Nx first: `yarn add -D nx`."),{exitCode:1};Ut(i,p);const M=Ot(p,"a"),O=Lt(E,["run",`${S.project}:${S.target}`],{cwd:i,detached:!0,stdio:["ignore",M,M],env:{...process.env,...t.env??{}}});return O.unref(),O.pid&&K(a,String(O.pid)),e(`gateway starting \u2192 ${S.project}:${S.target} (pid ${O.pid??"?"})`),e(` dashboard \u2192 ${v}`),e(` logs \u2192 ${p}`),e(" note: binding :443 needs privilege \u2014 if it fails, run the gateway target with sudo,"),e(" and run `nx run gateway:trust` once for the *.local hosts + TLS."),{exitCode:0}},"start"),y=d(()=>{if(b(g)&&V(i)===void 0){let E=l.key;try{E=JSON.parse(F(g,"utf8")).wsKey??l.key}catch{}const M=_("POST","/unregister",{wsKey:E});return W(g),e(M&&M.status===200?`unregistered '${E}' from the shared gateway daemon.`:"unregister sent (the daemon may already be gone)."),{exitCode:0}}W(g);const{pid:f,healed:C}=X(a,i);if(f===void 0)return e("gateway not running."),W(a),{exitCode:0};C&&e(`healed: found an orphaned gateway (pid ${f}) the pid file had lost \u2014 stopping it.`),Z(f);let h=!1;if(!he(6e3)){h=!0,Z(f,"SIGKILL");for(const E of G())try{process.kill(E,"SIGKILL")}catch{}he(3e3)}const S=V(i);return S!==void 0&&S!==f&&(Z(S,"SIGKILL"),h=!0),W(a),h&&Y(i,t.env,()=>{}),e(`gateway stopped (pid ${f})${h?" \u2014 forced (a child ignored SIGTERM)":""}.`),{exitCode:0}},"stop"),k=d(()=>{const{pid:f,healed:C}=X(a,i),h=f!==void 0&&z(f)?classifyAliveGateway({oursOn443:V(i)!==void 0,port443Held:G().length>0,pidFileAgeMs:ge(a)}):void 0,S=h==="serving"||h==="booting";if(c){const x=_("GET","/status"),I=x?.status===200&&x.json&&typeof x.json=="object"?x.json:null,B=b(g),Et=S?"daemon":I&&B?"guest":I?"other":"stopped";return e(JSON.stringify({thisWorkspace:l.key,role:Et,dashboardUrl:v,chooserUrl:"https://lensmcp.local/",logFile:p,mcp:we(i,t.env??process.env),daemon:I?.daemon??null,workspaces:I?.workspaces??[],services:I?.services??[]})),{exitCode:S||I&&B?0:1}}const E=_("GET","/list"),M=E?.status===200&&E.json&&typeof E.json=="object"?E.json.workspaces??[]:[],O=d(()=>{if(M.length!==0){e(` workspaces (${M.length} registered):`);for(const x of M)e(` \u2022 ${x.key} (${x.routes??0} routes, ${x.services??0} services)${x.key===l.key?" \u2190 this workspace":""}`)}},"printWorkspaces"),L=d(()=>{const x=we(i,t.env??process.env);e(x.mode==="shared"?` mcp \u2192 shared ${x.url} ${x.healthy?"(up)":`(DOWN \u2014 ${x.error??"no answer"})`}`:" mcp \u2192 stdio (one server per agent session)")},"printMcp"),T=d(()=>{const x=_("GET","/status"),I=x?.status===200&&x.json&&typeof x.json=="object"?x.json.daemon:void 0,B=ye(i);I?.version&&B&&I.version!==B&&e(` version \u2192 the daemon runs ${I.version} but this workspace depends on ${B} \u2014 \`lensmcp upgrade\` reconciles everything (or restart the daemon workspace's gateway).`)},"printVersionSkew");if(S)return e(h==="booting"?`gateway: starting (pid ${f}) \u2014 :443 not bound yet`:`gateway: running (pid ${f}) \u2014 DAEMON on :443${C?" \u2014 recovered a stale pid file":""}`),e(` workspace \u2192 ${l.key}`),e(` dashboard \u2192 ${v}`),e(" chooser \u2192 https://lensmcp.local/"),e(` logs \u2192 ${p}`),L(),T(),O(),{exitCode:0};if(b(g)&&E?.status===200){const x=G()[0];return e(`gateway: registered into a shared daemon${x!==void 0?` (pid ${x})`:""} \u2014 this workspace is a guest`),e(` workspace \u2192 ${l.key}`),e(` dashboard \u2192 ${v}`),e(" stop \u2192 `lensmcp gateway stop` (unregisters; the daemon keeps running)"),L(),T(),O(),{exitCode:0}}return h==="zombie"?(e(`gateway: NOT serving \u2014 tracked pid ${f} is alive but nothing holds :443 for this workspace`),e(" (a crashed gateway under a wedged nx wrapper) \u2014 run `lensmcp gateway restart` to recover."),e(` workspace \u2192 ${l.key}`),e(` logs \u2192 ${p}`),L(),O(),{exitCode:1}):(e("gateway: stopped"),e(` workspace \u2192 ${l.key}`),e(` dashboard \u2192 ${v}`),L(),{exitCode:1})},"status");switch(r){case"start":return m();case"stop":return y();case"restart":return y(),Y(i,t.env,e),m();case"status":return k();default:return s(`Unknown gateway subcommand: ${r} (use start | stop | status | restart)`),{exitCode:2}}}u(ue,"$e"),d(ue,"runGateway");function Y(t,n,e){const s=te(t);if(!s){e(" (graph refresh skipped \u2014 no nx binary found in the workspace)");return}const o={...process.env,...n??{}},r=d(c=>j(s,c,{cwd:t,env:o,stdio:"ignore"}).status===0,"tryNx");if(r(["reset","--only-daemon"])||r(["daemon","--stop"])){e(" nx project graph refreshed \u2014 a lib/app added since boot will now resolve.");return}e(" (graph refresh best-effort failed \u2014 if a new project does not resolve, run `nx reset`)")}u(Y,"z"),d(Y,"refreshNxGraph");function pe(t,n,e,s){if(e&&s)return{project:e,target:s};const o=J(t,r=>r==="project.json");for(const r of o){let c;try{c=JSON.parse(D(r))}catch{continue}const i=c.name??ne(R(r));for(const[l,a]of Object.entries(c.targets??{}))if(a.executor===n)return{project:e??i,target:s??l}}}u(pe,"Ce"),d(pe,"scanExecutorTarget");function He(t,n,e){return pe(t,"@lensmcp/cluster:gateway",n,e)||(n||e?{project:n??"gateway",target:e??"serve"}:void 0)}u(He,"En"),d(He,"findGatewayTarget");function Ve(t,n,e){return pe(t,"@lensmcp/cluster:trust",n,e)||(n?{project:n,target:e??"trust"}:We(t,"gateway")?{project:"gateway",target:e??"trust"}:void 0)}u(Ve,"Tn"),d(Ve,"findTrustTarget");function We(t,n){for(const e of J(t,s=>s==="project.json"))try{if((JSON.parse(D(e)).name??ne(R(e)))===n)return!0}catch{}return!1}u(We,"Mn"),d(We,"projectExists");function Je(t){try{const n=Number(F(t,"utf8").trim());return Number.isInteger(n)&&n>0?n:void 0}catch{return}}u(Je,"Ln"),d(Je,"readPid");function z(t){try{return process.kill(t,0),!0}catch(n){return n.code==="EPERM"}}u(z,"X"),d(z,"isAlive");function G(){try{const t=j("lsof",["-nP","-iTCP:443","-sTCP:LISTEN","-t"],{encoding:"utf8"});return t.status!==0||!t.stdout?[]:[...new Set(t.stdout.split(/\s+/).map(Number).filter(n=>Number.isInteger(n)&&n>0))]}catch{return[]}}u(G,"G"),d(G,"pidsOnPort443");function Be(t){try{return(j("ps",["-o","command=","-p",String(t)],{encoding:"utf8"}).stdout??"").trim()}catch{return""}}u(Be,"On"),d(Be,"processCmd");function V(t){const n=w(t,"node_modules","nx","dist","bin","run-executor.js"),e=w(t,"node_modules","@lensmcp","cluster");for(const s of G()){const o=Be(s);if(o.includes(n)||o.includes(e))return s}}u(V,"H"),d(V,"gatewayOnPort443");function ge(t){try{return Date.now()-Rt(t).mtimeMs}catch{return}}u(ge,"Se"),d(ge,"pidFileAgeMs");const ws=18e4;export function classifyAliveGateway(t){if(t.oursOn443)return"serving";const n=t.pidFileAgeMs!==void 0&&t.pidFileAgeMs<(t.bootGraceMs??ws);return!t.port443Held&&n?"booting":"zombie"}u(classifyAliveGateway,"classifyAliveGateway"),d(classifyAliveGateway,"classifyAliveGateway");function X(t,n){const e=Je(t);if(e!==void 0&&z(e))return{pid:e,healed:!1};const s=V(n);if(s!==void 0){try{K(t,String(s))}catch{}return{pid:s,healed:!0}}return{healed:!1}}u(X,"Z"),d(X,"reconcileGateway");function Z(t,n="SIGTERM"){try{process.kill(-t,n)}catch{try{process.kill(t,n)}catch{}}}u(Z,"Q"),d(Z,"killGatewayTree");function he(t){const n=Date.now()+t;for(;;){if(G().length===0)return!0;if(Date.now()>=n)return!1;j("sleep",["0.15"])}}u(he,"je"),d(he,"waitForPortFree");function Ue(t){const n=j("curl",["-s","-o","/dev/null","-m","8","-w","%{ssl_verify_result}",`https://${t}/`],{encoding:"utf8"}),e=(n.stdout??"").trim();return n.status===0&&e==="0"?"trusted":e&&e!=="0"?"untrusted":"unknown"}u(Ue,"_n"),d(Ue,"probeHostTrust");function qe(t){for(const n of J(t,e=>e==="project.json"))try{const e=JSON.parse(F(n,"utf8")),s=(e.cluster??e.davnx)?.host;if(s&&!s.includes("*"))return s}catch{}}u(qe,"An"),d(qe,"firstClusterHost");function W(t){try{At(t,{force:!0})}catch{}}u(W,"J"),d(W,"rmFile");function Ye(t,n,e,s){const o=A(n,{string:["--cwd","--host","--port"]}),r=P($(o.flags["--cwd"])??t.cwd),c=q(r),i=$(o.flags["--host"])??t.env?.LENSMCP_WS_HOST??process.env.LENSMCP_WS_HOST,l=$(o.flags["--port"])??t.env?.LENSMCP_WS_PORT??process.env.LENSMCP_WS_PORT??String(c.ports.bridge),a=kt(r);if(!a)return s(`Could not locate the lensmcp bridge bundle.
|
|
138
168
|
Looked in (in order):
|
|
139
169
|
\u2022 <cli-package>/bundled/bridge.js (published bin)
|
|
140
170
|
\u2022 <workspace>/libs/bridge/dist/main.js (dev build)
|
|
141
|
-
For dev: \`yarn nx build @lensmcp/bridge\`.`),{exitCode:1};
|
|
142
|
-
`)}catch{}}
|
|
143
|
-
`).some(
|
|
171
|
+
For dev: \`yarn nx build @lensmcp/bridge\`.`),{exitCode:1};se(r);const p=t.env?.LENSMCP_EVENT_FILE??process.env.LENSMCP_EVENT_FILE??w(r,".lensmcp","events.jsonl"),g={...process.env,...t.env??{},LENSMCP_EVENT_FILE:p};return i&&(g.LENSMCP_WS_HOST=i),l&&(g.LENSMCP_WS_PORT=l),e(`lensmcp bridge \u2192 ws://${i??"127.0.0.1"}:${l??"5747"} (forwarding to ${p})`),{exitCode:j(process.execPath,[a],{cwd:r,stdio:"inherit",env:g}).status??1}}u(Ye,"In"),d(Ye,"runBridge");function fe(t,n,e,s){const o=A(n,{string:["--cwd","--register-host-config"],boolean:["--skip-format","--yes"]}),r=P($(o.flags["--cwd"])??t.cwd);if(!b(w(r,"nx.json")))return s(`No nx.json found at ${r}.`),s("Run `lensmcp install` from inside a Nx workspace."),{exitCode:1};const c=te(r);if(!c)return s("Could not find the `nx` binary in the host workspace. Install Nx first: `yarn add -D nx`."),{exitCode:1};const i=["g","@lensmcp/nx-plugin:init","--no-interactive"];return o.flags["--skip-format"]===!0&&i.push("--skipFormat"),o.flags["--register-host-config"]&&i.push(`--registerHostConfig=${String(o.flags["--register-host-config"])}`),e(`> ${c} ${i.join(" ")}`),{exitCode:j(c,i,{cwd:r,stdio:"inherit",env:{...process.env,...t.env??{}}}).status??1}}u(fe,"xe"),d(fe,"runInstall");function me(t,n,e,s){const o=A(n,{string:["--cwd","--project","--target"]}),r=P($(o.flags["--cwd"])??t.cwd);if(!b(w(r,"nx.json")))return s(`No nx.json found at ${r}.`),s("Run `lensmcp trust` from inside a Nx workspace (after `lensmcp install`)."),{exitCode:1};const c=te(r);if(!c)return s("Could not find the `nx` binary in the workspace. Install Nx first: `yarn add -D nx`."),{exitCode:1};const i=Ve(r,$(o.flags["--project"]),$(o.flags["--target"])),l=_("GET","/list")?.status===200&&V(r)===void 0;if(!i){if(s("This workspace has no trust target \u2014 no project declares the `@lensmcp/cluster:trust` executor."),s(""),l){const p=Fe();s(` A shared gateway daemon${p?` (owned by '${p}')`:""} already serves this workspace, and`),s(" TLS rides ITS certificate authority. Run `lensmcp trust` in THAT workspace \u2014 it owns the CA \u2014"),s(" and this workspace is trusted too (restart the browser once if it cached a cert error).")}else s(" \u2022 Should this workspace own its gateway (and its CA)? Scaffold it first: `lensmcp install`,"),s(" then re-run `lensmcp trust`."),s(" \u2022 Joining a shared gateway? Run `lensmcp trust` in the workspace that owns the daemon.");return s(" \u2022 Already have a differently-named target? `lensmcp trust --project <p> --target <t>`."),{exitCode:1}}$t()||e(" note: trust runs `sudo` for the CA + /etc/hosts \u2014 run it in a real terminal if a step is needed."),l&&(e(" a shared gateway daemon serves this workspace \u2014 TLS rides ITS (already-trusted) CA, not a separate"),e(" one here. Setting up /etc/hosts only (trusting the CA is the DAEMON workspace's job).")),e(`> ${c} run ${i.project}:${i.target} (sudo prompts inline for ${l?"/etc/hosts":"the CA + /etc/hosts"})`);const a=j(c,["run",`${i.project}:${i.target}`],{cwd:r,stdio:"inherit",env:{...process.env,...t.env??{},...l?{LENSMCP_TRUST_HOSTS_ONLY:"1"}:{}}});if(l&&a.status===0){const p=qe(r);if(p){const g=Ue(p);e(""),e(g==="trusted"?` \u2713 ${p} \u2192 served by the daemon with a TRUSTED cert \u2014 you're set (restart the browser once if it cached an error).`:g==="untrusted"?` \u2717 ${p} \u2192 the daemon's cert is NOT trusted yet. Run \`lensmcp trust\` in the DAEMON workspace (it owns the CA), then restart your browser.`:` ? ${p} \u2192 couldn't reach it to verify (is the daemon running?). /etc/hosts is set; TLS rides the daemon's CA.`)}}return{exitCode:a.status??1}}u(me,"Ne"),d(me,"runTrust");function ze(t){se(t);const n={version:ee(),completedAt:new Date().toISOString(),steps:{install:!0,trust:!0}};try{K(w(t,".lensmcp","setup-state.json"),`${JSON.stringify(n,null,2)}
|
|
172
|
+
`)}catch{}}u(ze,"Dn"),d(ze,"writeSetupState");async function Xe(t,n,e,s){const o=je(n)?n:le(t,e).mode,r=Bt(t,o);if(s(""),s(`[mcp] mode \u2192 ${o}`),r.wrote&&s(` wrote ${Wt}`),o==="embedded"){s(" every agent session builds its OWN MCP server, and keeps serving"),s(" the bundle it started with until that session exits."),s(" one shared server for the workspace: `lensmcp setup --mcp shared`");return}s(` sessions proxy into ONE shared server at ${r.url}`),s(" .mcp.json is unchanged \u2014 `lensmcp mcp` becomes a thin stdio shim.");const c=await Ne(r.url);s(c.ok?` \u2713 already up \u2014 ${c.server??"MCP"} (protocol ${c.protocolVersion??"?"})`:` \xB7 not up yet (${c.error??"unknown"}) \u2014 the next session starts it on demand`)}u(Xe,"Fn"),d(Xe,"applyMcpModeStep");function Ze(t,n,e){const s=Le({cwd:t,cliVersion:ee()}),o=os({run:ns(n),facts:s});if(!(o.outcome==="current"||o.outcome==="unknown")){if(e(""),o.outcome==="updated"){e(`[plugin] ${o.from} \u2192 ${o.to}`),e(" restart Claude Code for the new plugin to load.");return}e(`[plugin] the Claude Code plugin is ${o.from}, this lensmcp is ${o.to}.`),e(` could not update it automatically (${o.detail??"unknown reason"}). Run:`);for(const r of o.commands??[])e(` ${r}`);e(" \u2026then restart Claude Code.")}}u(Ze,"Kn"),d(Ze,"applyPluginSyncStep");async function Qe(t,n,e,s){const o=A(n,{string:["--cwd","--mcp"]}),r=P($(o.flags["--cwd"])??t.cwd),c={...t,cwd:r},i=$(o.flags["--mcp"]);if(i!==void 0&&!je(i))return s(`setup: --mcp must be "embedded" or "shared" (got "${i}").`),{exitCode:1};e("lensmcp setup \u2014 first-time bootstrap (install \u2192 trust \u2192 pods)"),e(""),e("[1/3] install \u2014 plugin, .mcp.json, nx config, vite/nest wiring");const l=fe(c,n,e,s);if(l.exitCode!==0)return s(""),s("setup: the install step failed \u2014 fix the above and re-run `lensmcp setup`."),l;e(""),e("[2/3] trust \u2014 dev CA \u2192 System keychain, /etc/hosts, DNS flush (sudo)");const a=me(c,n,e,s);if(a.exitCode!==0)return s(""),s("setup: the trust step failed \u2014 run `lensmcp setup` directly in a terminal so sudo can prompt."),a;e(""),e("[3/3] pods \u2014 recycle this workspace's devservers, reconcile the gateway");const p=q(r).key,g=St(r,p,e);jt(c,r,p,e),await Xe(r,i,t.env??process.env,e);try{Ze(r,{...process.env,...t.env??{}},e)}catch{}return ze(r),e(""),e("\u2713 setup complete."),(g.killed>0||g.stale>0)&&e(` recycled ${g.killed} pod${g.killed===1?"":"s"}`+(g.stale>0?` (+${g.stale} stale socket${g.stale===1?"":"s"} cleared)`:"")+" \u2014 they come back on the new wiring."),e(" next: lensmcp gateway start (the :443 front door \u2014 needs sudo)"),e(" verify: lensmcp doctor"),{exitCode:0}}u(Qe,"Gn"),d(Qe,"runSetup");async function et(t,n,e){const s=A(n,{string:["--cwd","--hook","--timeout","--session"],boolean:["--json","--quiet"]}),o=P($(s.flags["--cwd"])??t.cwd),r=$(s.flags["--hook"]),c=Ee(r)?r:void 0,i=$(s.flags["--session"]);if(r!==void 0&&c===void 0)return{exitCode:0};const l=s.flags["--json"]===!0,a=s.flags["--quiet"]===!0||r!==void 0,p=Number($(s.flags["--timeout"])??""),g=Number.isFinite(p)&&p>0?p:Qt,v={...process.env,...t.env??{}};try{const m=await es({cwd:o,env:v,budgetMs:g,verify:d(f=>qt(f).map(C=>C.resource),"verify")});if(!m.available)return l&&!a?e(JSON.stringify({available:!1,workspace:m.source.workspace,mode:m.source.mode,reason:m.reason,elapsedMs:m.elapsedMs})):a||e(`lensmcp status \u2014 ${m.source.workspace}: unavailable (${m.reason})`),{exitCode:0};const y=Zt(m.status,m.evidence,Date.now()),k=Xt(y,{workspace:m.source.workspace,includeUnmonitored:c==="SessionStart"||!a});if(l)return a&&k===void 0?{exitCode:0}:(e(JSON.stringify({available:!0,workspace:m.source.workspace,mode:m.source.mode,url:m.source.url,aggregate:m.status.status??"unknown",elapsedMs:m.elapsedMs,findings:y.findings,suppressed:y.suppressed,unmonitored:y.unmonitored})),{exitCode:0});if(c){const f=zt(y.findings);return Ht(i,f)?(k!==void 0&&e(Pe(c,k)),i&&Gt(i,f),{exitCode:0}):{exitCode:0}}if(a)return k!==void 0&&e(k),{exitCode:0};if(e(`lensmcp status \u2014 ${m.source.workspace} (${m.source.mode}, ${m.source.url})`),e(` aggregate says: ${m.status.status??"unknown"} (read in ${m.elapsedMs}ms)`),y.findings.length===0)e(" nothing actionable that this command can vouch for.");else for(const f of y.findings)e(` \u2022 ${Yt(f)}`);for(const f of y.suppressed)e(` \xB7 not reported \u2014 ${f.kind}: ${f.reason}`);return y.unmonitored.length>0&&e(` \xB7 unmonitored (unknown, NOT clean): ${y.unmonitored.join(", ")}`),{exitCode:0}}catch(m){return a||e(`lensmcp status: unavailable (${m instanceof Error?m.message:String(m)})`),{exitCode:0}}}u(et,"Hn"),d(et,"runStatus");function tt(t,n,e){const s=A(n,{string:["--cwd","--hook","--plugin-root"],boolean:["--json","--quiet"]}),o=P($(s.flags["--cwd"])??t.cwd),r=$(s.flags["--hook"]),c=Ee(r)?r:void 0;if(r!==void 0&&c===void 0)return{exitCode:0};const i=s.flags["--json"]===!0,l=s.flags["--quiet"]===!0||r!==void 0,a=$(s.flags["--plugin-root"]);try{const p=Le({cwd:o,cliVersion:ee(),...a?{pluginRoot:a}:{}}),g=ts(p);if(i)return l&&!g?{exitCode:0}:(e(JSON.stringify({...p,skew:g??null})),{exitCode:0});if(c)return g&&e(Pe(c,Te(g))),{exitCode:0};if(l)return g&&e(Te(g)),{exitCode:0};if(e(`lensmcp version-check \u2014 ${p.pluginId??"lensmcp plugin"}`),e(` plugin ${p.plugin??"unknown"}${p.pluginSource?` (${p.pluginSource})`:""}`),e(` project ${p.project??"unknown"} (node_modules/lensmcp)`),e(` cli ${p.cli??"unknown"} (the binary running now)`),!g)return e(p.plugin&&(p.project??p.cli)?" \u2713 in step.":" ? cannot compare \u2014 one of the versions is unreadable (staying silent)."),{exitCode:0};e(` \u2717 the plugin is ${g.direction} \u2014 update it, then restart Claude Code:`);for(const v of rs(g))e(` ${v}`);return{exitCode:0}}catch(p){return l||e(`lensmcp version-check: unavailable (${p instanceof Error?p.message:String(p)})`),{exitCode:0}}}u(tt,"Jn"),d(tt,"runVersionCheck");async function st(t,n,e){const s=A(n,{string:["--cwd"],boolean:["--json"]}),o=P($(s.flags["--cwd"])??t.cwd),r=s.flags["--json"]===!0,c=[];nt(c),ot(o,c),rt(o,c),await it(o,t.env??process.env,c),at(o,c),ct(o,c),dt(o,c),lt(c);const i=c.some(a=>a.status==="fail"),l=c.some(a=>a.status==="warn");if(r)return e(JSON.stringify({ok:!i,summary:{total:c.length,failed:c.filter(a=>a.status==="fail").length,warnings:c.filter(a=>a.status==="warn").length},checks:c},null,2)),{exitCode:i?1:0};for(const a of c){const p=a.status==="pass"?"\u2713":a.status==="warn"?"!":"\u2717";e(`${p} ${a.label}${a.detail?` \u2014 ${a.detail}`:""}`),a.status!=="pass"&&a.hint&&e(` \u21B3 ${a.hint}`)}return e(""),e(i?"doctor: issues found (see \u2717 above).":l?"doctor: ok, with warnings (!).":"doctor: all checks passed."),{exitCode:i?1:0}}u(st,"Vn"),d(st,"runDoctor");function N(t,n,e,s,o){t.push({status:n,label:e,detail:s,hint:o})}u(N,"N"),d(N,"add");function nt(t){const n=Number(process.versions.node.split(".")[0]);n>=20?N(t,"pass",`Node ${process.versions.node}`):n>=18?N(t,"warn",`Node ${process.versions.node}`,"older than recommended","upgrade to Node 20+ for best results"):N(t,"fail",`Node ${process.versions.node}`,"too old","LensMCP needs Node 18+ (20+ recommended)")}u(nt,"Wn"),d(nt,"checkNode");function ot(t,n){const e=w(t,"nx.json");if(!b(e)){N(n,"fail","nx.json at workspace root","missing","run `lensmcp` from inside a Nx workspace");return}let s;try{s=JSON.parse(F(e,"utf8"))}catch(r){N(n,"fail","nx.json parses",r.message,"fix the JSON syntax in nx.json");return}const o=(s.plugins??[]).some(r=>typeof r=="string"?r==="@lensmcp/nx-plugin":!!r&&typeof r=="object"&&r.plugin==="@lensmcp/nx-plugin");N(n,o?"pass":"fail","@lensmcp/nx-plugin registered in nx.json#plugins",o?void 0:"not registered",o?void 0:"run `lensmcp install`"),N(n,s.lensmcp?.schemaVersion===1?"pass":"warn","lensmcp config block in nx.json",s.lensmcp?.schemaVersion===1?void 0:"missing",s.lensmcp?.schemaVersion===1?void 0:"run `lensmcp install` to write the config defaults")}u(ot,"Bn"),d(ot,"checkWorkspace");function rt(t,n){const e=w(t,".mcp.json");if(!b(e)){N(n,"warn",".mcp.json with a lensmcp server","missing","run `lensmcp install` so agents auto-discover the MCP server");return}try{const s=JSON.parse(F(e,"utf8")),o=!!s.mcpServers&&!!s.mcpServers.lensmcp;N(n,o?"pass":"warn",".mcp.json registers the lensmcp MCP server",o?void 0:"no lensmcp entry",o?void 0:"run `lensmcp install`")}catch(s){N(n,"warn",".mcp.json parses",s.message,"fix the JSON syntax in .mcp.json")}}u(rt,"Un"),d(rt,"checkMcpConfig");async function it(t,n,e){const s=le(t,n);if(N(e,"pass","MCP mode",s.mode==="shared"?`shared \u2014 sessions proxy into ${s.url}`:"embedded \u2014 one MCP server per agent session",s.mode==="shared"?void 0:"one shared server: `lensmcp setup --mcp shared`"),s.mode!=="shared")return;const o=await Ne(s.url);N(e,o.ok?"pass":"warn","shared MCP server",o.ok?`${o.server??"MCP"} @ ${s.url} (protocol ${o.protocolVersion??"?"})`:`not running at ${s.url} \u2014 ${o.error??"no answer"}`,o.ok?void 0:"the next agent session starts it on demand; `lensmcp gateway start` starts it now")}u(it,"qn"),d(it,"checkMcpMode");function we(t,n){const e=le(t,n);if(e.mode!=="shared")return{mode:e.mode,url:e.url,port:e.port,healthy:!1};const s=Jt(e.url);return{mode:e.mode,url:e.url,port:e.port,healthy:s.ok,...s.server?{server:s.server}:{},...s.error?{error:s.error}:{}}}u(we,"Pe"),d(we,"mcpStatusBlock");function at(t,n){const e=ie(t);N(n,e?"pass":"fail","lensmcp-mcp server bundle reachable",e??"missing",e?void 0:"dev: `yarn nx build @lensmcp/lensmcp-mcp`")}u(at,"Yn"),d(at,"checkServerBundle");function ct(t,n){const e=w(t,".lensmcp");N(n,b(e)?"pass":"warn",".lensmcp/ runtime directory",b(e)?void 0:"not created yet",b(e)?void 0:"created automatically on first `lensmcp mcp` run");const s=w(t,".gitignore"),o=(b(s)?D(s):"").split(`
|
|
173
|
+
`).some(r=>r.trim()===".lensmcp/"||r.trim()===".lensmcp");N(n,o?"pass":"warn",".gitignore excludes .lensmcp/",o?void 0:"not ignored",o?void 0:"add `.lensmcp/` to .gitignore (lensmcp install does this)")}u(ct,"zn"),d(ct,"checkRuntimeDir");function dt(t,n){const e=J(t,i=>/^vite\.config\.[cm]?[jt]s$/.test(i)),s=e.filter(i=>/lensmcpVitePlugin|@lensmcp\/vite-plugin/.test(D(i)));if(e.length>0){const i=s.length===e.length;N(n,i?"pass":"warn",`Vite wiring: ${s.length}/${e.length} config(s) use the LensMCP plugin`,void 0,i?void 0:"wire each: `nx g @lensmcp/nx-plugin:setup-vite <project>`")}const o=J(t,i=>i==="main.ts").filter(i=>/NestFactory\.create|createLensmcpNestApp/.test(D(i))),r=o.filter(i=>/createLensmcpNestApp/.test(D(i)));if(o.length>0){const i=r.length===o.length;N(n,i?"pass":"warn",`Nest wiring: ${r.length}/${o.length} bootstrap(s) use createLensmcpNestApp`,void 0,i?void 0:"wire each: `nx g @lensmcp/nx-plugin:setup-nest <project>`")}const c=J(t,i=>i==="project.json"||i==="package.json").filter(i=>/"agent-dev"/.test(D(i)));N(n,c.length>0?"pass":"warn",`agent-dev targets: ${c.length} project(s)`,void 0,c.length>0?void 0:"no project wired \u2014 run setup-vite / setup-nest")}u(dt,"Xn"),d(dt,"checkProjectWiring");function lt(t){const n=process.env.CHROME_PATH,e=process.platform==="darwin"?["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome","/Applications/Chromium.app/Contents/MacOS/Chromium"]:process.platform==="win32"?["C:/Program Files/Google/Chrome/Application/chrome.exe","C:/Program Files (x86)/Google/Chrome/Application/chrome.exe"]:["/usr/bin/google-chrome","/usr/bin/chromium-browser","/usr/bin/chromium","/usr/bin/google-chrome-stable"],s=(n&&b(n)?n:void 0)??e.find(o=>b(o));N(t,s?"pass":"warn","Chrome for browser:// capture",s?ne(s):"not found",s?void 0:"install Chrome or set CHROME_PATH (only needed for browser capture)")}u(lt,"Zn"),d(lt,"checkChrome");function D(t){try{return F(t,"utf8")}catch{return""}}u(D,"F"),d(D,"safeRead");function J(t,n,e=6,s=6e3){const o=new Set(["node_modules","dist","build",".git",".nx",".lensmcp","coverage","tmp",".cache",".yarn"]),r=[],c=[{dir:t,depth:0}];let i=0;for(;c.length>0;){const{dir:l,depth:a}=c.pop();let p;try{p=de(l,{withFileTypes:!0})}catch{continue}for(const g of p){if(i++>s)return r;if(g.isDirectory()){if(o.has(g.name)||g.name.startsWith("."))continue;a<e&&c.push({dir:w(l,g.name),depth:a+1})}else g.isFile()&&n(g.name)&&r.push(w(l,g.name))}}return r}u(J,"V"),d(J,"walkProjectFiles");function ut(t,n,e,s){const o=A(n,{string:["--cwd","--from","--scope"]}),r=(o.positional[0]??"").toLowerCase();if(r!=="install"&&r!=="status")return s("Usage: lensmcp plugin <install|status> [claude|codex] [--from <dir>] [--scope <user|project|local>] [--cwd <dir>]"),{exitCode:2};const c=P($(o.flags["--cwd"])??t.cwd),i=o.positional[1]?.toLowerCase(),l=i?is(i):void 0;if(i&&!l)return s(`Unknown agent: ${i} (use claude | codex)`),{exitCode:2};const a=l?[l]:[...Oe],p=$(o.flags["--from"]),g=us({cwd:c,...p?{from:p}:{},selfRoot:Q()});if(!g)return s("Could not find a lensmcp package to install the plugins from."),s("Run inside a workspace with lensmcp installed, or pass --from <package-root>."),{exitCode:1};e(`plugin package: ${g.root}${g.version?` (lensmcp ${g.version})`:""}`+(g.source==="self"?" \u2014 the running CLI itself (no workspace install found; prefer a workspace)":""));const v=Ae({...process.env,...t.env??{}});if(r==="status"){for(const k of a){const f=ls(g.root,k),C=k.id==="claude"?Me(c)?.version:as(v),h=C?f?C===f?`installed ${C} \u2014 current`:`installed ${C}, the package ships ${f} \u2014 \`lensmcp plugin install ${k.id}\``:`installed ${C} (this package ships no ${k.label} plugin \u2014 it arrived in ${k.since})`:`not installed \u2014 \`lensmcp plugin install ${k.id}\``;e(` ${k.label.padEnd(11)} \u2192 ${h}`)}return{exitCode:0}}const m=$(o.flags["--scope"]);let y=!1;for(const k of a){const f=k.id==="claude"?m??Me(c)?.scope:void 0,C=cs(k,{root:g.root,...f?{scope:f}:{}},{run:v});switch(C.outcome){case"installed":e(` ${k.label.padEnd(11)} \u2192 installed${C.version?` ${C.version}`:""} \u2014 ${C.restartHint}`);break;case"skipped-no-cli":l&&(y=!0),e(` ${k.label.padEnd(11)} \u2192 skipped (${C.detail})`);break;case"skipped-not-packaged":l&&(y=!0),e(` ${k.label.padEnd(11)} \u2192 skipped \u2014 ${C.detail}`);break;case"failed":y=!0,s(` ${k.label.padEnd(11)} \u2192 FAILED \u2014 ${C.detail}`),s(` run by hand:${C.commands.map(h=>`
|
|
174
|
+
${h}`).join("")}`);break}}return{exitCode:y?1:0}}u(ut,"Qn"),d(ut,"runPlugin");function ve(){const t=_("GET","/status");if(t?.status!==200||!t.json||typeof t.json!="object")return{registeredKeys:[]};const n=t.json;return{...typeof n.daemon?.wsKey=="string"?{wsKey:n.daemon.wsKey}:{},...typeof n.daemon?.version=="string"?{version:n.daemon.version}:{},registeredKeys:(n.workspaces??[]).flatMap(e=>typeof e.key=="string"?[e.key]:[])}}u(ve,"Ee"),d(ve,"snapshotDaemon");function pt(t){let n=P(t);for(let e=0;e<8;e++){const s=w(n,"package.json");if(b(s)&&/"(?:lensmcp|@lensmcp\/[^"]+)"\s*:/.test(D(s)))return{key:Kt(n).key,root:n};const o=R(n);if(o===n)break;n=o}}u(pt,"et"),d(pt,"findLensmcpWorkspace");function gt(t,n,e){if(t){const r=t.replace(/^v/,"");if(!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(r)){e(`Not a version: ${t} (expected e.g. 1.20.1)`);return}return r}const s=n("npm",["view","lensmcp","version"]),o=s&&s.code===0?(s.stdout??"").trim().split(`
|
|
175
|
+
`).filter(Boolean).pop():void 0;if(!o||!/^\d+\.\d+\.\d+/.test(o)){e("Could not resolve the latest lensmcp version from the npm registry \u2014 pass one explicitly: `lensmcp upgrade 1.20.1`.");return}return o}u(gt,"nt"),d(gt,"resolveTargetVersion");async function ht(t,n,e,s){const o=A(n,{string:["--cwd"],boolean:["--dry-run","--no-install","--no-plugins","--no-global","--no-restart"]}),r=P($(o.flags["--cwd"])??t.cwd),c=o.flags["--dry-run"]===!0,i={...process.env,...t.env??{}},l=Ae(i),a=gt(o.positional[0],l,s);if(!a)return{exitCode:1};const p=fs(),g=pt(r),v=[],m=new Set;for(const h of[...g?[g]:[],...p])m.has(h.root)||(m.add(h.root),v.push(h));if(v.length===0)return s("No LensMCP workspace found: ~/.lensmcp/workspaces.json is empty and no package.json above the current directory depends on lensmcp."),{exitCode:1};const y=ve();e(`upgrade \u2192 lensmcp ${a}${c?" (dry run \u2014 nothing will be changed)":""}`),y.wsKey&&e(` daemon \u2192 '${y.wsKey}' on :443${y.version?`, running ${y.version}`:""}, hosting ${y.registeredKeys.length} workspace${y.registeredKeys.length===1?"":"s"}`);const k=[];let f=0;for(const h of v){const S=w(h.root,"package.json"),E=D(S);if(!E){f++,s(` ${h.key}: cannot read ${S} \u2014 skipped`);continue}const M=ms(E,a);for(const T of M.skipped.filter(x=>x.reason==="non-version-spec"))e(` ${h.key}: left ${T.section}.${T.name} at '${T.spec}' (not a plain version \u2014 a link:/workspace: pin is deliberate)`);if(M.changes.length===0){const T=ye(h.root);e(` ${h.key}: package.json already at ${a}${T&&T!==a?` \u2014 but node_modules holds ${T}; run its install`:""}`);continue}if(e(` ${h.key}: ${M.changes.length} dep${M.changes.length===1?"":"s"} \u2192 ${a} (${M.changes.map(T=>T.name).join(", ")})`),c){k.push(h);continue}if(K(S,M.text),o.flags["--no-install"]===!0){k.push(h),e(" install skipped (--no-install) \u2014 run it before restarting anything.");continue}const O=gs(h.root);e(` ${O.name} ${O.install.join(" ")} (${h.root})`);const L=j(O.name,O.install,{cwd:h.root,stdio:"inherit",env:i});if((L.status??1)!==0){f++,s(` ${h.key}: \`${O.name} ${O.install.join(" ")}\` exited ${L.status??"by signal"} \u2014 fix it and re-run \`lensmcp upgrade ${a}\` (the rewrite is already in place).`);continue}k.push(h)}if(o.flags["--no-global"]!==!0){const h=ps(l);h&&h.version!==a?c?e(` global \u2192 ${h.version} \u2192 ${a} (npm i -g lensmcp@${a})`):(e(` global \u2192 ${h.version} \u2192 ${a}`),(j("npm",["install","-g",`lensmcp@${a}`],{stdio:"inherit",env:i,timeout:3e5}).status??1)!==0&&(f++,s(` the global install failed \u2014 run by hand: npm i -g lensmcp@${a}`))):h&&e(` global \u2192 already ${h.version}`)}if(o.flags["--no-plugins"]!==!0){const h=[y.wsKey?v.find(S=>S.key===y.wsKey):void 0,...k,...v].find(S=>!!S&&b(w(S.root,"node_modules","lensmcp","package.json")));h?c?e(` plugins \u2192 would re-install Claude Code + Codex from ${w(h.root,"node_modules","lensmcp")}`):mt(w(h.root,"node_modules","lensmcp"),r,i,e,s):e(" plugins \u2192 skipped (no workspace has lensmcp installed to point the marketplaces at)")}if(o.flags["--no-restart"]===!0)return e(" restart \u2192 skipped (--no-restart) \u2014 the daemon keeps serving the OLD code until `lensmcp gateway restart` runs in its workspace."),{exitCode:f>0?1:0};const C=hs({...y.wsKey?{daemonWsKey:y.wsKey}:{},...y.version?{daemonVersion:y.version}:{},targetVersion:a,registeredKeys:y.registeredKeys,known:p,upgraded:k});if(C.steps.length===0)return e(` restart \u2192 nothing to do${C.note?` (${C.note})`:""}`),{exitCode:f>0?1:0};for(const h of C.steps){if(c){e(` restart \u2192 would ${h.action==="restart"?`restart the daemon ('${h.key}')`:`re-register '${h.key}'`}${h.reason==="rehost"?" (not upgraded \u2014 re-hosted so the daemon restart does not drop it)":h.reason==="stale-daemon"?" (its files are current but the process still runs the old bundle)":""}`);continue}if(h.action==="restart"){if(e(` restart \u2192 gateway daemon ('${h.key}') \u2026`),ue({...t,cwd:h.root},["restart"],e,s).exitCode!==0){f++,s(` the daemon restart ('${h.key}') failed \u2014 fix it, then \`lensmcp gateway start\` in each guest workspace.`);break}if(!await ft(18e4)){f++,s(" the restarted daemon did not answer within 3 minutes \u2014 check `lensmcp gateway status`; guests need `lensmcp gateway start` once it is up.");break}}else{const S=_("POST","/register",re(h.root,h.key));if(S&&S.status===200){try{U(w(h.root,".lensmcp"),{recursive:!0}),K(w(h.root,".lensmcp","registered.json"),JSON.stringify({wsKey:h.key}))}catch{}e(` restart \u2192 re-registered '${h.key}'${h.reason==="rehost"?" (not upgraded \u2014 re-hosted after the daemon restart)":""}`)}else f++,s(` re-registering '${h.key}' failed \u2014 run \`lensmcp gateway start\` in ${h.root}.`)}}if(!c){const h=ve();h.version&&e(h.version===a?` daemon \u2192 now running ${h.version} \u2713`:` daemon \u2192 still reports ${h.version} (expected ${a}) \u2014 check \`lensmcp gateway status\`.`)}return{exitCode:f>0?1:0}}u(ht,"tt"),d(ht,"runUpgrade");async function ft(t){const n=Date.now()+t;for(;;){if(_("GET","/list")?.status===200)return!0;if(Date.now()>=n)return!1;await new Promise(e=>setTimeout(e,2e3))}}u(ft,"st"),d(ft,"waitForDaemon");function mt(t,n,e,s,o){const r=d((c,i,l)=>{try{const a=j(c,i,{encoding:"utf8",timeout:12e4,env:e,stdio:["ignore","pipe","pipe"],...l?{cwd:l}:{}});return a.error&&a.error.code==="ENOENT"?{ok:!1,detail:`\`${c}\` is not on PATH`}:(a.status??1)!==0?{ok:!1,detail:`${a.stderr??""}
|
|
176
|
+
${a.stdout??""}`.trim().split(`
|
|
177
|
+
`).filter(Boolean).pop()||`exited ${a.status}`}:{ok:!0}}catch(a){return{ok:!1,detail:a.message}}},"exec");for(const c of Oe){if(!b(w(t,c.manifest))){s(` plugins \u2192 ${c.label} skipped \u2014 ${t} ships no ${c.manifest} (the plugin arrived in ${c.since})`);continue}const i=r(c.bin,["plugin","marketplace","add",t]);if(!i.ok){s(` plugins \u2192 ${c.label} skipped (${i.detail})`);continue}if(c.id==="codex"){const a=r(c.bin,["plugin","add",oe]);s(a.ok?` plugins \u2192 Codex installed \u2014 ${c.restartHint}`:` plugins \u2192 Codex install failed (${a.detail}) \u2014 run: codex plugin add ${oe}`);continue}r(c.bin,["plugin","marketplace","update",ds]);const l=ss();if(l.length===0){const a=r(c.bin,["plugin","install",oe,"--yes"]);s(a.ok?` plugins \u2192 Claude Code installed \u2014 ${c.restartHint}`:` plugins \u2192 Claude Code install failed (${a.detail}) \u2014 run: claude plugin install ${oe} --yes`);continue}for(const a of l){const p=a.scope&&a.scope!=="user"?["--scope",a.scope]:[],g=r(c.bin,["plugin","update",a.id,...p],a.projectPath??n),v=a.projectPath?` (${a.projectPath})`:"";g.ok?s(` plugins \u2192 Claude Code updated${v} \u2014 ${c.restartHint}`):(o(` plugins \u2192 Claude Code update failed${v}: ${g.detail}`),o(` run by hand${a.projectPath?` from ${a.projectPath}`:""}: claude plugin update ${a.id}${p.length>0?` --scope ${a.scope}`:""}`))}}}u(mt,"rt"),d(mt,"syncAgentPlugins");function Q(){let t=R(Ft(import.meta.url));for(let n=0;n<4;n++){if(b(w(t,"package.json")))return t;const e=R(t);if(e===t)break;t=e}return t}u(Q,"ee"),d(Q,"cliPackageRoot");function ye(t){try{const n=JSON.parse(F(w(t,"node_modules","lensmcp","package.json"),"utf8"));return typeof n.version=="string"?n.version:void 0}catch{return}}u(ye,"Te"),d(ye,"readProjectLensmcpVersion");function ee(){try{return JSON.parse(F(w(Q(),"package.json"),"utf8")).version}catch{return"0.0.0"}}u(ee,"ne"),d(ee,"readCliVersion");function wt(t,n,e){const s=P(t.cwd),o=vt(s);return o?{exitCode:j(process.execPath,[o,...n],{stdio:"inherit",cwd:s,env:{...process.env,...t.env}}).status??1}:(e(`Could not locate the lensmcp rollout helper (@lensmcp/cluster).
|
|
144
178
|
Looked for main.rollout.js (walking up from cwd):
|
|
145
179
|
\u2022 <dir>/node_modules/@lensmcp/cluster/executors/gateway/main.rollout.js
|
|
146
180
|
\u2022 <dir>/dist/libs/cluster/executors/gateway/main.rollout.js
|
|
147
|
-
Install @lensmcp/cluster, or \`yarn nx build @lensmcp/cluster\` in the workspace.`),{exitCode:1})}
|
|
148
|
-
`)[0]?.trim());return Number.isInteger(e)&&e>0?e:void 0}catch{return}}
|
|
149
|
-
`).find(e=>e.startsWith("n"));return n&&n.slice(1).trim()||void 0}catch{return}}
|
|
181
|
+
Install @lensmcp/cluster, or \`yarn nx build @lensmcp/cluster\` in the workspace.`),{exitCode:1})}u(wt,"ot"),d(wt,"runRollout");function vt(t){let n=t;for(let e=0;e<6;e++){for(const o of[w(n,"node_modules","@lensmcp","cluster","executors","gateway","main.rollout.js"),w(n,"dist","libs","cluster","executors","gateway","main.rollout.js")])if(b(o))return o;const s=R(n);if(s===n)break;n=s}}u(vt,"it"),d(vt,"findRolloutBundle");function ie(t){return ae(t,"main.js")}u(ie,"ae"),d(ie,"findMcpBinary");function yt(t){return ae(t,"shim.js")}u(yt,"at"),d(yt,"findShimBinary");function ae(t,n){const e=w(Q(),"bundled",n);if(b(e))return e;const s=w(t,"servers","lensmcp-mcp","dist",n);if(b(s))return s;let o=t;for(let r=0;r<5;r++){const c=w(o,"servers","lensmcp-mcp","dist",n);if(b(c))return c;const i=R(o);if(i===o)break;o=i}}u(ae,"ce"),d(ae,"findServerBundle");function kt(t){const n=w(Q(),"bundled","bridge.js");if(b(n))return n;let e=t;for(let s=0;s<5;s++){const o=w(e,"libs","bridge","dist","main.js");if(b(o))return o;const r=R(e);if(r===e)break;e=r}}u(kt,"ct"),d(kt,"findBridgeBundle");function te(t){let n=t;for(let e=0;e<5;e++){const s=w(n,"node_modules",".bin","nx");if(b(s))return s;const o=R(n);if(o===n)break;n=o}}u(te,"te"),d(te,"findNxBinary");function se(t){const n=w(t,".lensmcp");b(n)||U(n,{recursive:!0})}u(se,"se"),d(se,"ensureLensmcpDir");function $t(){return!!(process.stdin.isTTY&&process.stdout.isTTY)}u($t,"dt"),d($t,"isInteractive");function ce(){const t=It(),n=[],e=d((s,o)=>{try{for(const r of de(s,{withFileTypes:!0})){if(!r.isDirectory()||!r.name.endsWith("-devserver"))continue;const c=w(s,r.name,"parent.sock");b(c)&&n.push({service:r.name.slice(0,-10),wsKey:o,sock:c})}}catch{}},"scan");e(t,"");try{for(const s of de(t,{withFileTypes:!0}))s.isDirectory()&&/^[a-z0-9][a-z0-9-]*$/.test(s.name)&&!s.name.endsWith("-devserver")&&e(w(t,s.name),s.name)}catch{}return n.sort((s,o)=>`${s.wsKey}/${s.service}`.localeCompare(`${o.wsKey}/${o.service}`))}u(ce,"de"),d(ce,"discoverDevserverSocks");const H=d(t=>t.wsKey?`${t.wsKey}/${t.service}`:t.service,"fullServiceName");function ke(t){try{const n=j("lsof",["-t",t],{encoding:"utf8"}),e=Number((n.stdout??"").split(`
|
|
182
|
+
`)[0]?.trim());return Number.isInteger(e)&&e>0?e:void 0}catch{return}}u(ke,"Me"),d(ke,"pidOnSocket");function $e(t){try{const n=j("ps",["-o","pgid=","-p",String(t)],{encoding:"utf8"}),e=Number((n.stdout??"").trim());return Number.isInteger(e)&&e>1?e:void 0}catch{return}}u($e,"Le"),d($e,"pgidOf");function bt(t){try{const n=(j("lsof",["-a","-p",String(t),"-d","cwd","-Fn"],{encoding:"utf8"}).stdout??"").split(`
|
|
183
|
+
`).find(e=>e.startsWith("n"));return n&&n.slice(1).trim()||void 0}catch{return}}u(bt,"lt"),d(bt,"cwdOfPid");function Ct(t,n){const e=Se(P(t),P(n));return e===""||!e.startsWith("..")&&!Dt(e)}u(Ct,"pt"),d(Ct,"isInside");function xt(t,n){return ce().filter(e=>{if(e.wsKey)return e.wsKey===n;const s=ke(e.sock),o=s!==void 0?bt(s):void 0;return o!==void 0&&Ct(t,o)})}u(xt,"ut"),d(xt,"workspaceDevservers");function St(t,n,e){const s=xt(t,n);if(s.length===0)return e(" no devserver pods running for this workspace \u2014 nothing to recycle."),{killed:0,stale:0};const o=new Set;for(const i of[process.pid,...G()]){const l=$e(i);l!==void 0&&o.add(l)}let r=0,c=0;for(const i of s){const l=H(i),a=ke(i.sock);if(a===void 0){W(i.sock),e(` \u2713 ${l} \u2014 stale socket removed (no process held it)`),c++;continue}const p=$e(a),g=p!==void 0&&!o.has(p),v=d(m=>{try{g?process.kill(-p,m):process.kill(a,m)}catch{}},"signal");v("SIGTERM"),be(a,5e3)||(v("SIGKILL"),be(a,2e3)),W(i.sock),e(` \u2713 ${l} \u2014 stopped (pid ${a}${g?`, group ${p}`:", parent only"})`),r++}return{killed:r,stale:c}}u(St,"ft"),d(St,"reapWorkspacePods");function be(t,n){const e=Date.now()+n;for(;;){if(!z(t))return!0;if(Date.now()>=e)return!1;j("sleep",["0.15"])}}u(be,"Oe"),d(be,"waitForPidGone");function jt(t,n,e,s){const o=w(n,".lensmcp","gateway.pid"),r=w(n,".lensmcp","registered.json"),{pid:c,healed:i}=X(o,n);if(i&&s(` healed: adopted a running gateway (pid ${c}) the pid file had lost.`),Y(n,t.env,p=>s(` ${p}`)),V(n)!==void 0){s(` gateway: this workspace owns the :443 daemon (pid ${c??"?"}) \u2014 left running, pods respawn fresh.`);return}const l=_("GET","/list");if(l?.status!==200){s(" gateway: not running. Start it when you are ready: lensmcp gateway start");return}if(l.json&&typeof l.json=="object"&&(l.json.workspaces??[]).some(p=>p.key===e)){U(R(r),{recursive:!0}),K(r,JSON.stringify({wsKey:e})),s(` gateway: '${e}' is already registered into the shared daemon \u2014 left serving.`);return}const a=_("POST","/register",re(n,e));if(a&&a.status===200){U(R(r),{recursive:!0}),K(r,JSON.stringify({wsKey:e})),s(` gateway: registered '${e}' into the shared daemon \u2014 it now hosts this workspace.`);return}s(" gateway: a shared daemon is up but registering this workspace failed \u2014 run `lensmcp gateway start`.")}u(jt,"gt"),d(jt,"reconcileGatewayAfterSetup");function Ce(t,n,e,s){let o=n.filter(r=>r.service===t||H(r)===t);if(o.length>1)try{const r=q(e).key,c=o.filter(i=>i.wsKey===r);c.length===1&&(o=c)}catch{}return o.length===0?(s(`No running service named "${t}".`),n.length>0&&s(`Running: ${n.map(H).join(", ")}`),null):o.length>1?(s(`"${t}" is ambiguous \u2014 use one of: ${o.map(H).join(", ")}`),null):o[0]}u(Ce,"Re"),d(Ce,"resolveServiceSock");async function Nt(t,n,e,s){const o=A(n,{string:["--tail","--cwd"],boolean:["--no-follow","--plain","--color"]}),r=o.positional[0],c=ce();if(!r){if(c.length===0)e("No running services found (no devserver control sockets under $TMPDIR)."),e("Start one with `yarn nx serve <project>` or `lensmcp gateway start`.");else{e("Running services \u2014 attach with `lensmcp logs <service>`:");for(const m of c)e(` ${H(m)}`)}return{exitCode:0}}const i=Ce(r,c,P($(o.flags["--cwd"])??t.cwd),s);if(!i)return{exitCode:1};const l=o.flags["--no-follow"]!==!0,a=Number($(o.flags["--tail"])??NaN),p=Number.isInteger(a)&&a>=0?a:100,g=o.flags["--plain"]===!0||o.flags["--color"]!==!0&&!process.stdout.isTTY,v=`/webpack/logs?tail=${p}&follow=${l?1:0}${g?"&plain=1":""}`;return new Promise(m=>{const y=xe({socketPath:i.sock,path:v,method:"GET"},k=>{k.on("data",f=>process.stdout.write(f)),k.on("end",()=>m({exitCode:0})),k.on("error",()=>m({exitCode:1}))});y.on("error",k=>{s(`Cannot attach to ${H(i)}: ${k.message}`),s("The devserver may have crashed leaving a stale socket \u2014 restart the service and retry."),m({exitCode:1})}),y.end()})}u(Nt,"ht"),d(Nt,"runLogs");async function Pt(t,n,e,s){const o=A(n,{string:["--port","--cwd"]}),r=o.positional[0],c=ce();if(!r)return s("Usage: lensmcp debug <service> [--port <base>]"),c.length>0&&s(`Running: ${c.map(H).join(", ")}`),{exitCode:2};const i=Ce(r,c,P($(o.flags["--cwd"])??t.cwd),s);if(!i)return{exitCode:1};const l=Number($(o.flags["--port"])??"")||void 0,a=`/webpack/debug${l?`?port=${l}`:""}`,p=await new Promise(v=>{const m=xe({socketPath:i.sock,path:a,method:"POST"},y=>{let k="";y.on("data",f=>k+=f),y.on("end",()=>v(k)),y.on("error",()=>v(null))});m.on("error",()=>v(null)),m.end()});if(p===null)return s(`Cannot reach ${H(i)} \u2014 the devserver may have crashed leaving a stale socket.`),{exitCode:1};let g;try{g=JSON.parse(p)}catch{return s(`Unexpected response from the devserver: ${p.slice(0,200)}`),{exitCode:1}}e(`Inspectors open on ${H(i)} \u2014 in place, no restart:`);for(const v of g.pods??[])e(` pod #${v.id} ${v.inspectorUrl??"(no url reported \u2014 check lensmcp logs)"}`);for(const v of g.workers??[])e(` worker ${v.name} 127.0.0.1:${v.port}`);return e(`Attach WebStorm (Run \u2192 Attach to Node.js/Chrome) to 127.0.0.1:${g.basePort??"?"} \u2014 or click the "Debugger listening" link in \`lensmcp logs ${r}\`.`),{exitCode:0}}u(Pt,"mt"),d(Pt,"runDebug");function $(t){if(t!==void 0)return typeof t=="string"?t:void 0}u($,"k"),d($,"stringFlag");function A(t,n={}){const e=new Set(n.string??[]),s=new Set(n.boolean??[]),o={},r=[];for(let c=0;c<t.length;c++){const i=t[c];if(i.startsWith("--")){const l=i.indexOf("="),a=l===-1?i:i.slice(0,l);if(s.has(a))o[a]=!0;else if(e.has(a))if(l!==-1)o[a]=i.slice(l+1);else{const p=t[c+1];p===void 0||p.startsWith("--")?o[a]="":(o[a]=p,c++)}else o[a]=l===-1?!0:i.slice(l+1)}else r.push(i)}return{flags:o,positional:r}}u(A,"O"),d(A,"parseFlags");
|
package/lib/plugin-version.d.ts
CHANGED
|
@@ -17,6 +17,13 @@ export interface InstalledPlugin {
|
|
|
17
17
|
* file is only ever READ.
|
|
18
18
|
*/
|
|
19
19
|
export declare function readInstalledPlugin(cwd: string, home?: string): InstalledPlugin | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* EVERY recorded lensmcp install, not just the one that best describes a cwd.
|
|
22
|
+
* `lensmcp upgrade` needs the full list: the plugin can be installed per-project
|
|
23
|
+
* at several roots (observed live: project-scope installs for two workspaces),
|
|
24
|
+
* and updating only "the best match" leaves the others on the old version.
|
|
25
|
+
*/
|
|
26
|
+
export declare function readInstalledPlugins(home?: string): InstalledPlugin[];
|
|
20
27
|
export interface VersionFacts {
|
|
21
28
|
/**
|
|
22
29
|
* The plugin actually loaded, read from `${CLAUDE_PLUGIN_ROOT}` when a hook
|
package/lib/plugin-version.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
"use strict";var
|
|
2
|
-
`)}
|
|
3
|
-
`)[0]||`\`${r}\` exited ${
|
|
1
|
+
"use strict";var h=Object.defineProperty;var s=(t,n)=>h(t,"name",{value:n,configurable:!0});var j=Object.defineProperty,i=s((t,n)=>j(t,"name",{value:n,configurable:!0}),"i");import{spawnSync as x}from"node:child_process";import{existsSync as k,readFileSync as v}from"node:fs";import{homedir as m}from"node:os";import{join as a}from"node:path";export const PLUGIN_NAME="lensmcp";function d(t){try{return JSON.parse(v(t,"utf8"))}catch{return}}s(d,"a"),i(d,"readJson");function g(t){const n=d(t)?.version;return typeof n=="string"&&n.length>0?n:void 0}s(g,"g"),i(g,"readVersionField");export function readInstalledPlugin(t,n=m()){const e=readInstalledPlugins(n);if(e.length!==0)return e.find(r=>r.projectPath===t)??e.find(r=>r.scope==="user")??e[0]}s(readInstalledPlugin,"readInstalledPlugin"),i(readInstalledPlugin,"readInstalledPlugin");export function readInstalledPlugins(t=m()){const n=d(a(t,".claude","plugins","installed_plugins.json"))?.plugins;if(!n||typeof n!="object")return[];const e=[];for(const[r,o]of Object.entries(n)){const[u,c]=r.split("@");if(!(u!==PLUGIN_NAME||!c))for(const l of Array.isArray(o)?o:[o]){if(typeof l!="object"||l===null)continue;const p=l;typeof p.version=="string"&&e.push({id:r,marketplace:c,version:p.version,...typeof p.scope=="string"?{scope:p.scope}:{},...typeof p.projectPath=="string"?{projectPath:p.projectPath}:{}})}}return e}s(readInstalledPlugins,"readInstalledPlugins"),i(readInstalledPlugins,"readInstalledPlugins");export function readVersions(t){const n={};if(t.pluginRoot){const o=g(a(t.pluginRoot,".claude-plugin","plugin.json"));o&&(n.plugin=o,n.pluginSource="plugin-root")}const e=readInstalledPlugin(t.cwd,t.home);e&&(n.pluginId=e.id,n.pluginMarketplace=e.marketplace,e.scope&&(n.pluginScope=e.scope),n.plugin||(n.plugin=e.version,n.pluginSource="install-index"));const r=g(a(t.cwd,"node_modules","lensmcp","package.json"));return r&&(n.project=r),t.cliVersion&&t.cliVersion!=="0.0.0"&&(n.cli=t.cliVersion),n}s(readVersions,"readVersions"),i(readVersions,"readVersions");export function compareVersions(t,n){const e=i(u=>{const c=u.trim().replace(/^v/,"").split(/[-+]/)[0];if(c===void 0)return;const l=c.split(".");if(l.length===0||l.length>3)return;const p=l.map(f=>/^\d+$/.test(f)?Number(f):Number.NaN);return p.some(Number.isNaN)?void 0:[p[0]??0,p[1]??0,p[2]??0]},"parse"),r=e(t),o=e(n);if(!(!r||!o)){for(let u=0;u<3;u++){const c=(r[u]??0)-(o[u]??0);if(c!==0)return c<0?-1:1}return 0}}s(compareVersions,"compareVersions"),i(compareVersions,"compareVersions");export function detectSkew(t){const n=t.plugin,e=t.project??t.cli;if(!n||!e||n===e)return;const r=compareVersions(n,e);if(r===void 0||r===0)return;const o=t.cli&&t.project&&t.cli!==t.project?{cli:t.cli,project:t.project}:void 0;return{mismatch:!0,plugin:n,expected:e,expectedSource:t.project?"project":"cli",direction:r<0?"behind":"ahead",...t.pluginId?{pluginId:t.pluginId}:{},...t.pluginMarketplace?{marketplace:t.pluginMarketplace}:{},...t.pluginScope?{scope:t.pluginScope}:{},...o?{cliSkew:o}:{}}}s(detectSkew,"detectSkew"),i(detectSkew,"detectSkew");export function updateCommands(t){const n=t.pluginId??PLUGIN_NAME,e=t.scope&&t.scope!=="user"?` --scope ${t.scope}`:"";return[...t.marketplace?[`claude plugin marketplace update ${t.marketplace}`]:[],`claude plugin update ${n}${e}`]}s(updateCommands,"updateCommands"),i(updateCommands,"updateCommands");export function formatSkewContext(t){const n=[`LensMCP plugin is out of step: plugin ${t.plugin} vs lensmcp ${t.expected} (${t.expectedSource==="project"?"this project\u2019s node_modules":"the running CLI"}). The plugin\u2019s skills, hooks and wiring are from ${t.plugin} \u2014 update before relying on them:`,...updateCommands(t).map(e=>` ${e}`)," \u2026then restart Claude Code for the new plugin to load."];return t.cliSkew&&n.push(` (also: the lensmcp CLI on PATH is ${t.cliSkew.cli} while this project depends on ${t.cliSkew.project}.)`),n.join(`
|
|
2
|
+
`)}s(formatSkewContext,"formatSkewContext"),i(formatSkewContext,"formatSkewContext");export function syncPlugin(t){const n=detectSkew(t.facts);if(!n)return{outcome:t.facts.plugin!==void 0&&(t.facts.project??t.facts.cli)!==void 0?"current":"unknown",...t.facts.plugin?{from:t.facts.plugin}:{}};const e=updateCommands(n);for(const r of e){const[o,...u]=r.split(" "),c=o===void 0?void 0:t.run(o,u);if(!c)return{outcome:"manual",from:n.plugin,to:n.expected,commands:e,detail:`\`${o??"claude"}\` is not on PATH`};if(c.code!==0)return{outcome:"manual",from:n.plugin,to:n.expected,commands:e,detail:(c.stderr??"").trim().split(`
|
|
3
|
+
`)[0]||`\`${r}\` exited ${c.code}`}}return{outcome:"updated",from:n.plugin,to:n.expected,commands:e}}s(syncPlugin,"syncPlugin"),i(syncPlugin,"syncPlugin");export function spawnRunner(t,n=6e4){return(e,r)=>{try{const o=x(e,r,{encoding:"utf8",timeout:n,env:t,stdio:["ignore","pipe","pipe"]});return o.error&&o.error.code==="ENOENT"?void 0:{code:o.status??1,...o.stderr?{stderr:o.stderr}:{}}}catch{return}}}s(spawnRunner,"spawnRunner"),i(spawnRunner,"spawnRunner");export const pluginManifestPath=i(t=>a(t,".claude-plugin","plugin.json"),"pluginManifestPath"),isPluginRoot=i(t=>k(pluginManifestPath(t)),"isPluginRoot");
|
package/lib/upgrade.d.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/** The sections a version bump belongs in. `peerDependencies` is deliberately absent:
|
|
2
|
+
* a peer range states what a package is COMPATIBLE with, which an upgrade does not decide. */
|
|
3
|
+
export declare const UPGRADE_SECTIONS: readonly ["dependencies", "devDependencies", "optionalDependencies", "resolutions", "overrides"];
|
|
4
|
+
/** `lensmcp` itself and everything under the scope. */
|
|
5
|
+
export declare function isLensmcpPackage(name: string): boolean;
|
|
6
|
+
export interface WorkspaceTarget {
|
|
7
|
+
key: string;
|
|
8
|
+
root: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ReadWorkspacesOptions {
|
|
11
|
+
home?: string;
|
|
12
|
+
exists?: (path: string) => boolean;
|
|
13
|
+
readFile?: (path: string) => string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The workspaces LensMCP knows about — `~/.lensmcp/workspaces.json`, the registry
|
|
17
|
+
* each gateway upserts on start.
|
|
18
|
+
*
|
|
19
|
+
* Entries survive a machine reboot and a repo move, so a root that no longer holds
|
|
20
|
+
* a `package.json` is dropped rather than reported as a target: the alternative is
|
|
21
|
+
* an upgrade that claims to have covered a workspace it never touched.
|
|
22
|
+
*/
|
|
23
|
+
export declare function readConnectedWorkspaces(options?: ReadWorkspacesOptions): WorkspaceTarget[];
|
|
24
|
+
export interface DepChange {
|
|
25
|
+
name: string;
|
|
26
|
+
section: string;
|
|
27
|
+
from: string;
|
|
28
|
+
to: string;
|
|
29
|
+
}
|
|
30
|
+
export interface SkippedDep {
|
|
31
|
+
name: string;
|
|
32
|
+
section: string;
|
|
33
|
+
spec: string;
|
|
34
|
+
reason: 'non-version-spec' | 'already-current';
|
|
35
|
+
}
|
|
36
|
+
export interface RewriteResult {
|
|
37
|
+
text: string;
|
|
38
|
+
changes: DepChange[];
|
|
39
|
+
skipped: SkippedDep[];
|
|
40
|
+
}
|
|
41
|
+
/** `^1.19.0` → `^`; `1.19.0` → ``; anything else → undefined (leave it alone). */
|
|
42
|
+
export declare function versionPrefix(spec: string): string | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Rewrite every LensMCP dependency in a package.json TEXT to `version`.
|
|
45
|
+
*
|
|
46
|
+
* Returns the text unchanged when nothing matches, so a caller can treat
|
|
47
|
+
* `changes.length === 0` as "this workspace was already there" without diffing.
|
|
48
|
+
*/
|
|
49
|
+
export declare function rewriteLensmcpDeps(text: string, version: string): RewriteResult;
|
|
50
|
+
export interface PackageManager {
|
|
51
|
+
name: 'yarn' | 'npm' | 'pnpm' | 'bun';
|
|
52
|
+
/** The argv that reconciles node_modules with the edited package.json. */
|
|
53
|
+
install: string[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The manager THIS workspace uses — `packageManager` first (Corepack's declaration
|
|
57
|
+
* and the only statement the repo actually made), lockfile second.
|
|
58
|
+
*
|
|
59
|
+
* Running the wrong one is not a slow path, it is a broken one: `npm install` in a
|
|
60
|
+
* Yarn-4 workspace writes a package-lock.json beside the yarn.lock and resolves a
|
|
61
|
+
* different tree.
|
|
62
|
+
*/
|
|
63
|
+
export declare function detectPackageManager(root: string, exists?: (path: string) => boolean, readFile?: (path: string) => string): PackageManager;
|
|
64
|
+
export interface GlobalInstall {
|
|
65
|
+
version: string;
|
|
66
|
+
/** The global node_modules root it lives under. */
|
|
67
|
+
root: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A machine-global `lensmcp` (`npm i -g`), when one exists.
|
|
71
|
+
*
|
|
72
|
+
* It matters because it SHADOWS the project: a bare `lensmcp` in a terminal runs
|
|
73
|
+
* the global binary, while agent sessions (`npx -y lensmcp mcp`) resolve the
|
|
74
|
+
* project's node_modules — so a stale global gives the HUMAN different behaviour
|
|
75
|
+
* than the agent, which is the most confusing skew of the lot. Measured on the
|
|
76
|
+
* machine this was written on: global 1.19.0 beside two 1.20.1 workspaces, and
|
|
77
|
+
* nothing reported it (the CLI-skew rider only prints when a PLUGIN skew exists).
|
|
78
|
+
*/
|
|
79
|
+
export declare function detectGlobalInstall(run: (bin: string, args: string[]) => {
|
|
80
|
+
code: number;
|
|
81
|
+
stdout?: string;
|
|
82
|
+
} | undefined, readFile?: (path: string) => string): GlobalInstall | undefined;
|
|
83
|
+
export type RestartAction =
|
|
84
|
+
/** This workspace's gateway OWNS :443 — stop + refresh the nx graph + start. */
|
|
85
|
+
'restart'
|
|
86
|
+
/** POST /register the workspace into the (fresh) daemon so it is hosted again. */
|
|
87
|
+
| 'reregister';
|
|
88
|
+
export interface RestartStep {
|
|
89
|
+
key: string;
|
|
90
|
+
root: string;
|
|
91
|
+
action: RestartAction;
|
|
92
|
+
/** Why this workspace is in the plan — for the report, not the mechanics. */
|
|
93
|
+
reason: 'upgraded' | 'rehost' | 'stale-daemon';
|
|
94
|
+
}
|
|
95
|
+
export interface RestartPlanInput {
|
|
96
|
+
/** The workspace key that owns the running daemon (`daemon.wsKey`), if any. */
|
|
97
|
+
daemonWsKey?: string;
|
|
98
|
+
/** The version the daemon REPORTS it is executing (absent on daemons that predate reporting). */
|
|
99
|
+
daemonVersion?: string;
|
|
100
|
+
/** The version this upgrade targets. */
|
|
101
|
+
targetVersion?: string;
|
|
102
|
+
/** Keys the daemon currently hosts (`GET /list`), captured BEFORE anything is touched. */
|
|
103
|
+
registeredKeys: string[];
|
|
104
|
+
/** Every workspace the registry knows a ROOT for — how a registered key becomes a re-register target. */
|
|
105
|
+
known: WorkspaceTarget[];
|
|
106
|
+
/** The workspaces whose dependencies actually moved. */
|
|
107
|
+
upgraded: WorkspaceTarget[];
|
|
108
|
+
}
|
|
109
|
+
export interface RestartPlan {
|
|
110
|
+
steps: RestartStep[];
|
|
111
|
+
/** Why the plan is empty — surfaced instead of silently doing nothing. */
|
|
112
|
+
note?: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* What has to bounce for the new code to actually be running.
|
|
116
|
+
*
|
|
117
|
+
* The daemon is ONE process on :443 hosting many workspaces, so this is not
|
|
118
|
+
* "restart each workspace":
|
|
119
|
+
*
|
|
120
|
+
* • Restarting the OWNER takes every guest down with it, and the registry is not
|
|
121
|
+
* replayed on boot (`workspace-registry.ts` — today it only powers the chooser).
|
|
122
|
+
* So after an owner restart EVERY previously-registered guest must be
|
|
123
|
+
* re-registered — including ones this upgrade never touched — or the upgrade
|
|
124
|
+
* "succeeds" and half the machine's dev hosts silently 404.
|
|
125
|
+
* • When the owner was NOT upgraded its daemon binary did not change, so it stays
|
|
126
|
+
* up and only the upgraded guests cycle: re-registering a workspace makes the
|
|
127
|
+
* daemon tear down + respawn its pods, which re-resolve from the fresh
|
|
128
|
+
* node_modules — the effect a restart would have had, minus the outage.
|
|
129
|
+
* • A registered guest whose root nothing on disk records cannot be re-registered
|
|
130
|
+
* by us; it is left out rather than guessed at (its own `lensmcp gateway start`
|
|
131
|
+
* re-registers it).
|
|
132
|
+
*/
|
|
133
|
+
export declare function planRestart(input: RestartPlanInput): RestartPlan;
|
package/lib/upgrade.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var h=Object.defineProperty;var d=(e,n)=>h(e,"name",{value:n,configurable:!0});var v=Object.defineProperty,u=d((e,n)=>v(e,"name",{value:n,configurable:!0}),"p");import{existsSync as k,readFileSync as g}from"node:fs";import{homedir as w}from"node:os";import{join as f}from"node:path";export const UPGRADE_SECTIONS=["dependencies","devDependencies","optionalDependencies","resolutions","overrides"];export function isLensmcpPackage(e){return e==="lensmcp"||e.startsWith("@lensmcp/")}d(isLensmcpPackage,"isLensmcpPackage"),u(isLensmcpPackage,"isLensmcpPackage");export function readConnectedWorkspaces(e={}){const n=e.home??process.env.LENSMCP_HOME??w(),c=e.exists??k,o=e.readFile??(s=>g(s,"utf8"));let r;try{r=JSON.parse(o(f(n,".lensmcp","workspaces.json")))}catch{return[]}if(!Array.isArray(r))return[];const i=new Set,p=[];for(const s of r){if(!s||typeof s!="object")continue;const{key:t,root:a}=s;typeof t!="string"||typeof a!="string"||a.length===0||i.has(a)||c(f(a,"package.json"))&&(i.add(a),p.push({key:t,root:a}))}return p}d(readConnectedWorkspaces,"readConnectedWorkspaces"),u(readConnectedWorkspaces,"readConnectedWorkspaces");export function versionPrefix(e){const n=/^([~^]?)(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/.exec(e.trim());return n?n[1]:void 0}d(versionPrefix,"versionPrefix"),u(versionPrefix,"versionPrefix");function m(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}d(m,"l"),u(m,"escapeRegExp");export function rewriteLensmcpDeps(e,n){let c;try{c=JSON.parse(e)}catch{return{text:e,changes:[],skipped:[]}}const o=[],r=[];for(const p of UPGRADE_SECTIONS){const s=c[p];if(!(!s||typeof s!="object"))for(const[t,a]of Object.entries(s)){if(typeof a!="string"||!isLensmcpPackage(t))continue;const y=versionPrefix(a);if(y===void 0){r.push({name:t,section:p,spec:a,reason:"non-version-spec"});continue}const l=`${y}${n}`;if(l===a){r.push({name:t,section:p,spec:a,reason:"already-current"});continue}o.push({name:t,section:p,from:a,to:l})}}let i=e;for(const p of o){const s=new RegExp(`("${m(p.name)}"\\s*:\\s*)"${m(p.from)}"`,"g");i=i.replace(s,`$1"${p.to}"`)}return{text:i,changes:o,skipped:r}}d(rewriteLensmcpDeps,"rewriteLensmcpDeps"),u(rewriteLensmcpDeps,"rewriteLensmcpDeps");export function detectPackageManager(e,n=k,c=o=>g(o,"utf8")){let o;try{const i=JSON.parse(c(f(e,"package.json")));typeof i.packageManager=="string"&&(o=i.packageManager.split("@")[0])}catch{}const r=u(i=>({name:i,install:["install"]}),"manager");return o==="yarn"||o==="npm"||o==="pnpm"||o==="bun"?r(o):n(f(e,"yarn.lock"))?r("yarn"):n(f(e,"pnpm-lock.yaml"))?r("pnpm"):n(f(e,"bun.lockb"))?r("bun"):r("npm")}d(detectPackageManager,"detectPackageManager"),u(detectPackageManager,"detectPackageManager");export function detectGlobalInstall(e,n=c=>g(c,"utf8")){const c=e("npm",["root","-g"]);if(!c||c.code!==0||!c.stdout)return;const o=c.stdout.trim();if(o)try{const r=JSON.parse(n(f(o,"lensmcp","package.json")));return typeof r.version=="string"?{version:r.version,root:o}:void 0}catch{return}}d(detectGlobalInstall,"detectGlobalInstall"),u(detectGlobalInstall,"detectGlobalInstall");export function planRestart(e){const n=new Map(e.known.map(t=>[t.key,t.root]));for(const t of e.upgraded)n.set(t.key,t.root);const c=new Set(e.upgraded.map(t=>t.key)),o=new Set(e.registeredKeys),r=e.daemonWsKey!==void 0&&e.daemonVersion!==void 0&&e.targetVersion!==void 0&&e.daemonVersion!==e.targetVersion;if(e.upgraded.length===0&&!r)return{steps:[],note:"nothing was upgraded"};if(!e.daemonWsKey)return{steps:[],note:"no gateway daemon is running \u2014 the next `lensmcp gateway start` picks up the new version"};const i=e.upgraded.find(t=>t.key===e.daemonWsKey),p=i?.root??n.get(e.daemonWsKey),s=[];if(i||r&&p){s.push({key:e.daemonWsKey,root:p,action:"restart",reason:i?"upgraded":"stale-daemon"});for(const t of e.registeredKeys){if(t===e.daemonWsKey)continue;const a=n.get(t);a&&s.push({key:t,root:a,action:"reregister",reason:c.has(t)?"upgraded":"rehost"})}return{steps:s}}if(r&&!p)return{steps:[],note:`the daemon ('${e.daemonWsKey}') runs ${e.daemonVersion} but its root is unknown \u2014 restart it with \`lensmcp gateway restart\` in its workspace`};for(const t of e.upgraded)o.has(t.key)&&s.push({key:t.key,root:t.root,action:"reregister",reason:"upgraded"});return s.length===0?{steps:[],note:`the running daemon ('${e.daemonWsKey}') hosts none of the upgraded workspaces`}:{steps:s}}d(planRestart,"planRestart"),u(planRestart,"planRestart");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lensmcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"module": "./index.js",
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@frontmcp/sdk": "^1.6.1",
|
|
21
|
+
"@lensmcp/cluster": "1.21.0",
|
|
22
|
+
"@lensmcp/nx-plugin": "1.21.0",
|
|
21
23
|
"reflect-metadata": "^0.2.2",
|
|
22
24
|
"tslib": "^2.3.0",
|
|
23
25
|
"vectoriadb": "^2.2.0"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "lensmcp",
|
|
3
3
|
"displayName": "LensMCP",
|
|
4
4
|
"description": "The observability lens for coding agents. One command brings up the dev cluster gateway (every project.json `cluster` decl → its host on :443), the per-project lens dashboard at https://lensmcp.local/<project>/, and the MCP server your agent connects to — scoped automatically to whatever project you opened Claude Code in.",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.21.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "David Antoon",
|
|
8
8
|
"email": "davidmantoon@gmail.com"
|