feat: sync upstream pi-mono (386 commits)
从上游 badlogic/pi-mono 同步 386 个提交,手动解决所有冲突: 保留 SproutClaw 独有功能: - RPC: reload 命令、bash 流式输出、get_extensions 命令 - settings-manager: showChangelogOnStartup 设置 - agent-session: turnIndex getter - 移除 pi 版本检查通知(interactive-mode) - skills 系统、启动脚本、自定义工具脚本 直接采用上游版本: - packages/ai 模型列表及测试文件 - packages/coding-agent 文档 - 其余未冲突的全部上游代码 升级 @mistralai/mistralai 至 2.2.6(修复 promptCacheKey 类型错误) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,11 +4,14 @@
|
||||
# Mirrors .github/workflows/build-binaries.yml
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/build-binaries.sh [--skip-deps] [--platform <platform>]
|
||||
# ./scripts/build-binaries.sh [--skip-install] [--skip-deps] [--skip-build] [--platform <platform>] [--out <dir>]
|
||||
#
|
||||
# Options:
|
||||
# --skip-install Skip npm ci
|
||||
# --skip-deps Skip installing cross-platform dependencies
|
||||
# --skip-build Skip npm run build
|
||||
# --platform <name> Build only for specified platform (darwin-arm64, darwin-x64, linux-x64, linux-arm64, windows-x64, windows-arm64)
|
||||
# --out <dir> Output directory (default: packages/coding-agent/binaries)
|
||||
#
|
||||
# Output:
|
||||
# packages/coding-agent/binaries/
|
||||
@@ -23,19 +26,34 @@ set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
SKIP_INSTALL=false
|
||||
SKIP_DEPS=false
|
||||
SKIP_BUILD=false
|
||||
PLATFORM=""
|
||||
OUTPUT_DIR=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--skip-install)
|
||||
SKIP_INSTALL=true
|
||||
shift
|
||||
;;
|
||||
--skip-deps)
|
||||
SKIP_DEPS=true
|
||||
shift
|
||||
;;
|
||||
--skip-build)
|
||||
SKIP_BUILD=true
|
||||
shift
|
||||
;;
|
||||
--platform)
|
||||
PLATFORM="$2"
|
||||
shift 2
|
||||
;;
|
||||
--out)
|
||||
OUTPUT_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
@@ -56,45 +74,52 @@ if [[ -n "$PLATFORM" ]]; then
|
||||
esac
|
||||
fi
|
||||
|
||||
echo "==> Installing dependencies..."
|
||||
npm ci --ignore-scripts
|
||||
if [[ -z "$OUTPUT_DIR" ]]; then
|
||||
OUTPUT_DIR="packages/coding-agent/binaries"
|
||||
fi
|
||||
if [[ "$OUTPUT_DIR" != /* ]]; then
|
||||
OUTPUT_DIR="$(pwd)/$OUTPUT_DIR"
|
||||
fi
|
||||
|
||||
if [[ "$SKIP_INSTALL" == "false" ]]; then
|
||||
echo "==> Installing dependencies..."
|
||||
npm ci --ignore-scripts
|
||||
else
|
||||
echo "==> Skipping npm ci (--skip-install)"
|
||||
fi
|
||||
|
||||
if [[ "$SKIP_DEPS" == "false" ]]; then
|
||||
echo "==> Installing cross-platform native bindings..."
|
||||
CLIPBOARD_VERSION=$(node -p "require('./packages/coding-agent/package.json').optionalDependencies['@mariozechner/clipboard']")
|
||||
# npm ci only installs optional deps for the current platform
|
||||
# We need all platform bindings for bun cross-compilation
|
||||
# We need the base clipboard package and all platform bindings for bun cross-compilation
|
||||
# Use --force to bypass platform checks (os/cpu restrictions in package.json)
|
||||
# Install all in one command to avoid npm removing packages from previous installs
|
||||
npm install --no-save --package-lock=false --force --ignore-scripts \
|
||||
@mariozechner/clipboard-darwin-arm64@0.3.2 \
|
||||
@mariozechner/clipboard-darwin-x64@0.3.2 \
|
||||
@mariozechner/clipboard-linux-x64-gnu@0.3.2 \
|
||||
@mariozechner/clipboard-linux-arm64-gnu@0.3.2 \
|
||||
@mariozechner/clipboard-win32-x64-msvc@0.3.2 \
|
||||
@mariozechner/clipboard-win32-arm64-msvc@0.3.2 \
|
||||
@img/sharp-darwin-arm64@0.34.5 \
|
||||
@img/sharp-darwin-x64@0.34.5 \
|
||||
@img/sharp-linux-x64@0.34.5 \
|
||||
@img/sharp-linux-arm64@0.34.5 \
|
||||
@img/sharp-win32-x64@0.34.5 \
|
||||
@img/sharp-win32-arm64@0.34.5 \
|
||||
@img/sharp-libvips-darwin-arm64@1.2.4 \
|
||||
@img/sharp-libvips-darwin-x64@1.2.4 \
|
||||
@img/sharp-libvips-linux-x64@1.2.4 \
|
||||
@img/sharp-libvips-linux-arm64@1.2.4
|
||||
npm install --include=optional --no-save --package-lock=false --force --ignore-scripts \
|
||||
@mariozechner/clipboard@"$CLIPBOARD_VERSION" \
|
||||
@mariozechner/clipboard-darwin-arm64@"$CLIPBOARD_VERSION" \
|
||||
@mariozechner/clipboard-darwin-x64@"$CLIPBOARD_VERSION" \
|
||||
@mariozechner/clipboard-linux-x64-gnu@"$CLIPBOARD_VERSION" \
|
||||
@mariozechner/clipboard-linux-arm64-gnu@"$CLIPBOARD_VERSION" \
|
||||
@mariozechner/clipboard-win32-x64-msvc@"$CLIPBOARD_VERSION" \
|
||||
@mariozechner/clipboard-win32-arm64-msvc@"$CLIPBOARD_VERSION"
|
||||
else
|
||||
echo "==> Skipping cross-platform native bindings (--skip-deps)"
|
||||
fi
|
||||
|
||||
echo "==> Building all packages..."
|
||||
npm run build
|
||||
if [[ "$SKIP_BUILD" == "false" ]]; then
|
||||
echo "==> Building all packages..."
|
||||
npm run build
|
||||
else
|
||||
echo "==> Skipping package build (--skip-build)"
|
||||
fi
|
||||
|
||||
echo "==> Building binaries..."
|
||||
cd packages/coding-agent
|
||||
|
||||
# Clean previous builds
|
||||
rm -rf binaries
|
||||
mkdir -p binaries/{darwin-arm64,darwin-x64,linux-x64,linux-arm64,windows-x64,windows-arm64}
|
||||
rm -rf "$OUTPUT_DIR"
|
||||
mkdir -p "$OUTPUT_DIR"/{darwin-arm64,darwin-x64,linux-x64,linux-arm64,windows-x64,windows-arm64}
|
||||
|
||||
# Determine which platforms to build
|
||||
if [[ -n "$PLATFORM" ]]; then
|
||||
@@ -105,10 +130,13 @@ fi
|
||||
|
||||
for platform in "${PLATFORMS[@]}"; do
|
||||
echo "Building for $platform..."
|
||||
# Bun compiled executables only embed worker scripts when they are passed as
|
||||
# explicit build entrypoints. The runtime can still use new URL(...), but the
|
||||
# worker must be present in the compiled executable.
|
||||
if [[ "$platform" == windows-* ]]; then
|
||||
bun build --compile --target=bun-$platform ./dist/bun/cli.js --outfile binaries/$platform/pi.exe
|
||||
bun build --compile --target=bun-$platform ./dist/bun/cli.js ./src/utils/image-resize-worker.ts --outfile "$OUTPUT_DIR/$platform/pi.exe"
|
||||
else
|
||||
bun build --compile --target=bun-$platform ./dist/bun/cli.js --outfile binaries/$platform/pi
|
||||
bun build --compile --target=bun-$platform ./dist/bun/cli.js ./src/utils/image-resize-worker.ts --outfile "$OUTPUT_DIR/$platform/pi"
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -116,66 +144,94 @@ echo "==> Creating release archives..."
|
||||
|
||||
# Copy shared files to each platform directory
|
||||
for platform in "${PLATFORMS[@]}"; do
|
||||
cp package.json binaries/$platform/
|
||||
cp README.md binaries/$platform/
|
||||
cp CHANGELOG.md binaries/$platform/
|
||||
cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm binaries/$platform/
|
||||
mkdir -p binaries/$platform/theme
|
||||
cp dist/modes/interactive/theme/*.json binaries/$platform/theme/
|
||||
mkdir -p binaries/$platform/assets
|
||||
cp dist/modes/interactive/assets/* binaries/$platform/assets/
|
||||
cp -r dist/core/export-html binaries/$platform/
|
||||
cp -r docs binaries/$platform/
|
||||
cp -r examples binaries/$platform/
|
||||
cp package.json "$OUTPUT_DIR/$platform/"
|
||||
cp README.md "$OUTPUT_DIR/$platform/"
|
||||
cp CHANGELOG.md "$OUTPUT_DIR/$platform/"
|
||||
cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm "$OUTPUT_DIR/$platform/"
|
||||
mkdir -p "$OUTPUT_DIR/$platform/theme"
|
||||
cp dist/modes/interactive/theme/*.json "$OUTPUT_DIR/$platform/theme/"
|
||||
mkdir -p "$OUTPUT_DIR/$platform/assets"
|
||||
cp dist/modes/interactive/assets/* "$OUTPUT_DIR/$platform/assets/"
|
||||
cp -r dist/core/export-html "$OUTPUT_DIR/$platform/"
|
||||
cp -r docs "$OUTPUT_DIR/$platform/"
|
||||
cp -r examples "$OUTPUT_DIR/$platform/"
|
||||
|
||||
# Copy Windows VT input native helper next to compiled Windows binaries.
|
||||
case "$platform" in
|
||||
darwin-arm64)
|
||||
clipboard_native_package="clipboard-darwin-arm64"
|
||||
;;
|
||||
darwin-x64)
|
||||
clipboard_native_package="clipboard-darwin-x64"
|
||||
;;
|
||||
linux-x64)
|
||||
clipboard_native_package="clipboard-linux-x64-gnu"
|
||||
;;
|
||||
linux-arm64)
|
||||
clipboard_native_package="clipboard-linux-arm64-gnu"
|
||||
;;
|
||||
windows-x64)
|
||||
clipboard_native_package="clipboard-win32-x64-msvc"
|
||||
;;
|
||||
windows-arm64)
|
||||
clipboard_native_package="clipboard-win32-arm64-msvc"
|
||||
;;
|
||||
esac
|
||||
mkdir -p "$OUTPUT_DIR/$platform/node_modules/@mariozechner"
|
||||
cp -r ../../node_modules/@mariozechner/clipboard "$OUTPUT_DIR/$platform/node_modules/@mariozechner/"
|
||||
cp -r ../../node_modules/@mariozechner/$clipboard_native_package "$OUTPUT_DIR/$platform/node_modules/@mariozechner/"
|
||||
|
||||
# Copy terminal input native helpers next to compiled binaries.
|
||||
if [[ "$platform" == darwin-* ]]; then
|
||||
mkdir -p "$OUTPUT_DIR/$platform/native/darwin/prebuilds/$platform"
|
||||
cp ../tui/native/darwin/prebuilds/$platform/darwin-modifiers.node "$OUTPUT_DIR/$platform/native/darwin/prebuilds/$platform/"
|
||||
fi
|
||||
if [[ "$platform" == windows-* ]]; then
|
||||
if [[ "$platform" == "windows-arm64" ]]; then
|
||||
win32_arch_dir="win32-arm64"
|
||||
else
|
||||
win32_arch_dir="win32-x64"
|
||||
fi
|
||||
mkdir -p binaries/$platform/native/win32/prebuilds/$win32_arch_dir
|
||||
cp ../tui/native/win32/prebuilds/$win32_arch_dir/win32-console-mode.node binaries/$platform/native/win32/prebuilds/$win32_arch_dir/
|
||||
mkdir -p "$OUTPUT_DIR/$platform/native/win32/prebuilds/$win32_arch_dir"
|
||||
cp ../tui/native/win32/prebuilds/$win32_arch_dir/win32-console-mode.node "$OUTPUT_DIR/$platform/native/win32/prebuilds/$win32_arch_dir/"
|
||||
fi
|
||||
done
|
||||
|
||||
# Create archives
|
||||
cd binaries
|
||||
cd "$OUTPUT_DIR"
|
||||
|
||||
for platform in "${PLATFORMS[@]}"; do
|
||||
if [[ "$platform" == windows-* ]]; then
|
||||
# Windows (zip)
|
||||
echo "Creating pi-$platform.zip..."
|
||||
(cd $platform && zip -r ../pi-$platform.zip .)
|
||||
(cd "$platform" && zip -r ../pi-$platform.zip .)
|
||||
else
|
||||
# Unix platforms (tar.gz) - use wrapper directory for mise compatibility
|
||||
echo "Creating pi-$platform.tar.gz..."
|
||||
mv $platform pi && tar -czf pi-$platform.tar.gz pi && mv pi $platform
|
||||
mv "$platform" pi && tar -czf pi-$platform.tar.gz pi && mv pi "$platform"
|
||||
fi
|
||||
done
|
||||
|
||||
# Extract archives for easy local testing
|
||||
echo "==> Extracting archives for testing..."
|
||||
for platform in "${PLATFORMS[@]}"; do
|
||||
rm -rf $platform
|
||||
rm -rf "$platform"
|
||||
if [[ "$platform" == windows-* ]]; then
|
||||
mkdir -p $platform && (cd $platform && unzip -q ../pi-$platform.zip)
|
||||
mkdir -p "$platform" && (cd "$platform" && unzip -q ../pi-$platform.zip)
|
||||
else
|
||||
tar -xzf pi-$platform.tar.gz && mv pi $platform
|
||||
tar -xzf pi-$platform.tar.gz && mv pi "$platform"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "==> Build complete!"
|
||||
echo "Archives available in packages/coding-agent/binaries/"
|
||||
echo "Archives available in $OUTPUT_DIR/"
|
||||
ls -lh *.tar.gz *.zip 2>/dev/null || true
|
||||
echo ""
|
||||
echo "Extracted directories for testing:"
|
||||
for platform in "${PLATFORMS[@]}"; do
|
||||
if [[ "$platform" == windows-* ]]; then
|
||||
echo " binaries/$platform/pi.exe"
|
||||
echo " $OUTPUT_DIR/$platform/pi.exe"
|
||||
else
|
||||
echo " binaries/$platform/pi"
|
||||
echo " $OUTPUT_DIR/$platform/pi"
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -4,7 +4,21 @@ import { join } from "node:path";
|
||||
import { build } from "esbuild";
|
||||
|
||||
const outputPath = join(tmpdir(), "pi-browser-smoke.js");
|
||||
const baseOutputPath = join(tmpdir(), "pi-browser-base-smoke.js");
|
||||
const selectiveOutputPath = join(tmpdir(), "pi-browser-selective-smoke.js");
|
||||
const errorLogPath = join(tmpdir(), "pi-browser-smoke-errors.log");
|
||||
const providerImplementationInputs = [
|
||||
"packages/ai/src/providers/amazon-bedrock.ts",
|
||||
"packages/ai/src/providers/anthropic.ts",
|
||||
"packages/ai/src/providers/azure-openai-responses.ts",
|
||||
"packages/ai/src/providers/google.ts",
|
||||
"packages/ai/src/providers/google-vertex.ts",
|
||||
"packages/ai/src/providers/images/openrouter.ts",
|
||||
"packages/ai/src/providers/mistral.ts",
|
||||
"packages/ai/src/providers/openai-codex-responses.ts",
|
||||
"packages/ai/src/providers/openai-completions.ts",
|
||||
"packages/ai/src/providers/openai-responses.ts",
|
||||
];
|
||||
|
||||
try {
|
||||
await build({
|
||||
@@ -15,6 +29,36 @@ try {
|
||||
logLevel: "silent",
|
||||
outfile: outputPath,
|
||||
});
|
||||
const baseBuild = await build({
|
||||
stdin: {
|
||||
contents: `import { complete } from "@earendil-works/pi-ai/base";\nimport { Agent } from "@earendil-works/pi-agent-core/base";\nconsole.log(typeof complete, typeof Agent);\n`,
|
||||
resolveDir: process.cwd(),
|
||||
sourcefile: "pi-browser-base-smoke-entry.ts",
|
||||
},
|
||||
bundle: true,
|
||||
platform: "browser",
|
||||
format: "esm",
|
||||
logLevel: "silent",
|
||||
metafile: true,
|
||||
outfile: baseOutputPath,
|
||||
});
|
||||
const bundledInputs = new Set(Object.keys(baseBuild.metafile.inputs));
|
||||
const reachableProviderImplementations = providerImplementationInputs.filter((input) => bundledInputs.has(input));
|
||||
if (reachableProviderImplementations.length > 0) {
|
||||
throw new Error(`Base browser bundle reached provider implementations:\n${reachableProviderImplementations.join("\n")}`);
|
||||
}
|
||||
await build({
|
||||
stdin: {
|
||||
contents: `import { register as registerAnthropic } from "@earendil-works/pi-ai/anthropic";\nimport { register as registerOpenAICompletions } from "@earendil-works/pi-ai/openai-completions";\nimport { register as registerOpenRouterImages } from "@earendil-works/pi-ai/openrouter-images";\nconsole.log(typeof registerAnthropic, typeof registerOpenAICompletions, typeof registerOpenRouterImages);\n`,
|
||||
resolveDir: process.cwd(),
|
||||
sourcefile: "pi-browser-selective-smoke-entry.ts",
|
||||
},
|
||||
bundle: true,
|
||||
platform: "browser",
|
||||
format: "esm",
|
||||
logLevel: "silent",
|
||||
outfile: selectiveOutputPath,
|
||||
});
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
let detailedErrors = "";
|
||||
|
||||
@@ -30,30 +30,50 @@ function packageLabel(lockPath, entry) {
|
||||
return entry?.version ? `${name}@${entry.version}` : name;
|
||||
}
|
||||
|
||||
function summarizeLockfileChange() {
|
||||
function getLockfilePackageChanges() {
|
||||
const before = readJsonFromGit("HEAD:package-lock.json");
|
||||
const after = readJsonFromGit(":package-lock.json");
|
||||
if (!before?.packages || !after?.packages) return [];
|
||||
if (!before?.packages || !after?.packages) return undefined;
|
||||
|
||||
const changes = [];
|
||||
const paths = new Set([...Object.keys(before.packages), ...Object.keys(after.packages)]);
|
||||
for (const lockPath of [...paths].sort()) {
|
||||
if (!lockPath.includes("node_modules/")) continue;
|
||||
const oldEntry = before.packages[lockPath];
|
||||
const newEntry = after.packages[lockPath];
|
||||
if (!oldEntry && newEntry) {
|
||||
changes.push(`added ${packageLabel(lockPath, newEntry)}`);
|
||||
} else if (oldEntry && !newEntry) {
|
||||
changes.push(`removed ${packageLabel(lockPath, oldEntry)}`);
|
||||
} else if (oldEntry?.version !== newEntry?.version) {
|
||||
changes.push(
|
||||
`changed ${packageNameFromLockPath(lockPath)} ${oldEntry?.version ?? "<none>"} -> ${newEntry?.version ?? "<none>"}`,
|
||||
);
|
||||
if (JSON.stringify(oldEntry) !== JSON.stringify(newEntry)) {
|
||||
changes.push({ lockPath, oldEntry, newEntry });
|
||||
}
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
|
||||
function isWorkspacePackagePath(lockPath) {
|
||||
return lockPath.startsWith("packages/");
|
||||
}
|
||||
|
||||
function hasOnlyWorkspacePackageChanges(changes) {
|
||||
return changes.length > 0 && changes.every((change) => isWorkspacePackagePath(change.lockPath));
|
||||
}
|
||||
|
||||
function summarizeLockfileChange(changes) {
|
||||
const nodeModuleChanges = changes.filter((change) => change.lockPath.includes("node_modules/"));
|
||||
const summary = [];
|
||||
for (const { lockPath, oldEntry, newEntry } of nodeModuleChanges) {
|
||||
if (!oldEntry && newEntry) {
|
||||
summary.push(`added ${packageLabel(lockPath, newEntry)}`);
|
||||
} else if (oldEntry && !newEntry) {
|
||||
summary.push(`removed ${packageLabel(lockPath, oldEntry)}`);
|
||||
} else if (oldEntry?.version !== newEntry?.version) {
|
||||
summary.push(
|
||||
`changed ${packageNameFromLockPath(lockPath)} ${oldEntry?.version ?? "<none>"} -> ${newEntry?.version ?? "<none>"}`,
|
||||
);
|
||||
} else {
|
||||
summary.push(`changed ${packageLabel(lockPath, newEntry)}`);
|
||||
}
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
const stagedFiles = git(["diff", "--cached", "--name-only"])
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
@@ -68,6 +88,12 @@ if (allowed) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const changes = getLockfilePackageChanges();
|
||||
if (changes && hasOnlyWorkspacePackageChanges(changes)) {
|
||||
console.error("package-lock.json only updates workspace package metadata; allowing commit.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.error("package-lock.json is staged.");
|
||||
console.error("");
|
||||
console.error("Review lockfile changes before committing:");
|
||||
@@ -76,15 +102,15 @@ console.error(" - confirm npm age gates were active for resolution");
|
||||
console.error(" - review any new lifecycle scripts in the dependency tree");
|
||||
console.error(" - regenerate/check coding-agent shrinkwrap if release deps changed");
|
||||
|
||||
const changes = summarizeLockfileChange();
|
||||
if (changes.length > 0) {
|
||||
const summary = changes ? summarizeLockfileChange(changes) : [];
|
||||
if (summary.length > 0) {
|
||||
console.error("");
|
||||
console.error("Detected package version changes:");
|
||||
for (const change of changes.slice(0, 40)) {
|
||||
for (const change of summary.slice(0, 40)) {
|
||||
console.error(` - ${change}`);
|
||||
}
|
||||
if (changes.length > 40) {
|
||||
console.error(` ... ${changes.length - 40} more`);
|
||||
if (summary.length > 40) {
|
||||
console.error(` ... ${summary.length - 40} more`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ const shrinkwrapPath = join(codingAgentDir, "npm-shrinkwrap.json");
|
||||
const internalPackagePrefix = "@earendil-works/pi-";
|
||||
const allowedInstallScriptPackages = new Map([
|
||||
["@google/genai@1.52.0", "preinstall is a no-op in the published package"],
|
||||
["protobufjs@7.5.9", "postinstall only warns about protobufjs version scheme mismatches"],
|
||||
["protobufjs@7.6.4", "postinstall only warns about protobufjs version scheme mismatches"],
|
||||
]);
|
||||
|
||||
const args = new Set(process.argv.slice(2));
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
||||
import { isAbsolute, join, relative, resolve } from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const packages = [
|
||||
@@ -131,48 +131,25 @@ function currentBinaryPlatform() {
|
||||
throw new Error(`Unsupported binary platform: ${process.platform} ${process.arch}`);
|
||||
}
|
||||
|
||||
function copyBinaryAssets(targetDirectory) {
|
||||
const codingAgentDirectory = join(repoRoot, "packages", "coding-agent");
|
||||
const distDirectory = join(codingAgentDirectory, "dist");
|
||||
cpSync(join(codingAgentDirectory, "package.json"), join(targetDirectory, "package.json"));
|
||||
cpSync(join(codingAgentDirectory, "README.md"), join(targetDirectory, "README.md"));
|
||||
cpSync(join(codingAgentDirectory, "CHANGELOG.md"), join(targetDirectory, "CHANGELOG.md"));
|
||||
cpSync(join(repoRoot, "node_modules", "@silvia-odwyer", "photon-node", "photon_rs_bg.wasm"), join(targetDirectory, "photon_rs_bg.wasm"));
|
||||
cpSync(join(distDirectory, "modes", "interactive", "theme"), join(targetDirectory, "theme"), { recursive: true });
|
||||
cpSync(join(distDirectory, "modes", "interactive", "assets"), join(targetDirectory, "assets"), { recursive: true });
|
||||
cpSync(join(distDirectory, "core", "export-html"), join(targetDirectory, "export-html"), { recursive: true });
|
||||
cpSync(join(codingAgentDirectory, "docs"), join(targetDirectory, "docs"), { recursive: true });
|
||||
cpSync(join(codingAgentDirectory, "examples"), join(targetDirectory, "examples"), { recursive: true });
|
||||
}
|
||||
|
||||
function copyWindowsConsoleModeHelper(targetDirectory, platform) {
|
||||
if (!platform.startsWith("windows-")) return;
|
||||
const win32Arch = platform === "windows-arm64" ? "win32-arm64" : "win32-x64";
|
||||
const relativeNativePath = join("native", "win32", "prebuilds", win32Arch);
|
||||
mkdirSync(join(targetDirectory, relativeNativePath), { recursive: true });
|
||||
cpSync(
|
||||
join(repoRoot, "packages", "tui", "native", "win32", "prebuilds", win32Arch, "win32-console-mode.node"),
|
||||
join(targetDirectory, relativeNativePath, "win32-console-mode.node"),
|
||||
);
|
||||
}
|
||||
|
||||
function buildBunBinaryRelease(targetDirectory, archiveDirectory) {
|
||||
if (!commandExists("bun")) {
|
||||
throw new Error("Bun is required for the local binary release build.");
|
||||
}
|
||||
const platform = currentBinaryPlatform();
|
||||
mkdirSync(targetDirectory, { recursive: true });
|
||||
const executableName = platform.startsWith("windows-") ? "pi.exe" : "pi";
|
||||
const executablePath = join(targetDirectory, executableName);
|
||||
const target = `bun-${platform}`;
|
||||
run("bun", ["build", "--compile", `--target=${target}`, join(repoRoot, "packages", "coding-agent", "dist", "bun", "cli.js"), "--outfile", executablePath]);
|
||||
copyBinaryAssets(targetDirectory);
|
||||
copyWindowsConsoleModeHelper(targetDirectory, platform);
|
||||
if (platform.startsWith("windows-")) {
|
||||
run("powershell", ["-NoProfile", "-Command", `Compress-Archive -Path '${join(targetDirectory, "*").replaceAll("'", "''")}' -DestinationPath '${join(archiveDirectory, `pi-${platform}.zip`).replaceAll("'", "''")}' -Force`]);
|
||||
} else {
|
||||
run("tar", ["-czf", join(archiveDirectory, `pi-${platform}.tar.gz`), "-C", targetDirectory, "."]);
|
||||
}
|
||||
const binaryBuildDirectory = join(archiveDirectory, "binary-build");
|
||||
run("./scripts/build-binaries.sh", [
|
||||
"--skip-install",
|
||||
"--skip-deps",
|
||||
"--skip-build",
|
||||
"--platform",
|
||||
platform,
|
||||
"--out",
|
||||
binaryBuildDirectory,
|
||||
]);
|
||||
rmSync(targetDirectory, { force: true, recursive: true });
|
||||
cpSync(join(binaryBuildDirectory, platform), targetDirectory, { recursive: true });
|
||||
const archiveName = platform.startsWith("windows-") ? `pi-${platform}.zip` : `pi-${platform}.tar.gz`;
|
||||
cpSync(join(binaryBuildDirectory, archiveName), join(archiveDirectory, archiveName));
|
||||
return platform;
|
||||
}
|
||||
|
||||
|
||||
115
scripts/publish.mjs
Normal file
115
scripts/publish.mjs
Normal file
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const packages = [
|
||||
{ directory: "packages/ai", name: "@earendil-works/pi-ai" },
|
||||
{ directory: "packages/agent", name: "@earendil-works/pi-agent-core" },
|
||||
{ directory: "packages/tui", name: "@earendil-works/pi-tui" },
|
||||
{ directory: "packages/coding-agent", name: "@earendil-works/pi-coding-agent" },
|
||||
];
|
||||
|
||||
const dryRun = process.argv.includes("--dry-run");
|
||||
const unknownArgs = process.argv.slice(2).filter((arg) => arg !== "--dry-run");
|
||||
|
||||
if (unknownArgs.length > 0) {
|
||||
console.error(`Usage: node scripts/publish.mjs [--dry-run]`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function commandForPlatform(command) {
|
||||
return process.platform === "win32" ? `${command}.cmd` : command;
|
||||
}
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
console.log(`$ ${[command, ...args].join(" ")}`);
|
||||
const result = spawnSync(commandForPlatform(command), args, {
|
||||
cwd: options.cwd,
|
||||
encoding: "utf8",
|
||||
stdio: options.capture ? ["inherit", "pipe", "pipe"] : "inherit",
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
||||
throw new Error(output ? `Command failed: ${command} ${args.join(" ")}\n${output}` : `Command failed: ${command} ${args.join(" ")}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function readPackageJson(directory) {
|
||||
return JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
|
||||
}
|
||||
|
||||
function assertBuildOutputExists(directory) {
|
||||
if (!existsSync(join(directory, "dist"))) {
|
||||
throw new Error(`${directory}/dist does not exist. Run npm run build before publishing.`);
|
||||
}
|
||||
}
|
||||
|
||||
function validatePack(directory) {
|
||||
const result = run("npm", ["pack", "--dry-run", "--ignore-scripts", "--json"], { capture: true, cwd: directory });
|
||||
const packed = JSON.parse(result.stdout)[0];
|
||||
console.log(` ${packed.filename}: ${packed.files.length} files, ${packed.size} bytes packed, ${packed.unpackedSize} bytes unpacked`);
|
||||
}
|
||||
|
||||
function isPublished(name, version) {
|
||||
const result = spawnSync(commandForPlatform("npm"), ["view", `${name}@${version}`, "version", "--json"], {
|
||||
encoding: "utf8",
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
if (result.status === 0 && result.stdout.trim()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
||||
if (result.status !== 0 && (output.includes("E404") || output.includes("404 Not Found"))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new Error(output ? `Failed to query ${name}@${version}\n${output}` : `Failed to query ${name}@${version}`);
|
||||
}
|
||||
|
||||
const packageVersions = new Map();
|
||||
for (const pkg of packages) {
|
||||
const packageJson = readPackageJson(pkg.directory);
|
||||
if (packageJson.name !== pkg.name) {
|
||||
throw new Error(`${pkg.directory}/package.json has name ${packageJson.name}, expected ${pkg.name}`);
|
||||
}
|
||||
packageVersions.set(pkg.name, packageJson.version);
|
||||
}
|
||||
|
||||
const versions = [...new Set(packageVersions.values())];
|
||||
if (versions.length !== 1) {
|
||||
throw new Error(`Publish packages are not lockstep versioned: ${versions.join(", ")}`);
|
||||
}
|
||||
|
||||
console.log(`Publishing pi packages at ${versions[0]}${dryRun ? " (dry run)" : ""}\n`);
|
||||
|
||||
for (const pkg of packages) {
|
||||
const version = packageVersions.get(pkg.name);
|
||||
assertBuildOutputExists(pkg.directory);
|
||||
const published = isPublished(pkg.name, version);
|
||||
|
||||
if (dryRun) {
|
||||
if (published) {
|
||||
console.log(`${pkg.name}@${version} is already published; validating package contents only.`);
|
||||
} else {
|
||||
console.log(`${pkg.name}@${version} is not published; validating package contents before publish.`);
|
||||
}
|
||||
validatePack(pkg.directory);
|
||||
console.log();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (published) {
|
||||
console.log(`Skipping ${pkg.name}@${version}: already published\n`);
|
||||
continue;
|
||||
}
|
||||
|
||||
run("npm", ["publish", "--access", "public", "--provenance", "--ignore-scripts"], { cwd: pkg.directory });
|
||||
console.log();
|
||||
}
|
||||
364
scripts/release-notes.mjs
Normal file
364
scripts/release-notes.mjs
Normal file
@@ -0,0 +1,364 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const DEFAULT_REPO = "earendil-works/pi";
|
||||
const DEFAULT_BASE_PATH = "packages/coding-agent";
|
||||
const DEFAULT_CHANGELOG = "packages/coding-agent/CHANGELOG.md";
|
||||
const DEFAULT_FIX_SINCE_TAG = "v0.74.0";
|
||||
const LEGACY_REPO_RE = /^https:\/\/github\.com\/(?:badlogic|earendil-works)\/pi-mono(?=\/|$)/;
|
||||
const URL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;
|
||||
const INLINE_MARKDOWN_LINK_RE = /(!?\[[^\]\n]+\]\()([^\s)]+)((?:\s+[^)]*)?\))/g;
|
||||
|
||||
function printUsage() {
|
||||
console.log(`Usage: node scripts/release-notes.mjs <command> [options]
|
||||
|
||||
Commands:
|
||||
extract Extract release notes from the coding-agent changelog
|
||||
fix-github-releases Rewrite existing GitHub release note links in place
|
||||
|
||||
extract options:
|
||||
--version <x.y.z> Version to extract
|
||||
--tag <vX.Y.Z> Release tag used for repository links (defaults to v<version>)
|
||||
--changelog <path> Changelog path (default: ${DEFAULT_CHANGELOG})
|
||||
--out <path> Output file (default: stdout)
|
||||
--repo <owner/repo> GitHub repository for generated links (default: ${DEFAULT_REPO})
|
||||
--base-path <path> Base path for relative changelog links (default: ${DEFAULT_BASE_PATH})
|
||||
|
||||
fix-github-releases options:
|
||||
--repo <owner/repo> GitHub repository to patch (default: ${DEFAULT_REPO})
|
||||
--tag <vX.Y.Z> Patch only one release tag
|
||||
--since-tag <vX.Y.Z> Oldest release tag to patch (default: ${DEFAULT_FIX_SINCE_TAG})
|
||||
--base-path <path> Base path for relative changelog links (default: ${DEFAULT_BASE_PATH})
|
||||
--dry-run Print releases that would change without updating GitHub
|
||||
`);
|
||||
}
|
||||
|
||||
function commandForPlatform(command) {
|
||||
return process.platform === "win32" ? `${command}.cmd` : command;
|
||||
}
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
const result = spawnSync(commandForPlatform(command), args, {
|
||||
cwd: options.cwd,
|
||||
encoding: "utf8",
|
||||
maxBuffer: options.maxBuffer ?? 20 * 1024 * 1024,
|
||||
stdio: options.capture ? ["inherit", "pipe", "pipe"] : "inherit",
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
||||
throw new Error(output ? `Command failed: ${command} ${args.join(" ")}\n${output}` : `Command failed: ${command} ${args.join(" ")}`);
|
||||
}
|
||||
|
||||
return result.stdout ?? "";
|
||||
}
|
||||
|
||||
function parseOptions(args) {
|
||||
const options = {
|
||||
basePath: DEFAULT_BASE_PATH,
|
||||
changelog: DEFAULT_CHANGELOG,
|
||||
dryRun: false,
|
||||
out: undefined,
|
||||
repo: DEFAULT_REPO,
|
||||
sinceTag: DEFAULT_FIX_SINCE_TAG,
|
||||
tag: undefined,
|
||||
version: undefined,
|
||||
};
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (arg === "--help") {
|
||||
printUsage();
|
||||
process.exit(0);
|
||||
}
|
||||
if (arg === "--dry-run") {
|
||||
options.dryRun = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const optionNames = new Set(["--base-path", "--changelog", "--out", "--repo", "--since-tag", "--tag", "--version"]);
|
||||
if (!optionNames.has(arg)) {
|
||||
throw new Error(`Unknown option: ${arg}`);
|
||||
}
|
||||
|
||||
const value = args[++i];
|
||||
if (!value) {
|
||||
throw new Error(`${arg} requires a value`);
|
||||
}
|
||||
|
||||
if (arg === "--base-path") options.basePath = value;
|
||||
if (arg === "--changelog") options.changelog = value;
|
||||
if (arg === "--out") options.out = value;
|
||||
if (arg === "--repo") options.repo = value;
|
||||
if (arg === "--since-tag") options.sinceTag = value;
|
||||
if (arg === "--tag") options.tag = value;
|
||||
if (arg === "--version") options.version = value;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function normalizeTag(tagOrVersion) {
|
||||
if (!tagOrVersion) {
|
||||
return undefined;
|
||||
}
|
||||
return tagOrVersion.startsWith("v") ? tagOrVersion : `v${tagOrVersion}`;
|
||||
}
|
||||
|
||||
function versionFromTag(tag) {
|
||||
return tag.startsWith("v") ? tag.slice(1) : tag;
|
||||
}
|
||||
|
||||
function compareVersions(a, b) {
|
||||
const aParts = versionFromTag(a).split(".").map(Number);
|
||||
const bParts = versionFromTag(b).split(".").map(Number);
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const diff = (aParts[i] || 0) - (bParts[i] || 0);
|
||||
if (diff !== 0) {
|
||||
return diff;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function extractChangelogSection(changelog, version) {
|
||||
const headingRe = new RegExp(`^## \\[${escapeRegExp(version)}\\](?:\\s+-\\s+\\d{4}-\\d{2}-\\d{2})?\\s*$`, "m");
|
||||
const heading = headingRe.exec(changelog);
|
||||
|
||||
if (!heading) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const sectionStart = heading.index + heading[0].length;
|
||||
const rest = changelog.slice(sectionStart);
|
||||
const nextHeading = rest.search(/^## \[/m);
|
||||
const section = nextHeading === -1 ? rest : rest.slice(0, nextHeading);
|
||||
return section.trim();
|
||||
}
|
||||
|
||||
function splitLocalTarget(target) {
|
||||
const hashIndex = target.indexOf("#");
|
||||
const beforeHash = hashIndex === -1 ? target : target.slice(0, hashIndex);
|
||||
const fragment = hashIndex === -1 ? "" : target.slice(hashIndex);
|
||||
const queryIndex = beforeHash.indexOf("?");
|
||||
|
||||
if (queryIndex === -1) {
|
||||
return { fragment, pathPart: beforeHash, query: "" };
|
||||
}
|
||||
|
||||
return {
|
||||
fragment,
|
||||
pathPart: beforeHash.slice(0, queryIndex),
|
||||
query: beforeHash.slice(queryIndex),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePathPart(value) {
|
||||
return value.replaceAll("\\", "/");
|
||||
}
|
||||
|
||||
function normalizeBasePath(basePath) {
|
||||
const normalized = path.posix.normalize(normalizePathPart(basePath)).replace(/\/+$/, "");
|
||||
return normalized === "." ? "" : normalized;
|
||||
}
|
||||
|
||||
function resolveRepositoryPath(targetPath, basePath) {
|
||||
const normalizedTarget = normalizePathPart(targetPath);
|
||||
const joined = normalizedTarget.startsWith("/")
|
||||
? path.posix.normalize(normalizedTarget.replace(/^\/+/, ""))
|
||||
: path.posix.normalize(path.posix.join(normalizeBasePath(basePath), normalizedTarget));
|
||||
|
||||
if (joined === "." || joined.startsWith("../") || joined === "..") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return joined;
|
||||
}
|
||||
|
||||
function isDirectoryTarget(originalPath, repositoryPath) {
|
||||
if (originalPath.endsWith("/")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const basename = path.posix.basename(repositoryPath);
|
||||
return !basename.includes(".");
|
||||
}
|
||||
|
||||
function normalizeLinkTarget(target, options) {
|
||||
let canonicalTarget = target.replace(LEGACY_REPO_RE, `https://github.com/${options.repo}`);
|
||||
const repoUrl = `https://github.com/${options.repo}`;
|
||||
|
||||
for (const route of ["blob", "tree"]) {
|
||||
for (const branch of ["main", "master"]) {
|
||||
const floatingRefPrefix = `${repoUrl}/${route}/${branch}/`;
|
||||
if (canonicalTarget.startsWith(floatingRefPrefix)) {
|
||||
canonicalTarget = `${repoUrl}/${route}/${options.tag}/${canonicalTarget.slice(floatingRefPrefix.length)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (canonicalTarget.startsWith("#") || canonicalTarget.startsWith("//") || URL_SCHEME_RE.test(canonicalTarget)) {
|
||||
return canonicalTarget;
|
||||
}
|
||||
|
||||
const { fragment, pathPart, query } = splitLocalTarget(canonicalTarget);
|
||||
if (!pathPart) {
|
||||
return canonicalTarget;
|
||||
}
|
||||
|
||||
const repositoryPath = resolveRepositoryPath(pathPart, options.basePath);
|
||||
if (!repositoryPath) {
|
||||
return canonicalTarget;
|
||||
}
|
||||
|
||||
const route = isDirectoryTarget(pathPart, repositoryPath) ? "tree" : "blob";
|
||||
return `https://github.com/${options.repo}/${route}/${options.tag}/${encodeURI(repositoryPath)}${query}${fragment}`;
|
||||
}
|
||||
|
||||
function normalizeReleaseNoteLinks(markdown, options) {
|
||||
const changes = [];
|
||||
const normalized = markdown.replace(INLINE_MARKDOWN_LINK_RE, (match, prefix, target, suffix) => {
|
||||
const normalizedTarget = normalizeLinkTarget(target, options);
|
||||
if (normalizedTarget !== target) {
|
||||
changes.push({ from: target, to: normalizedTarget });
|
||||
}
|
||||
return `${prefix}${normalizedTarget}${suffix}`;
|
||||
});
|
||||
|
||||
return { changes, markdown: normalized };
|
||||
}
|
||||
|
||||
function writeOutput(content, outPath) {
|
||||
if (outPath) {
|
||||
writeFileSync(outPath, content);
|
||||
return;
|
||||
}
|
||||
|
||||
process.stdout.write(content);
|
||||
}
|
||||
|
||||
function extractReleaseNotes(options) {
|
||||
const version = options.version ?? (options.tag ? versionFromTag(options.tag) : undefined);
|
||||
if (!version) {
|
||||
throw new Error("extract requires --version or --tag");
|
||||
}
|
||||
|
||||
if (!existsSync(options.changelog)) {
|
||||
throw new Error(`Changelog does not exist: ${options.changelog}`);
|
||||
}
|
||||
|
||||
const tag = normalizeTag(options.tag ?? version);
|
||||
const changelog = readFileSync(options.changelog, "utf8");
|
||||
const section = extractChangelogSection(changelog, version);
|
||||
const rawNotes = section ? `${section}\n` : `Release ${version}\n`;
|
||||
const { markdown } = normalizeReleaseNoteLinks(rawNotes, { basePath: options.basePath, repo: options.repo, tag });
|
||||
writeOutput(markdown, options.out);
|
||||
}
|
||||
|
||||
function listGithubReleases(repo) {
|
||||
const output = run("gh", ["api", `repos/${repo}/releases`, "--paginate", "--jq", ".[] | {id, tag_name, body} | @json"], {
|
||||
capture: true,
|
||||
});
|
||||
return output
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((line) => JSON.parse(line));
|
||||
}
|
||||
|
||||
function uniqueChanges(changes) {
|
||||
const seen = new Set();
|
||||
const unique = [];
|
||||
for (const change of changes) {
|
||||
const key = `${change.from}\n${change.to}`;
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
unique.push(change);
|
||||
}
|
||||
return unique;
|
||||
}
|
||||
|
||||
function updateGithubRelease(repo, tag, body) {
|
||||
const tempDir = mkdtempSync(path.join(tmpdir(), "pi-release-notes-"));
|
||||
try {
|
||||
const notesPath = path.join(tempDir, "notes.md");
|
||||
writeFileSync(notesPath, body);
|
||||
run("gh", ["release", "edit", tag, "--repo", repo, "--notes-file", notesPath], { capture: true });
|
||||
} finally {
|
||||
rmSync(tempDir, { force: true, recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function fixGithubReleases(options) {
|
||||
const tagFilter = normalizeTag(options.tag);
|
||||
const sinceTag = normalizeTag(options.sinceTag);
|
||||
const matchingReleases = listGithubReleases(options.repo).filter((release) => !tagFilter || release.tag_name === tagFilter);
|
||||
|
||||
if (tagFilter && matchingReleases.length === 0) {
|
||||
throw new Error(`Release not found: ${tagFilter}`);
|
||||
}
|
||||
|
||||
const releases = matchingReleases.filter((release) => compareVersions(release.tag_name, sinceTag) >= 0);
|
||||
if (tagFilter && releases.length === 0) {
|
||||
console.log(`Skipping ${tagFilter}: older than ${sinceTag}.`);
|
||||
console.log(`${options.dryRun ? "Would update" : "Updated"} 0 releases.`);
|
||||
return;
|
||||
}
|
||||
|
||||
let changedCount = 0;
|
||||
for (const release of releases) {
|
||||
const tag = release.tag_name;
|
||||
const body = release.body ?? "";
|
||||
const result = normalizeReleaseNoteLinks(body, { basePath: options.basePath, repo: options.repo, tag });
|
||||
if (result.markdown === body) {
|
||||
continue;
|
||||
}
|
||||
|
||||
changedCount++;
|
||||
const unique = uniqueChanges(result.changes);
|
||||
console.log(`${options.dryRun ? "Would update" : "Updating"} ${tag} (${unique.length} link${unique.length === 1 ? "" : "s"})`);
|
||||
for (const change of unique) {
|
||||
console.log(` ${change.from}`);
|
||||
console.log(` -> ${change.to}`);
|
||||
}
|
||||
|
||||
if (!options.dryRun) {
|
||||
updateGithubRelease(options.repo, tag, result.markdown);
|
||||
}
|
||||
}
|
||||
|
||||
const prefix = options.dryRun ? "Would update" : "Updated";
|
||||
console.log(`${prefix} ${changedCount} release${changedCount === 1 ? "" : "s"}.`);
|
||||
}
|
||||
|
||||
try {
|
||||
const [command, ...args] = process.argv.slice(2);
|
||||
if (!command || command === "--help") {
|
||||
printUsage();
|
||||
process.exit(command ? 0 : 1);
|
||||
}
|
||||
|
||||
const options = parseOptions(args);
|
||||
if (command === "extract") {
|
||||
extractReleaseNotes(options);
|
||||
} else if (command === "fix-github-releases") {
|
||||
fixGithubReleases(options);
|
||||
} else {
|
||||
throw new Error(`Unknown command: ${command}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : error);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -10,11 +10,12 @@
|
||||
* 1. Check for uncommitted changes
|
||||
* 2. Bump version via npm run version:xxx or set an explicit version
|
||||
* 3. Update CHANGELOG.md files: [Unreleased] -> [version] - date
|
||||
* 4. Generate the coding-agent npm-shrinkwrap.json
|
||||
* 5. Commit and tag
|
||||
* 6. Publish to npm
|
||||
* 4. Regenerate release artifacts
|
||||
* 5. Run checks
|
||||
* 6. Commit and tag the release
|
||||
* 7. Add new [Unreleased] section to changelogs
|
||||
* 8. Commit
|
||||
* 8. Commit next-cycle changelog updates
|
||||
* 9. Push main and the tag to trigger CI publishing
|
||||
*/
|
||||
|
||||
import { execSync } from "child_process";
|
||||
@@ -91,7 +92,7 @@ function bumpOrSetVersion(target) {
|
||||
}
|
||||
|
||||
console.log(`Setting explicit version (${target})...`);
|
||||
run(`npm version ${target} -ws --no-git-tag-version && node scripts/sync-versions.js && npm install --package-lock-only`);
|
||||
run(`npm version ${target} -ws --no-git-tag-version && node scripts/sync-versions.js && npm install --package-lock-only --ignore-scripts`);
|
||||
return getVersion();
|
||||
}
|
||||
|
||||
@@ -163,23 +164,25 @@ console.log("Updating CHANGELOG.md files...");
|
||||
updateChangelogsForRelease(version);
|
||||
console.log();
|
||||
|
||||
// 4. Generate publish shrinkwrap
|
||||
console.log("Generating coding-agent shrinkwrap...");
|
||||
// 4. Regenerate release artifacts
|
||||
console.log("Regenerating release artifacts...");
|
||||
run("npm --prefix packages/ai run generate-models");
|
||||
run("npm --prefix packages/ai run generate-image-models");
|
||||
run("npm run shrinkwrap:coding-agent");
|
||||
console.log();
|
||||
|
||||
// 5. Commit and tag
|
||||
// 5. Run checks
|
||||
console.log("Running checks...");
|
||||
run("npm run check");
|
||||
console.log();
|
||||
|
||||
// 6. Commit and tag
|
||||
console.log("Committing and tagging...");
|
||||
stageChangedFiles();
|
||||
run(`git commit -m "Release v${version}"`);
|
||||
run(`git tag v${version}`);
|
||||
console.log();
|
||||
|
||||
// 6. Publish
|
||||
console.log("Publishing to npm...");
|
||||
run("npm run publish");
|
||||
console.log();
|
||||
|
||||
// 7. Add new [Unreleased] sections
|
||||
console.log("Adding [Unreleased] sections for next cycle...");
|
||||
addUnreleasedSection();
|
||||
@@ -197,4 +200,4 @@ run("git push origin main");
|
||||
run(`git push origin v${version}`);
|
||||
console.log();
|
||||
|
||||
console.log(`=== Released v${version} ===`);
|
||||
console.log(`=== Prepared release v${version}; CI publishing starts after the tag push ===`);
|
||||
|
||||
55
scripts/repro-5893-wsl-bash.mjs
Normal file
55
scripts/repro-5893-wsl-bash.mjs
Normal file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env node
|
||||
import { existsSync } from "node:fs";
|
||||
import { createBashTool } from "../packages/coding-agent/src/core/tools/bash.ts";
|
||||
|
||||
const shellPath = "C:\\Windows\\System32\\bash.exe";
|
||||
const nameExpansion = "$" + "{name}";
|
||||
const countExpansion = "$" + "{count}";
|
||||
const iExpansion = "$" + "{i}";
|
||||
|
||||
function getTextOutput(result) {
|
||||
return result.content
|
||||
.filter((content) => content.type === "text")
|
||||
.map((content) => content.text ?? "")
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
async function runCase(label, command, expectedOutput) {
|
||||
const tool = createBashTool(process.cwd(), { shellPath });
|
||||
const result = await tool.execute(label, { command });
|
||||
const output = getTextOutput(result).trimEnd();
|
||||
if (output !== expectedOutput) {
|
||||
throw new Error(
|
||||
[
|
||||
`${label} failed`,
|
||||
"Expected:",
|
||||
expectedOutput,
|
||||
"Actual:",
|
||||
output,
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
console.log(output);
|
||||
}
|
||||
|
||||
if (process.platform !== "win32") {
|
||||
throw new Error("This repro must run from Windows PowerShell/CMD, not macOS/Linux or inside WSL.");
|
||||
}
|
||||
|
||||
if (!existsSync(shellPath)) {
|
||||
throw new Error(`WSL bash launcher not found at ${shellPath}. Install/enable WSL first.`);
|
||||
}
|
||||
|
||||
await runCase(
|
||||
"issue-5893-simple-variable",
|
||||
`name='World'; echo "Hello, ${nameExpansion}!"`,
|
||||
"Hello, World!",
|
||||
);
|
||||
|
||||
await runCase(
|
||||
"issue-5893-loop-variable",
|
||||
`count=3; for i in $(seq 1 ${countExpansion}); do echo "Iteration ${iExpansion} of ${countExpansion}"; done`,
|
||||
"Iteration 1 of 3\nIteration 2 of 3\nIteration 3 of 3",
|
||||
);
|
||||
|
||||
console.log("issue #5893 WSL bash repro passed");
|
||||
@@ -3,7 +3,7 @@
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { homedir, tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { spawn } from "node:child_process";
|
||||
import { openBrowser } from "../packages/coding-agent/src/utils/open-browser.ts";
|
||||
|
||||
interface TextContent { type: "text"; text: string }
|
||||
interface ImageContent { type: "image"; data: string; mimeType?: string }
|
||||
@@ -229,4 +229,4 @@ const html = `<!doctype html>
|
||||
mkdirSync(resolve(output, ".."), { recursive: true });
|
||||
writeFileSync(output, html);
|
||||
console.log(`Wrote ${output}`);
|
||||
spawn(process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open", process.platform === "win32" ? ["/c", "start", output] : [output], { detached: true, stdio: "ignore" }).unref();
|
||||
openBrowser(output);
|
||||
|
||||
Reference in New Issue
Block a user