Update runtime checks and installer behavior

This commit is contained in:
Advait Paliwal
2026-03-25 13:55:32 -07:00
parent 572de7ba85
commit f6dbacc9d5
24 changed files with 431 additions and 21 deletions

154
.astro/content.d.ts vendored Normal file
View File

@@ -0,0 +1,154 @@
declare module 'astro:content' {
export interface RenderResult {
Content: import('astro/runtime/server/index.js').AstroComponentFactory;
headings: import('astro').MarkdownHeading[];
remarkPluginFrontmatter: Record<string, any>;
}
interface Render {
'.md': Promise<RenderResult>;
}
export interface RenderedContent {
html: string;
metadata?: {
imagePaths: Array<string>;
[key: string]: unknown;
};
}
type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
export type CollectionKey = keyof DataEntryMap;
export type CollectionEntry<C extends CollectionKey> = Flatten<DataEntryMap[C]>;
type AllValuesOf<T> = T extends any ? T[keyof T] : never;
export type ReferenceDataEntry<
C extends CollectionKey,
E extends keyof DataEntryMap[C] = string,
> = {
collection: C;
id: E;
};
export type ReferenceLiveEntry<C extends keyof LiveContentConfig['collections']> = {
collection: C;
id: string;
};
export function getCollection<C extends keyof DataEntryMap, E extends CollectionEntry<C>>(
collection: C,
filter?: (entry: CollectionEntry<C>) => entry is E,
): Promise<E[]>;
export function getCollection<C extends keyof DataEntryMap>(
collection: C,
filter?: (entry: CollectionEntry<C>) => unknown,
): Promise<CollectionEntry<C>[]>;
export function getLiveCollection<C extends keyof LiveContentConfig['collections']>(
collection: C,
filter?: LiveLoaderCollectionFilterType<C>,
): Promise<
import('astro').LiveDataCollectionResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>
>;
export function getEntry<
C extends keyof DataEntryMap,
E extends keyof DataEntryMap[C] | (string & {}),
>(
entry: ReferenceDataEntry<C, E>,
): E extends keyof DataEntryMap[C]
? Promise<DataEntryMap[C][E]>
: Promise<CollectionEntry<C> | undefined>;
export function getEntry<
C extends keyof DataEntryMap,
E extends keyof DataEntryMap[C] | (string & {}),
>(
collection: C,
id: E,
): E extends keyof DataEntryMap[C]
? string extends keyof DataEntryMap[C]
? Promise<DataEntryMap[C][E]> | undefined
: Promise<DataEntryMap[C][E]>
: Promise<CollectionEntry<C> | undefined>;
export function getLiveEntry<C extends keyof LiveContentConfig['collections']>(
collection: C,
filter: string | LiveLoaderEntryFilterType<C>,
): Promise<import('astro').LiveDataEntryResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>>;
/** Resolve an array of entry references from the same collection */
export function getEntries<C extends keyof DataEntryMap>(
entries: ReferenceDataEntry<C, keyof DataEntryMap[C]>[],
): Promise<CollectionEntry<C>[]>;
export function render<C extends keyof DataEntryMap>(
entry: DataEntryMap[C][string],
): Promise<RenderResult>;
export function reference<
C extends
| keyof DataEntryMap
// Allow generic `string` to avoid excessive type errors in the config
// if `dev` is not running to update as you edit.
// Invalid collection names will be caught at build time.
| (string & {}),
>(
collection: C,
): import('astro/zod').ZodPipe<
import('astro/zod').ZodString,
import('astro/zod').ZodTransform<
C extends keyof DataEntryMap
? {
collection: C;
id: string;
}
: never,
string
>
>;
type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
type InferEntrySchema<C extends keyof DataEntryMap> = import('astro/zod').infer<
ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
>;
type ExtractLoaderConfig<T> = T extends { loader: infer L } ? L : never;
type InferLoaderSchema<
C extends keyof DataEntryMap,
L = ExtractLoaderConfig<ContentConfig['collections'][C]>,
> = L extends { schema: import('astro/zod').ZodSchema }
? import('astro/zod').infer<L['schema']>
: any;
type DataEntryMap = {
};
type ExtractLoaderTypes<T> = T extends import('astro/loaders').LiveLoader<
infer TData,
infer TEntryFilter,
infer TCollectionFilter,
infer TError
>
? { data: TData; entryFilter: TEntryFilter; collectionFilter: TCollectionFilter; error: TError }
: { data: never; entryFilter: never; collectionFilter: never; error: never };
type ExtractEntryFilterType<T> = ExtractLoaderTypes<T>['entryFilter'];
type ExtractCollectionFilterType<T> = ExtractLoaderTypes<T>['collectionFilter'];
type ExtractErrorType<T> = ExtractLoaderTypes<T>['error'];
type LiveLoaderDataType<C extends keyof LiveContentConfig['collections']> =
LiveContentConfig['collections'][C]['schema'] extends undefined
? ExtractDataType<LiveContentConfig['collections'][C]['loader']>
: import('astro/zod').infer<
Exclude<LiveContentConfig['collections'][C]['schema'], undefined>
>;
type LiveLoaderEntryFilterType<C extends keyof LiveContentConfig['collections']> =
ExtractEntryFilterType<LiveContentConfig['collections'][C]['loader']>;
type LiveLoaderCollectionFilterType<C extends keyof LiveContentConfig['collections']> =
ExtractCollectionFilterType<LiveContentConfig['collections'][C]['loader']>;
type LiveLoaderErrorType<C extends keyof LiveContentConfig['collections']> = ExtractErrorType<
LiveContentConfig['collections'][C]['loader']
>;
export type ContentConfig = never;
export type LiveContentConfig = never;
}

