@hienlh/ppm 0.19.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.19.1] - 2026-09-09
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **Remote Desktop no longer claims ffmpeg is missing on a Mac that has it** — a PPM started by launchd inherits a bare `PATH` without `/opt/homebrew/bin`; ffmpeg is now also looked for in the usual Homebrew / winget / chocolatey / apt locations, and a "not installed" answer is re-checked instead of being cached until restart, so the checklist really does update once `brew install ffmpeg` finishes.
|
|
9
|
+
- **"Session not found" when opening a terminal from outside a project** — the remote-desktop checklist's install button opened a shell with no project; such a terminal now starts in your home directory.
|
|
10
|
+
|
|
5
11
|
## [0.19.0] - 2026-09-08
|
|
6
12
|
|
|
7
13
|
### Added
|
|
@@ -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.19.
|
|
74
|
+
<!-- Generated for PPM v0.19.1 at build time. Re-run `ppm export skill --install` to refresh. -->
|
|
@@ -335,4 +335,4 @@ _Base URL: `http://localhost:8080` (default; override via `ppm config set port <
|
|
|
335
335
|
- `ws://<host>/ws/terminal` — PTY terminal multiplexer
|
|
336
336
|
- `ws://<host>/ws/extensions` — extension host channel
|
|
337
337
|
|
|
338
|
-
<!-- Generated from src/server/routes/ for PPM v0.19.
|
|
338
|
+
<!-- Generated from src/server/routes/ for PPM v0.19.1 -->
|
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { terminalService } from "../../services/terminal.service.ts";
|
|
3
4
|
import { resolveProjectPath } from "../helpers/resolve-project.ts";
|
|
4
5
|
import { assertAllowed, resolvePath } from "../../services/fs-path-guard.service.ts";
|
|
@@ -7,6 +8,9 @@ import { assertAllowed, resolvePath } from "../../services/fs-path-guard.service
|
|
|
7
8
|
* Where a new shell starts. An explicit `cwd` (explorer "Open in Terminal") wins
|
|
8
9
|
* over the project root and goes through the same allowlist as the filesystem
|
|
9
10
|
* API, so a terminal cannot be spawned anywhere the explorer could not browse.
|
|
11
|
+
* Neither given (a project-less "run this command" — the remote-desktop
|
|
12
|
+
* checklist's `brew install ffmpeg` button) → the user's home, the same place the
|
|
13
|
+
* OS explorer starts browsing from.
|
|
10
14
|
*/
|
|
11
15
|
function resolveStartDir(projectName: string | undefined, cwd: string | undefined): string {
|
|
12
16
|
if (cwd) {
|
|
@@ -16,7 +20,7 @@ function resolveStartDir(projectName: string | undefined, cwd: string | undefine
|
|
|
16
20
|
return resolved;
|
|
17
21
|
}
|
|
18
22
|
if (projectName) return resolveProjectPath(projectName);
|
|
19
|
-
|
|
23
|
+
return homedir();
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
/** Control message prefix for resize commands */
|
|
@@ -36,8 +40,10 @@ export const terminalWebSocket = {
|
|
|
36
40
|
|
|
37
41
|
let session = id !== "new" ? terminalService.get(id) : undefined;
|
|
38
42
|
|
|
39
|
-
//
|
|
40
|
-
|
|
43
|
+
// A fresh socket ("new") always gets a shell — project root, explicit cwd, or home — and
|
|
44
|
+
// so does a stale id that still knows its project/cwd (tab reopened after a restart).
|
|
45
|
+
// Only a stale id with no context left is "Session not found".
|
|
46
|
+
if (!session && (id === "new" || projectName || cwd)) {
|
|
41
47
|
try {
|
|
42
48
|
const startDir = resolveStartDir(projectName, cwd);
|
|
43
49
|
// Create session with the requested ID — but TerminalService generates its own ID.
|
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
* Discover ffmpeg/ffprobe on this machine and pick the fastest working H.264 encoder.
|
|
3
3
|
*
|
|
4
4
|
* ffmpeg is an *optional* dependency: without it PPM still plays browser-native
|
|
5
|
-
* media (mp4/webm) via Range requests, it just cannot transcode AVI/MKV.
|
|
6
|
-
* result is cached for the process lifetime —
|
|
7
|
-
*
|
|
5
|
+
* media (mp4/webm) via Range requests, it just cannot transcode AVI/MKV. A
|
|
6
|
+
* positive result is cached for the process lifetime — probing hardware encoders
|
|
7
|
+
* costs a real encode each. A *negative* result is not: the remote-desktop
|
|
8
|
+
* checklist tells the user to install ffmpeg and promises to notice, so "not
|
|
9
|
+
* found" is re-checked on every call (one `which` + a few `stat`s).
|
|
10
|
+
*
|
|
11
|
+
* `PATH` alone is not enough to find it. A daemon started by launchd inherits
|
|
12
|
+
* `/usr/bin:/bin:/usr/sbin:/sbin` — Homebrew's `/opt/homebrew/bin` is not on it —
|
|
13
|
+
* so the well-known install directories are probed after `Bun.which`.
|
|
8
14
|
*
|
|
9
15
|
* Hardware encoders are verified by encoding a few synthetic frames rather than by
|
|
10
16
|
* grepping `ffmpeg -encoders`: the list only says the build was compiled with
|
|
@@ -60,11 +66,42 @@ async function encoderWorks(ffmpeg: string, encoder: string): Promise<boolean> {
|
|
|
60
66
|
}
|
|
61
67
|
}
|
|
62
68
|
|
|
69
|
+
/** Where package managers put ffmpeg when it is not on the daemon's PATH. */
|
|
70
|
+
function wellKnownBinDirs(): string[] {
|
|
71
|
+
switch (process.platform) {
|
|
72
|
+
case "darwin": return ["/opt/homebrew/bin", "/usr/local/bin", "/opt/local/bin"];
|
|
73
|
+
case "win32": {
|
|
74
|
+
const local = process.env.LOCALAPPDATA;
|
|
75
|
+
return [
|
|
76
|
+
...(local ? [`${local}\\Microsoft\\WinGet\\Links`] : []),
|
|
77
|
+
"C:\\ProgramData\\chocolatey\\bin", "C:\\ffmpeg\\bin",
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
default: return ["/usr/local/bin", "/usr/bin", "/snap/bin", `${process.env.HOME ?? ""}/.local/bin`];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** `Bun.which` on `searchPath` (the process PATH by default) first, then the well-known
|
|
85
|
+
* directories. Exported for tests. */
|
|
86
|
+
export function findFfmpegBinary(
|
|
87
|
+
name: "ffmpeg" | "ffprobe",
|
|
88
|
+
dirs: string[] = wellKnownBinDirs(),
|
|
89
|
+
searchPath: string | undefined = process.env.PATH,
|
|
90
|
+
): string | null {
|
|
91
|
+
const onPath = Bun.which(name, searchPath ? { PATH: searchPath } : undefined);
|
|
92
|
+
if (onPath) return onPath;
|
|
93
|
+
for (const dir of dirs) {
|
|
94
|
+
const found = Bun.which(name, { PATH: dir });
|
|
95
|
+
if (found) return found;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
63
100
|
let cached: Promise<FfmpegCapabilities> | null = null;
|
|
64
101
|
|
|
65
102
|
async function detect(): Promise<FfmpegCapabilities> {
|
|
66
|
-
const ffmpeg =
|
|
67
|
-
const ffprobe =
|
|
103
|
+
const ffmpeg = findFfmpegBinary("ffmpeg");
|
|
104
|
+
const ffprobe = findFfmpegBinary("ffprobe");
|
|
68
105
|
if (!ffmpeg) return { ffmpeg: null, ffprobe, encoder: null };
|
|
69
106
|
for (const candidate of encoderCandidates()) {
|
|
70
107
|
if (await encoderWorks(ffmpeg, candidate)) return { ffmpeg, ffprobe, encoder: candidate };
|
|
@@ -72,10 +109,14 @@ async function detect(): Promise<FfmpegCapabilities> {
|
|
|
72
109
|
return { ffmpeg, ffprobe, encoder: null };
|
|
73
110
|
}
|
|
74
111
|
|
|
75
|
-
/** Cached capability lookup; the first call pays for the encoder probes.
|
|
112
|
+
/** Cached capability lookup; the first call that finds ffmpeg pays for the encoder probes.
|
|
113
|
+
* "Not installed" is never cached — the user may be running `brew install ffmpeg` right now. */
|
|
76
114
|
export function getFfmpegCapabilities(): Promise<FfmpegCapabilities> {
|
|
77
115
|
if (!cached) {
|
|
78
|
-
cached = detect().
|
|
116
|
+
cached = detect().then((caps) => {
|
|
117
|
+
if (!caps.ffmpeg) cached = null;
|
|
118
|
+
return caps;
|
|
119
|
+
}).catch((e) => {
|
|
79
120
|
cached = null; // let a later call retry after an unexpected failure
|
|
80
121
|
throw e;
|
|
81
122
|
});
|