@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.
- package/CHANGELOG.md +5 -0
- package/README.md +81 -23
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/package.json +1 -1
- package/.spike/cdp-client.ts +0 -116
- package/.spike/cleanup-ppm-workspace.mjs +0 -44
- package/.spike/group-protected-probe.ts +0 -21
- package/.spike/pip-eval.ts +0 -17
- package/.spike/pip-page-helpers.js +0 -119
- package/.spike/pip-results-editor-glue.json +0 -105
- package/.spike/pip-results-editor.json +0 -158
- package/.spike/pip-results-input-isolation.json +0 -28
- package/.spike/pip-results-key-probe.json +0 -58
- package/.spike/pip-results-listener-survival.json +0 -98
- package/.spike/pip-results-listeners.json +0 -36
- package/.spike/pip-results-pagehide.json +0 -92
- package/.spike/pip-results-resize.json +0 -104
- package/.spike/pip-results-terminal-debug.json +0 -99
- package/.spike/pip-results-terminal-v2.json +0 -191
- package/.spike/pip-results-terminal.json +0 -220
- package/.spike/pip-results-xterm-trace.json +0 -73
- package/.spike/pip-spike-common.ts +0 -83
- package/.spike/pip-spike-editor-glue.ts +0 -82
- package/.spike/pip-spike-editor.ts +0 -129
- package/.spike/pip-spike-input-isolation.ts +0 -38
- package/.spike/pip-spike-key-probe.ts +0 -34
- package/.spike/pip-spike-listener-survival.ts +0 -54
- package/.spike/pip-spike-listeners.ts +0 -19
- package/.spike/pip-spike-pagehide.ts +0 -59
- package/.spike/pip-spike-resize.ts +0 -42
- package/.spike/pip-spike-setup.ts +0 -63
- package/.spike/pip-spike-terminal-debug.ts +0 -81
- package/.spike/pip-spike-terminal-v2.ts +0 -139
- package/.spike/pip-spike-terminal.ts +0 -169
- package/.spike/pip-spike-xterm-trace.ts +0 -36
- package/.spike/probe-drag-memo-error.mjs +0 -67
- package/.spike/read-dev-db.ts +0 -15
- package/.spike/s.err +0 -63
- package/.spike/scratch-editor-target.ts +0 -7
- package/.spike/smoke-unified-chrome-pip.mjs +0 -353
- package/.spike/tunnel.err +0 -63
- package/.spike/vite-restart.err +0 -663
- package/.spike/w.err +0 -571
package/CHANGELOG.md
CHANGED
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
|
-
|
|
70
|
-
|
|
71
|
-
- **
|
|
72
|
-
- **
|
|
73
|
-
- **
|
|
74
|
-
- **
|
|
75
|
-
- **
|
|
76
|
-
- **
|
|
77
|
-
- **
|
|
78
|
-
- **
|
|
79
|
-
- **
|
|
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
|
|
89
|
-
ppm
|
|
90
|
-
ppm
|
|
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
|
|
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-
|
|
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
|
-
- **
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|
package/.spike/cdp-client.ts
DELETED
|
@@ -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));
|
package/.spike/pip-eval.ts
DELETED
|
@@ -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
|
-
}
|