2
.astro/types.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
/// <reference types="astro/client" />
/// <reference path="content.d.ts" />

View File

@@ -9,7 +9,7 @@ Operating rules:
- State uncertainty explicitly. - State uncertainty explicitly.
- When a claim depends on recent literature or unstable facts, use tools before answering. - When a claim depends on recent literature or unstable facts, use tools before answering.
- When discussing papers, cite title, year, and identifier or URL when possible. - When discussing papers, cite title, year, and identifier or URL when possible.
- Use the alpha-research skill for academic paper search, paper reading, paper Q&A, repository inspection, and persistent annotations. - Use the `alpha` CLI for academic paper search, paper reading, paper Q&A, repository inspection, and persistent annotations.
- Use `web_search`, `fetch_content`, and `get_search_content` first for current topics: products, companies, markets, regulations, software releases, model availability, model pricing, benchmarks, docs, or anything phrased as latest/current/recent/today. - Use `web_search`, `fetch_content`, and `get_search_content` first for current topics: products, companies, markets, regulations, software releases, model availability, model pricing, benchmarks, docs, or anything phrased as latest/current/recent/today.
- For mixed topics, combine both: use web sources for current reality and paper sources for background literature. - For mixed topics, combine both: use web sources for current reality and paper sources for background literature.
- Never answer a latest/current question from arXiv or alpha-backed paper search alone. - Never answer a latest/current question from arXiv or alpha-backed paper search alone.

View File

