@hienlh/ppm 0.18.11 → 0.18.12

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +81 -23
  3. package/assets/skills/ppm/SKILL.md +1 -1
  4. package/assets/skills/ppm/references/http-api.md +1 -1
  5. package/package.json +1 -1
  6. package/.spike/cdp-client.ts +0 -116
  7. package/.spike/cleanup-ppm-workspace.mjs +0 -44
  8. package/.spike/group-protected-probe.ts +0 -21
  9. package/.spike/pip-eval.ts +0 -17
  10. package/.spike/pip-page-helpers.js +0 -119
  11. package/.spike/pip-results-editor-glue.json +0 -105
  12. package/.spike/pip-results-editor.json +0 -158
  13. package/.spike/pip-results-input-isolation.json +0 -28
  14. package/.spike/pip-results-key-probe.json +0 -58
  15. package/.spike/pip-results-listener-survival.json +0 -98
  16. package/.spike/pip-results-listeners.json +0 -36
  17. package/.spike/pip-results-pagehide.json +0 -92
  18. package/.spike/pip-results-resize.json +0 -104
  19. package/.spike/pip-results-terminal-debug.json +0 -99
  20. package/.spike/pip-results-terminal-v2.json +0 -191
  21. package/.spike/pip-results-terminal.json +0 -220
  22. package/.spike/pip-results-xterm-trace.json +0 -73
  23. package/.spike/pip-spike-common.ts +0 -83
  24. package/.spike/pip-spike-editor-glue.ts +0 -82
  25. package/.spike/pip-spike-editor.ts +0 -129
  26. package/.spike/pip-spike-input-isolation.ts +0 -38
  27. package/.spike/pip-spike-key-probe.ts +0 -34
  28. package/.spike/pip-spike-listener-survival.ts +0 -54
  29. package/.spike/pip-spike-listeners.ts +0 -19
  30. package/.spike/pip-spike-pagehide.ts +0 -59
  31. package/.spike/pip-spike-resize.ts +0 -42
  32. package/.spike/pip-spike-setup.ts +0 -63
  33. package/.spike/pip-spike-terminal-debug.ts +0 -81
  34. package/.spike/pip-spike-terminal-v2.ts +0 -139
  35. package/.spike/pip-spike-terminal.ts +0 -169
  36. package/.spike/pip-spike-xterm-trace.ts +0 -36
  37. package/.spike/probe-drag-memo-error.mjs +0 -67
  38. package/.spike/read-dev-db.ts +0 -15
  39. package/.spike/s.err +0 -63
  40. package/.spike/scratch-editor-target.ts +0 -7
  41. package/.spike/smoke-unified-chrome-pip.mjs +0 -353
  42. package/.spike/tunnel.err +0 -63
  43. package/.spike/vite-restart.err +0 -663
  44. package/.spike/w.err +0 -571
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.18.12] - 2026-09-05
4
+
5
+ ### Fixed
6
+ - **npm package no longer includes local investigation scripts** — the 0.18.11 tarball shipped a `.spike/` scratch folder by mistake; it is now excluded.
7
+
3
8
  ## [0.18.11] - 2026-09-05
4
9
 
5
10
  ### Added
package/README.md CHANGED
@@ -42,7 +42,7 @@ curl -fsSL https://ppm.sh/install | sh
42
42
  irm https://ppm.sh/install.ps1 | iex
