pi-ele-picker 0.1.2 → 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/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.2",
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",