@@ -1,5 +1,6 @@
{ {
"packages": [ "packages": [
"npm:@companion-ai/alpha-hub",
"npm:pi-subagents", "npm:pi-subagents",
"npm:pi-btw", "npm:pi-btw",
"npm:pi-docparser", "npm:pi-docparser",

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
20.18.1

View File

@@ -14,3 +14,39 @@ Use this file to track chronology, not release notes. Keep entries short, factua
- Failed / learned: ... - Failed / learned: ...
- Blockers: ... - Blockers: ...
- Next: ... - Next: ...
### 2026-03-25 00:00 local — scaling-laws
- Objective: Set up a deep research workflow for scaling laws.
- Changed: Created plan artifact at `outputs/.plans/scaling-laws.md`; defined 4 disjoint researcher dimensions and acceptance criteria.
- Verified: Read `CHANGELOG.md` and checked prior memory for related plan `scaling-laws-implications`.
- Failed / learned: No prior run-specific changelog entries existed beyond the template.
- Blockers: Waiting for user confirmation before launching researcher round 1.
- Next: On confirmation, spawn 4 parallel researcher subagents and begin evidence collection.
### 2026-03-25 00:30 local — scaling-laws (T4 inference/time-scale pass)
- Objective: Complete T4 on inference/test-time scaling and reasoning-time compute, scoped to 20232026.
- Changed: Wrote `notes/scaling-laws-research-inference.md`; updated `outputs/.plans/scaling-laws.md` to mark T4 done and log the inference-scaling verification pass.
- Verified: Cross-read 13 primary/official sources covering Tree-of-Thoughts, PRMs, repeated sampling, compute-optimal test-time scaling, provable laws, o1, DeepSeek-R1, s1, verifier failures, Anthropic extended thinking, and OpenAI reasoning API docs.
- Failed / learned: OpenAI blog fetch for `learning-to-reason-with-llms` returned malformed content, so the note leans on the o1 system card and API docs instead of that blog post.
- Blockers: T2 and T5 remain open before final synthesis; no single unified law for inference-time scaling emerged from public sources.
- Next: Complete T5 implications synthesis, then reconcile T3/T4 with foundational T2 before drafting the cited brief.
### 2026-03-25 11:20 local — scaling-laws (T6 draft synthesis)
- Objective: Synthesize the four research notes into a single user-facing draft brief for the scaling-laws workflow.
- Changed: Wrote `outputs/.drafts/scaling-laws-draft.md` with an executive summary, curated reading list, qualitative meta-analysis, core-paper comparison table, explicit training-vs-inference distinction, and numbered inline citations with direct-URL sources.
- Verified: Cross-checked the draft against `notes/scaling-laws-research-foundations.md`, `notes/scaling-laws-research-revisions.md`, `notes/scaling-laws-research-inference.md`, and `notes/scaling-laws-research-implications.md` to ensure the brief explicitly states the literature is too heterogeneous for a pooled effect-size estimate.
- Failed / learned: The requested temp-run `context.md` and `plan.md` were absent, so the synthesis used `outputs/.plans/scaling-laws.md` plus the four note files as the working context.
- Blockers: Citation/claim verification pass still pending; this draft should be treated as pre-verification.
- Next: Run verifier/reviewer passes, then promote the draft into the final cited brief and provenance sidecar.
### 2026-03-25 11:28 local — scaling-laws (final brief + pdf)
- Objective: Deliver a paper guide and qualitative meta-analysis on AI scaling laws.
- Changed: Finalized `outputs/scaling-laws.md` and sidecar `outputs/scaling-laws.provenance.md`; rendered preview PDF at `outputs/scaling-laws.pdf`; updated plan ledger and verification log in `outputs/.plans/scaling-laws.md`.
- Verified: Ran a reviewer pass recorded in `notes/scaling-laws-verification.md`; spot-checked key primary papers via alpha-backed reads for Kaplan 2020, Chinchilla 2022, and Snell 2024; confirmed PDF render output exists.
- Failed / learned: A pooled statistical meta-analysis would be misleading because the literature mixes heterogeneous outcomes, scaling axes, and evaluation regimes; final deliverable uses a qualitative meta-analysis instead.
- Blockers: None for this brief.
- Next: If needed, extend into a narrower sub-survey (e.g. only pretraining laws, only inference-time scaling, or only post-Chinchilla data-quality revisions).

View File

@@ -17,6 +17,8 @@
curl -fsSL https://feynman.is/install | bash curl -fsSL https://feynman.is/install | bash
``` ```
If you install via `pnpm` or `bun` instead of the standalone bundle, Feynman requires Node.js `20.18.1` or newer.
--- ---
### What you type → what happens ### What you type → what happens
@@ -92,7 +94,10 @@ Built on [Pi](https://github.com/badlogic/pi-mono) for the agent runtime, [alpha
```bash ```bash
git clone https://github.com/getcompanion-ai/feynman.git git clone https://github.com/getcompanion-ai/feynman.git
cd feynman && pnpm install && pnpm start cd feynman
nvm use || nvm install
pnpm install
pnpm start
``` ```
[Docs](https://feynman.is/docs) · [MIT License](LICENSE) [Docs](https://feynman.is/docs) · [MIT License](LICENSE)

View File

@@ -1,8 +1,25 @@
#!/usr/bin/env node #!/usr/bin/env node
const v = process.versions.node.split(".").map(Number); const MIN_NODE_VERSION = "20.18.1";
if (v[0] < 20) {
console.error(`feynman requires Node.js 20 or later (you have ${process.versions.node})`); function parseNodeVersion(version) {
console.error("upgrade: https://nodejs.org or nvm install 20"); const [major = "0", minor = "0", patch = "0"] = version.replace(/^v/, "").split(".");
return {
major: Number.parseInt(major, 10) || 0,
minor: Number.parseInt(minor, 10) || 0,
patch: Number.parseInt(patch, 10) || 0,
};
}
function compareNodeVersions(left, right) {
if (left.major !== right.major) return left.major - right.major;
if (left.minor !== right.minor) return left.minor - right.minor;
return left.patch - right.patch;
}
if (compareNodeVersions(parseNodeVersion(process.versions.node), parseNodeVersion(MIN_NODE_VERSION)) < 0) {
console.error(`feynman requires Node.js ${MIN_NODE_VERSION} or later (detected ${process.versions.node}).`);
console.error("Switch to Node 20 with `nvm install 20 && nvm use 20`, or use the standalone installer:");
console.error("curl -fsSL https://feynman.is/install | bash");
process.exit(1); process.exit(1);
} }
await import("../scripts/patch-embedded-pi.mjs"); await import("../scripts/patch-embedded-pi.mjs");

View File

@@ -30,6 +30,7 @@
".env.example" ".env.example"
], ],
"scripts": { "scripts": {
"preinstall": "node ./scripts/check-node-version.mjs",
"build": "tsc -p tsconfig.build.json", "build": "tsc -p tsconfig.build.json",
"build:native-bundle": "node ./scripts/build-native-bundle.mjs", "build:native-bundle": "node ./scripts/build-native-bundle.mjs",
"dev": "tsx src/index.ts", "dev": "tsx src/index.ts",

View File

@@ -0,0 +1,35 @@
const MIN_NODE_VERSION = "20.18.1";
function parseNodeVersion(version) {
const [major = "0", minor = "0", patch = "0"] = version.replace(/^v/, "").split(".");
return {
major: Number.parseInt(major, 10) || 0,
minor: Number.parseInt(minor, 10) || 0,
patch: Number.parseInt(patch, 10) || 0,
};
}
function compareNodeVersions(left, right) {
if (left.major !== right.major) return left.major - right.major;
if (left.minor !== right.minor) return left.minor - right.minor;
return left.patch - right.patch;
}
function isSupportedNodeVersion(version = process.versions.node) {
return compareNodeVersions(parseNodeVersion(version), parseNodeVersion(MIN_NODE_VERSION)) >= 0;
}
function getUnsupportedNodeVersionLines(version = process.versions.node) {
return [
`feynman requires Node.js ${MIN_NODE_VERSION} or later (detected ${version}).`,
"Switch to Node 20 with `nvm install 20 && nvm use 20`, or use the standalone installer:",
"curl -fsSL https://feynman.is/install | bash",
];
}
if (!isSupportedNodeVersion()) {
for (const line of getUnsupportedNodeVersionLines()) {
console.error(line);
}
process.exit(1);
}

View File

@@ -146,6 +146,16 @@ Workarounds:
Write-Host "$installBinDir is already on PATH." Write-Host "$installBinDir is already on PATH."
} }
$resolvedCommand = Get-Command feynman -ErrorAction SilentlyContinue
if ($resolvedCommand -and $resolvedCommand.Source -ne $shimPath) {
Write-Warning "Current shell resolves feynman to $($resolvedCommand.Source)"
Write-Host "Run in a new shell, or run: `$env:Path = '$installBinDir;' + `$env:Path"
Write-Host "Then run: feynman"
if ($resolvedCommand.Source -like "*node_modules*@companion-ai*feynman*") {
Write-Host "If that path is an old global npm install, remove it with: npm uninstall -g @companion-ai/feynman"
}
}
Write-Host "Feynman $resolvedVersion installed successfully." Write-Host "Feynman $resolvedVersion installed successfully."
} finally { } finally {
if (Test-Path $tmpDir) { if (Test-Path $tmpDir) {

View File

@@ -160,6 +160,27 @@ require_command() {
fi fi
} }
warn_command_conflict() {
expected_path="$INSTALL_BIN_DIR/feynman"
resolved_path="$(command -v feynman 2>/dev/null || true)"
if [ -z "$resolved_path" ]; then
return
fi
if [ "$resolved_path" != "$expected_path" ]; then
step "Warning: current shell resolves feynman to $resolved_path"
step "Run now: export PATH=\"$INSTALL_BIN_DIR:\$PATH\" && hash -r && feynman"
step "Or launch directly: $expected_path"
case "$resolved_path" in
*"/node_modules/@companion-ai/feynman/"* | *"/node_modules/.bin/feynman")
step "If that path is an old global npm install, remove it with: npm uninstall -g @companion-ai/feynman"
;;
esac
fi
}
resolve_release_metadata() { resolve_release_metadata() {
normalized_version="$(normalize_version "$VERSION")" normalized_version="$(normalize_version "$VERSION")"
@@ -290,20 +311,22 @@ add_to_path
case "$path_action" in case "$path_action" in
added) added)
step "PATH updated for future shells in $path_profile" step "PATH updated for future shells in $path_profile"
step "Run now: export PATH=\"$INSTALL_BIN_DIR:\$PATH\" && feynman" step "Run now: export PATH=\"$INSTALL_BIN_DIR:\$PATH\" && hash -r && feynman"
;; ;;
configured) configured)
step "PATH is already configured for future shells in $path_profile" step "PATH is already configured for future shells in $path_profile"
step "Run now: export PATH=\"$INSTALL_BIN_DIR:\$PATH\" && feynman" step "Run now: export PATH=\"$INSTALL_BIN_DIR:\$PATH\" && hash -r && feynman"
;; ;;
skipped) skipped)
step "PATH update skipped" step "PATH update skipped"
step "Run now: export PATH=\"$INSTALL_BIN_DIR:\$PATH\" && feynman" step "Run now: export PATH=\"$INSTALL_BIN_DIR:\$PATH\" && hash -r && feynman"
;; ;;
*) *)
step "$INSTALL_BIN_DIR is already on PATH" step "$INSTALL_BIN_DIR is already on PATH"
step "Run: feynman" step "Run: hash -r && feynman"
;; ;;
esac esac
warn_command_conflict
printf 'Feynman %s installed successfully.\n' "$resolved_version" printf 'Feynman %s installed successfully.\n' "$resolved_version"

