@ammduncan/easel 0.6.1 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  All notable changes to easel. This project adheres to [Semantic Versioning](https://semver.org/).
4
4
 
5
+ ## 0.6.2 — 2026-06-04
6
+
7
+ ### Fixed
8
+ - **`setup` now actually puts `easel` on PATH, making the documented bare CLI commands truthful.** The README's CLI section documents `easel open / update / restart / …`, but neither install path ever exposed the binary: `npx -y @ammduncan/easel setup` runs once from the ephemeral npx cache and leaves nothing behind, and clone installs' `bin/easel setup` never linked. Setup now closes the gap per install flavor: clone installs get `npm link` (skipped when `easel` already resolves); npx-cache runs get a real `npm install -g` pinned to their own version, then **delegate setup to the global copy** so the MCP/hook registrations point at paths that survive npx cache pruning (the cache-pruning drift left one machine's MCP pinned to a stale 0.5.1 cache while 0.6.x shipped). Failures degrade to a printed hint — setup never hard-fails on the PATH step.
9
+ - **`easel update` now works for npm installs.** It previously assumed a git checkout (`git pull` flavor, "clone installs only") and just failed elsewhere. Without a `.git` dir it now runs `npm install -g @ammduncan/easel@latest` and re-runs setup from the new copy.
10
+
5
11
  ## 0.6.1 — 2026-06-04
6
12
 
7
13
  ### Changed
package/README.md CHANGED
@@ -31,7 +31,7 @@ Requires Node 20+.
31
31
  npx -y @ammduncan/easel setup
32
32
  ```
33
33
 
34
- That registers the MCP at user scope, installs the `using-easel` skill so the agent knows when to push, and adds the `SessionStart` hooks that resolve session IDs and auto-open the tab. Restart Claude Code and you're done.
34
+ That registers the MCP at user scope, installs the `using-easel` skill so the agent knows when to push, adds the `SessionStart` hooks that resolve session IDs and auto-open the tab, and installs the package globally so the bare `easel` command works from any shell. Restart Claude Code and you're done.
35
35
 
36
36
  ### Cursor / Claude Desktop / Windsurf / Codex
37
37
 
@@ -81,6 +81,8 @@ npm install && npm run build
81
81
  bin/easel setup
82
82
  ```
83
83
 
84
+ Setup runs `npm link`, so bare `easel` works from any shell afterwards.
85
+
84
86
  ## Tools the agent gets
85
87
 
86
88
  | Tool | What it does |
@@ -134,7 +136,7 @@ easel config print / set { preset, theme, density }
134
136
  easel setup Claude Code: hooks + MCP + skill
135
137
  easel setup --client <name> register the MCP in another client (cursor, claude-desktop, windsurf, codex)
136
138
  easel restart kill + respawn the HTTP server (handy after a build)
137
- easel update git pull + build + setup (clone installs only)
139
+ easel update clone installs: git pull + build + setup · npm installs: npm install -g @latest + setup
138
140
  easel server run the HTTP server in the foreground (debug)
139
141
  easel version
140
142
  ```
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn, spawnSync } from "node:child_process";
3
3
  import { copyFileSync, mkdirSync, readFileSync, rmSync, writeFileSync, existsSync } from "node:fs";
4
- import { dirname, join, resolve } from "node:path";
4
+ import { dirname, join, resolve, sep } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { homedir } from "node:os";
7
7
  import { ensureHttpServer, readLock } from "./server-manager.js";
@@ -11,6 +11,7 @@ import { registerSession } from "./session-store.js";
11
11
  import { listClients, setupClient, } from "./client-setup.js";
12
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
13
  const PROJECT_ROOT = resolve(__dirname, "..");
14
+ const NPM_PKG = "@ammduncan/easel";
14
15
  function help() {
15
16
  console.log(`easel — live browser feed for Claude Code for Claude Code sessions
16
17
 
@@ -28,7 +29,7 @@ Usage:
28
29
  easel setup --client claude-desktop register MCP in ~/Library/Application Support/Claude/claude_desktop_config.json
29
30
  easel setup --client windsurf register MCP in ~/.codeium/windsurf/mcp_config.json
30
31
  easel setup --client codex register MCP in ~/.codex/config.toml + copy skill to ~/.codex/skills/
31
- easel update git pull + npm install + build + setup (re-runs setup to apply new conventions)
32
+ easel update clone installs: git pull + build + setup · npm installs: npm install -g @latest + setup
32
33
  easel mcp run the stdio MCP server in the foreground (used by clients)
33
34
  easel restart kill the running HTTP server and respawn it (picks up new builds/paths)
34
35
  easel server run the HTTP server in the foreground (debug)
@@ -97,7 +98,91 @@ function openInBrowser(url) {
97
98
  console.error(`[easel] couldn't open browser: ${err.message}`);
98
99
  }
99
100
  }
101
+ function binResolvesOnPath() {
102
+ const cmd = process.platform === "win32" ? "where" : "which";
103
+ try {
104
+ const r = spawnSync(cmd, ["easel"], { encoding: "utf-8" });
105
+ return r.status === 0 && r.stdout.trim().length > 0;
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ }
111
+ function pkgVersion() {
112
+ try {
113
+ const pkg = JSON.parse(readFileSync(join(PROJECT_ROOT, "package.json"), "utf-8"));
114
+ return pkg.version ?? "latest";
115
+ }
116
+ catch {
117
+ return "latest";
118
+ }
119
+ }
120
+ function globalPkgDir() {
121
+ try {
122
+ const r = spawnSync("npm", ["root", "-g"], { encoding: "utf-8" });
123
+ if (r.status !== 0)
124
+ return null;
125
+ const dir = join(r.stdout.trim(), NPM_PKG);
126
+ return existsSync(dir) ? dir : null;
127
+ }
128
+ catch {
129
+ return null;
130
+ }
131
+ }
132
+ // Re-runs setup from the globally installed copy so its registrations point at
133
+ // the global paths. Returns false if there is no global copy to delegate to.
134
+ function rerunSetupFromGlobal() {
135
+ const dir = globalPkgDir();
136
+ if (!dir) {
137
+ return false;
138
+ }
139
+ const r = spawnSync("node", [join(dir, "dist", "cli.js"), "setup"], {
140
+ stdio: "inherit",
141
+ env: { ...process.env, EASEL_SETUP_CHILD: "1" },
142
+ });
143
+ return r.status === 0;
144
+ }
145
+ // Make bare `easel` work after setup, as the README documents. Clone installs
146
+ // get `npm link`. npx-cache installs get a real global install — the cache is
147
+ // pruned unpredictably — and setup is then re-run from the global copy so the
148
+ // MCP/hook registrations point at paths that survive pruning.
149
+ // Returns true when setup was fully delegated to the global copy.
150
+ function ensureBinOnPath() {
151
+ const inNpxCache = PROJECT_ROOT.split(sep).includes("_npx");
152
+ if (!inNpxCache) {
153
+ if (binResolvesOnPath()) {
154
+ return false;
155
+ }
156
+ const r = spawnSync("npm", ["link", "--silent", "--no-audit", "--no-fund"], {
157
+ cwd: PROJECT_ROOT,
158
+ stdio: "ignore",
159
+ });
160
+ if (r.status === 0) {
161
+ console.log(" - linked `easel` into the global bin (npm link)");
162
+ }
163
+ else {
164
+ console.warn(` - couldn't put \`easel\` on PATH — run \`npm link\` in ${PROJECT_ROOT}`);
165
+ }
166
+ return false;
167
+ }
168
+ const version = pkgVersion();
169
+ console.log(`[easel] installing ${NPM_PKG}@${version} globally so \`easel\` is on PATH…`);
170
+ const installed = spawnSync("npm", ["install", "-g", "--silent", "--no-audit", "--no-fund", `${NPM_PKG}@${version}`], { stdio: "inherit" });
171
+ if (installed.status !== 0 || !rerunSetupFromGlobal()) {
172
+ console.warn(` - global install failed — \`easel\` won't be on PATH; run \`npm install -g ${NPM_PKG}\` manually`);
173
+ return false;
174
+ }
175
+ return true;
176
+ }
100
177
  function cmdSetup() {
178
+ // Put `easel` on PATH first — for npx-cache runs this delegates the whole
179
+ // setup to a freshly installed global copy (so registered paths outlive the
180
+ // cache), in which case there's nothing left to do here.
181
+ if (!process.env.EASEL_SETUP_CHILD) {
182
+ if (ensureBinOnPath()) {
183
+ return;
184
+ }
185
+ }
101
186
  mkdirSync(HOOK_DIR, { recursive: true });
102
187
  const settingsPath = join(homedir(), ".claude", "settings.json");
103
188
  const hookScript = resolve(PROJECT_ROOT, "scripts", "easel-session-id.mjs");
@@ -264,6 +349,25 @@ async function cmdConfig(args) {
264
349
  function cmdUpdate() {
265
350
  console.log("[easel] checking for updates…");
266
351
  const run = (cmd, args) => spawnSync(cmd, args, { stdio: "inherit", cwd: PROJECT_ROOT });
352
+ // npm/global installs have no git checkout — update from the registry and
353
+ // re-run setup from the new copy so registrations track the new paths.
354
+ if (!existsSync(join(PROJECT_ROOT, ".git"))) {
355
+ const installed = run("npm", [
356
+ "install",
357
+ "-g",
358
+ "--no-audit",
359
+ "--no-fund",
360
+ `${NPM_PKG}@latest`,
361
+ ]);
362
+ if (installed.status !== 0) {
363
+ console.error("[easel] npm install -g failed");
364
+ process.exitCode = 1;
365
+ return;
366
+ }
367
+ rerunSetupFromGlobal();
368
+ console.log("[easel] updated. Restart Claude Code to pick up tool/skill changes.");
369
+ return;
370
+ }
267
371
  let r = run("git", ["fetch", "--quiet", "origin", "main"]);
268
372
  if (r.status !== 0) {
269
373
  console.error("[easel] git fetch failed");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ammduncan/easel",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "A live browser tab for every Claude Code (and MCP) session. The push MCP tool appends HTML cards to a scrolling feed you keep open in split-screen.",
5
5
  "type": "module",
6
6
  "license": "MIT",