Add native installers and release bundles
This commit is contained in:
102
.github/workflows/publish.yml
vendored
102
.github/workflows/publish.yml
vendored
@@ -1,11 +1,42 @@
|
|||||||
name: Publish to npm
|
name: Publish and Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
version-check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.version.outputs.version }}
|
||||||
|
should_publish: ${{ steps.version.outputs.should_publish }}
|
||||||
|
should_build_release: ${{ steps.version.outputs.should_build_release }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24.14.0
|
||||||
|
- id: version
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
CURRENT=$(npm view @companion-ai/feynman version 2>/dev/null || echo "0.0.0")
|
||||||
|
LOCAL=$(node -p "require('./package.json').version")
|
||||||
|
echo "version=$LOCAL" >> "$GITHUB_OUTPUT"
|
||||||
|
if [ "$CURRENT" != "$LOCAL" ]; then
|
||||||
|
echo "should_publish=true" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "should_build_release=true" >> "$GITHUB_OUTPUT"
|
||||||
|
elif [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
|
||||||
|
echo "should_publish=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "should_build_release=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "should_publish=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "should_build_release=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
publish-npm:
|
||||||
|
needs: version-check
|
||||||
|
if: needs.version-check.outputs.should_publish == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -13,19 +44,64 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 24.14.0
|
||||||
registry-url: https://registry.npmjs.org
|
registry-url: https://registry.npmjs.org
|
||||||
- run: npm ci
|
- run: npm ci --ignore-scripts
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npm test
|
- run: npm test
|
||||||
- name: Publish if version changed
|
- run: npm publish --access public
|
||||||
run: |
|
|
||||||
CURRENT=$(npm view @companion-ai/feynman version 2>/dev/null || echo "0.0.0")
|
|
||||||
LOCAL=$(node -p "require('./package.json').version")
|
|
||||||
if [ "$CURRENT" != "$LOCAL" ]; then
|
|
||||||
npm publish --access public
|
|
||||||
else
|
|
||||||
echo "Version $LOCAL already published, skipping"
|
|
||||||
fi
|
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
|
build-native-bundles:
|
||||||
|
needs: version-check
|
||||||
|
if: needs.version-check.outputs.should_build_release == 'true'
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- id: linux-x64
|
||||||
|
os: ubuntu-latest
|
||||||
|
- id: darwin-x64
|
||||||
|
os: macos-13
|
||||||
|
- id: darwin-arm64
|
||||||
|
os: macos-14
|
||||||
|
- id: win32-x64
|
||||||
|
os: windows-latest
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24.14.0
|
||||||
|
- run: npm ci --ignore-scripts
|
||||||
|
- run: npm run build
|
||||||
|
- run: npm run build:native-bundle
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: native-${{ matrix.id }}
|
||||||
|
path: dist/release/*
|
||||||
|
|
||||||
|
release-github:
|
||||||
|
needs:
|
||||||
|
- version-check
|
||||||
|
- publish-npm
|
||||||
|
- build-native-bundles
|
||||||
|
if: needs.version-check.outputs.should_publish == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: release-assets
|
||||||
|
merge-multiple: true
|
||||||
|
- shell: bash
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
VERSION: ${{ needs.version-check.outputs.version }}
|
||||||
|
run: |
|
||||||
|
gh release create "v$VERSION" release-assets/* \
|
||||||
|
--title "v$VERSION" \
|
||||||
|
--notes "Standalone Feynman bundles for native installation." \
|
||||||
|
--target "$GITHUB_SHA"
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
The open source AI research agent
|
The open source AI research agent
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/getcompanion-ai/feynman/main/scripts/install/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
irm https://raw.githubusercontent.com/getcompanion-ai/feynman/main/scripts/install/install.ps1 | iex
|
||||||
|
```
|
||||||
|
|
||||||
|
Or install the npm fallback:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -g @companion-ai/feynman
|
npm install -g @companion-ai/feynman
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -p tsconfig.build.json",
|
"build": "tsc -p tsconfig.build.json",
|
||||||
|
"build:native-bundle": "node ./scripts/build-native-bundle.mjs",
|
||||||
"dev": "tsx src/index.ts",
|
"dev": "tsx src/index.ts",
|
||||||
"prepack": "node ./scripts/prepare-runtime-workspace.mjs",
|
"prepack": "node ./scripts/prepare-runtime-workspace.mjs",
|
||||||
"postinstall": "node ./scripts/patch-embedded-pi.mjs",
|
"postinstall": "node ./scripts/patch-embedded-pi.mjs",
|
||||||
|
|||||||
271
scripts/build-native-bundle.mjs
Normal file
271
scripts/build-native-bundle.mjs
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
import { chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { basename, dirname, join, resolve } from "node:path";
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
|
||||||
|
const appRoot = resolve(import.meta.dirname, "..");
|
||||||
|
const packageJson = JSON.parse(readFileSync(resolve(appRoot, "package.json"), "utf8"));
|
||||||
|
const packageLockPath = resolve(appRoot, "package-lock.json");
|
||||||
|
const bundledNodeVersion = process.env.FEYNMAN_BUNDLED_NODE_VERSION ?? process.version.slice(1);
|
||||||
|
|
||||||
|
function fail(message) {
|
||||||
|
console.error(`[feynman] ${message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(command, args, options = {}) {
|
||||||
|
const result = spawnSync(command, args, {
|
||||||
|
stdio: "inherit",
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
if (result.status !== 0) {
|
||||||
|
fail(`${command} ${args.join(" ")} failed with code ${result.status ?? 1}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCapture(command, args, options = {}) {
|
||||||
|
const result = spawnSync(command, args, {
|
||||||
|
encoding: "utf8",
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
if (result.status !== 0) {
|
||||||
|
const errorOutput = result.stderr?.trim() || result.stdout?.trim() || "unknown error";
|
||||||
|
fail(`${command} ${args.join(" ")} failed: ${errorOutput}`);
|
||||||
|
}
|
||||||
|
return result.stdout.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectTarget() {
|
||||||
|
if (process.platform === "darwin" && process.arch === "arm64") {
|
||||||
|
return {
|
||||||
|
id: "darwin-arm64",
|
||||||
|
nodePlatform: "darwin",
|
||||||
|
nodeArch: "arm64",
|
||||||
|
bundleExtension: "tar.gz",
|
||||||
|
launcher: "unix",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (process.platform === "darwin" && process.arch === "x64") {
|
||||||
|
return {
|
||||||
|
id: "darwin-x64",
|
||||||
|
nodePlatform: "darwin",
|
||||||
|
nodeArch: "x64",
|
||||||
|
bundleExtension: "tar.gz",
|
||||||
|
launcher: "unix",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (process.platform === "linux" && process.arch === "arm64") {
|
||||||
|
return {
|
||||||
|
id: "linux-arm64",
|
||||||
|
nodePlatform: "linux",
|
||||||
|
nodeArch: "arm64",
|
||||||
|
bundleExtension: "tar.gz",
|
||||||
|
launcher: "unix",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (process.platform === "linux" && process.arch === "x64") {
|
||||||
|
return {
|
||||||
|
id: "linux-x64",
|
||||||
|
nodePlatform: "linux",
|
||||||
|
nodeArch: "x64",
|
||||||
|
bundleExtension: "tar.gz",
|
||||||
|
launcher: "unix",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (process.platform === "win32" && process.arch === "arm64") {
|
||||||
|
return {
|
||||||
|
id: "win32-arm64",
|
||||||
|
nodePlatform: "win",
|
||||||
|
nodeArch: "arm64",
|
||||||
|
bundleExtension: "zip",
|
||||||
|
launcher: "windows",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (process.platform === "win32" && process.arch === "x64") {
|
||||||
|
return {
|
||||||
|
id: "win32-x64",
|
||||||
|
nodePlatform: "win",
|
||||||
|
nodeArch: "x64",
|
||||||
|
bundleExtension: "zip",
|
||||||
|
launcher: "windows",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fail(`unsupported platform ${process.platform}/${process.arch}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nodeArchiveName(target) {
|
||||||
|
if (target.nodePlatform === "win") {
|
||||||
|
return `node-v${bundledNodeVersion}-${target.nodePlatform}-${target.nodeArch}.zip`;
|
||||||
|
}
|
||||||
|
return `node-v${bundledNodeVersion}-${target.nodePlatform}-${target.nodeArch}.tar.xz`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureBundledWorkspace() {
|
||||||
|
run(process.execPath, [resolve(appRoot, "scripts", "prepare-runtime-workspace.mjs")], { cwd: appRoot });
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyPackageFiles(appDir) {
|
||||||
|
cpSync(resolve(appRoot, "package.json"), resolve(appDir, "package.json"));
|
||||||
|
for (const entry of packageJson.files) {
|
||||||
|
const normalized = entry.endsWith("/") ? entry.slice(0, -1) : entry;
|
||||||
|
const source = resolve(appRoot, normalized);
|
||||||
|
if (!existsSync(source)) continue;
|
||||||
|
const destination = resolve(appDir, normalized);
|
||||||
|
mkdirSync(dirname(destination), { recursive: true });
|
||||||
|
cpSync(source, destination, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
cpSync(packageLockPath, resolve(appDir, "package-lock.json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractTarball(archivePath, destination, compressionFlag) {
|
||||||
|
run("tar", [compressionFlag, archivePath, "-C", destination]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractZip(archivePath, destination) {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
run("powershell", [
|
||||||
|
"-NoProfile",
|
||||||
|
"-Command",
|
||||||
|
`Expand-Archive -LiteralPath '${archivePath.replace(/'/g, "''")}' -DestinationPath '${destination.replace(/'/g, "''")}' -Force`,
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
run("unzip", ["-q", archivePath, "-d", destination]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findSingleDirectory(path) {
|
||||||
|
const entries = readdirSync(path).filter((entry) => !entry.startsWith("."));
|
||||||
|
if (entries.length !== 1) {
|
||||||
|
fail(`expected exactly one directory in ${path}, found: ${entries.join(", ")}`);
|
||||||
|
}
|
||||||
|
const child = resolve(path, entries[0]);
|
||||||
|
if (!statSync(child).isDirectory()) {
|
||||||
|
fail(`expected ${child} to be a directory`);
|
||||||
|
}
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
function installBundledNode(bundleRoot, target, stagingRoot) {
|
||||||
|
const archiveName = nodeArchiveName(target);
|
||||||
|
const archivePath = resolve(stagingRoot, archiveName);
|
||||||
|
const url = `https://nodejs.org/dist/v${bundledNodeVersion}/${archiveName}`;
|
||||||
|
|
||||||
|
run("curl", ["-fsSL", url, "-o", archivePath]);
|
||||||
|
|
||||||
|
const extractRoot = resolve(stagingRoot, "node-dist");
|
||||||
|
mkdirSync(extractRoot, { recursive: true });
|
||||||
|
if (archiveName.endsWith(".zip")) {
|
||||||
|
extractZip(archivePath, extractRoot);
|
||||||
|
} else {
|
||||||
|
extractTarball(archivePath, extractRoot, "-xJf");
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractedDir = findSingleDirectory(extractRoot);
|
||||||
|
renameSync(extractedDir, resolve(bundleRoot, "node"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeLauncher(bundleRoot, target) {
|
||||||
|
if (target.launcher === "unix") {
|
||||||
|
const launcherPath = resolve(bundleRoot, "feynman");
|
||||||
|
writeFileSync(
|
||||||
|
launcherPath,
|
||||||
|
[
|
||||||
|
"#!/bin/sh",
|
||||||
|
"set -eu",
|
||||||
|
'ROOT="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"',
|
||||||
|
'exec "$ROOT/node/bin/node" "$ROOT/app/bin/feynman.js" "$@"',
|
||||||
|
"",
|
||||||
|
].join("\n"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
chmodSync(launcherPath, 0o755);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
resolve(bundleRoot, "feynman.cmd"),
|
||||||
|
[
|
||||||
|
"@echo off",
|
||||||
|
"setlocal",
|
||||||
|
'set "ROOT=%~dp0"',
|
||||||
|
'"%ROOT%node\\node.exe" "%ROOT%app\\bin\\feynman.js" %*',
|
||||||
|
"",
|
||||||
|
].join("\r\n"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
writeFileSync(
|
||||||
|
resolve(bundleRoot, "feynman.ps1"),
|
||||||
|
[
|
||||||
|
'$Root = Split-Path -Parent $MyInvocation.MyCommand.Path',
|
||||||
|
'& "$Root\\node\\node.exe" "$Root\\app\\bin\\feynman.js" @args',
|
||||||
|
"",
|
||||||
|
].join("\r\n"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateBundle(bundleRoot, target) {
|
||||||
|
const nodeExecutable =
|
||||||
|
target.launcher === "windows"
|
||||||
|
? resolve(bundleRoot, "node", "node.exe")
|
||||||
|
: resolve(bundleRoot, "node", "bin", "node");
|
||||||
|
|
||||||
|
run(nodeExecutable, ["-e", "require('./app/.feynman/npm/node_modules/better-sqlite3'); console.log('better-sqlite3 ok')"], {
|
||||||
|
cwd: bundleRoot,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function packBundle(bundleRoot, target, outDir) {
|
||||||
|
const archiveName = `${basename(bundleRoot)}.${target.bundleExtension}`;
|
||||||
|
const archivePath = resolve(outDir, archiveName);
|
||||||
|
rmSync(archivePath, { force: true });
|
||||||
|
|
||||||
|
if (target.bundleExtension === "zip") {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
run("powershell", [
|
||||||
|
"-NoProfile",
|
||||||
|
"-Command",
|
||||||
|
`Compress-Archive -Path '${bundleRoot.replace(/'/g, "''")}\\*' -DestinationPath '${archivePath.replace(/'/g, "''")}' -Force`,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
run("zip", ["-qr", archivePath, basename(bundleRoot)], { cwd: resolve(bundleRoot, "..") });
|
||||||
|
}
|
||||||
|
return archivePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
run("tar", ["-czf", archivePath, basename(bundleRoot)], { cwd: resolve(bundleRoot, "..") });
|
||||||
|
return archivePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const target = detectTarget();
|
||||||
|
const stagingRoot = mkdtempSync(join(tmpdir(), "feynman-native-"));
|
||||||
|
const outDir = resolve(appRoot, "dist", "release");
|
||||||
|
const bundleRoot = resolve(stagingRoot, `feynman-${packageJson.version}-${target.id}`);
|
||||||
|
const appDir = resolve(bundleRoot, "app");
|
||||||
|
|
||||||
|
mkdirSync(outDir, { recursive: true });
|
||||||
|
mkdirSync(appDir, { recursive: true });
|
||||||
|
|
||||||
|
ensureBundledWorkspace();
|
||||||
|
copyPackageFiles(appDir);
|
||||||
|
run("npm", ["ci", "--omit=dev", "--ignore-scripts", "--no-audit", "--no-fund", "--loglevel", "error"], { cwd: appDir });
|
||||||
|
|
||||||
|
const appFeynmanDir = resolve(appDir, ".feynman");
|
||||||
|
extractTarball(resolve(appFeynmanDir, "runtime-workspace.tgz"), appFeynmanDir, "-xzf");
|
||||||
|
rmSync(resolve(appFeynmanDir, "runtime-workspace.tgz"), { force: true });
|
||||||
|
run(process.execPath, [resolve(appDir, "scripts", "patch-embedded-pi.mjs")], { cwd: appDir });
|
||||||
|
|
||||||
|
installBundledNode(bundleRoot, target, stagingRoot);
|
||||||
|
writeLauncher(bundleRoot, target);
|
||||||
|
validateBundle(bundleRoot, target);
|
||||||
|
|
||||||
|
const archivePath = packBundle(bundleRoot, target, outDir);
|
||||||
|
console.log(`[feynman] native bundle ready: ${archivePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
82
scripts/install/install.ps1
Normal file
82
scripts/install/install.ps1
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
param(
|
||||||
|
[string]$Version = "latest"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
function Resolve-Version {
|
||||||
|
param([string]$RequestedVersion)
|
||||||
|
|
||||||
|
if ($RequestedVersion -and $RequestedVersion -ne "latest") {
|
||||||
|
return $RequestedVersion.TrimStart("v")
|
||||||
|
}
|
||||||
|
|
||||||
|
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/getcompanion-ai/feynman/releases/latest"
|
||||||
|
if (-not $release.tag_name) {
|
||||||
|
throw "Failed to resolve the latest Feynman release version."
|
||||||
|
}
|
||||||
|
|
||||||
|
return $release.tag_name.TrimStart("v")
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ArchSuffix {
|
||||||
|
$arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture
|
||||||
|
switch ($arch.ToString()) {
|
||||||
|
"X64" { return "x64" }
|
||||||
|
"Arm64" { return "arm64" }
|
||||||
|
default { throw "Unsupported architecture: $arch" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolvedVersion = Resolve-Version -RequestedVersion $Version
|
||||||
|
$archSuffix = Get-ArchSuffix
|
||||||
|
$bundleName = "feynman-$resolvedVersion-win32-$archSuffix"
|
||||||
|
$archiveName = "$bundleName.zip"
|
||||||
|
$baseUrl = if ($env:FEYNMAN_INSTALL_BASE_URL) { $env:FEYNMAN_INSTALL_BASE_URL } else { "https://github.com/getcompanion-ai/feynman/releases/download/v$resolvedVersion" }
|
||||||
|
$downloadUrl = "$baseUrl/$archiveName"
|
||||||
|
|
||||||
|
$installRoot = Join-Path $env:LOCALAPPDATA "Programs\feynman"
|
||||||
|
$installBinDir = Join-Path $installRoot "bin"
|
||||||
|
$bundleDir = Join-Path $installRoot $bundleName
|
||||||
|
|
||||||
|
$tmpDir = Join-Path ([System.IO.Path]::GetTempPath()) ("feynman-install-" + [System.Guid]::NewGuid().ToString("N"))
|
||||||
|
New-Item -ItemType Directory -Path $tmpDir | Out-Null
|
||||||
|
|
||||||
|
try {
|
||||||
|
$archivePath = Join-Path $tmpDir $archiveName
|
||||||
|
Invoke-WebRequest -Uri $downloadUrl -OutFile $archivePath
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Path $installRoot -Force | Out-Null
|
||||||
|
if (Test-Path $bundleDir) {
|
||||||
|
Remove-Item -Recurse -Force $bundleDir
|
||||||
|
}
|
||||||
|
|
||||||
|
Expand-Archive -LiteralPath $archivePath -DestinationPath $installRoot -Force
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Path $installBinDir -Force | Out-Null
|
||||||
|
|
||||||
|
$shimPath = Join-Path $installBinDir "feynman.cmd"
|
||||||
|
@"
|
||||||
|
@echo off
|
||||||
|
"$bundleDir\feynman.cmd" %*
|
||||||
|
"@ | Set-Content -Path $shimPath -Encoding ASCII
|
||||||
|
|
||||||
|
$currentUserPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
||||||
|
if (-not $currentUserPath.Split(';').Contains($installBinDir)) {
|
||||||
|
$updatedPath = if ([string]::IsNullOrWhiteSpace($currentUserPath)) {
|
||||||
|
$installBinDir
|
||||||
|
} else {
|
||||||
|
"$currentUserPath;$installBinDir"
|
||||||
|
}
|
||||||
|
[Environment]::SetEnvironmentVariable("Path", $updatedPath, "User")
|
||||||
|
Write-Host "Updated user PATH. Open a new shell to run feynman."
|
||||||
|
} else {
|
||||||
|
Write-Host "$installBinDir is already on PATH."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Feynman $resolvedVersion installed successfully."
|
||||||
|
} finally {
|
||||||
|
if (Test-Path $tmpDir) {
|
||||||
|
Remove-Item -Recurse -Force $tmpDir
|
||||||
|
}
|
||||||
|
}
|
||||||
212
scripts/install/install.sh
Normal file
212
scripts/install/install.sh
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
VERSION="${1:-latest}"
|
||||||
|
INSTALL_BIN_DIR="${FEYNMAN_INSTALL_BIN_DIR:-$HOME/.local/bin}"
|
||||||
|
INSTALL_APP_DIR="${FEYNMAN_INSTALL_APP_DIR:-$HOME/.local/share/feynman}"
|
||||||
|
SKIP_PATH_UPDATE="${FEYNMAN_INSTALL_SKIP_PATH_UPDATE:-0}"
|
||||||
|
path_action="already"
|
||||||
|
path_profile=""
|
||||||
|
|
||||||
|
step() {
|
||||||
|
printf '==> %s\n' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_version() {
|
||||||
|
case "$1" in
|
||||||
|
"" | latest)
|
||||||
|
printf 'latest\n'
|
||||||
|
;;
|
||||||
|
v*)
|
||||||
|
printf '%s\n' "${1#v}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf '%s\n' "$1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
download_file() {
|
||||||
|
url="$1"
|
||||||
|
output="$2"
|
||||||
|
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
curl -fsSL "$url" -o "$output"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v wget >/dev/null 2>&1; then
|
||||||
|
wget -q -O "$output" "$url"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "curl or wget is required to install Feynman." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
download_text() {
|
||||||
|
url="$1"
|
||||||
|
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
curl -fsSL "$url"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v wget >/dev/null 2>&1; then
|
||||||
|
wget -q -O - "$url"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "curl or wget is required to install Feynman." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
add_to_path() {
|
||||||
|
path_action="already"
|
||||||
|
path_profile=""
|
||||||
|
|
||||||
|
case ":$PATH:" in
|
||||||
|
*":$INSTALL_BIN_DIR:"*)
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "$SKIP_PATH_UPDATE" = "1" ]; then
|
||||||
|
path_action="skipped"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
profile="${FEYNMAN_INSTALL_SHELL_PROFILE:-$HOME/.profile}"
|
||||||
|
if [ -z "${FEYNMAN_INSTALL_SHELL_PROFILE:-}" ]; then
|
||||||
|
case "${SHELL:-}" in
|
||||||
|
*/zsh)
|
||||||
|
profile="$HOME/.zshrc"
|
||||||
|
;;
|
||||||
|
*/bash)
|
||||||
|
profile="$HOME/.bashrc"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
path_profile="$profile"
|
||||||
|
path_line="export PATH=\"$INSTALL_BIN_DIR:\$PATH\""
|
||||||
|
if [ -f "$profile" ] && grep -F "$path_line" "$profile" >/dev/null 2>&1; then
|
||||||
|
path_action="configured"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
printf '\n# Added by Feynman installer\n'
|
||||||
|
printf '%s\n' "$path_line"
|
||||||
|
} >>"$profile"
|
||||||
|
path_action="added"
|
||||||
|
}
|
||||||
|
|
||||||
|
require_command() {
|
||||||
|
if ! command -v "$1" >/dev/null 2>&1; then
|
||||||
|
echo "$1 is required to install Feynman." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_version() {
|
||||||
|
normalized_version="$(normalize_version "$VERSION")"
|
||||||
|
|
||||||
|
if [ "$normalized_version" != "latest" ]; then
|
||||||
|
printf '%s\n' "$normalized_version"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
release_json="$(download_text "https://api.github.com/repos/getcompanion-ai/feynman/releases/latest")"
|
||||||
|
resolved="$(printf '%s\n' "$release_json" | sed -n 's/.*"tag_name":[[:space:]]*"v\([^"]*\)".*/\1/p' | head -n 1)"
|
||||||
|
|
||||||
|
if [ -z "$resolved" ]; then
|
||||||
|
echo "Failed to resolve the latest Feynman release version." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$resolved"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Darwin)
|
||||||
|
os="darwin"
|
||||||
|
;;
|
||||||
|
Linux)
|
||||||
|
os="linux"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "install.sh supports macOS and Linux. Use install.ps1 on Windows." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64 | amd64)
|
||||||
|
arch="x64"
|
||||||
|
;;
|
||||||
|
arm64 | aarch64)
|
||||||
|
arch="arm64"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported architecture: $(uname -m)" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
require_command mktemp
|
||||||
|
require_command tar
|
||||||
|
|
||||||
|
resolved_version="$(resolve_version)"
|
||||||
|
asset_target="$os-$arch"
|
||||||
|
bundle_name="feynman-${resolved_version}-${asset_target}"
|
||||||
|
archive_name="${bundle_name}.tar.gz"
|
||||||
|
base_url="${FEYNMAN_INSTALL_BASE_URL:-https://github.com/getcompanion-ai/feynman/releases/download/v${resolved_version}}"
|
||||||
|
download_url="${base_url}/${archive_name}"
|
||||||
|
|
||||||
|
step "Installing Feynman ${resolved_version} for ${asset_target}"
|
||||||
|
|
||||||
|
tmp_dir="$(mktemp -d)"
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$tmp_dir"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
archive_path="$tmp_dir/$archive_name"
|
||||||
|
download_file "$download_url" "$archive_path"
|
||||||
|
|
||||||
|
mkdir -p "$INSTALL_APP_DIR"
|
||||||
|
rm -rf "$INSTALL_APP_DIR/$bundle_name"
|
||||||
|
tar -xzf "$archive_path" -C "$INSTALL_APP_DIR"
|
||||||
|
|
||||||
|
mkdir -p "$INSTALL_BIN_DIR"
|
||||||
|
cat >"$INSTALL_BIN_DIR/feynman" <<EOF
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
exec "$INSTALL_APP_DIR/$bundle_name/feynman" "\$@"
|
||||||
|
EOF
|
||||||
|
chmod 0755 "$INSTALL_BIN_DIR/feynman"
|
||||||
|
|
||||||
|
add_to_path
|
||||||
|
|
||||||
|
case "$path_action" in
|
||||||
|
added)
|
||||||
|
step "PATH updated for future shells in $path_profile"
|
||||||
|
step "Run now: export PATH=\"$INSTALL_BIN_DIR:\$PATH\" && feynman"
|
||||||
|
;;
|
||||||
|
configured)
|
||||||
|
step "PATH is already configured for future shells in $path_profile"
|
||||||
|
step "Run now: export PATH=\"$INSTALL_BIN_DIR:\$PATH\" && feynman"
|
||||||
|
;;
|
||||||
|
skipped)
|
||||||
|
step "PATH update skipped"
|
||||||
|
step "Run now: export PATH=\"$INSTALL_BIN_DIR:\$PATH\" && feynman"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
step "$INSTALL_BIN_DIR is already on PATH"
|
||||||
|
step "Run: feynman"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
printf 'Feynman %s installed successfully.\n' "$resolved_version"
|
||||||
Reference in New Issue
Block a user