View File

@@ -11,7 +11,7 @@ Use the `alpha` CLI via bash for all paper research operations.
| Command | Description | | Command | Description |
|---------|-------------| |---------|-------------|
| `alpha search "<query>"` | Search papers. Modes: `--mode semantic`, `--mode keyword`, `--mode agentic` | | `alpha search "<query>"` | Search papers. Prefer `--mode semantic` by default; use `--mode keyword` only for exact-term lookup and `--mode agentic` for broader retrieval. |
| `alpha get <arxiv-id-or-url>` | Fetch paper content and any local annotation | | `alpha get <arxiv-id-or-url>` | Fetch paper content and any local annotation |
| `alpha get --full-text <arxiv-id>` | Get raw full text instead of AI report | | `alpha get --full-text <arxiv-id>` | Get raw full text instead of AI report |
| `alpha ask <arxiv-id> "<question>"` | Ask a question about a paper's PDF | | `alpha ask <arxiv-id> "<question>"` | Ask a question about a paper's PDF |
@@ -22,7 +22,7 @@ Use the `alpha` CLI via bash for all paper research operations.
## Auth ## Auth
Run `alpha login` to authenticate with alphaXiv. Check status with `alpha status`. Run `alpha login` to authenticate with alphaXiv. Check status with `feynman alpha status`, or `alpha status` once your installed `alpha-hub` version includes it.
## Examples ## Examples

