chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
export interface BuildOptions {
|
||||
single: boolean;
|
||||
skipInstall: boolean;
|
||||
skipSdkBuild: boolean;
|
||||
installNativeVariants: boolean;
|
||||
}
|
||||
|
||||
export function parseBuildOptions(args: readonly string[]): BuildOptions {
|
||||
return {
|
||||
single: args.includes("--single"),
|
||||
skipInstall: args.includes("--skip-install"),
|
||||
skipSdkBuild: args.includes("--skip-sdk-build"),
|
||||
installNativeVariants: args.includes("--install-native-variants"),
|
||||
};
|
||||
}
|
||||
|
||||
export function shouldInstallNativeVariants(input: {
|
||||
options: BuildOptions;
|
||||
opentuiVersion: string | undefined;
|
||||
}): boolean {
|
||||
return Boolean(
|
||||
input.opentuiVersion &&
|
||||
input.options.installNativeVariants &&
|
||||
!input.options.skipInstall,
|
||||
);
|
||||
}
|
||||
|
||||
export function validateBuildOptions(input: {
|
||||
options: BuildOptions;
|
||||
opentuiVersion: string | undefined;
|
||||
targetCount: number;
|
||||
}): string | undefined {
|
||||
if (input.targetCount === 0) {
|
||||
return "No matching targets for this platform.";
|
||||
}
|
||||
if (
|
||||
input.opentuiVersion &&
|
||||
!input.options.single &&
|
||||
!input.options.skipInstall &&
|
||||
!input.options.installNativeVariants
|
||||
) {
|
||||
return [
|
||||
"Cross-platform OpenTUI builds require native package variants.",
|
||||
"Pass --install-native-variants to allow the build script to run bun install for all OpenTUI native packages.",
|
||||
"Pass --skip-install only when those packages are already installed.",
|
||||
].join("\n");
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import {
|
||||
cpSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
realpathSync,
|
||||
statSync,
|
||||
} from "node:fs";
|
||||
import { join, relative, resolve } from "node:path";
|
||||
import { $ } from "bun";
|
||||
import {
|
||||
parseBuildOptions,
|
||||
shouldInstallNativeVariants,
|
||||
validateBuildOptions,
|
||||
} from "./build-options";
|
||||
|
||||
const cliDir = resolve(import.meta.dir, "..");
|
||||
const rootDir = resolve(cliDir, "../..");
|
||||
process.chdir(cliDir);
|
||||
|
||||
// Telemetry / OTEL environment variables that should be baked into the
|
||||
// compiled binary at build time. Mirrors the list of secrets injected by the
|
||||
// `cli-publish` GitHub Actions workflow. These are inlined via Bun's `define`
|
||||
// so the CLI ships with the production telemetry configuration without
|
||||
// requiring the end user to set any env vars.
|
||||
const BUILD_TIME_INLINED_ENV_VARS = [
|
||||
"TELEMETRY_SERVICE_API_KEY",
|
||||
"ERROR_SERVICE_API_KEY",
|
||||
"OTEL_TELEMETRY_ENABLED",
|
||||
"OTEL_LOGS_EXPORTER",
|
||||
"OTEL_METRICS_EXPORTER",
|
||||
"OTEL_EXPORTER_OTLP_PROTOCOL",
|
||||
"OTEL_EXPORTER_OTLP_ENDPOINT",
|
||||
"OTEL_EXPORTER_OTLP_HEADERS",
|
||||
] as const;
|
||||
|
||||
function buildInlinedEnvDefines(): Record<string, string> {
|
||||
const defines: Record<string, string> = {};
|
||||
for (const name of BUILD_TIME_INLINED_ENV_VARS) {
|
||||
defines[`process.env.${name}`] = JSON.stringify(process.env[name] ?? "");
|
||||
}
|
||||
return defines;
|
||||
}
|
||||
|
||||
const pkg = JSON.parse(readFileSync(join(cliDir, "package.json"), "utf-8"));
|
||||
const version: string = pkg.version;
|
||||
const repository: unknown = pkg.repository;
|
||||
|
||||
console.log(`Building @cline/cli v${version}`);
|
||||
|
||||
const buildOptions = parseBuildOptions(process.argv.slice(2));
|
||||
|
||||
const allTargets: {
|
||||
os: string;
|
||||
arch: "arm64" | "x64";
|
||||
}[] = [
|
||||
{ os: "linux", arch: "arm64" },
|
||||
{ os: "linux", arch: "x64" },
|
||||
{ os: "darwin", arch: "arm64" },
|
||||
{ os: "darwin", arch: "x64" },
|
||||
{ os: "win32", arch: "x64" },
|
||||
{ os: "win32", arch: "arm64" },
|
||||
];
|
||||
|
||||
const targets = buildOptions.single
|
||||
? allTargets.filter(
|
||||
(item) => item.os === process.platform && item.arch === process.arch,
|
||||
)
|
||||
: allTargets;
|
||||
|
||||
const opentuiVersion = pkg.dependencies["@opentui/core"];
|
||||
const optionsError = validateBuildOptions({
|
||||
options: buildOptions,
|
||||
opentuiVersion,
|
||||
targetCount: targets.length,
|
||||
});
|
||||
if (optionsError) {
|
||||
console.error(optionsError);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await $`rm -rf dist`;
|
||||
|
||||
// Pre-install all platform variants of native packages so cross-compilation
|
||||
// can resolve them. Without this, Bun only has the host platform's native
|
||||
// binary and cross-compiled builds fail to resolve @opentui/core's FFI layer.
|
||||
if (shouldInstallNativeVariants({ options: buildOptions, opentuiVersion })) {
|
||||
console.log(
|
||||
`Installing all platform variants of @opentui/core@${opentuiVersion}...`,
|
||||
);
|
||||
await $`bun install --os="*" --cpu="*" @opentui/core@${opentuiVersion}`;
|
||||
}
|
||||
|
||||
// Build the SDK first (the CLI bundles workspace packages)
|
||||
if (!buildOptions.skipSdkBuild) {
|
||||
console.log("Building SDK packages...");
|
||||
await $`bun run build:sdk`.cwd(rootDir);
|
||||
|
||||
console.log("Building CLI bundle...");
|
||||
await $`bun -F @cline/cli build`.cwd(rootDir);
|
||||
}
|
||||
|
||||
const hubWebviewSource = join(cliDir, "../cline-hub/src/webview");
|
||||
const hubWebviewDist = join(cliDir, "../cline-hub/dist/webview");
|
||||
const hubWebviewIndex = join(hubWebviewDist, "index.html");
|
||||
|
||||
function newestFileMtimeMs(dir: string): number {
|
||||
let newest = 0;
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
if (
|
||||
entry.name === "node_modules" ||
|
||||
entry.name === "dist" ||
|
||||
entry.name === ".turbo"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const path = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
newest = Math.max(newest, newestFileMtimeMs(path));
|
||||
} else if (entry.isFile()) {
|
||||
newest = Math.max(newest, statSync(path).mtimeMs);
|
||||
}
|
||||
}
|
||||
return newest;
|
||||
}
|
||||
|
||||
function shouldBuildHubWebview(): boolean {
|
||||
if (!existsSync(hubWebviewIndex)) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return (
|
||||
newestFileMtimeMs(hubWebviewSource) > statSync(hubWebviewIndex).mtimeMs
|
||||
);
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldBuildHubWebview()) {
|
||||
console.log("Building Cline Hub webview...");
|
||||
await $`bun -F @cline/cline-hub build:webview`.cwd(rootDir);
|
||||
}
|
||||
|
||||
const binaries: Record<string, string> = {};
|
||||
|
||||
function findOpenTuiParserWorker(): string {
|
||||
const localPath = resolve(
|
||||
cliDir,
|
||||
"node_modules/@opentui/core/parser.worker.js",
|
||||
);
|
||||
const rootPath = resolve(
|
||||
rootDir,
|
||||
"node_modules/@opentui/core/parser.worker.js",
|
||||
);
|
||||
const parserWorkerPath = existsSync(localPath) ? localPath : rootPath;
|
||||
return realpathSync(parserWorkerPath);
|
||||
}
|
||||
|
||||
function getBunTarget(
|
||||
item: (typeof allTargets)[number],
|
||||
): Bun.Build.CompileTarget {
|
||||
const targetOs = item.os === "win32" ? "windows" : item.os;
|
||||
return `bun-${targetOs}-${item.arch}` as Bun.Build.CompileTarget;
|
||||
}
|
||||
|
||||
async function buildCompiledBinary(input: {
|
||||
bunTarget: Bun.Build.CompileTarget;
|
||||
dirName: string;
|
||||
outfile: string;
|
||||
}): Promise<void> {
|
||||
const parserWorker = findOpenTuiParserWorker();
|
||||
const targetOs = input.bunTarget.includes("windows") ? "windows" : "posix";
|
||||
const bunfsRoot = targetOs === "windows" ? "B:/~BUN/root/" : "/$bunfs/root/";
|
||||
const parserWorkerPath = relative(rootDir, parserWorker).replaceAll(
|
||||
"\\",
|
||||
"/",
|
||||
);
|
||||
|
||||
// Build to /tmp first so Bun's temp-file rename stays on one filesystem
|
||||
// layer in containerized environments (virtiofs, overlayfs).
|
||||
const entrypoint = join(cliDir, "src/index.ts");
|
||||
const tmpDir = join("/tmp", `cline-build-${input.dirName}`);
|
||||
const tmpOutfile = join(
|
||||
tmpDir,
|
||||
input.outfile.endsWith(".exe") ? "cline.exe" : "cline",
|
||||
);
|
||||
mkdirSync(tmpDir, { recursive: true });
|
||||
|
||||
process.chdir("/tmp");
|
||||
const result = await Bun.build({
|
||||
entrypoints: [entrypoint, parserWorker],
|
||||
splitting: true,
|
||||
compile: {
|
||||
target: input.bunTarget,
|
||||
outfile: tmpOutfile,
|
||||
},
|
||||
minify: true,
|
||||
external: ["@anthropic-ai/vertex-sdk"],
|
||||
define: {
|
||||
OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + parserWorkerPath,
|
||||
// Inline telemetry/OTEL env vars at build time so the compiled
|
||||
// binary ships with production telemetry configuration baked in.
|
||||
...buildInlinedEnvDefines(),
|
||||
},
|
||||
throw: false,
|
||||
});
|
||||
process.chdir(cliDir);
|
||||
|
||||
if (!result.success) {
|
||||
console.error(`Build failed for ${input.dirName}:`);
|
||||
for (const log of result.logs) {
|
||||
console.error(log);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await $`cp ${tmpOutfile} ${input.outfile} && chmod 755 ${input.outfile}`;
|
||||
await $`rm -rf ${tmpDir}`;
|
||||
}
|
||||
|
||||
for (const item of targets) {
|
||||
// npm treats "win32" specially in os field, but for package naming use "windows"
|
||||
const displayOs = item.os === "win32" ? "windows" : item.os;
|
||||
const name = `@cline/cli-${displayOs}-${item.arch}`;
|
||||
const dirName = `cli-${displayOs}-${item.arch}`;
|
||||
const binaryName = item.os === "win32" ? "cline.exe" : "cline";
|
||||
const bunTarget = getBunTarget(item);
|
||||
|
||||
console.log(`\nBuilding ${name} (target: ${bunTarget})...`);
|
||||
const outDir = join(cliDir, `dist/${dirName}/bin`);
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
|
||||
const outfile = join(outDir, binaryName);
|
||||
|
||||
await buildCompiledBinary({ bunTarget, dirName, outfile });
|
||||
|
||||
// Smoke test: only run on current platform
|
||||
if (item.os === process.platform && item.arch === process.arch) {
|
||||
console.log(` Smoke test: ${outfile} --version`);
|
||||
try {
|
||||
const output = await $`${outfile} --version`.text();
|
||||
const actualVersion = output.trim();
|
||||
if (actualVersion !== version) {
|
||||
throw new Error(
|
||||
`Expected --version to print ${version}, got ${actualVersion}`,
|
||||
);
|
||||
}
|
||||
console.log(` Passed: ${actualVersion}`);
|
||||
} catch (e) {
|
||||
console.error(` Smoke test FAILED for ${name}:`, e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy plugin sandbox bootstrap if it exists
|
||||
const bootstrapSrc = join(
|
||||
rootDir,
|
||||
"sdk/packages/core/dist/extensions/plugin-sandbox-bootstrap.js",
|
||||
);
|
||||
if (existsSync(bootstrapSrc)) {
|
||||
const bootstrapDir = join(cliDir, `dist/${dirName}/extensions`);
|
||||
mkdirSync(bootstrapDir, { recursive: true });
|
||||
const content = readFileSync(bootstrapSrc);
|
||||
await Bun.write(join(bootstrapDir, "plugin-sandbox-bootstrap.js"), content);
|
||||
}
|
||||
|
||||
if (existsSync(hubWebviewDist)) {
|
||||
const hubWebviewDest = join(cliDir, `dist/${dirName}/cline-hub/webview`);
|
||||
mkdirSync(join(cliDir, `dist/${dirName}/cline-hub`), {
|
||||
recursive: true,
|
||||
});
|
||||
cpSync(hubWebviewDist, hubWebviewDest, { recursive: true });
|
||||
}
|
||||
|
||||
// Generate platform package.json
|
||||
await Bun.write(
|
||||
join(cliDir, `dist/${dirName}/package.json`),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
name,
|
||||
version,
|
||||
description: `Cline CLI binary for ${displayOs} ${item.arch}`,
|
||||
os: [item.os],
|
||||
cpu: [item.arch],
|
||||
...(repository ? { repository } : {}),
|
||||
bin: {
|
||||
cline: `bin/${binaryName}`,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
|
||||
binaries[name] = version;
|
||||
console.log(` Built ${name}`);
|
||||
}
|
||||
|
||||
console.log(`\nBuild complete. ${Object.keys(binaries).length} targets built.`);
|
||||
console.log("Packages:");
|
||||
for (const [name, ver] of Object.entries(binaries)) {
|
||||
console.log(` ${name}@${ver}`);
|
||||
}
|
||||
|
||||
export { binaries, version };
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
export const DIRECT_PUBLISH_GUARD_MESSAGE = [
|
||||
"Direct packaging or publishing from apps/cli is disabled.",
|
||||
"The source package points its development bin at src/index.ts, while the npm package is generated under dist/cli.",
|
||||
"Run `bun run build:platforms` first, then `bun run publish:npm:dry` to preview the generated npm packages.",
|
||||
"Use `bun run publish:npm` to publish those generated packages.",
|
||||
].join("\n");
|
||||
|
||||
export function shouldAllowDirectPublish(env: NodeJS.ProcessEnv): boolean {
|
||||
return env.CLINE_ALLOW_DIRECT_PUBLISH === "1";
|
||||
}
|
||||
|
||||
if (import.meta.main && !shouldAllowDirectPublish(process.env)) {
|
||||
console.error(DIRECT_PUBLISH_GUARD_MESSAGE);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Post-install script for Cline CLI.
|
||||
//
|
||||
// Creates a hard link (or copy fallback) from the platform-specific binary
|
||||
// to bin/.cline for fast startup on subsequent runs.
|
||||
//
|
||||
// This script must use only Node.js APIs (no Bun) since it runs via
|
||||
// "node script/postinstall.mjs" in the npm lifecycle.
|
||||
|
||||
import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
function main() {
|
||||
if (os.platform() === "win32") {
|
||||
// On Windows, npm creates .cmd shims from the bin field.
|
||||
// The resolver script handles binary lookup at runtime.
|
||||
console.log("Windows detected: skipping binary cache setup");
|
||||
return;
|
||||
}
|
||||
|
||||
const platformMap = {
|
||||
darwin: "darwin",
|
||||
linux: "linux",
|
||||
};
|
||||
const platform = platformMap[os.platform()] || os.platform();
|
||||
const arch = os.arch();
|
||||
const packageName = `@cline/cli-${platform}-${arch}`;
|
||||
const binaryName = "cline";
|
||||
|
||||
let binaryPath;
|
||||
try {
|
||||
const packageJsonPath = require.resolve(`${packageName}/package.json`);
|
||||
const packageDir = path.dirname(packageJsonPath);
|
||||
binaryPath = path.join(packageDir, "bin", binaryName);
|
||||
|
||||
if (!fs.existsSync(binaryPath)) {
|
||||
throw new Error(`Binary not found at ${binaryPath}`);
|
||||
}
|
||||
} catch (_error) {
|
||||
// Platform package not available. The resolver script will find
|
||||
// it at runtime by walking node_modules. This is expected on
|
||||
// platforms we don't ship binaries for.
|
||||
console.log(`Note: ${packageName} not found, skipping binary cache`);
|
||||
return;
|
||||
}
|
||||
|
||||
const binDir =
|
||||
path.basename(__dirname) === "script"
|
||||
? path.join(__dirname, "..", "bin")
|
||||
: path.join(__dirname, "bin");
|
||||
const target = path.join(binDir, ".cline");
|
||||
|
||||
// Ensure bin directory exists
|
||||
if (!fs.existsSync(binDir)) {
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Remove existing cached binary
|
||||
if (fs.existsSync(target)) {
|
||||
fs.unlinkSync(target);
|
||||
}
|
||||
|
||||
// Hard link preferred (shares disk space), copy as fallback
|
||||
// (hard links fail on some filesystems like NFS or cross-device)
|
||||
try {
|
||||
fs.linkSync(binaryPath, target);
|
||||
} catch {
|
||||
fs.copyFileSync(binaryPath, target);
|
||||
}
|
||||
|
||||
fs.chmodSync(target, 0o755);
|
||||
console.log(`Cached cline binary at ${target}`);
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (error) {
|
||||
// postinstall failures should never block npm install.
|
||||
// The resolver script will find the binary at runtime.
|
||||
console.error(`postinstall: ${error.message}`);
|
||||
process.exit(0);
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
// Publishes cline and all platform-specific binary packages to npm.
|
||||
//
|
||||
// Usage:
|
||||
// bun script/publish-npm.ts # publish with "latest" tag
|
||||
// bun script/publish-npm.ts --tag next # publish with "next" tag
|
||||
// bun script/publish-npm.ts --dry-run # preview without publishing
|
||||
//
|
||||
// Prerequisites:
|
||||
// - Run script/build.ts first to generate dist/ packages
|
||||
// - GitHub trusted publishing or `npm login` for authentication
|
||||
|
||||
import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { parseArgs } from "node:util";
|
||||
import { $ } from "bun";
|
||||
|
||||
const cliDir = join(import.meta.dir, "..");
|
||||
process.chdir(cliDir);
|
||||
|
||||
const { values } = parseArgs({
|
||||
args: Bun.argv.slice(2),
|
||||
options: {
|
||||
"dry-run": { type: "boolean", default: false },
|
||||
tag: { type: "string", default: "latest" },
|
||||
},
|
||||
strict: true,
|
||||
});
|
||||
|
||||
const dryRun = values["dry-run"] ?? false;
|
||||
const npmTag = values.tag ?? "latest";
|
||||
const wrapperPackageName = "cline";
|
||||
|
||||
const expectedPlatformPackages = [
|
||||
"@cline/cli-darwin-arm64",
|
||||
"@cline/cli-darwin-x64",
|
||||
"@cline/cli-linux-arm64",
|
||||
"@cline/cli-linux-x64",
|
||||
"@cline/cli-windows-arm64",
|
||||
"@cline/cli-windows-x64",
|
||||
] as const;
|
||||
|
||||
const hostSdkPackages = [
|
||||
{ name: "@cline/sdk", directory: "sdk" },
|
||||
{ name: "@cline/core", directory: "core" },
|
||||
{ name: "@cline/agents", directory: "agents" },
|
||||
{ name: "@cline/llms", directory: "llms" },
|
||||
{ name: "@cline/shared", directory: "shared" },
|
||||
] as const;
|
||||
|
||||
interface PlatformPackageManifest {
|
||||
name: string;
|
||||
version: string;
|
||||
os: string[];
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isStringArray(value: unknown): value is string[] {
|
||||
return (
|
||||
Array.isArray(value) && value.every((item) => typeof item === "string")
|
||||
);
|
||||
}
|
||||
|
||||
function isPlatformPackageManifest(
|
||||
value: unknown,
|
||||
): value is PlatformPackageManifest {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
typeof value.name === "string" &&
|
||||
typeof value.version === "string" &&
|
||||
isStringArray(value.os)
|
||||
);
|
||||
}
|
||||
|
||||
function readPackageVersion(name: string, packageJsonPath: string): string {
|
||||
const pkg: unknown = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
||||
if (!isRecord(pkg) || pkg.name !== name || typeof pkg.version !== "string") {
|
||||
console.error(`Invalid package manifest for ${name}: ${packageJsonPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
return pkg.version;
|
||||
}
|
||||
|
||||
function buildHostSdkDependencies(): Record<string, string> {
|
||||
const dependencies: Record<string, string> = {};
|
||||
for (const pkg of hostSdkPackages) {
|
||||
dependencies[pkg.name] = readPackageVersion(
|
||||
pkg.name,
|
||||
join(cliDir, "../../sdk/packages", pkg.directory, "package.json"),
|
||||
);
|
||||
}
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
function removePackedTarballs(dir: string): void {
|
||||
for (const entry of readdirSync(dir)) {
|
||||
if (entry.endsWith(".tgz")) {
|
||||
rmSync(join(dir, entry), { force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function npmPackageVersionExists(
|
||||
name: string,
|
||||
version: string,
|
||||
): Promise<boolean> {
|
||||
const result = Bun.spawnSync(
|
||||
["npm", "view", `${name}@${version}`, "version"],
|
||||
{
|
||||
cwd: cliDir,
|
||||
stdout: "ignore",
|
||||
stderr: "ignore",
|
||||
},
|
||||
);
|
||||
return result.exitCode === 0;
|
||||
}
|
||||
|
||||
async function verifyPublishedDependencies(
|
||||
dependencies: Record<string, string>,
|
||||
): Promise<void> {
|
||||
const missingDependencies: string[] = [];
|
||||
for (const [name, version] of Object.entries(dependencies).sort()) {
|
||||
if (!(await npmPackageVersionExists(name, version))) {
|
||||
missingDependencies.push(`${name}@${version}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingDependencies.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error("Wrapper package dependencies are not published:");
|
||||
for (const dependency of missingDependencies) {
|
||||
console.error(` ${dependency}`);
|
||||
}
|
||||
console.error("Publish the SDK packages before publishing the CLI wrapper.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function publishPackage(input: {
|
||||
name: string;
|
||||
version: string;
|
||||
dir: string;
|
||||
tag: string;
|
||||
dryRun: boolean;
|
||||
}): Promise<void> {
|
||||
if (process.platform !== "win32") {
|
||||
await $`chmod -R 755 .`.cwd(input.dir);
|
||||
}
|
||||
|
||||
if (input.dryRun) {
|
||||
console.log(` [dry-run] Would publish ${input.name}@${input.version}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (await npmPackageVersionExists(input.name, input.version)) {
|
||||
console.log(` ${input.name}@${input.version} already exists, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(` Publishing ${input.name}@${input.version}...`);
|
||||
removePackedTarballs(input.dir);
|
||||
await $`bun pm pack`.cwd(input.dir);
|
||||
await $`npm publish *.tgz --access public --tag ${input.tag}`.cwd(input.dir);
|
||||
console.log(` Published ${input.name}@${input.version}`);
|
||||
}
|
||||
|
||||
// Discover built platform packages from dist/
|
||||
const binaries: Record<string, string> = {};
|
||||
for await (const filepath of new Bun.Glob("*/package.json").scan({
|
||||
cwd: join(cliDir, "dist"),
|
||||
})) {
|
||||
const pkg: unknown = JSON.parse(
|
||||
readFileSync(join(cliDir, "dist", filepath), "utf-8"),
|
||||
);
|
||||
if (isPlatformPackageManifest(pkg)) {
|
||||
binaries[pkg.name] = pkg.version;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(binaries).length === 0) {
|
||||
console.error("No platform packages found in dist/.");
|
||||
console.error("Run `bun script/build.ts` first.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const missingPackages = expectedPlatformPackages.filter(
|
||||
(name) => !(name in binaries),
|
||||
);
|
||||
if (missingPackages.length > 0) {
|
||||
console.error("Missing platform packages in dist/:");
|
||||
for (const name of missingPackages) {
|
||||
console.error(` ${name}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const versions = new Set(Object.values(binaries));
|
||||
if (versions.size !== 1) {
|
||||
console.error("Platform package versions do not match:");
|
||||
for (const [name, packageVersion] of Object.entries(binaries).sort()) {
|
||||
console.error(` ${name}@${packageVersion}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const version = Object.values(binaries)[0];
|
||||
const sourcePkg: unknown = JSON.parse(
|
||||
readFileSync(join(cliDir, "package.json"), "utf-8"),
|
||||
);
|
||||
const sourcePkgRecord = isRecord(sourcePkg) ? sourcePkg : {};
|
||||
const sourceVersion =
|
||||
"version" in sourcePkgRecord && typeof sourcePkgRecord.version === "string"
|
||||
? sourcePkgRecord.version
|
||||
: undefined;
|
||||
if (sourceVersion !== version) {
|
||||
console.error(
|
||||
`Built package version ${version} does not match apps/cli/package.json version ${sourceVersion ?? "(missing)"}.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const sourceRepository =
|
||||
"repository" in sourcePkgRecord ? sourcePkgRecord.repository : undefined;
|
||||
const hostSdkDependencies = buildHostSdkDependencies();
|
||||
|
||||
console.log(`Publishing ${wrapperPackageName} v${version}`);
|
||||
console.log(` Tag: ${npmTag}`);
|
||||
console.log(` Dry run: ${dryRun}`);
|
||||
console.log(` Platform packages: ${Object.keys(binaries).length}`);
|
||||
for (const name of Object.keys(binaries)) {
|
||||
console.log(` ${name}`);
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
await verifyPublishedDependencies(hostSdkDependencies);
|
||||
}
|
||||
|
||||
// Step 1: Publish platform-specific packages (in parallel)
|
||||
console.log("\nPublishing platform packages...");
|
||||
const platformTasks = Object.keys(binaries)
|
||||
.sort()
|
||||
.map(async (name) => {
|
||||
const dirName = name.replace("@cline/", "");
|
||||
const pkgDir = join(cliDir, "dist", dirName);
|
||||
|
||||
await publishPackage({
|
||||
name,
|
||||
version,
|
||||
dir: pkgDir,
|
||||
tag: npmTag,
|
||||
dryRun,
|
||||
});
|
||||
});
|
||||
await Promise.all(platformTasks);
|
||||
|
||||
// Step 2: Generate and publish the main wrapper package
|
||||
console.log("\nPreparing main package...");
|
||||
const mainPkgDir = join(cliDir, "dist", "cli");
|
||||
|
||||
await $`rm -rf ${mainPkgDir}`;
|
||||
await $`mkdir -p ${mainPkgDir}`;
|
||||
await $`cp -r ${join(cliDir, "bin")} ${join(mainPkgDir, "bin")}`;
|
||||
await $`cp ${join(cliDir, "script/postinstall.mjs")} ${join(mainPkgDir, "postinstall.mjs")}`;
|
||||
|
||||
// Copy LICENSE from repo root if it exists
|
||||
const licenseFrom = join(cliDir, "../../LICENSE");
|
||||
if (existsSync(licenseFrom)) {
|
||||
await $`cp ${licenseFrom} ${join(mainPkgDir, "LICENSE")}`;
|
||||
}
|
||||
|
||||
// Copy README.md so the npm registry listing has the same landing page
|
||||
// as the repo. The published wrapper package is generated fresh in
|
||||
// dist/cli/ each release, so the source README is not picked up
|
||||
// automatically.
|
||||
const readmeFrom = join(cliDir, "README.md");
|
||||
if (existsSync(readmeFrom)) {
|
||||
await $`cp ${readmeFrom} ${join(mainPkgDir, "README.md")}`;
|
||||
} else {
|
||||
console.error(
|
||||
`Missing ${readmeFrom}. The CLI README must exist before publishing.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const mainPkg: unknown = JSON.parse(
|
||||
readFileSync(join(cliDir, "package.json"), "utf-8"),
|
||||
);
|
||||
const mainPkgRecord = isRecord(mainPkg) ? mainPkg : {};
|
||||
const description =
|
||||
"description" in mainPkgRecord &&
|
||||
typeof mainPkgRecord.description === "string"
|
||||
? mainPkgRecord.description
|
||||
: undefined;
|
||||
const license =
|
||||
"license" in mainPkgRecord && typeof mainPkgRecord.license === "string"
|
||||
? mainPkgRecord.license
|
||||
: undefined;
|
||||
const keywords =
|
||||
"keywords" in mainPkgRecord && isStringArray(mainPkgRecord.keywords)
|
||||
? mainPkgRecord.keywords
|
||||
: undefined;
|
||||
const author = "author" in mainPkgRecord ? mainPkgRecord.author : undefined;
|
||||
const homepage =
|
||||
"homepage" in mainPkgRecord && typeof mainPkgRecord.homepage === "string"
|
||||
? mainPkgRecord.homepage
|
||||
: undefined;
|
||||
const bugs = "bugs" in mainPkgRecord ? mainPkgRecord.bugs : undefined;
|
||||
const wrapperPackageJson = {
|
||||
name: wrapperPackageName,
|
||||
version,
|
||||
description: description || "Cline CLI",
|
||||
license: license || "Apache-2.0",
|
||||
...(keywords ? { keywords } : {}),
|
||||
...(author ? { author } : {}),
|
||||
...(homepage ? { homepage } : {}),
|
||||
...(bugs ? { bugs } : {}),
|
||||
...(sourceRepository ? { repository: sourceRepository } : {}),
|
||||
bin: {
|
||||
cline: "./bin/cline",
|
||||
},
|
||||
scripts: {
|
||||
postinstall: "node ./postinstall.mjs || true",
|
||||
},
|
||||
dependencies: hostSdkDependencies,
|
||||
optionalDependencies: binaries,
|
||||
};
|
||||
|
||||
await Bun.write(
|
||||
join(mainPkgDir, "package.json"),
|
||||
`${JSON.stringify(wrapperPackageJson, null, 2)}\n`,
|
||||
);
|
||||
|
||||
if (dryRun) {
|
||||
console.log(
|
||||
` [dry-run] Would publish ${wrapperPackageName}@${version} with tag ${npmTag}`,
|
||||
);
|
||||
console.log("\nDry run complete. No packages were published.");
|
||||
} else {
|
||||
await publishPackage({
|
||||
name: wrapperPackageName,
|
||||
version,
|
||||
dir: mainPkgDir,
|
||||
tag: npmTag,
|
||||
dryRun: false,
|
||||
});
|
||||
console.log(
|
||||
`\nPublished ${wrapperPackageName}@${version} with tag ${npmTag}`,
|
||||
);
|
||||
|
||||
console.log("\nInstall with:");
|
||||
console.log(` npm install -g ${wrapperPackageName}`);
|
||||
}
|
||||
Reference in New Issue
Block a user