harden installers rendering and dependency hygiene

This commit is contained in:
Advait Paliwal
2026-04-09 10:27:23 -07:00
parent 3148f2e62b
commit 96234425ba
14 changed files with 138 additions and 75 deletions

View File

@@ -131,3 +131,12 @@ Use this file to track chronology, not release notes. Keep entries short, factua
- Failed / learned: The skills-only install issue was not just docs drift; the shipped `SKILL.md` files referenced prompt paths that only made sense after installation, so the repo needed both path normalization and packaging changes.
- Blockers: Remote issue/PR closure and merge actions still depend on the final reviewed branch state being pushed.
- Next: Push the validated fixes, close the duplicate Windows/reporting issues they supersede, reject the promotional ValiChord PR explicitly, and then review whether the remaining docs-only or feature PRs should be merged separately.
### 2026-04-09 10:28 PDT — verification-and-security-pass
- Objective: Run a deeper install/security verification pass against the post-cleanup `0.2.17` tree instead of assuming the earlier targeted fixes covered the shipped artifacts.
- Changed: Reworked `extensions/research-tools/header.ts` to use `@mariozechner/pi-tui` width-aware helpers for truncation/wrapping so wide Unicode text does not overflow custom header rows; changed `src/pi/launch.ts` to stop mirroring child crash signals back onto the parent process and instead emit a conventional exit code; added `FEYNMAN_INSTALL_SKILLS_ARCHIVE_URL` overrides to the skills installers for pre-release smoke testing; aligned root and website dependency trees with patched transitive versions using npm `overrides`; fixed `src/pi/web-access.ts` so `search status` respects `FEYNMAN_HOME` semantics instead of hardcoding the current shell home directory; added `tests/pi-launch.test.ts`.
- Verified: Ran `npm test`, `npm run typecheck`, `npm run build`, `cd website && npm run build`, `npm run build:native-bundle`; smoke-tested `scripts/install/install.sh` against a locally served `dist/release/feynman-0.2.17-darwin-arm64.tar.gz`; smoke-tested `scripts/install/install-skills.sh` against a local source archive; confirmed installed `feynman --version`, `feynman --help`, `feynman doctor`, and packaged `feynman search status` work from the installed bundle; `npm audit --omit=dev` is clean in the root app and website after overrides.
- Failed / learned: The first packaged `search status` smoke test still showed the user home path because the native bundle had been built before the `FEYNMAN_HOME` path fix; rebuilding the native bundle resolved that mismatch.
- Blockers: PowerShell runtime was unavailable locally, so Windows installer execution remained code-path validated rather than actually executed.
- Next: Push the second-pass hardening commit, then keep issue `#46` and issue `#47` open until users on the affected Linux/CJK environments confirm whether the launcher/header fixes fully resolve them.

View File

