pi-ele-picker 0.1.1 → 0.1.3

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/client/picker.js CHANGED
@@ -125,6 +125,9 @@
125
125
  var picking = false;
126
126
  var target = null;
127
127
  var shots = [];
128
+ // Set when a pick commits on pointerdown; swallows the follow-up click so the
129
+ // page (links, buttons) never reacts to the click that finished the pick.
130
+ var swallowClick = false;
128
131
 
129
132
  document.documentElement.appendChild(host);
130
133
 
@@ -288,15 +291,34 @@
288
291
  showHighlight(el);
289
292
  }
290
293
 
291
- function onClick(e) {
294
+ /**
295
+ * Commit the pick on `pointerdown`, not `click`: disabled form controls never
296
+ * dispatch `click` (so they could never be picked), but `pointerdown` still
297
+ * fires on them with the correct target.
298
+ */
299
+ function onPointerDown(e) {
300
+ if (e.button !== 0) return;
292
301
  var el = elFromEvent(e);
293
302
  if (!el) return;
294
303
  e.preventDefault();
295
304
  e.stopPropagation();
305
+ // preventDefault above only cancels the compat mousedown/mouseup, not the
306
+ // click, so flag it for the one-shot click swallower below. The flag is
307
+ // cleared at the start of every pointerdown (see the always-on listener at
308
+ // the bottom), so it can never strand on a disabled target — which fires
309
+ // pointerdown but no click at all — and eat an unrelated later click.
310
+ swallowClick = true;
296
311
  setPicking(false);
297
312
  select(el);
298
313
  }
299
314
 
315
+ function onClick(e) {
316
+ if (!swallowClick) return;
317
+ swallowClick = false;
318
+ e.preventDefault();
319
+ e.stopPropagation();
320
+ }
321
+
300
322
  function onKey(e) {
301
323
  if (e.key === 'Escape') {
302
324
  setPicking(false);
@@ -311,11 +333,11 @@
311
333
  trigger.style.display = on ? 'none' : 'flex';
312
334
  if (on) {
313
335
  window.addEventListener('mousemove', onMove, true);
314
- window.addEventListener('click', onClick, true);
336
+ window.addEventListener('pointerdown', onPointerDown, true);
315
337
  window.addEventListener('keydown', onKey, true);
316
338
  } else {
317
339
  window.removeEventListener('mousemove', onMove, true);
318
- window.removeEventListener('click', onClick, true);
340
+ window.removeEventListener('pointerdown', onPointerDown, true);
319
341
  window.removeEventListener('keydown', onKey, true);
320
342
  hideHighlight();
321
343
  }
@@ -562,6 +584,12 @@
562
584
  });
563
585
  }
564
586
 
