diff --git a/README.md b/README.md index 38f567f..34e110e 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ curl -fsSL https://feynman.is/install | bash irm https://feynman.is/install.ps1 | iex ``` -The one-line installer fetches the latest tagged release. To pin a version, pass it explicitly, for example `curl -fsSL https://feynman.is/install | bash -s -- 0.2.23`. +The one-line installer fetches the latest tagged release. To pin a version, pass it explicitly, for example `curl -fsSL https://feynman.is/install | bash -s -- 0.2.24`. The installer downloads a standalone native bundle with its own Node.js runtime. diff --git a/package-lock.json b/package-lock.json index 8ae97c5..228131c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@companion-ai/feynman", - "version": "0.2.23", + "version": "0.2.24", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@companion-ai/feynman", - "version": "0.2.23", + "version": "0.2.24", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index e61e1f0..a1bc5f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@companion-ai/feynman", - "version": "0.2.23", + "version": "0.2.24", "description": "Research-first CLI agent built on Pi and alphaXiv", "license": "MIT", "type": "module", diff --git a/scripts/install/install.ps1 b/scripts/install/install.ps1 index 6eb640d..4b85a40 100644 --- a/scripts/install/install.ps1 +++ b/scripts/install/install.ps1 @@ -110,7 +110,7 @@ This usually means the release exists, but not all platform bundles were uploade Workarounds: - try again after the release finishes publishing - pass the latest published version explicitly, e.g.: - & ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.23 + & ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.24 "@ } diff --git a/scripts/install/install.sh b/scripts/install/install.sh index abb1bec..e69ce8b 100644 --- a/scripts/install/install.sh +++ b/scripts/install/install.sh @@ -261,7 +261,7 @@ This usually means the release exists, but not all platform bundles were uploade Workarounds: - try again after the release finishes publishing - pass the latest published version explicitly, e.g.: - curl -fsSL https://feynman.is/install | bash -s -- 0.2.23 + curl -fsSL https://feynman.is/install | bash -s -- 0.2.24 EOF exit 1 fi diff --git a/scripts/patch-embedded-pi.mjs b/scripts/patch-embedded-pi.mjs index 7aae03d..4f36f4d 100644 --- a/scripts/patch-embedded-pi.mjs +++ b/scripts/patch-embedded-pi.mjs @@ -1,5 +1,5 @@ import { spawnSync } from "node:child_process"; -import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { createRequire } from "node:module"; import { homedir } from "node:os"; import { delimiter, dirname, resolve } from "node:path"; @@ -286,28 +286,53 @@ function linkPointsTo(linkPath, targetPath) { } } +function listWorkspacePackageNames(root) { + if (!existsSync(root)) return []; + const names = []; + for (const entry of readdirSync(root, { withFileTypes: true })) { + if (!entry.isDirectory() && !entry.isSymbolicLink()) continue; + if (entry.name.startsWith(".")) continue; + if (entry.name.startsWith("@")) { + const scopeRoot = resolve(root, entry.name); + for (const scopedEntry of readdirSync(scopeRoot, { withFileTypes: true })) { + if (!scopedEntry.isDirectory() && !scopedEntry.isSymbolicLink()) continue; + names.push(`${entry.name}/${scopedEntry.name}`); + } + continue; + } + names.push(entry.name); + } + return names; +} + +function linkBundledPackage(packageName) { + const sourcePath = resolve(workspaceRoot, packageName); + const targetPath = resolve(globalNodeModulesRoot, packageName); + if (!existsSync(sourcePath)) return false; + if (linkPointsTo(targetPath, sourcePath)) return false; + try { + if (lstatSync(targetPath).isSymbolicLink()) { + rmSync(targetPath, { force: true }); + } else if (!installedPackageLooksUsable(targetPath, globalNodeModulesRoot)) { + rmSync(targetPath, { recursive: true, force: true }); + } + } catch {} + if (existsSync(targetPath)) return false; + + ensureParentDir(targetPath); + try { + symlinkSync(sourcePath, targetPath, process.platform === "win32" ? "junction" : "dir"); + return true; + } catch { + return false; + } +} + function ensureBundledPackageLinks(packageSpecs) { if (!workspaceMatchesRuntime(packageSpecs)) return; - for (const spec of packageSpecs) { - const packageName = parsePackageName(spec); - const sourcePath = resolve(workspaceRoot, packageName); - const targetPath = resolve(globalNodeModulesRoot, packageName); - if (!existsSync(sourcePath)) continue; - if (linkPointsTo(targetPath, sourcePath)) continue; - try { - if (lstatSync(targetPath).isSymbolicLink()) { - rmSync(targetPath, { force: true }); - } else if (!installedPackageLooksUsable(targetPath, globalNodeModulesRoot)) { - rmSync(targetPath, { recursive: true, force: true }); - } - } catch {} - if (existsSync(targetPath)) continue; - - ensureParentDir(targetPath); - try { - symlinkSync(sourcePath, targetPath, process.platform === "win32" ? "junction" : "dir"); - } catch {} + for (const packageName of listWorkspacePackageNames(workspaceRoot)) { + linkBundledPackage(packageName); } } diff --git a/src/pi/package-ops.ts b/src/pi/package-ops.ts index 6494c6a..31038b5 100644 --- a/src/pi/package-ops.ts +++ b/src/pi/package-ops.ts @@ -1,5 +1,5 @@ import { spawn } from "node:child_process"; -import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, join, resolve } from "node:path"; @@ -427,6 +427,28 @@ function packageNameToPath(root: string, packageName: string): string { return resolve(root, packageName); } +function listBundledWorkspacePackageNames(root: string): string[] { + if (!existsSync(root)) { + return []; + } + + const names: string[] = []; + for (const entry of readdirSync(root, { withFileTypes: true })) { + if (!entry.isDirectory() && !entry.isSymbolicLink()) continue; + if (entry.name.startsWith(".")) continue; + if (entry.name.startsWith("@")) { + const scopeRoot = resolve(root, entry.name); + for (const scopedEntry of readdirSync(scopeRoot, { withFileTypes: true })) { + if (!scopedEntry.isDirectory() && !scopedEntry.isSymbolicLink()) continue; + names.push(`${entry.name}/${scopedEntry.name}`); + } + continue; + } + names.push(entry.name); + } + return names; +} + function packageDependencyExists(packagePath: string, globalNodeModulesRoot: string, dependency: string): boolean { return existsSync(packageNameToPath(resolve(packagePath, "node_modules"), dependency)) || existsSync(packageNameToPath(globalNodeModulesRoot, dependency)); @@ -464,6 +486,23 @@ function replaceBrokenPackageWithBundledCopy(targetPath: string, bundledPackageP return true; } +function seedBundledPackage(globalNodeModulesRoot: string, bundledNodeModulesRoot: string, packageName: string): boolean { + const bundledPackagePath = resolve(bundledNodeModulesRoot, packageName); + if (!existsSync(bundledPackagePath)) { + return false; + } + + const targetPath = resolve(globalNodeModulesRoot, packageName); + if (replaceBrokenPackageWithBundledCopy(targetPath, bundledPackagePath, globalNodeModulesRoot)) { + return true; + } + if (!existsSync(targetPath)) { + linkDirectory(targetPath, bundledPackagePath); + return true; + } + return false; +} + export function seedBundledWorkspacePackages( agentDir: string, appRoot: string, @@ -476,6 +515,10 @@ export function seedBundledWorkspacePackages( const globalNodeModulesRoot = resolve(getFeynmanNpmPrefixPath(agentDir), "lib", "node_modules"); const seeded: string[] = []; + const bundledPackageNames = listBundledWorkspacePackageNames(bundledNodeModulesRoot); + for (const packageName of bundledPackageNames) { + seedBundledPackage(globalNodeModulesRoot, bundledNodeModulesRoot, packageName); + } for (const source of sources) { if (shouldSkipNativeSource(source)) continue; @@ -483,16 +526,8 @@ export function seedBundledWorkspacePackages( const parsed = parseNpmSource(source); if (!parsed) continue; - const bundledPackagePath = resolve(bundledNodeModulesRoot, parsed.name); - if (!existsSync(bundledPackagePath)) continue; - const targetPath = resolve(globalNodeModulesRoot, parsed.name); - if (replaceBrokenPackageWithBundledCopy(targetPath, bundledPackagePath, globalNodeModulesRoot)) { - seeded.push(source); - continue; - } - if (!existsSync(targetPath)) { - linkDirectory(targetPath, bundledPackagePath); + if (pathsMatchSymlinkTarget(targetPath, resolve(bundledNodeModulesRoot, parsed.name))) { seeded.push(source); } } diff --git a/tests/package-ops.test.ts b/tests/package-ops.test.ts index 3d815a1..902e78e 100644 --- a/tests/package-ops.test.ts +++ b/tests/package-ops.test.ts @@ -101,6 +101,7 @@ test("seedBundledWorkspacePackages repairs broken existing bundled packages", () assert.deepEqual(seeded, ["npm:pi-markdown-preview"]); assert.equal(lstatSync(existingPackageDir).isSymbolicLink(), true); + assert.equal(lstatSync(resolve(homeRoot, "npm-global", "lib", "node_modules", "puppeteer-core")).isSymbolicLink(), true); assert.equal( readFileSync(resolve(existingPackageDir, "package.json"), "utf8").includes('"version": "1.0.0"'), true, diff --git a/website/public/install b/website/public/install index abb1bec..e69ce8b 100644 --- a/website/public/install +++ b/website/public/install @@ -261,7 +261,7 @@ This usually means the release exists, but not all platform bundles were uploade Workarounds: - try again after the release finishes publishing - pass the latest published version explicitly, e.g.: - curl -fsSL https://feynman.is/install | bash -s -- 0.2.23 + curl -fsSL https://feynman.is/install | bash -s -- 0.2.24 EOF exit 1 fi diff --git a/website/public/install.ps1 b/website/public/install.ps1 index 6eb640d..4b85a40 100644 --- a/website/public/install.ps1 +++ b/website/public/install.ps1 @@ -110,7 +110,7 @@ This usually means the release exists, but not all platform bundles were uploade Workarounds: - try again after the release finishes publishing - pass the latest published version explicitly, e.g.: - & ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.23 + & ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.24 "@ } diff --git a/website/src/content/docs/getting-started/installation.md b/website/src/content/docs/getting-started/installation.md index c3d4a06..f42e6af 100644 --- a/website/src/content/docs/getting-started/installation.md +++ b/website/src/content/docs/getting-started/installation.md @@ -117,13 +117,13 @@ These installers download the bundled `skills/` and `prompts/` trees plus the re The one-line installer already targets the latest tagged release. To pin an exact version, pass it explicitly: ```bash -curl -fsSL https://feynman.is/install | bash -s -- 0.2.23 +curl -fsSL https://feynman.is/install | bash -s -- 0.2.24 ``` On Windows: ```powershell -& ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.23 +& ([scriptblock]::Create((irm https://feynman.is/install.ps1))) -Version 0.2.24 ``` ## Post-install setup