@@ -4,6 +4,7 @@ import { execSync } from "node:child_process";
import { resolve as resolvePath } from "node:path";
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
import {
APP_ROOT,
@@ -11,10 +12,8 @@ import {
FEYNMAN_VERSION,
} from "./shared.js";
const ANSI_RE = /\x1b\[[0-9;]*m/g;
function visibleLength(text: string): number {
return text.replace(ANSI_RE, "").length;
return visibleWidth(text);
}
function formatHeaderPath(path: string): string {
@@ -23,10 +22,8 @@ function formatHeaderPath(path: string): string {
}
function truncateVisible(text: string, maxVisible: number): string {
const raw = text.replace(ANSI_RE, "");
if (raw.length <= maxVisible) return text;
if (maxVisible <= 3) return ".".repeat(maxVisible);
return `${raw.slice(0, maxVisible - 3)}...`;
if (visibleWidth(text) <= maxVisible) return text;
return truncateToWidth(text, maxVisible, maxVisible <= 3 ? "" : "...");
}
function wrapWords(text: string, maxW: number): string[] {
@@ -34,12 +31,12 @@ function wrapWords(text: string, maxW: number): string[] {
const lines: string[] = [];
let cur = "";
for (let word of words) {
if (word.length > maxW) {
if (visibleWidth(word) > maxW) {
if (cur) { lines.push(cur); cur = ""; }
word = maxW > 3 ? `${word.slice(0, maxW - 1)}` : word.slice(0, maxW);
word = truncateToWidth(word, maxW, maxW > 3 ? "…" : "");
}
const test = cur ? `${cur} ${word}` : word;
if (cur && test.length > maxW) {
if (cur && visibleWidth(test) > maxW) {
lines.push(cur);
cur = word;
} else {
@@ -56,9 +53,10 @@ function padRight(text: string, width: number): string {
}
function centerText(text: string, width: number): string {
if (text.length >= width) return text.slice(0, width);
const left = Math.floor((width - text.length) / 2);
const right = width - text.length - left;
const textWidth = visibleWidth(text);
if (textWidth >= width) return truncateToWidth(text, width, "");
const left = Math.floor((width - textWidth) / 2);
const right = width - textWidth - left;
return `${" ".repeat(left)}${text}${" ".repeat(right)}`;
}
@@ -287,8 +285,8 @@ export function installFeynmanHeader(
if (activity) {
const maxActivityLen = leftW * 2;
const trimmed = activity.length > maxActivityLen
? `${activity.slice(0, maxActivityLen - 1)}`
const trimmed = visibleWidth(activity) > maxActivityLen
? truncateToWidth(activity, maxActivityLen, "…")
: activity;
leftLines.push("");
leftLines.push(theme.fg("accent", theme.bold("Last Activity")));

30
package-lock.json generated
View File

@@ -1265,9 +1265,9 @@
}
},
"node_modules/@hono/node-server": {
"version": "1.19.11",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz",
"integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==",
"version": "1.19.13",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz",
"integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==",
"license": "MIT",
"engines": {
"node": ">=18.14.1"
@@ -2530,9 +2530,9 @@
"license": "MIT"
},
"node_modules/basic-ftp": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz",
"integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==",
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.1.tgz",
"integrity": "sha512-0yaL8JdxTknKDILitVpfYfV2Ob6yb3udX/hK97M7I3jOeznBNxQPtVvTUtnhUkyHlxFWyr5Lvknmgzoc7jf+1Q==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
@@ -2578,9 +2578,9 @@
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
@@ -3623,9 +3623,9 @@
}
},
"node_modules/hono": {
"version": "4.12.9",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz",
"integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==",
"version": "4.12.12",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz",
"integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"
@@ -4218,9 +4218,9 @@
}
},
"node_modules/path-to-regexp": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
"license": "MIT",
"funding": {
"type": "opencollective",

View File

@@ -66,6 +66,28 @@
"@sinclair/typebox": "^0.34.48",
"dotenv": "^17.3.1"
},
"overrides": {
"basic-ftp": "5.2.1",
"@modelcontextprotocol/sdk": {
"@hono/node-server": "1.19.13",
"hono": "4.12.12"
},
"express": {
"router": {
"path-to-regexp": "8.4.2"
}
},
"proxy-agent": {
"pac-proxy-agent": {
"get-uri": {
"basic-ftp": "5.2.1"
}
}
},
"minimatch": {
"brace-expansion": "5.0.5"
}
},
"devDependencies": {
"@types/node": "^25.5.0",
"tsx": "^4.21.0",

View File

@@ -46,7 +46,7 @@ function Resolve-VersionMetadata {
return [PSCustomObject]@{
ResolvedVersion = $resolvedVersion
GitRef = "v$resolvedVersion"
DownloadUrl = "https://github.com/getcompanion-ai/feynman/archive/refs/tags/v$resolvedVersion.zip"
DownloadUrl = if ($env:FEYNMAN_INSTALL_SKILLS_ARCHIVE_URL) { $env:FEYNMAN_INSTALL_SKILLS_ARCHIVE_URL } else { "https://github.com/getcompanion-ai/feynman/archive/refs/tags/v$resolvedVersion.zip" }
}
}

View File

@@ -146,15 +146,17 @@ archive_metadata="$(resolve_version)"
resolved_version="$(printf '%s\n' "$archive_metadata" | sed -n '1p')"
git_ref="$(printf '%s\n' "$archive_metadata" | sed -n '2p')"
archive_url=""
case "$git_ref" in
archive_url="${FEYNMAN_INSTALL_SKILLS_ARCHIVE_URL:-}"
if [ -z "$archive_url" ]; then
case "$git_ref" in
main)
archive_url="https://github.com/getcompanion-ai/feynman/archive/refs/heads/main.tar.gz"
;;
v*)
archive_url="https://github.com/getcompanion-ai/feynman/archive/refs/tags/${git_ref}.tar.gz"
;;
esac
esac
fi
if [ -z "$archive_url" ]; then
echo "Could not resolve a download URL for ref: $git_ref" >&2

View File

@@ -1,9 +1,15 @@
import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import { constants } from "node:os";
import { buildPiArgs, buildPiEnv, type PiRuntimeOptions, resolvePiPaths, toNodeImportSpecifier } from "./runtime.js";
import { ensureSupportedNodeVersion } from "../system/node-version.js";
export function exitCodeFromSignal(signal: NodeJS.Signals): number {
const signalNumber = constants.signals[signal];
return typeof signalNumber === "number" ? 128 + signalNumber : 1;
}
export async function launchPiChat(options: PiRuntimeOptions): Promise<void> {
ensureSupportedNodeVersion();
@@ -36,11 +42,9 @@ export async function launchPiChat(options: PiRuntimeOptions): Promise<void> {
child.on("error", reject);
child.on("exit", (code, signal) => {
if (signal) {
try {
process.kill(process.pid, signal);
} catch {
process.exitCode = 1;
}
console.error(`feynman terminated because the Pi child exited with ${signal}.`);
process.exitCode = exitCodeFromSignal(signal);
resolvePromise();
return;
}
process.exitCode = code ?? 0;

View File

@@ -1,6 +1,7 @@
import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { resolve } from "node:path";
import { getFeynmanHome } from "../config/paths.js";
export type PiWebSearchProvider = "auto" | "perplexity" | "exa" | "gemini";
@@ -26,8 +27,9 @@ export type PiWebAccessStatus = {
note: string;
};
export function getPiWebSearchConfigPath(home = process.env.HOME ?? homedir()): string {
return resolve(home, ".feynman", "web-search.json");
export function getPiWebSearchConfigPath(home?: string): string {
const feynmanHome = home ? resolve(home, ".feynman") : getFeynmanHome();
return resolve(feynmanHome, "web-search.json");
}
function normalizeProvider(value: unknown): PiWebSearchProvider | undefined {

9
tests/pi-launch.test.ts Normal file
View File

@@ -0,0 +1,9 @@
import test from "node:test";
import assert from "node:assert/strict";
import { exitCodeFromSignal } from "../src/pi/launch.js";
test("exitCodeFromSignal maps POSIX signals to conventional shell exit codes", () => {
assert.equal(exitCodeFromSignal("SIGTERM"), 143);
assert.equal(exitCodeFromSignal("SIGSEGV"), 139);
});

View File

@@ -18,6 +18,10 @@ test("loadPiWebAccessConfig returns empty config when Pi web config is missing",
assert.deepEqual(loadPiWebAccessConfig(configPath), {});
});
test("getPiWebSearchConfigPath respects FEYNMAN_HOME semantics", () => {
assert.equal(getPiWebSearchConfigPath("/tmp/custom-home"), "/tmp/custom-home/.feynman/web-search.json");
});
test("getPiWebAccessStatus reads Pi web-access config directly", () => {
const root = mkdtempSync(join(tmpdir(), "feynman-pi-web-"));
const configPath = getPiWebSearchConfigPath(root);

View File

@@ -7,9 +7,6 @@
"": {
"name": "website",
"version": "0.0.1",
"engines": {
"node": ">=20.19.0"
},
"dependencies": {
"@astrojs/react": "^4.4.2",
"@fontsource-variable/ibm-plex-sans": "^5.2.8",
@@ -39,6 +36,9 @@
"prettier-plugin-tailwindcss": "^0.7.2",
"typescript": "~5.9.3",
"typescript-eslint": "^8.57.1"
},
"engines": {
"node": ">=20.19.0"
}
},
"node_modules/@astrojs/compiler": {
@@ -1369,9 +1369,9 @@
}
},
"node_modules/@hono/node-server": {
"version": "1.19.11",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz",
"integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==",
"version": "1.19.13",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz",
"integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==",
"license": "MIT",
"engines": {
"node": ">=18.14.1"
@@ -6186,9 +6186,9 @@
}
},
"node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"version": "6.1.7",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
"license": "MIT"
},
"node_modules/depd": {
@@ -7677,9 +7677,9 @@
}
},
"node_modules/hono": {
"version": "4.12.9",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz",
"integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==",
"version": "4.12.12",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz",
"integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"
@@ -10994,9 +10994,9 @@
}
},
"node_modules/router/node_modules/path-to-regexp": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
"version": "8.4.2",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -12367,9 +12367,9 @@
}
},
"node_modules/vite": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
"integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",

View File

@@ -33,6 +33,17 @@
"tailwindcss": "^4.2.1",
"tw-animate-css": "^1.4.0"
},
"overrides": {
"@modelcontextprotocol/sdk": {
"@hono/node-server": "1.19.13",
"hono": "4.12.12"
},
"router": {
"path-to-regexp": "8.4.2"
},
"defu": "6.1.7",
"vite": "6.4.2"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"eslint": "^9.39.4",

View File

@@ -146,15 +146,17 @@ archive_metadata="$(resolve_version)"
resolved_version="$(printf '%s\n' "$archive_metadata" | sed -n '1p')"
git_ref="$(printf '%s\n' "$archive_metadata" | sed -n '2p')"
archive_url=""
case "$git_ref" in
archive_url="${FEYNMAN_INSTALL_SKILLS_ARCHIVE_URL:-}"
if [ -z "$archive_url" ]; then
case "$git_ref" in
main)
archive_url="https://github.com/getcompanion-ai/feynman/archive/refs/heads/main.tar.gz"
;;
v*)
archive_url="https://github.com/getcompanion-ai/feynman/archive/refs/tags/${git_ref}.tar.gz"
;;
esac
esac
fi
if [ -z "$archive_url" ]; then
echo "Could not resolve a download URL for ref: $git_ref" >&2

View File

@@ -46,7 +46,7 @@ function Resolve-VersionMetadata {
return [PSCustomObject]@{
ResolvedVersion = $resolvedVersion
GitRef = "v$resolvedVersion"
DownloadUrl = "https://github.com/getcompanion-ai/feynman/archive/refs/tags/v$resolvedVersion.zip"
DownloadUrl = if ($env:FEYNMAN_INSTALL_SKILLS_ARCHIVE_URL) { $env:FEYNMAN_INSTALL_SKILLS_ARCHIVE_URL } else { "https://github.com/getcompanion-ai/feynman/archive/refs/tags/v$resolvedVersion.zip" }
}
}