587
+ // Always-on. The first one resets the swallow flag before `onPointerDown`
588
+ // runs (registered earlier, so same-node capture listeners fire in order);
589
+ // the second consumes the flag to cancel the click that finishes a pick.
590
+ window.addEventListener('pointerdown', function () { swallowClick = false; }, true);
591
+ window.addEventListener('click', onClick, true);
592
+
565
593
  trigger.addEventListener('click', function () {
566
594
  if (suppressClick) {
567
595
  suppressClick = false;
package/dist/index.cjs CHANGED
@@ -35,7 +35,16 @@ var import_node_os = require("os");
35
35
  var import_node_url = require("url");
36
36
  var REG_DIR = (0, import_node_path.join)((0, import_node_os.homedir)(), ".pi", "agent", "pi-picker");
37
37
  var ENDPOINT = "/__pi_picker__";
38
- var CLIENT_FILE = (0, import_node_url.fileURLToPath)(new URL("../client/picker.js", importMetaUrl));
38
+ var OWN_CLIENT_FILE = (0, import_node_url.fileURLToPath)(new URL("../client/picker.js", importMetaUrl));
39
+ function resolveClientFile(projectRoot) {
40
+ for (let dir = projectRoot; dir; ) {
41
+ const candidate = (0, import_node_path.join)(dir, "node_modules", "pi-ele-picker", "client", "picker.js");
42
+ if ((0, import_node_fs.existsSync)(candidate)) return candidate;
43
+ const parent = (0, import_node_path.dirname)(dir);
44
+ dir = parent === dir ? "" : parent;
45
+ }
46
+ return (0, import_node_fs.existsSync)(OWN_CLIENT_FILE) ? OWN_CLIENT_FILE : null;
47
+ }
39
48
  function isPidAlive(pid) {
40
49
  try {
41
50
  process.kill(pid, 0);
@@ -88,14 +97,15 @@ function piElePicker(options = {}) {
88
97
  return;
89
98
  }
90
99
  if (url === "/picker.js") {
91
- if (!(0, import_node_fs.existsSync)(CLIENT_FILE)) {
100
+ const clientFile = resolveClientFile(server.config.root);
101
+ if (!clientFile) {
92
102
  res.statusCode = 404;
93
103
  res.end("pi-ele-picker client missing");
94
104
  return;
95
105
  }
96
106
  res.setHeader("content-type", "text/javascript");
97
107
  res.setHeader("cache-control", "no-store");
98
- res.end((0, import_node_fs.readFileSync)(CLIENT_FILE));
108
+ res.end((0, import_node_fs.readFileSync)(clientFile));
99
109
  return;
100
110
  }
101
111
  next();
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../node_modules/tsup/assets/cjs_shims.js"],"sourcesContent":["import type { Plugin } from 'vite';\nimport { existsSync, readFileSync, readdirSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { homedir } from 'node:os';\nimport { fileURLToPath } from 'node:url';\n\n/**\n * pi-ele-picker\n *\n * A Vite dev-only plugin that injects the pi element picker into the page and\n * exposes a same-origin endpoint the picker uses to discover the running pi\n * bridge.\n *\n * The bridge registry is written by the pi extension at\n * `~/.pi/agent/extensions/pi-picker.ts` (or any extension implementing the same\n * `~/.pi/agent/pi-picker/<pid>.json` registry + `POST /pick` protocol).\n */\n\n/** Directory where pi bridge extensions register themselves. */\nconst REG_DIR = join(homedir(), '.pi', 'agent', 'pi-picker');\n\n/** URL prefix under which the picker client and discovery endpoint are served. */\nconst ENDPOINT = '/__pi_picker__';\n\n/** Absolute path to the browser client that ships with this package. */\nconst CLIENT_FILE = fileURLToPath(new URL('../client/picker.js', import.meta.url));\n\nexport interface PiElePickerOptions {\n /**\n * Directory whose pi bridge to discover. Must match the `cwd` the pi session\n * was started in. Defaults to `process.cwd()`.\n */\n root?: string;\n}\n\ninterface BridgeInfo {\n port: number;\n pid: number;\n cwd: string;\n token: string;\n updatedAt: number;\n}\n\nfunction isPidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\n/** Find the freshest live bridge registered for the given project root. */\nfunction discoverBridge(root: string): BridgeInfo | null {\n let files: string[];\n try {\n files = readdirSync(REG_DIR).filter((f) => f.endsWith('.json'));\n } catch {\n return null;\n }\n let best: BridgeInfo | null = null;\n for (const f of files) {\n try {\n const info = JSON.parse(readFileSync(join(REG_DIR, f), 'utf8')) as BridgeInfo;\n if (info.cwd !== root) continue;\n if (!isPidAlive(info.pid)) continue;\n if (!best || info.updatedAt > best.updatedAt) best = info;\n } catch {\n /* skip malformed */\n }\n }\n return best;\n}\n\nexport default function piElePicker(options: PiElePickerOptions = {}): Plugin {\n const root = options.root ?? process.cwd();\n\n return {\n name: 'pi-ele-picker',\n apply: 'serve',\n transformIndexHtml() {\n return [\n {\n tag: 'script',\n attrs: { type: 'module', src: `${ENDPOINT}/picker.js` },\n injectTo: 'body',\n },\n ];\n },\n configureServer(server) {\n server.middlewares.use(ENDPOINT, (req, res, next) => {\n const url = (req.url ?? '').split('?')[0];\n\n if (url === '/config') {\n const info = discoverBridge(root);\n res.setHeader('content-type', 'application/json');\n res.setHeader('cache-control', 'no-store');\n res.end(JSON.stringify(info ? { port: info.port, token: info.token } : { port: null }));\n return;\n }\n\n if (url === '/picker.js') {\n if (!existsSync(CLIENT_FILE)) {\n res.statusCode = 404;\n res.end('pi-ele-picker client missing');\n return;\n }\n res.setHeader('content-type', 'text/javascript');\n res.setHeader('cache-control', 'no-store');\n res.end(readFileSync(CLIENT_FILE));\n return;\n }\n\n next();\n });\n },\n };\n}\n","// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () => \n typeof document === \"undefined\" \n ? new URL(`file:${__filename}`).href \n : (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') \n ? document.currentScript.src \n : new URL(\"main.js\", document.baseURI).href;\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKA,IAAM,mBAAmB,MACvB,OAAO,aAAa,cAChB,IAAI,IAAI,QAAQ,UAAU,EAAE,EAAE,OAC7B,SAAS,iBAAiB,SAAS,cAAc,QAAQ,YAAY,MAAM,WAC1E,SAAS,cAAc,MACvB,IAAI,IAAI,WAAW,SAAS,OAAO,EAAE;AAEtC,IAAM,gBAAgC,iCAAiB;;;ADX9D,qBAAsD;AACtD,uBAAqB;AACrB,qBAAwB;AACxB,sBAA8B;AAe9B,IAAM,cAAU,2BAAK,wBAAQ,GAAG,OAAO,SAAS,WAAW;AAG3D,IAAM,WAAW;AAGjB,IAAM,kBAAc,+BAAc,IAAI,IAAI,uBAAuB,aAAe,CAAC;AAkBjF,SAAS,WAAW,KAAsB;AACxC,MAAI;AACF,YAAQ,KAAK,KAAK,CAAC;AACnB,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,SAAS,eAAe,MAAiC;AACvD,MAAI;AACJ,MAAI;AACF,gBAAQ,4BAAY,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,OAAO,CAAC;AAAA,EAChE,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,OAA0B;AAC9B,aAAW,KAAK,OAAO;AACrB,QAAI;AACF,YAAM,OAAO,KAAK,UAAM,iCAAa,uBAAK,SAAS,CAAC,GAAG,MAAM,CAAC;AAC9D,UAAI,KAAK,QAAQ,KAAM;AACvB,UAAI,CAAC,WAAW,KAAK,GAAG,EAAG;AAC3B,UAAI,CAAC,QAAQ,KAAK,YAAY,KAAK,UAAW,QAAO;AAAA,IACvD,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO;AACT;AAEe,SAAR,YAA6B,UAA8B,CAAC,GAAW;AAC5E,QAAM,OAAO,QAAQ,QAAQ,QAAQ,IAAI;AAEzC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,IACP,qBAAqB;AACnB,aAAO;AAAA,QACL;AAAA,UACE,KAAK;AAAA,UACL,OAAO,EAAE,MAAM,UAAU,KAAK,GAAG,QAAQ,aAAa;AAAA,UACtD,UAAU;AAAA,QACZ;AAAA,MACF;AAAA,IACF;AAAA,IACA,gBAAgB,QAAQ;AACtB,aAAO,YAAY,IAAI,UAAU,CAAC,KAAK,KAAK,SAAS;AACnD,cAAM,OAAO,IAAI,OAAO,IAAI,MAAM,GAAG,EAAE,CAAC;AAExC,YAAI,QAAQ,WAAW;AACrB,gBAAM,OAAO,eAAe,IAAI;AAChC,cAAI,UAAU,gBAAgB,kBAAkB;AAChD,cAAI,UAAU,iBAAiB,UAAU;AACzC,cAAI,IAAI,KAAK,UAAU,OAAO,EAAE,MAAM,KAAK,MAAM,OAAO,KAAK,MAAM,IAAI,EAAE,MAAM,KAAK,CAAC,CAAC;AACtF;AAAA,QACF;AAEA,YAAI,QAAQ,cAAc;AACxB,cAAI,KAAC,2BAAW,WAAW,GAAG;AAC5B,gBAAI,aAAa;AACjB,gBAAI,IAAI,8BAA8B;AACtC;AAAA,UACF;AACA,cAAI,UAAU,gBAAgB,iBAAiB;AAC/C,cAAI,UAAU,iBAAiB,UAAU;AACzC,cAAI,QAAI,6BAAa,WAAW,CAAC;AACjC;AAAA,QACF;AAEA,aAAK;AAAA,MACP,CAAC;AAAA,IACH;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../node_modules/tsup/assets/cjs_shims.js"],"sourcesContent":["import type { Plugin } from 'vite';\nimport { existsSync, readFileSync, readdirSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { homedir } from 'node:os';\nimport { fileURLToPath } from 'node:url';\n\n/**\n * pi-ele-picker\n *\n * A Vite dev-only plugin that injects the pi element picker into the page and\n * exposes a same-origin endpoint the picker uses to discover the running pi\n * bridge.\n *\n * The bridge registry is written by the pi extension at\n * `~/.pi/agent/extensions/pi-picker.ts` (or any extension implementing the same\n * `~/.pi/agent/pi-picker/<pid>.json` registry + `POST /pick` protocol).\n */\n\n/** Directory where pi bridge extensions register themselves. */\nconst REG_DIR = join(homedir(), '.pi', 'agent', 'pi-picker');\n\n/** URL prefix under which the picker client and discovery endpoint are served. */\nconst ENDPOINT = '/__pi_picker__';\n\n/** Path to this package's own browser client, anchored at the plugin's install location. */\nconst OWN_CLIENT_FILE = fileURLToPath(new URL('../client/picker.js', import.meta.url));\n\n/**\n * Resolve the browser client to serve. Called per request, never cached.\n *\n * The plugin module is evaluated once when the dev server boots, so anything\n * captured at module scope is frozen for the life of that process. Upgrading\n * pi-ele-picker while a dev server is running therefore used to keep serving the\n * old client forever: the frozen path still pointed at\n * `node_modules/.pnpm/pi-ele-picker@<old>/...`, pnpm leaves that directory on\n * disk, and the stale client was handed out silently — refreshing never helped.\n *\n * Resolving from the project root instead means the currently installed copy\n * wins, with no server restart.\n *\n * Two traps this deliberately avoids:\n *\n * 1. Anchoring at `import.meta.url`. From inside `.../pi-ele-picker@<old>/dist/`\n * Node resolves `pi-ele-picker` straight back to that same old copy.\n * 2. `require.resolve()`. Node memoises resolutions in `Module._pathCache` and\n * never invalidates them when `node_modules` changes underneath a running\n * process, which pins the first lookup forever — the same staleness, moved.\n *\n * So the lookup is a plain filesystem walk, mirroring Node's own node_modules\n * search (upward from the project root, for workspace/monorepo layouts), and it\n * falls back to this package's own copy when nothing matches.\n */\nfunction resolveClientFile(projectRoot: string): string | null {\n for (let dir = projectRoot; dir; ) {\n const candidate = join(dir, 'node_modules', 'pi-ele-picker', 'client', 'picker.js');\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n dir = parent === dir ? '' : parent; // stop at the filesystem root\n }\n return existsSync(OWN_CLIENT_FILE) ? OWN_CLIENT_FILE : null;\n}\n\nexport interface PiElePickerOptions {\n /**\n * Directory whose pi bridge to discover. Must match the `cwd` the pi session\n * was started in. Defaults to `process.cwd()`.\n */\n root?: string;\n}\n\ninterface BridgeInfo {\n port: number;\n pid: number;\n cwd: string;\n token: string;\n updatedAt: number;\n}\n\nfunction isPidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\n/** Find the freshest live bridge registered for the given project root. */\nfunction discoverBridge(root: string): BridgeInfo | null {\n let files: string[];\n try {\n files = readdirSync(REG_DIR).filter((f) => f.endsWith('.json'));\n } catch {\n return null;\n }\n let best: BridgeInfo | null = null;\n for (const f of files) {\n try {\n const info = JSON.parse(readFileSync(join(REG_DIR, f), 'utf8')) as BridgeInfo;\n if (info.cwd !== root) continue;\n if (!isPidAlive(info.pid)) continue;\n if (!best || info.updatedAt > best.updatedAt) best = info;\n } catch {\n /* skip malformed */\n }\n }\n return best;\n}\n\nexport default function piElePicker(options: PiElePickerOptions = {}): Plugin {\n const root = options.root ?? process.cwd();\n\n return {\n name: 'pi-ele-picker',\n apply: 'serve',\n transformIndexHtml() {\n return [\n {\n tag: 'script',\n attrs: { type: 'module', src: `${ENDPOINT}/picker.js` },\n injectTo: 'body',\n },\n ];\n },\n configureServer(server) {\n server.middlewares.use(ENDPOINT, (req, res, next) => {\n const url = (req.url ?? '').split('?')[0];\n\n if (url === '/config') {\n const info = discoverBridge(root);\n res.setHeader('content-type', 'application/json');\n res.setHeader('cache-control', 'no-store');\n res.end(JSON.stringify(info ? { port: info.port, token: info.token } : { port: null }));\n return;\n }\n\n if (url === '/picker.js') {\n const clientFile = resolveClientFile(server.config.root);\n if (!clientFile) {\n res.statusCode = 404;\n res.end('pi-ele-picker client missing');\n return;\n }\n res.setHeader('content-type', 'text/javascript');\n res.setHeader('cache-control', 'no-store');\n res.end(readFileSync(clientFile));\n return;\n }\n\n next();\n });\n },\n };\n}\n","// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () => \n typeof document === \"undefined\" \n ? new URL(`file:${__filename}`).href \n : (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') \n ? document.currentScript.src \n : new URL(\"main.js\", document.baseURI).href;\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n"],"mappings":"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","names":[]}
package/dist/index.js CHANGED
@@ -1,11 +1,20 @@
1
1
  // src/index.ts
2
2
  import { existsSync, readFileSync, readdirSync } from "fs";
3
- import { join } from "path";
3
+ import { dirname, join } from "path";
4
4
  import { homedir } from "os";
5
5
  import { fileURLToPath } from "url";
6
6
  var REG_DIR = join(homedir(), ".pi", "agent", "pi-picker");
7
7
  var ENDPOINT = "/__pi_picker__";
8
- var CLIENT_FILE = fileURLToPath(new URL("../client/picker.js", import.meta.url));
8
+ var OWN_CLIENT_FILE = fileURLToPath(new URL("../client/picker.js", import.meta.url));
9
+ function resolveClientFile(projectRoot) {
10
+ for (let dir = projectRoot; dir; ) {
11
+ const candidate = join(dir, "node_modules", "pi-ele-picker", "client", "picker.js");
12
+ if (existsSync(candidate)) return candidate;
13
+ const parent = dirname(dir);
14
+ dir = parent === dir ? "" : parent;
15
+ }
16
+ return existsSync(OWN_CLIENT_FILE) ? OWN_CLIENT_FILE : null;
17
+ }
9
18
  function isPidAlive(pid) {
10
19
  try {
11
20
  process.kill(pid, 0);
@@ -58,14 +67,15 @@ function piElePicker(options = {}) {
58
67
  return;
59
68
  }
60
69
  if (url === "/picker.js") {
61
- if (!existsSync(CLIENT_FILE)) {
70
+ const clientFile = resolveClientFile(server.config.root);
71
+ if (!clientFile) {
62
72
  res.statusCode = 404;
63
73
  res.end("pi-ele-picker client missing");
64
74
  return;
65
75
  }
66
76
  res.setHeader("content-type", "text/javascript");
67
77
  res.setHeader("cache-control", "no-store");
68
- res.end(readFileSync(CLIENT_FILE));
78
+ res.end(readFileSync(clientFile));
69
79
  return;
70
80
  }
71
81
  next();
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Plugin } from 'vite';\nimport { existsSync, readFileSync, readdirSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { homedir } from 'node:os';\nimport { fileURLToPath } from 'node:url';\n\n/**\n * pi-ele-picker\n *\n * A Vite dev-only plugin that injects the pi element picker into the page and\n * exposes a same-origin endpoint the picker uses to discover the running pi\n * bridge.\n *\n * The bridge registry is written by the pi extension at\n * `~/.pi/agent/extensions/pi-picker.ts` (or any extension implementing the same\n * `~/.pi/agent/pi-picker/<pid>.json` registry + `POST /pick` protocol).\n */\n\n/** Directory where pi bridge extensions register themselves. */\nconst REG_DIR = join(homedir(), '.pi', 'agent', 'pi-picker');\n\n/** URL prefix under which the picker client and discovery endpoint are served. */\nconst ENDPOINT = '/__pi_picker__';\n\n/** Absolute path to the browser client that ships with this package. */\nconst CLIENT_FILE = fileURLToPath(new URL('../client/picker.js', import.meta.url));\n\nexport interface PiElePickerOptions {\n /**\n * Directory whose pi bridge to discover. Must match the `cwd` the pi session\n * was started in. Defaults to `process.cwd()`.\n */\n root?: string;\n}\n\ninterface BridgeInfo {\n port: number;\n pid: number;\n cwd: string;\n token: string;\n updatedAt: number;\n}\n\nfunction isPidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\n/** Find the freshest live bridge registered for the given project root. */\nfunction discoverBridge(root: string): BridgeInfo | null {\n let files: string[];\n try {\n files = readdirSync(REG_DIR).filter((f) => f.endsWith('.json'));\n } catch {\n return null;\n }\n let best: BridgeInfo | null = null;\n for (const f of files) {\n try {\n const info = JSON.parse(readFileSync(join(REG_DIR, f), 'utf8')) as BridgeInfo;\n if (info.cwd !== root) continue;\n if (!isPidAlive(info.pid)) continue;\n if (!best || info.updatedAt > best.updatedAt) best = info;\n } catch {\n /* skip malformed */\n }\n }\n return best;\n}\n\nexport default function piElePicker(options: PiElePickerOptions = {}): Plugin {\n const root = options.root ?? process.cwd();\n\n return {\n name: 'pi-ele-picker',\n apply: 'serve',\n transformIndexHtml() {\n return [\n {\n tag: 'script',\n attrs: { type: 'module', src: `${ENDPOINT}/picker.js` },\n injectTo: 'body',\n },\n ];\n },\n configureServer(server) {\n server.middlewares.use(ENDPOINT, (req, res, next) => {\n const url = (req.url ?? '').split('?')[0];\n\n if (url === '/config') {\n const info = discoverBridge(root);\n res.setHeader('content-type', 'application/json');\n res.setHeader('cache-control', 'no-store');\n res.end(JSON.stringify(info ? { port: info.port, token: info.token } : { port: null }));\n return;\n }\n\n if (url === '/picker.js') {\n if (!existsSync(CLIENT_FILE)) {\n res.statusCode = 404;\n res.end('pi-ele-picker client missing');\n return;\n }\n res.setHeader('content-type', 'text/javascript');\n res.setHeader('cache-control', 'no-store');\n res.end(readFileSync(CLIENT_FILE));\n return;\n }\n\n next();\n });\n },\n };\n}\n"],"mappings":";AACA,SAAS,YAAY,cAAc,mBAAmB;AACtD,SAAS,YAAY;AACrB,SAAS,eAAe;AACxB,SAAS,qBAAqB;AAe9B,IAAM,UAAU,KAAK,QAAQ,GAAG,OAAO,SAAS,WAAW;AAG3D,IAAM,WAAW;AAGjB,IAAM,cAAc,cAAc,IAAI,IAAI,uBAAuB,YAAY,GAAG,CAAC;AAkBjF,SAAS,WAAW,KAAsB;AACxC,MAAI;AACF,YAAQ,KAAK,KAAK,CAAC;AACnB,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,SAAS,eAAe,MAAiC;AACvD,MAAI;AACJ,MAAI;AACF,YAAQ,YAAY,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,OAAO,CAAC;AAAA,EAChE,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,OAA0B;AAC9B,aAAW,KAAK,OAAO;AACrB,QAAI;AACF,YAAM,OAAO,KAAK,MAAM,aAAa,KAAK,SAAS,CAAC,GAAG,MAAM,CAAC;AAC9D,UAAI,KAAK,QAAQ,KAAM;AACvB,UAAI,CAAC,WAAW,KAAK,GAAG,EAAG;AAC3B,UAAI,CAAC,QAAQ,KAAK,YAAY,KAAK,UAAW,QAAO;AAAA,IACvD,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO;AACT;AAEe,SAAR,YAA6B,UAA8B,CAAC,GAAW;AAC5E,QAAM,OAAO,QAAQ,QAAQ,QAAQ,IAAI;AAEzC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,IACP,qBAAqB;AACnB,aAAO;AAAA,QACL;AAAA,UACE,KAAK;AAAA,UACL,OAAO,EAAE,MAAM,UAAU,KAAK,GAAG,QAAQ,aAAa;AAAA,UACtD,UAAU;AAAA,QACZ;AAAA,MACF;AAAA,IACF;AAAA,IACA,gBAAgB,QAAQ;AACtB,aAAO,YAAY,IAAI,UAAU,CAAC,KAAK,KAAK,SAAS;AACnD,cAAM,OAAO,IAAI,OAAO,IAAI,MAAM,GAAG,EAAE,CAAC;AAExC,YAAI,QAAQ,WAAW;AACrB,gBAAM,OAAO,eAAe,IAAI;AAChC,cAAI,UAAU,gBAAgB,kBAAkB;AAChD,cAAI,UAAU,iBAAiB,UAAU;AACzC,cAAI,IAAI,KAAK,UAAU,OAAO,EAAE,MAAM,KAAK,MAAM,OAAO,KAAK,MAAM,IAAI,EAAE,MAAM,KAAK,CAAC,CAAC;AACtF;AAAA,QACF;AAEA,YAAI,QAAQ,cAAc;AACxB,cAAI,CAAC,WAAW,WAAW,GAAG;AAC5B,gBAAI,aAAa;AACjB,gBAAI,IAAI,8BAA8B;AACtC;AAAA,UACF;AACA,cAAI,UAAU,gBAAgB,iBAAiB;AAC/C,cAAI,UAAU,iBAAiB,UAAU;AACzC,cAAI,IAAI,aAAa,WAAW,CAAC;AACjC;AAAA,QACF;AAEA,aAAK;AAAA,MACP,CAAC;AAAA,IACH;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Plugin } from 'vite';\nimport { existsSync, readFileSync, readdirSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { homedir } from 'node:os';\nimport { fileURLToPath } from 'node:url';\n\n/**\n * pi-ele-picker\n *\n * A Vite dev-only plugin that injects the pi element picker into the page and\n * exposes a same-origin endpoint the picker uses to discover the running pi\n * bridge.\n *\n * The bridge registry is written by the pi extension at\n * `~/.pi/agent/extensions/pi-picker.ts` (or any extension implementing the same\n * `~/.pi/agent/pi-picker/<pid>.json` registry + `POST /pick` protocol).\n */\n\n/** Directory where pi bridge extensions register themselves. */\nconst REG_DIR = join(homedir(), '.pi', 'agent', 'pi-picker');\n\n/** URL prefix under which the picker client and discovery endpoint are served. */\nconst ENDPOINT = '/__pi_picker__';\n\n/** Path to this package's own browser client, anchored at the plugin's install location. */\nconst OWN_CLIENT_FILE = fileURLToPath(new URL('../client/picker.js', import.meta.url));\n\n/**\n * Resolve the browser client to serve. Called per request, never cached.\n *\n * The plugin module is evaluated once when the dev server boots, so anything\n * captured at module scope is frozen for the life of that process. Upgrading\n * pi-ele-picker while a dev server is running therefore used to keep serving the\n * old client forever: the frozen path still pointed at\n * `node_modules/.pnpm/pi-ele-picker@<old>/...`, pnpm leaves that directory on\n * disk, and the stale client was handed out silently — refreshing never helped.\n *\n * Resolving from the project root instead means the currently installed copy\n * wins, with no server restart.\n *\n * Two traps this deliberately avoids:\n *\n * 1. Anchoring at `import.meta.url`. From inside `.../pi-ele-picker@<old>/dist/`\n * Node resolves `pi-ele-picker` straight back to that same old copy.\n * 2. `require.resolve()`. Node memoises resolutions in `Module._pathCache` and\n * never invalidates them when `node_modules` changes underneath a running\n * process, which pins the first lookup forever — the same staleness, moved.\n *\n * So the lookup is a plain filesystem walk, mirroring Node's own node_modules\n * search (upward from the project root, for workspace/monorepo layouts), and it\n * falls back to this package's own copy when nothing matches.\n */\nfunction resolveClientFile(projectRoot: string): string | null {\n for (let dir = projectRoot; dir; ) {\n const candidate = join(dir, 'node_modules', 'pi-ele-picker', 'client', 'picker.js');\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n dir = parent === dir ? '' : parent; // stop at the filesystem root\n }\n return existsSync(OWN_CLIENT_FILE) ? OWN_CLIENT_FILE : null;\n}\n\nexport interface PiElePickerOptions {\n /**\n * Directory whose pi bridge to discover. Must match the `cwd` the pi session\n * was started in. Defaults to `process.cwd()`.\n */\n root?: string;\n}\n\ninterface BridgeInfo {\n port: number;\n pid: number;\n cwd: string;\n token: string;\n updatedAt: number;\n}\n\nfunction isPidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\n/** Find the freshest live bridge registered for the given project root. */\nfunction discoverBridge(root: string): BridgeInfo | null {\n let files: string[];\n try {\n files = readdirSync(REG_DIR).filter((f) => f.endsWith('.json'));\n } catch {\n return null;\n }\n let best: BridgeInfo | null = null;\n for (const f of files) {\n try {\n const info = JSON.parse(readFileSync(join(REG_DIR, f), 'utf8')) as BridgeInfo;\n if (info.cwd !== root) continue;\n if (!isPidAlive(info.pid)) continue;\n if (!best || info.updatedAt > best.updatedAt) best = info;\n } catch {\n /* skip malformed */\n }\n }\n return best;\n}\n\nexport default function piElePicker(options: PiElePickerOptions = {}): Plugin {\n const root = options.root ?? process.cwd();\n\n return {\n name: 'pi-ele-picker',\n apply: 'serve',\n transformIndexHtml() {\n return [\n {\n tag: 'script',\n attrs: { type: 'module', src: `${ENDPOINT}/picker.js` },\n injectTo: 'body',\n },\n ];\n },\n configureServer(server) {\n server.middlewares.use(ENDPOINT, (req, res, next) => {\n const url = (req.url ?? '').split('?')[0];\n\n if (url === '/config') {\n const info = discoverBridge(root);\n res.setHeader('content-type', 'application/json');\n res.setHeader('cache-control', 'no-store');\n res.end(JSON.stringify(info ? { port: info.port, token: info.token } : { port: null }));\n return;\n }\n\n if (url === '/picker.js') {\n const clientFile = resolveClientFile(server.config.root);\n if (!clientFile) {\n res.statusCode = 404;\n res.end('pi-ele-picker client missing');\n return;\n }\n res.setHeader('content-type', 'text/javascript');\n res.setHeader('cache-control', 'no-store');\n res.end(readFileSync(clientFile));\n return;\n }\n\n next();\n });\n },\n };\n}\n"],"mappings":";AACA,SAAS,YAAY,cAAc,mBAAmB;AACtD,SAAS,SAAS,YAAY;AAC9B,SAAS,eAAe;AACxB,SAAS,qBAAqB;AAe9B,IAAM,UAAU,KAAK,QAAQ,GAAG,OAAO,SAAS,WAAW;AAG3D,IAAM,WAAW;AAGjB,IAAM,kBAAkB,cAAc,IAAI,IAAI,uBAAuB,YAAY,GAAG,CAAC;AA2BrF,SAAS,kBAAkB,aAAoC;AAC7D,WAAS,MAAM,aAAa,OAAO;AACjC,UAAM,YAAY,KAAK,KAAK,gBAAgB,iBAAiB,UAAU,WAAW;AAClF,QAAI,WAAW,SAAS,EAAG,QAAO;AAClC,UAAM,SAAS,QAAQ,GAAG;AAC1B,UAAM,WAAW,MAAM,KAAK;AAAA,EAC9B;AACA,SAAO,WAAW,eAAe,IAAI,kBAAkB;AACzD;AAkBA,SAAS,WAAW,KAAsB;AACxC,MAAI;AACF,YAAQ,KAAK,KAAK,CAAC;AACnB,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,SAAS,eAAe,MAAiC;AACvD,MAAI;AACJ,MAAI;AACF,YAAQ,YAAY,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,OAAO,CAAC;AAAA,EAChE,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,OAA0B;AAC9B,aAAW,KAAK,OAAO;AACrB,QAAI;AACF,YAAM,OAAO,KAAK,MAAM,aAAa,KAAK,SAAS,CAAC,GAAG,MAAM,CAAC;AAC9D,UAAI,KAAK,QAAQ,KAAM;AACvB,UAAI,CAAC,WAAW,KAAK,GAAG,EAAG;AAC3B,UAAI,CAAC,QAAQ,KAAK,YAAY,KAAK,UAAW,QAAO;AAAA,IACvD,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO;AACT;AAEe,SAAR,YAA6B,UAA8B,CAAC,GAAW;AAC5E,QAAM,OAAO,QAAQ,QAAQ,QAAQ,IAAI;AAEzC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,IACP,qBAAqB;AACnB,aAAO;AAAA,QACL;AAAA,UACE,KAAK;AAAA,UACL,OAAO,EAAE,MAAM,UAAU,KAAK,GAAG,QAAQ,aAAa;AAAA,UACtD,UAAU;AAAA,QACZ;AAAA,MACF;AAAA,IACF;AAAA,IACA,gBAAgB,QAAQ;AACtB,aAAO,YAAY,IAAI,UAAU,CAAC,KAAK,KAAK,SAAS;AACnD,cAAM,OAAO,IAAI,OAAO,IAAI,MAAM,GAAG,EAAE,CAAC;AAExC,YAAI,QAAQ,WAAW;AACrB,gBAAM,OAAO,eAAe,IAAI;AAChC,cAAI,UAAU,gBAAgB,kBAAkB;AAChD,cAAI,UAAU,iBAAiB,UAAU;AACzC,cAAI,IAAI,KAAK,UAAU,OAAO,EAAE,MAAM,KAAK,MAAM,OAAO,KAAK,MAAM,IAAI,EAAE,MAAM,KAAK,CAAC,CAAC;AACtF;AAAA,QACF;AAEA,YAAI,QAAQ,cAAc;AACxB,gBAAM,aAAa,kBAAkB,OAAO,OAAO,IAAI;AACvD,cAAI,CAAC,YAAY;AACf,gBAAI,aAAa;AACjB,gBAAI,IAAI,8BAA8B;AACtC;AAAA,UACF;AACA,cAAI,UAAU,gBAAgB,iBAAiB;AAC/C,cAAI,UAAU,iBAAiB,UAAU;AACzC,cAAI,IAAI,aAAa,UAAU,CAAC;AAChC;AAAA,QACF;AAEA,aAAK;AAAA,MACP,CAAC;AAAA,IACH;AAAA,EACF;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ele-picker",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Pick an element in your running page, describe the change, and send it to a pi session. Ships a Vite dev plugin (browser picker + discovery endpoint) and the matching pi bridge extension.",
5
5
  "type": "module",
6
6
  "license": "MIT",