@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.0 at build time. Re-run `ppm export skill --install` to refresh. -->
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.0 -->
338
+ <!-- Generated from src/server/routes/ for PPM v0.19.1 -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.19.0",
3
+ "version": "0.19.1",
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,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
- throw new Error("Session not found");
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
- // If session doesn't exist and a start directory is known, create one
40
- if (!session && (projectName || cwd)) {
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. The
6
- * result is cached for the process lifetime — the binary does not appear
7
- * mid-session, and probing hardware encoders costs a real encode each.
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 = Bun.which("ffmpeg");
67
- const ffprobe = Bun.which("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().catch((e) => {
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
  });