View File

@@ -1,6 +1,12 @@
import { main } from "./cli.js"; import { ensureSupportedNodeVersion } from "./system/node-version.js";
main().catch((error) => { async function run(): Promise<void> {
ensureSupportedNodeVersion();
const { main } = await import("./cli.js");
await main();
}
run().catch((error) => {
console.error(error instanceof Error ? error.message : String(error)); console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1; process.exitCode = 1;
}); });

View File

@@ -2,8 +2,11 @@ import { spawn } from "node:child_process";
import { existsSync } from "node:fs"; import { existsSync } from "node:fs";
import { buildPiArgs, buildPiEnv, type PiRuntimeOptions, resolvePiPaths } from "./runtime.js"; import { buildPiArgs, buildPiEnv, type PiRuntimeOptions, resolvePiPaths } from "./runtime.js";
import { ensureSupportedNodeVersion } from "../system/node-version.js";
export async function launchPiChat(options: PiRuntimeOptions): Promise<void> { export async function launchPiChat(options: PiRuntimeOptions): Promise<void> {
ensureSupportedNodeVersion();
const { piCliPath, promisePolyfillPath } = resolvePiPaths(options.appRoot); const { piCliPath, promisePolyfillPath } = resolvePiPaths(options.appRoot);
if (!existsSync(piCliPath)) { if (!existsSync(piCliPath)) {
throw new Error(`Pi CLI not found: ${piCliPath}`); throw new Error(`Pi CLI not found: ${piCliPath}`);

View File

@@ -1,6 +1,7 @@
import type { PackageSource } from "@mariozechner/pi-coding-agent"; import type { PackageSource } from "@mariozechner/pi-coding-agent";
export const CORE_PACKAGE_SOURCES = [ export const CORE_PACKAGE_SOURCES = [
"npm:@companion-ai/alpha-hub",
"npm:pi-subagents", "npm:pi-subagents",
"npm:pi-btw", "npm:pi-btw",
"npm:pi-docparser", "npm:pi-docparser",

View File

@@ -79,7 +79,8 @@ export function buildPiEnv(options: PiRuntimeOptions): NodeJS.ProcessEnv {
const paths = resolvePiPaths(options.appRoot); const paths = resolvePiPaths(options.appRoot);
const currentPath = process.env.PATH ?? ""; const currentPath = process.env.PATH ?? "";
const binPath = paths.nodeModulesBinPath; const binEntries = [paths.nodeModulesBinPath, resolve(paths.piWorkspaceNodeModulesPath, ".bin")];
const binPath = binEntries.join(":");
return { return {
...process.env, ...process.env,

View File

@@ -0,0 +1,40 @@
export const MIN_NODE_VERSION = "20.18.1";
type ParsedNodeVersion = {
major: number;
minor: number;
patch: number;
};
function parseNodeVersion(version: string): ParsedNodeVersion {
const [major = "0", minor = "0", patch = "0"] = version.replace(/^v/, "").split(".");
return {
major: Number.parseInt(major, 10) || 0,
minor: Number.parseInt(minor, 10) || 0,
patch: Number.parseInt(patch, 10) || 0,
};
}
function compareNodeVersions(left: ParsedNodeVersion, right: ParsedNodeVersion): number {
if (left.major !== right.major) return left.major - right.major;
if (left.minor !== right.minor) return left.minor - right.minor;
return left.patch - right.patch;
}
export function isSupportedNodeVersion(version = process.versions.node): boolean {
return compareNodeVersions(parseNodeVersion(version), parseNodeVersion(MIN_NODE_VERSION)) >= 0;
}
export function getUnsupportedNodeVersionLines(version = process.versions.node): string[] {
return [
`feynman requires Node.js ${MIN_NODE_VERSION} or later (detected ${version}).`,
"Switch to Node 20 with `nvm install 20 && nvm use 20`, or use the standalone installer:",
"curl -fsSL https://feynman.is/install | bash",
];
}
export function ensureSupportedNodeVersion(version = process.versions.node): void {
if (!isSupportedNodeVersion(version)) {
throw new Error(getUnsupportedNodeVersionLines(version).join("\n"));
}
}

View File

@@ -0,0 +1,35 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
MIN_NODE_VERSION,
ensureSupportedNodeVersion,
getUnsupportedNodeVersionLines,
isSupportedNodeVersion,
} from "../src/system/node-version.js";
test("isSupportedNodeVersion enforces the exact minimum floor", () => {
assert.equal(isSupportedNodeVersion("20.18.1"), true);
assert.equal(isSupportedNodeVersion("20.19.0"), true);
assert.equal(isSupportedNodeVersion("21.0.0"), true);
assert.equal(isSupportedNodeVersion("20.18.0"), false);
assert.equal(isSupportedNodeVersion("18.17.0"), false);
});
test("ensureSupportedNodeVersion throws a guided upgrade message", () => {
assert.throws(
() => ensureSupportedNodeVersion("18.17.0"),
(error: unknown) =>
error instanceof Error &&
error.message.includes(`Node.js ${MIN_NODE_VERSION}`) &&
error.message.includes("nvm install 20 && nvm use 20") &&
error.message.includes("https://feynman.is/install"),
);
});
test("unsupported version guidance reports the detected version", () => {
const lines = getUnsupportedNodeVersionLines("18.17.0");
assert.equal(lines[0], "feynman requires Node.js 20.18.1 or later (detected 18.17.0).");
assert.ok(lines.some((line) => line.includes("curl -fsSL https://feynman.is/install | bash")));
});

View File

@@ -41,6 +41,7 @@ test("buildPiEnv wires Feynman paths into the Pi environment", () => {
assert.equal(env.FEYNMAN_SESSION_DIR, "/sessions"); assert.equal(env.FEYNMAN_SESSION_DIR, "/sessions");
assert.equal(env.FEYNMAN_BIN_PATH, "/repo/feynman/bin/feynman.js"); assert.equal(env.FEYNMAN_BIN_PATH, "/repo/feynman/bin/feynman.js");
assert.equal(env.FEYNMAN_MEMORY_DIR, "/home/.feynman/memory"); assert.equal(env.FEYNMAN_MEMORY_DIR, "/home/.feynman/memory");
assert.ok(env.PATH?.startsWith("/repo/feynman/node_modules/.bin:/repo/feynman/.feynman/npm/node_modules/.bin:"));
}); });
test("resolvePiPaths includes the Promise.withResolvers polyfill path", () => { test("resolvePiPaths includes the Promise.withResolvers polyfill path", () => {

File diff suppressed because one or more lines are too long

View File

@@ -160,6 +160,27 @@ require_command() {
fi fi
} }
warn_command_conflict() {
expected_path="$INSTALL_BIN_DIR/feynman"
resolved_path="$(command -v feynman 2>/dev/null || true)"
if [ -z "$resolved_path" ]; then
return
fi
if [ "$resolved_path" != "$expected_path" ]; then
step "Warning: current shell resolves feynman to $resolved_path"
step "Run now: export PATH=\"$INSTALL_BIN_DIR:\$PATH\" && hash -r && feynman"
step "Or launch directly: $expected_path"
case "$resolved_path" in
*"/node_modules/@companion-ai/feynman/"* | *"/node_modules/.bin/feynman")
step "If that path is an old global npm install, remove it with: npm uninstall -g @companion-ai/feynman"
;;
esac
fi
}
resolve_release_metadata() { resolve_release_metadata() {
normalized_version="$(normalize_version "$VERSION")" normalized_version="$(normalize_version "$VERSION")"
@@ -290,20 +311,22 @@ add_to_path
case "$path_action" in case "$path_action" in
added) added)
step "PATH updated for future shells in $path_profile" step "PATH updated for future shells in $path_profile"
step "Run now: export PATH=\"$INSTALL_BIN_DIR:\$PATH\" && feynman" step "Run now: export PATH=\"$INSTALL_BIN_DIR:\$PATH\" && hash -r && feynman"
;; ;;
configured) configured)
step "PATH is already configured for future shells in $path_profile" step "PATH is already configured for future shells in $path_profile"
step "Run now: export PATH=\"$INSTALL_BIN_DIR:\$PATH\" && feynman" step "Run now: export PATH=\"$INSTALL_BIN_DIR:\$PATH\" && hash -r && feynman"
;; ;;
skipped) skipped)
step "PATH update skipped" step "PATH update skipped"
step "Run now: export PATH=\"$INSTALL_BIN_DIR:\$PATH\" && feynman" step "Run now: export PATH=\"$INSTALL_BIN_DIR:\$PATH\" && hash -r && feynman"
;; ;;
*) *)
step "$INSTALL_BIN_DIR is already on PATH" step "$INSTALL_BIN_DIR is already on PATH"
step "Run: feynman" step "Run: hash -r && feynman"
;; ;;
esac esac
warn_command_conflict
printf 'Feynman %s installed successfully.\n' "$resolved_version" printf 'Feynman %s installed successfully.\n' "$resolved_version"

View File

@@ -146,6 +146,16 @@ Workarounds:
Write-Host "$installBinDir is already on PATH." Write-Host "$installBinDir is already on PATH."
} }
$resolvedCommand = Get-Command feynman -ErrorAction SilentlyContinue
if ($resolvedCommand -and $resolvedCommand.Source -ne $shimPath) {
Write-Warning "Current shell resolves feynman to $($resolvedCommand.Source)"
Write-Host "Run in a new shell, or run: `$env:Path = '$installBinDir;' + `$env:Path"
Write-Host "Then run: feynman"
if ($resolvedCommand.Source -like "*node_modules*@companion-ai*feynman*") {
Write-Host "If that path is an old global npm install, remove it with: npm uninstall -g @companion-ai/feynman"
}
}
Write-Host "Feynman $resolvedVersion installed successfully." Write-Host "Feynman $resolvedVersion installed successfully."
} finally { } finally {
if (Test-Path $tmpDir) { if (Test-Path $tmpDir) {

View File

@@ -17,6 +17,8 @@ curl -fsSL https://feynman.is/install | bash
The installer detects your OS and architecture automatically. On macOS it supports both Intel and Apple Silicon. On Linux it supports x64 and arm64. The launcher is installed to `~/.local/bin`, the bundled runtime is unpacked into `~/.local/share/feynman`, and your `PATH` is updated when needed. The installer detects your OS and architecture automatically. On macOS it supports both Intel and Apple Silicon. On Linux it supports x64 and arm64. The launcher is installed to `~/.local/bin`, the bundled runtime is unpacked into `~/.local/share/feynman`, and your `PATH` is updated when needed.
If you previously installed Feynman via `npm`, `pnpm`, or `bun` and still see local Node.js errors after a curl install, your shell is probably still resolving the older global binary first. Run `which -a feynman`, then `hash -r`, or launch the standalone shim directly with `~/.local/bin/feynman`.
By default, the one-line installer tracks the rolling `edge` channel from `main`. By default, the one-line installer tracks the rolling `edge` channel from `main`.
On **Windows**, open PowerShell as Administrator and run: On **Windows**, open PowerShell as Administrator and run:
@@ -45,7 +47,7 @@ You can also pin an exact version by replacing `stable` with a version such as `
## pnpm ## pnpm
If you already have Node.js 20.18.1+ installed, you can install Feynman globally via `pnpm`: If you already have Node.js `20.18.1` or newer installed, you can install Feynman globally via `pnpm`:
```bash ```bash
pnpm add -g @companion-ai/feynman pnpm add -g @companion-ai/feynman
@@ -59,6 +61,8 @@ pnpm dlx @companion-ai/feynman
## bun ## bun
`bun add -g` and `bunx` still use your local Node runtime for Feynman itself, so the same Node.js `20.18.1+` requirement applies.
```bash ```bash
bun add -g @companion-ai/feynman bun add -g @companion-ai/feynman
``` ```
@@ -98,6 +102,7 @@ For contributing or running Feynman from source:
```bash ```bash
git clone https://github.com/getcompanion-ai/feynman.git git clone https://github.com/getcompanion-ai/feynman.git
cd feynman cd feynman
nvm use || nvm install
pnpm install pnpm install
pnpm start pnpm start
``` ```