43
43
  ```
44
44
 
45
- Downloads the latest binary, adds to PATH, and shows next steps. To upgrade, run the same command again.
45
+ Downloads the latest binary, adds to PATH, and shows next steps. To upgrade, run the same command again (or `ppm upgrade`).
46
46
 
47
47
  ### Via Bun
48
48
 
@@ -66,30 +66,60 @@ After setup, open the URL shown in terminal and enter your access password.
66
66
 
67
67
  ## What You Get
68
68
 
69
- - **AI Chat** — Claude AI with tool execution, file attachments, streaming, session history, slash commands
70
- - **Terminal** — Full PTY terminal (xterm.js), multiple sessions per project
71
- - **File Explorer** — Browse, edit, create, delete files with Monaco editor
72
- - **Git** — Status, diff, commit, push/pull, branching, merge, rebase, commit graph
73
- - **Database** — SQLite + PostgreSQL viewer with query editor, data grid, cell editing
74
- - **Notifications** — Telegram bot integration
75
- - **Remote Access** — Cloudflare tunnel for public URL sharing (always enabled)
76
- - **PPM Cloud** *(optional)* Permanent private link to each machine, phone dashboard to see and open them, remote restart. Stores only your email, machine names and their tunnel URLs — never your code or chats. PPM works fully without it.
77
- - **Command Palette** — Fuzzy search for commands, files, tables (Shift+Shift or F1)
78
- - **PWA** — Installable as a progressive web app
79
- - **Mobile-First** — Responsive UI with bottom sheets and touch optimization
69
+ ### AI
70
+
71
+ - **AI Chat** — tool execution, file attachments, streaming, session history, search, branching, drafts. Chat output is durable: leave the tab, come back, the turn is still there.
72
+ - **Multiple AI providers** — Claude Agent SDK built in; [OpenAI Codex](https://github.com/openai/codex) and Cursor CLI register themselves when their binaries are installed. Pick provider, model, reasoning effort and permission mode per session.
73
+ - **Slash commands, skills, subagents** — the skills, commands and plugin items in your `~/.claude` directory are discovered and runnable from chat.
74
+ - **Agent teams** — a live panel of who is working, the step they are on, elapsed time and model; open any teammate to replay its whole work session.
75
+ - **Group chat** — a multi-agent group conversation where several agents answer in turns.
76
+ - **MCP servers** — add, edit and import MCP servers (auto-imports from `~/.claude.json`).
77
+ - **Scheduled agents** — cron jobs inside PPM that wake a session to work unattended, with turn/timeout budgets and a summary notification.
78
+ - **Multi-account** — store several Claude/Codex accounts, rotate on cooldown, track usage.
79
+ - **API proxy** — expose your accounts as an Anthropic-compatible (and OpenAI-compatible) endpoint under `/proxy/v1/*`, guarded by its own auth key, with request logging.
80
+
81
+ ### Workspace
82
+
83
+ - **Terminal** — full PTY terminal (xterm.js), multiple sessions per project, plus a dock terminal you can open on any folder. Send a code block from chat into a terminal, and terminal output back into chat.
84
+ - **Editor** — Monaco with syntax highlighting, plus viewers for images, PDF, CSV and video (unsupported video formats are transcoded on the fly).
85
+ - **Project explorer** — browse, edit, create, delete, upload (per-file progress, collision prompts) and drag-and-drop files across registered projects.
86
+ - **OS File Explorer** — a floating, resizable, multi-instance window that browses the whole machine: drives, known folders and the pinned folders of your OS; List/Icons/Column views; Cut/Copy/Paste, Rename, Trash, Properties; drag between windows. Full-screen sheet on mobile.
87
+ - **Git** — status, diff, stage, commit, push/pull, branches, merge, rebase, stash, worktrees, an interactive commit graph, and an inline conflict-resolution editor.
88
+ - **Database** — SQLite + PostgreSQL viewer with query editor, data grid, cell editing, and a query audit log. External `.db`/`.sqlite` files open in the same viewer.
89
+ - **System Monitor** — a task manager for the whole machine: CPU per core, RAM, disk, network and NVIDIA GPU charts, plus every process grouped by app with live usage. End a process or a whole app; PPM's own and OS-critical processes are refused.
90
+ - **Floating windows** — Explorer and System Monitor open as windows, and any tab can be popped out into one without reloading its state. Chrome/Edge can send a window into always-on-top picture-in-picture.
91
+ - **Extensions** — VSCode-compatible npm extensions run in isolated Bun workers, contributing tree views, webviews, quick picks and status bar items. See the [extension guide](docs/extension-development-guide.md).
92
+
93
+ ### Access & integrations
94
+
95
+ - **Remote access** — a Cloudflare tunnel gives your machine a public URL (always enabled). Port forwarding opens a tunnel for any localhost dev server.
96
+ - **PPM Cloud** *(optional)* — permanent private link to each machine, phone dashboard to see and open them, remote restart. Stores only your email, machine names and their tunnel URLs — never your code or chats. PPM works fully without it.
97
+ - **Telegram bot (PPMBot)** — a coordinator chat that delegates project tasks to agents, keeps its own memory, and reports back.
98
+ - **Jira** — per-project credentials plus watchers that poll JQL searches and notify you on new results.
99
+ - **Notifications** — cloud push and Telegram when a turn finishes, needs approval, or asks a question.
100
+
101
+ ### The app itself
102
+
103
+ - **Command palette** — fuzzy search for commands, files, tables (Shift+Shift or F1).
104
+ - **Themes** — built-in themes plus import of VSCode themes by URL or `.vsix`.
105
+ - **Stays up** — a supervisor keeps the server alive, checks for new versions, and self-replaces on a one-click upgrade. `ppm autostart` registers it at login.
106
+ - **PWA** — installable as a progressive web app.
107
+ - **Mobile-first** — responsive UI with bottom sheets, long-press menus and touch optimization.
80
108
 
81
109
  ## CLI
82
110
 
83
111
  ```bash
84
112
  # Server
85
113
  ppm start # Start (background daemon, port 3210, tunnel auto-enabled)
86
- ppm start -f # Foreground mode (for debugging)
87
114
  ppm start -p 4000 # Custom port
88
- ppm stop # Stop daemon
89
- ppm restart # Restart
90
- ppm status # Show status
115
+ ppm stop # Stop the server (supervisor stays alive)
116
+ ppm down # Full shutdown (supervisor + server + tunnel)
117
+ ppm restart # Restart, keeping the tunnel alive
118
+ ppm status # Show status (--json for machine-readable)
91
119
  ppm open # Open in browser
92
120
  ppm logs -f # Tail logs
121
+ ppm upgrade # Install updates (--check to only look)
122
+ ppm autostart enable # Start PPM at login (also: disable, status)
93
123
 
94
124
  # Projects
95
125
  ppm projects list
@@ -103,18 +133,42 @@ ppm git commit -m "message"
103
133
  ppm git push
104
134
 
105
135
  # Database
106
- ppm db connections # List DB connections
136
+ ppm db list # Configured connections
137
+ ppm db tables my-db
107
138
  ppm db query my-db "SELECT * FROM users LIMIT 10"
139
+ ppm db run my-db ./migration.sql
140
+
141
+ # Chat
142
+ ppm chat list
143
+ ppm chat create
144
+ ppm chat send <session-id> "message"
145
+ ppm chat resume <session-id>
146
+
147
+ # Scheduled agents
148
+ ppm schedule add
149
+ ppm schedule list
150
+ ppm schedule run-now <id>
151
+ ppm schedule runs <id>
152
+
153
+ # Extensions & skills
154
+ ppm ext list
155
+ ppm ext install <name>
156
+ ppm ext dev /path/to/extension
157
+ ppm skills list
158
+ ppm skills search <query>
159
+
160
+ # Integrations
161
+ ppm cloud login # PPM Cloud (also: status, devices, alias, logout)
162
+ ppm bot status # Telegram coordinator (delegate, tasks, memory, ...)
163
+ ppm jira config set <project>
108
164
 
109
165
  # Config
110
166
  ppm config get port
111
167
  ppm config set port 4000
112
168
 
113
- # Chat (CLI mode)
114
- ppm chat
115
-
116
169
  # Other
117
170
  ppm init # Re-run setup wizard
171
+ ppm export skill # Export the PPM skill for Claude Code
118
172
  ppm report # File bug report on GitHub
119
173
  ```
120
174
 
@@ -136,7 +190,7 @@ The `-y` flag skips all prompts and applies these defaults:
136
190
  - Port: `3210`
137
191
  - Scan directory: `$HOME`
138
192
  - Auth: enabled, password auto-generated (printed at end of `init` output)
139
- - AI model: `claude-opus-4-8`
193
+ - AI model: `claude-opus-5`
140
194
 
141
195
  Override any default with flags:
142
196
 
@@ -162,9 +216,10 @@ Then in Claude Code: `/ppm list my projects` → Claude invokes `ppm projects li
162
216
 
163
217
  ## Requirements
164
218
 
165
- - **Bun** v1.3.6+ ([install](https://bun.sh))
219
+ - **Bun** v1.3.6+ ([install](https://bun.sh)) — not needed for the binary install, which bundles its own runtime
166
220
  - **Git** v2.0+ (for git features)
167
221
  - **Claude Code** authenticated (`claude` CLI logged in) — for AI chat
222
+ - *Optional:* `@openai/codex` or `cursor-agent` on PATH to enable those providers
168
223
 
169
224
  ## Development
170
225
 
@@ -192,12 +247,15 @@ For architecture details, API reference, and contribution guidelines, see the [d
192
247
  | [Code Standards](docs/code-standards.md) | Conventions and patterns |
193
248
  | [Deployment Guide](docs/deployment-guide.md) | Installation, config, troubleshooting |
194
249
  | [Design Guidelines](docs/design-guidelines.md) | UI framework, colors, components |
250
+ | [Extension Development](docs/extension-development-guide.md) | Building PPM extensions |
195
251
  | [Project Roadmap](docs/project-roadmap.md) | Status and plans |
252
+ | [Changelog](CHANGELOG.md) | Release-by-release changes |
196
253
 
197
254
  ## Known Gotchas
198
255
 
199
256
  - **SDK .env poisoning**: Projects with `ANTHROPIC_API_KEY` in `.env` can break SDK tool execution. PPM neutralizes these vars automatically.
200
- - **Windows**: SDK uses CLI fallback (`claude -p`) due to Bun pipe buffering issues. Ensure `claude` is in PATH.
257
+ - **Claude CLI discovery**: PPM looks for the `claude` CLI on PATH, in the usual install locations, and in the `cli/` folder shipped beside the binary. If none is found, point at it with `PPM_CLAUDE_CLI=/path/to/claude`.
258
+ - **Project Claude settings**: a project's `.claude/settings.local.json` can restrict tools even under bypass permissions; PPM overrides it with empty settings so chat tools keep working.
201
259
 
202
260
  ---
203
261
 
@@ -71,4 +71,4 @@ This skill covers the `ppm` CLI, its HTTP API, and its config DB. It does **not*
71
71
  - Third-party extensions (inspect via `ppm ext list`).
72
72
  - The Claude Agent SDK internals (separate skill).
73
73
 
74
- <!-- Generated for PPM v0.18.11 at build time. Re-run `ppm export skill --install` to refresh. -->
74
+ <!-- Generated for PPM v0.18.12 at build time. Re-run `ppm export skill --install` to refresh. -->
@@ -309,4 +309,4 @@ _Base URL: `http://localhost:8080` (default; override via `ppm config set port <
309
309
  - `ws://<host>/ws/terminal` — PTY terminal multiplexer
310
310
  - `ws://<host>/ws/extensions` — extension host channel
311
311
 
312
- <!-- Generated from src/server/routes/ for PPM v0.18.11 -->
312
+ <!-- Generated from src/server/routes/ for PPM v0.18.12 -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.18.11",
3
+ "version": "0.18.12",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -1,116 +0,0 @@
1
- // Minimal Chrome DevTools Protocol client over the browser WebSocket endpoint.
2
- // Spike helper only (untracked). Flattened sessions: one WS, per-target sessionId.
3
-
4
- export interface CdpTarget {
5
- targetId: string;
6
- type: string;
7
- title: string;
8
- url: string;
9
- attached: boolean;
10
- }
11
-
12
- type Pending = { resolve: (v: any) => void; reject: (e: Error) => void; method: string };
13
-
14
- export class CdpClient {
15
- private ws!: WebSocket;
16
- private nextId = 1;
17
- private pending = new Map<number, Pending>();
18
- private listeners: Array<(msg: any) => void> = [];
19
-
20
- static async connect(port: number): Promise<CdpClient> {
21
- const res = await fetch(`http://127.0.0.1:${port}/json/version`);
22
- const info = (await res.json()) as { webSocketDebuggerUrl: string };
23
- const client = new CdpClient();
24
- await client.open(info.webSocketDebuggerUrl);
25
- return client;
26
- }
27
-
28
- private open(url: string): Promise<void> {
29
- return new Promise((resolve, reject) => {
30
- this.ws = new WebSocket(url);
31
- this.ws.onopen = () => resolve();
32
- this.ws.onerror = (e) => reject(new Error(`ws error ${String(e)}`));
33
- this.ws.onmessage = (ev) => {
34
- const msg = JSON.parse(String(ev.data));
35
- if (msg.id && this.pending.has(msg.id)) {
36
- const p = this.pending.get(msg.id)!;
37
- this.pending.delete(msg.id);
38
- if (msg.error) p.reject(new Error(`${p.method}: ${msg.error.message} ${msg.error.data ?? ""}`));
39
- else p.resolve(msg.result);
40
- } else {
41
- for (const l of this.listeners) l(msg);
42
- }
43
- };
44
- });
45
- }
46
-
47
- onEvent(fn: (msg: any) => void) {
48
- this.listeners.push(fn);
49
- }
50
-
51
- send(method: string, params: Record<string, unknown> = {}, sessionId?: string): Promise<any> {
52
- const id = this.nextId++;
53
- const payload: Record<string, unknown> = { id, method, params };
54
- if (sessionId) payload.sessionId = sessionId;
55
- return new Promise((resolve, reject) => {
56
- this.pending.set(id, { resolve, reject, method });
57
- this.ws.send(JSON.stringify(payload));
58
- });
59
- }
60
-
61
- async targets(): Promise<CdpTarget[]> {
62
- const r = await this.send("Target.getTargets");
63
- return r.targetInfos as CdpTarget[];
64
- }
65
-
66
- async attach(targetId: string): Promise<string> {
67
- const r = await this.send("Target.attachToTarget", { targetId, flatten: true });
68
- return r.sessionId as string;
69
- }
70
-
71
- /** Runtime.evaluate with awaitPromise + returnByValue; throws on JS exception. */
72
- async eval<T = unknown>(sessionId: string, expression: string): Promise<T> {
73
- const r = await this.send(
74
- "Runtime.evaluate",
75
- { expression, awaitPromise: true, returnByValue: true, userGesture: false },
76
- sessionId,
77
- );
78
- if (r.exceptionDetails) {
79
- const d = r.exceptionDetails;
80
- throw new Error(`JS exception: ${d.exception?.description ?? d.text}`);
81
- }
82
- return r.result?.value as T;
83
- }
84
-
85
- async click(sessionId: string, x: number, y: number) {
86
- const base = { x, y, button: "left", clickCount: 1 };
87
- await this.send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y }, sessionId);
88
- await this.send("Input.dispatchMouseEvent", { ...base, type: "mousePressed" }, sessionId);
89
- await this.send("Input.dispatchMouseEvent", { ...base, type: "mouseReleased" }, sessionId);
90
- }
91
-
92
- /** Type printable text as trusted key events (one keyDown/keyUp per char). */
93
- async typeText(sessionId: string, text: string) {
94
- for (const ch of text) {
95
- await this.send("Input.dispatchKeyEvent", { type: "keyDown", text: ch, key: ch, unmodifiedText: ch }, sessionId);
96
- await this.send("Input.dispatchKeyEvent", { type: "keyUp", key: ch }, sessionId);
97
- }
98
- }
99
-
100
- async pressKey(sessionId: string, key: string, code: string, vk: number, text?: string, modifiers = 0) {
101
- const common = { key, code, windowsVirtualKeyCode: vk, nativeVirtualKeyCode: vk, modifiers };
102
- await this.send("Input.dispatchKeyEvent", { type: text ? "keyDown" : "rawKeyDown", ...common, ...(text ? { text, unmodifiedText: text } : {}) }, sessionId);
103
- await this.send("Input.dispatchKeyEvent", { type: "keyUp", ...common }, sessionId);
104
- }
105
-
106
- async screenshot(sessionId: string, path: string) {
107
- const r = await this.send("Page.captureScreenshot", { format: "png" }, sessionId);
108
- await Bun.write(path, Buffer.from(r.data, "base64"));
109
- }
110
-
111
- close() {
112
- this.ws.close();
113
- }
114
- }
115
-
116
- export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
@@ -1,44 +0,0 @@
1
- // One-off: remove the duplicate `terminal:1@panel-*` tabs an earlier smoke run left in the
2
- // real `ppm` project workspace. Prints before/after; makes no other change.
3
- import { homedir } from "node:os";
4
- import { join } from "node:path";
5
- import { Database } from "bun:sqlite";
6
-
7
- const API = "http://127.0.0.1:8099";
8
- const db = new Database(join(homedir(), ".ppm", "ppm.dev.db"), { readonly: true });
9
- const TOKEN = JSON.parse(db.query("SELECT value FROM config WHERE key='auth'").get().value).token;
10
- db.close();
11
-
12
- const H = { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" };
13
- const res = await fetch(`${API}/api/project/ppm/workspace`, { headers: H });
14
- const json = await res.json();
15
- if (!json.ok || !json.data) {
16
- console.log("no server workspace for ppm:", JSON.stringify(json).slice(0, 300));
17
- process.exit(0);
18
- }
19
- const layout = json.data.layout;
20
- const dump = (l) =>
21
- JSON.stringify(Object.fromEntries(Object.entries(l.panels).map(([k, v]) => [k, v.tabs.map((t) => t.id)])));
22
- console.log("before:", dump(layout));
23
-
24
- const BAD = /^terminal:1@panel-/;
25
- let removed = 0;
26
- for (const panel of Object.values(layout.panels)) {
27
- const keep = panel.tabs.filter((t) => !BAD.test(t.id));
28
- removed += panel.tabs.length - keep.length;
29
- const ids = new Set(keep.map((t) => t.id));
30
- panel.tabs = keep;
31
- panel.tabHistory = (panel.tabHistory ?? []).filter((id) => ids.has(id));
32
- if (panel.activeTabId && !ids.has(panel.activeTabId)) {
33
- panel.activeTabId = panel.tabHistory.at(-1) ?? keep.at(-1)?.id ?? null;
34
- }
35
- }
36
- console.log("after: ", dump(layout), `removed=${removed}`);
37
- if (removed === 0) process.exit(0);
38
-
39
- const put = await fetch(`${API}/api/project/ppm/workspace`, {
40
- method: "PUT",
41
- headers: H,
42
- body: JSON.stringify({ layout }),
43
- });
44
- console.log("PUT:", put.status, JSON.stringify(await put.json()).slice(0, 200));
@@ -1,21 +0,0 @@
1
- // How many groups would the whole-app kill button be enabled for, per the live snapshot?
2
- import { Database } from "bun:sqlite";
3
- import { homedir } from "node:os";
4
- import { resolve } from "node:path";
5
- import { isGroupProtected } from "../src/web/components/system/build-kill-request.ts";
6
-
7
- const db = new Database(resolve(homedir(), ".ppm", "ppm.dev.db"), { readonly: true });
8
- const row = db.query("SELECT value FROM config WHERE key = 'auth'").get() as { value: string } | null;
9
- const token = row ? JSON.parse(row.value)?.token : undefined;
10
- const res = await fetch("http://localhost:8081/api/system/resources?processes=1", { headers: token ? { Authorization: `Bearer ${token}` } : {} });
11
- const snap = (await res.json()).data;
12
- const byPid = new Map(snap.processes.map((p: any) => [p.pid, p]));
13
- let enabled = 0, disabled = 0;
14
- const protectedNames = new Map<string, number>();
15
- for (const g of snap.groups) {
16
- const members = g.pids.map((pid: number) => byPid.get(pid)).filter(Boolean);
17
- if (isGroupProtected(members)) { disabled++; for (const m of members) if (m.protected) protectedNames.set(m.name, (protectedNames.get(m.name) ?? 0) + 1); }
18
- else enabled++;
19
- }
20
- console.log({ groups: snap.groups.length, enabled, disabled, processes: snap.processes.length, protectedProcs: snap.processes.filter((p: any) => p.protected).length });
21
- console.log("protected names in disabled groups:", [...protectedNames].slice(0, 12));
@@ -1,17 +0,0 @@
1
- // Ad-hoc: evaluate a JS expression in the spike Chrome's app page. Usage: bun pip-eval.ts "<expr>" [--shot path]
2
- import { CdpClient } from "./cdp-client";
3
- const expr = process.argv[2] ?? "location.href";
4
- const shotIdx = process.argv.indexOf("--shot");
5
- const cdp = await CdpClient.connect(9333);
6
- const targets = await cdp.targets();
7
- const page = targets.find((t) => t.type === "page" && t.url.startsWith("http://localhost:5173"));
8
- if (!page) throw new Error("no page: " + JSON.stringify(targets));
9
- const sid = await cdp.attach(page.targetId);
10
- try {
11
- const v = await cdp.eval(sid, expr);
12
- console.log(typeof v === "string" ? v : JSON.stringify(v, null, 2));
13
- } catch (e) {
14
- console.error(String(e));
15
- }
16
- if (shotIdx > 0) await cdp.screenshot(sid, process.argv[shotIdx + 1]!);
17
- cdp.close();
@@ -1,119 +0,0 @@
1
- // Injected into the PPM page via Runtime.evaluate. Defines window.__spike helpers.
2
- // Spike helper only (untracked). Plain JS, no bundling.
3
- (() => {
4
- const S = (window.__spike = window.__spike || { log: [], results: {} });
5
- S.note = (m) => { S.log.push(`${new Date().toISOString().slice(11, 23)} ${m}`); console.log("[spike]", m); };
6
-
7
- // ---- React fiber walk: find xterm Terminal instance owned by a tab wrapper ----
8
- S.fiberOf = (el) => {
9
- const k = Object.keys(el).find((k) => k.startsWith("__reactFiber$"));
10
- return k ? el[k] : null;
11
- };
12
- S.findTerminal = (wrapper) => {
13
- const container = wrapper.querySelector(".xterm")?.parentElement;
14
- if (!container) return null;
15
- let f = S.fiberOf(container);
16
- for (let depth = 0; f && depth < 40; depth++, f = f.return) {
17
- let hook = f.memoizedState;
18
- for (let i = 0; hook && i < 200; i++, hook = hook.next) {
19
- const cur = hook.memoizedState && hook.memoizedState.current;
20
- if (cur && typeof cur.getSelection === "function" && cur.buffer && typeof cur.rows === "number") return cur;
21
- }
22
- }
23
- return null;
24
- };
25
- S.termText = (term, lastN = 60) => {
26
- const buf = term.buffer.active;
27
- const out = [];
28
- for (let i = Math.max(0, buf.length - lastN); i < buf.length; i++) out.push(buf.getLine(i)?.translateToString(true) ?? "");
29
- return out.join("\n");
30
- };
31
-
32
- // ---- Monaco editor instance owned by a wrapper ----
33
- S.findEditor = (wrapper) => {
34
- const m = window.monaco;
35
- if (!m) return null;
36
- return m.editor.getEditors().find((e) => wrapper.contains(e.getDomNode())) ?? null;
37
- };
38
-
39
- // ---- Style copy into PiP document ----
40
- S.copyStyles = (pipDoc) => {
41
- const stats = { inlineSheets: 0, clonedLinks: 0, failed: 0, rules: 0 };
42
- for (const sheet of document.styleSheets) {
43
- try {
44
- const rules = [...sheet.cssRules];
45
- const style = pipDoc.createElement("style");
46
- style.textContent = rules.map((r) => r.cssText).join("\n");
47
- pipDoc.head.append(style);
48
- stats.inlineSheets++; stats.rules += rules.length;
49
- } catch (e) {
50
- if (sheet.href) {
51
- const link = pipDoc.createElement("link");
52
- link.rel = "stylesheet"; link.href = sheet.href;
53
- pipDoc.head.append(link); stats.clonedLinks++;
54
- } else stats.failed++;
55
- }
56
- }
57
- // Theme lives on <html class="dark|light" style="--token: ...">
58
- pipDoc.documentElement.className = document.documentElement.className;
59
- pipDoc.documentElement.style.cssText = document.documentElement.style.cssText;
60
- pipDoc.body.className = document.body.className;
61
- pipDoc.body.style.cssText = "margin:0;width:100vw;height:100vh;overflow:hidden;position:relative;";
62
- return stats;
63
- };
64
-
65
- // ---- Move / return ----
66
- S.moveToPip = (pip, wrapper) => {
67
- const slot = wrapper.parentElement;
68
- S.origin = { slot, next: wrapper.nextSibling, display: wrapper.style.display, cssText: wrapper.style.cssText };
69
- pip.document.body.append(wrapper); // adopts node into pip document
70
- wrapper.style.position = "relative"; wrapper.style.inset = ""; wrapper.style.width = "100%"; wrapper.style.height = "100%";
71
- return { adopted: wrapper.ownerDocument === pip.document, slotTag: slot.tagName, slotHasWrapper: slot.contains(wrapper) };
72
- };
73
- // Spike-only: stop ReparentingTab's layout effect (slot.appendChild(wrapper) on every render) from pulling the wrapper back.
74
- S.pinToPip = (wrapper) => {
75
- const slot = S.origin.slot; S.pinned = wrapper; S.pullbackAttempts = 0;
76
- if (!slot.__spikePatched) {
77
- const orig = slot.appendChild.bind(slot);
78
- slot.appendChild = (n) => { if (n === S.pinned) { S.pullbackAttempts++; return n; } return orig(n); };
79
- slot.__spikePatched = true;
80
- }
81
- };
82
- S.unpin = () => { S.pinned = null; };
83
- S.moveBack = (wrapper) => {
84
- const { slot, cssText } = S.origin;
85
- S.unpin();
86
- slot.append(wrapper);
87
- wrapper.style.cssText = cssText;
88
- return { back: wrapper.ownerDocument === document && wrapper.parentElement === slot };
89
- };
90
-
91
- // ---- Trusted-gesture button: onclick runs the async fn, stores result ----
92
- S.installButton = (id, fn) => {
93
- document.getElementById(id)?.remove();
94
- const b = document.createElement("button");
95
- b.id = id; b.textContent = id;
96
- b.style.cssText = "position:fixed;left:8px;bottom:8px;z-index:2147483647;padding:8px 12px;background:#e11;color:#fff;font:14px sans-serif;";
97
- b.onclick = async () => {
98
- S.results[id] = { phase: "running" };
99
- try { S.results[id] = { phase: "done", value: await fn() }; }
100
- catch (e) { S.results[id] = { phase: "error", error: String(e && e.stack || e) }; S.note(`button ${id} error: ${e}`); }
101
- };
102
- document.body.append(b);
103
- const r = b.getBoundingClientRect();
104
- return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
105
- };
106
-
107
- // ---- Event probes (native listeners in main doc + pip doc + on wrapper) ----
108
- S.installClickProbes = (pip, wrapper) => {
109
- S.probe = { mainDocClick: 0, pipDocClick: 0, wrapperClick: 0, reactRootClick: 0 };
110
- document.addEventListener("click", () => S.probe.mainDocClick++, true);
111
- pip.document.addEventListener("click", () => S.probe.pipDocClick++, true);
112
- wrapper.addEventListener("click", () => S.probe.wrapperClick++, true);
113
- document.getElementById("root")?.addEventListener("click", () => S.probe.reactRootClick++, true);
114
- };
115
-
116
- S.toastText = () => [...document.querySelectorAll("[data-sonner-toast]")].map((t) => t.textContent).join(" | ");
117
- S.note("helpers installed");
118
- return "ok";
119
- })();
@@ -1,105 +0,0 @@
1
- {
2
- "mainState": {
3
- "layout": [
4
- 1395,
5
- 814
6
- ],
7
- "slot": [
8
- 1033,
9
- 655
10
- ],
11
- "editContextOpt": true,
12
- "nativeEditContextEl": true,
13
- "editContextApi": true,
14
- "minimapEnabled": false
15
- },
16
- "layoutFixInMain": [
17
- 1033,
18
- 627
19
- ],
20
- "move": {
21
- "phase": "done",
22
- "value": {
23
- "pipSize": [
24
- 1395,
25
- 842
26
- ],
27
- "layoutBeforeExplicit": [
28
- 1033,
29
- 627
30
- ],
31
- "layoutAfterExplicit": [
32
- 1395,
33
- 814
34
- ],
35
- "wrapper": [
36
- 1395,
37
- 842
38
- ]
39
- }
40
- },
41
- "pipFocusTarget": {
42
- "focused": "native-edit-context",
43
- "hasEditContext": true
44
- },
45
- "typingEditContext": {
46
- "line2": "EC export const spikeLine1 = \"one\";",
47
- "pass": true
48
- },
49
- "insertTextEditContext": {
50
- "line2": "EC IT export const spikeLine1 = \"one\";",
51
- "pass": true
52
- },
53
- "switchToTextarea": {
54
- "editContextOpt": false,
55
- "textarea": true,
56
- "nec": false
57
- },
58
- "typingTextarea": {
59
- "line2": "TA EC IT export const spikeLine1 = \"one\";",
60
- "pass": true
61
- },
62
- "suggestWidget": {
63
- "inWrapper": true,
64
- "visible": true,
65
- "inMainDoc": false,
66
- "items": 12
67
- },
68
- "resizeGlue": {
69
- "pipResizes": 2,
70
- "layout": [
71
- 684,
72
- 408
73
- ],
74
- "pip": [
75
- 684,
76
- 408
77
- ]
78
- },
79
- "back": {
80
- "back": true,
81
- "layoutStaleAfterReturn": [
82
- 684,
83
- 408
84
- ],
85
- "layoutAfterExplicit": [
86
- 1033,
87
- 627
88
- ],
89
- "slot": [
90
- 1033,
91
- 655
92
- ]
93
- },
94
- "undoAfterReturn": {
95
- "steps": [
96
- "TA EC IT export const spikeLine1 = \"one\"; || L8=",
97
- "TAEC IT export const spikeLine1 = \"one\"; || L8=",
98
- "EC ITexport const spikeLine1 = \"one\"; || L8=",
99
- "ECexport const spikeLine1 = \"one\"; || L8=",
100
- "export const spikeLine1 = \"one\"; || L8=",
101
- "export const spikeLine1 = \"one\"; || L8="
102
- ],
103
- "line2Restored": true
104
- }
105
- }