mirror of
https://github.com/cline/cline.git
synced 2026-09-19 10:13:34 +08:00
* refactor: simplify API configuration management and state handling Refactored `StateManager` and `ApiConfiguration` handling to use a more maintainable, data-driven approach. Replaced manual key mapping in `setApiConfiguration` with automated categorization based on static definitions. - Updated `buildApiHandler` and `createHandlerForProvider` to accept `Partial<ApiConfiguration>`, improving flexibility. - Introduced `categorizeApiConfigurationKeys` and other helpers to separate settings from secrets automatically. - Centralized secret key definitions in `state-keys.ts` to reduce boilerplate and potential for errors when adding new providers. - Cleaned up redundant imports and type definitions across the core API and storage modules. * apply feedback * clean up * refactor: consolidate API configuration types and state key definitions - Rename `ApiHandlerSecrets` to `Secrets` for consistency across codebase - Merge `ApiHandlerOptions` with `ApiHandlerSettings` to reduce duplication - Extract `GlobalStateAndSettingKeys` as a computed constant from state field definitions - Consolidate remote configuration fields into `REMOTE_CONFIG_EXTRA_FIELDS` group - Remove redundant type definitions and improve type safety in state management This refactoring simplifies the type system by eliminating duplicate interfaces and ensures consistent naming conventions throughout the storage and API layers. * Clean up * rename type with default * type safe * add unit test * Apply suggestions from code review Co-authored-by: Tomás Barreiro <52393857+BarreiroT@users.noreply.github.com> * apply feedback --------- Co-authored-by: Tomás Barreiro <52393857+BarreiroT@users.noreply.github.com>
487 lines
15 KiB
JavaScript
487 lines
15 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* NPM Package Builder for Cline CLI
|
|
*
|
|
* This script builds the Cline CLI NPM package (dist-standalone/).
|
|
* It is completely independent from package-standalone.mjs (JetBrains build).
|
|
*
|
|
* Usage: node scripts/package-npm.mjs
|
|
*
|
|
* Prerequisites:
|
|
* - npm run protos && npm run protos-go
|
|
* - npm run compile-cli
|
|
* - npm run compile-cli-all-platforms
|
|
* - npm run download-ripgrep
|
|
*/
|
|
|
|
import { execSync } from "child_process"
|
|
import fs from "fs"
|
|
import { cp } from "fs/promises"
|
|
import path from "path"
|
|
|
|
const BUILD_DIR = "dist-standalone"
|
|
const RUNTIME_DEPS_DIR = "standalone/runtime-files"
|
|
const RIPGREP_BINARIES_DIR = `${BUILD_DIR}/ripgrep-binaries`
|
|
const CLI_BINARIES_DIR = "cli/bin"
|
|
const IS_VERBOSE = process.argv.includes("-v") || process.argv.includes("--verbose")
|
|
|
|
async function main() {
|
|
console.log("🚀 Building Cline NPM Package\n")
|
|
|
|
await installNodeDependencies()
|
|
await copyCliBinaries()
|
|
await copyRipgrepBinaries()
|
|
await copyProtoDescriptors()
|
|
await createNpmPackageFiles()
|
|
await createFakeNodeModules()
|
|
await createNpmIgnoreFile()
|
|
await createPostinstallScript()
|
|
|
|
console.log("\n✅ Build complete!")
|
|
console.log(`\n📦 NPM package ready in ${BUILD_DIR}/`)
|
|
console.log(`To publish: cd ${BUILD_DIR} && npm publish`)
|
|
}
|
|
|
|
/**
|
|
* Install node dependencies in the build directory
|
|
*/
|
|
async function installNodeDependencies() {
|
|
// Clean modules from any previous builds
|
|
await rmrf(path.join(BUILD_DIR, "node_modules"))
|
|
|
|
await cpr(RUNTIME_DEPS_DIR, BUILD_DIR)
|
|
|
|
console.log("Running npm install in distribution directory...")
|
|
execSync("npm install", { stdio: "inherit", cwd: BUILD_DIR })
|
|
|
|
// Move the vscode directory into node_modules.
|
|
// It can't be installed using npm because it will create a symlink which cannot be unzipped correctly on windows.
|
|
fs.renameSync(`${BUILD_DIR}/vscode`, `${BUILD_DIR}/node_modules/vscode`)
|
|
}
|
|
|
|
/**
|
|
* Copy CLI binaries (cline and cline-host) for all platforms
|
|
* The Go binaries are cross-compiled for darwin/linux arm64/amd64
|
|
*/
|
|
async function copyCliBinaries() {
|
|
console.log("Copying CLI binaries for all platforms...")
|
|
|
|
const platforms = [
|
|
{ os: "darwin", arch: "arm64" },
|
|
{ os: "darwin", arch: "amd64" },
|
|
{ os: "linux", arch: "amd64" },
|
|
{ os: "linux", arch: "arm64" },
|
|
]
|
|
|
|
const binDir = path.join(BUILD_DIR, "bin")
|
|
|
|
// Create bin directory
|
|
fs.mkdirSync(binDir, { recursive: true })
|
|
|
|
// Copy all platform-specific binaries
|
|
for (const { os, arch } of platforms) {
|
|
const platformSuffix = `${os}-${arch}`
|
|
|
|
// Copy cline binary
|
|
const clineSource = path.join(CLI_BINARIES_DIR, `cline-${platformSuffix}`)
|
|
const clineDest = path.join(binDir, `cline-${platformSuffix}`)
|
|
|
|
if (!fs.existsSync(clineSource)) {
|
|
console.error(`Error: CLI binary not found at ${clineSource}`)
|
|
console.error(`Please run: npm run compile-cli-all-platforms`)
|
|
process.exit(1)
|
|
}
|
|
|
|
await cpr(clineSource, clineDest)
|
|
fs.chmodSync(clineDest, 0o755)
|
|
console.log(`✓ cline-${platformSuffix} copied`)
|
|
|
|
// Copy cline-host binary
|
|
const hostSource = path.join(CLI_BINARIES_DIR, `cline-host-${platformSuffix}`)
|
|
const hostDest = path.join(binDir, `cline-host-${platformSuffix}`)
|
|
|
|
if (!fs.existsSync(hostSource)) {
|
|
console.error(`Error: CLI binary not found at ${hostSource}`)
|
|
console.error(`Please run: npm run compile-cli-all-platforms`)
|
|
process.exit(1)
|
|
}
|
|
|
|
await cpr(hostSource, hostDest)
|
|
fs.chmodSync(hostDest, 0o755)
|
|
console.log(`✓ cline-host-${platformSuffix} copied`)
|
|
}
|
|
|
|
console.log(`✓ All CLI binaries copied to ${binDir}`)
|
|
}
|
|
|
|
/**
|
|
* Copy ripgrep binaries for ALL platforms
|
|
* Ripgrep is needed by cline-core for file searching
|
|
* The postinstall script will select the correct binary for the user's platform
|
|
*/
|
|
async function copyRipgrepBinaries() {
|
|
console.log("Copying ripgrep binaries for all platforms...")
|
|
|
|
const platforms = [
|
|
{ dir: "darwin-arm64", binary: "rg" },
|
|
{ dir: "darwin-x64", binary: "rg" },
|
|
{ dir: "linux-x64", binary: "rg" },
|
|
{ dir: "linux-arm64", binary: "rg" },
|
|
// { dir: "win-x64", binary: "rg.exe" }, // Windows not supported yet
|
|
]
|
|
|
|
const ripgrepDir = path.join(BUILD_DIR, "ripgrep")
|
|
|
|
// Create ripgrep directory
|
|
fs.mkdirSync(ripgrepDir, { recursive: true })
|
|
|
|
// Check if ripgrep binaries exist, download if missing
|
|
const firstPlatform = platforms[0]
|
|
const firstBinaryPath = path.join(RIPGREP_BINARIES_DIR, firstPlatform.dir, firstPlatform.binary)
|
|
if (!fs.existsSync(firstBinaryPath)) {
|
|
console.log(`Ripgrep binaries not found, downloading...`)
|
|
try {
|
|
execSync("npm run download-ripgrep", { stdio: "inherit" })
|
|
} catch (error) {
|
|
console.error(`Error downloading ripgrep: ${error.message}`)
|
|
console.error(`Please run: npm run download-ripgrep`)
|
|
process.exit(1)
|
|
}
|
|
}
|
|
|
|
// Copy all platform-specific binaries
|
|
for (const { dir, binary } of platforms) {
|
|
const source = path.join(RIPGREP_BINARIES_DIR, dir, binary)
|
|
const dest = path.join(ripgrepDir, `rg-${dir}`)
|
|
|
|
if (!fs.existsSync(source)) {
|
|
console.error(`Error: Ripgrep binary not found at ${source}`)
|
|
console.error(`Please run: npm run download-ripgrep`)
|
|
process.exit(1)
|
|
}
|
|
|
|
await cpr(source, dest)
|
|
fs.chmodSync(dest, 0o755)
|
|
console.log(`✓ rg-${dir} copied`)
|
|
}
|
|
|
|
console.log(`✓ All ripgrep binaries copied to ${ripgrepDir}`)
|
|
}
|
|
|
|
/**
|
|
* Verify proto descriptors exist in the build directory
|
|
* The proto/descriptor_set.pb file is generated by build-proto.mjs to dist-standalone/proto/
|
|
* We do NOT copy from proto/ source because that would overwrite the freshly generated descriptor
|
|
*/
|
|
async function copyProtoDescriptors() {
|
|
console.log("Verifying proto descriptors...")
|
|
|
|
const protoDest = path.join(BUILD_DIR, "proto")
|
|
const descriptorPath = path.join(protoDest, "descriptor_set.pb")
|
|
|
|
// Check if descriptor_set.pb exists in the build directory
|
|
// It should have been generated by `npm run protos` which runs build-proto.mjs
|
|
if (!fs.existsSync(descriptorPath)) {
|
|
console.error(`Error: proto/descriptor_set.pb not found at ${descriptorPath}`)
|
|
console.error(`Please run: npm run protos`)
|
|
console.error(`Note: build-proto.mjs generates the descriptor to dist-standalone/proto/`)
|
|
process.exit(1)
|
|
}
|
|
|
|
// Verify the descriptor is recent (not stale)
|
|
const stats = fs.statSync(descriptorPath)
|
|
const ageMinutes = (Date.now() - stats.mtimeMs) / 1000 / 60
|
|
if (ageMinutes > 60) {
|
|
console.warn(`Warning: descriptor_set.pb is ${Math.round(ageMinutes)} minutes old`)
|
|
console.warn(`Consider running: npm run protos`)
|
|
}
|
|
|
|
console.log(`✓ Proto descriptors verified at ${protoDest}`)
|
|
}
|
|
|
|
/**
|
|
* Copy NPM package files (package.json, README.md, and man page) from cli/ directory
|
|
*/
|
|
async function createNpmPackageFiles() {
|
|
console.log("Copying NPM package files...")
|
|
|
|
// Copy package.json from cli/ directory
|
|
const packageJsonSource = path.join("cli", "package.json")
|
|
const packageJsonDest = path.join(BUILD_DIR, "package.json")
|
|
|
|
if (!fs.existsSync(packageJsonSource)) {
|
|
console.error(`Error: NPM package.json not found at ${packageJsonSource}`)
|
|
process.exit(1)
|
|
}
|
|
|
|
await cpr(packageJsonSource, packageJsonDest)
|
|
console.log(`✓ package.json copied from ${packageJsonSource}`)
|
|
|
|
// Copy README.md from cli/ directory
|
|
const readmeSource = path.join("cli", "README.md")
|
|
const readmeDest = path.join(BUILD_DIR, "README.md")
|
|
|
|
if (!fs.existsSync(readmeSource)) {
|
|
console.error(`Error: NPM README.md not found at ${readmeSource}`)
|
|
process.exit(1)
|
|
}
|
|
|
|
await cpr(readmeSource, readmeDest)
|
|
console.log(`✓ README.md copied from ${readmeSource}`)
|
|
|
|
// Copy man page from cli/man/ directory
|
|
const manPageSource = path.join("cli", "man", "cline.1")
|
|
const manDir = path.join(BUILD_DIR, "man")
|
|
const manPageDest = path.join(manDir, "cline.1")
|
|
|
|
if (!fs.existsSync(manPageSource)) {
|
|
console.error(`Error: Man page not found at ${manPageSource}`)
|
|
process.exit(1)
|
|
}
|
|
|
|
// Create man directory if it doesn't exist
|
|
fs.mkdirSync(manDir, { recursive: true })
|
|
|
|
await cpr(manPageSource, manPageDest)
|
|
console.log(`✓ Man page copied from ${manPageSource}`)
|
|
}
|
|
|
|
/**
|
|
* Create fake_node_modules directory with vscode stub
|
|
* This directory will be added to NODE_PATH so Node.js can find the vscode module
|
|
* without npm interfering with the real node_modules directory
|
|
*/
|
|
async function createFakeNodeModules() {
|
|
console.log("Creating fake_node_modules with vscode stub...")
|
|
|
|
const vscodeSource = path.join(BUILD_DIR, "node_modules", "vscode")
|
|
const fakeNodeModulesDir = path.join(BUILD_DIR, "fake_node_modules")
|
|
const vscodeDest = path.join(fakeNodeModulesDir, "vscode")
|
|
|
|
if (!fs.existsSync(vscodeSource)) {
|
|
console.error(`Error: vscode stub module not found at ${vscodeSource}`)
|
|
process.exit(1)
|
|
}
|
|
|
|
// Create fake_node_modules directory
|
|
fs.mkdirSync(fakeNodeModulesDir, { recursive: true })
|
|
|
|
// Copy vscode stub into fake_node_modules
|
|
await cpr(vscodeSource, vscodeDest)
|
|
|
|
console.log(`✓ fake_node_modules/vscode created at ${vscodeDest}`)
|
|
}
|
|
|
|
/**
|
|
* Create .npmignore file to ensure necessary files are included
|
|
*/
|
|
async function createNpmIgnoreFile() {
|
|
console.log("Creating .npmignore file...")
|
|
|
|
// Create .npmignore that excludes build artifacts
|
|
// Note: proto/ directory is NOT excluded because proto/descriptor_set.pb is needed at runtime
|
|
const npmignoreContent = `# Exclude build artifacts and unnecessary files
|
|
binaries/
|
|
ripgrep-binaries/
|
|
standalone.zip
|
|
cline-core.js.map
|
|
package-lock.json
|
|
tree-sitter*.wasm
|
|
node_modules/vscode
|
|
`
|
|
|
|
const npmignorePath = path.join(BUILD_DIR, ".npmignore")
|
|
fs.writeFileSync(npmignorePath, npmignoreContent)
|
|
|
|
console.log(`✓ .npmignore created`)
|
|
}
|
|
|
|
/**
|
|
* Create postinstall script for NPM package
|
|
* This script selects the correct platform-specific binary and creates symlinks
|
|
*/
|
|
async function createPostinstallScript() {
|
|
console.log("Creating postinstall script...")
|
|
|
|
const postinstallScript = `#!/usr/bin/env node
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const os = require('os');
|
|
|
|
// Detect current platform and architecture
|
|
function getPlatformInfo() {
|
|
const platform = os.platform();
|
|
const arch = os.arch();
|
|
|
|
// Map Node.js arch names to Go arch names (for CLI binaries)
|
|
let goArch = arch;
|
|
if (arch === 'x64') {
|
|
goArch = 'amd64';
|
|
}
|
|
|
|
// Map for ripgrep binaries (uses different naming)
|
|
let rgArch = arch;
|
|
if (arch === 'arm64') {
|
|
rgArch = 'arm64';
|
|
} else if (arch === 'x64') {
|
|
rgArch = 'x64';
|
|
}
|
|
|
|
return { platform, arch, goArch, rgArch };
|
|
}
|
|
|
|
// Setup platform-specific binaries
|
|
function setupBinaries() {
|
|
const { platform, goArch, rgArch } = getPlatformInfo();
|
|
const cliPlatformSuffix = \`\${platform}-\${goArch}\`;
|
|
const rgPlatformSuffix = \`\${platform}-\${rgArch}\`;
|
|
|
|
console.log(\`Setting up Cline CLI for \${cliPlatformSuffix}...\`);
|
|
|
|
// Setup CLI binaries
|
|
const binDir = path.join(__dirname, 'bin');
|
|
|
|
// Check if platform-specific binaries exist
|
|
const clineSource = path.join(binDir, \`cline-\${cliPlatformSuffix}\`);
|
|
const clineHostSource = path.join(binDir, \`cline-host-\${cliPlatformSuffix}\`);
|
|
|
|
if (!fs.existsSync(clineSource)) {
|
|
console.error(\`Error: Binary not found for platform \${cliPlatformSuffix}\`);
|
|
console.error(\`Expected: \${clineSource}\`);
|
|
console.error(\`Supported platforms: darwin-arm64, darwin-amd64, linux-amd64, linux-arm64\`);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!fs.existsSync(clineHostSource)) {
|
|
console.error(\`Error: Binary not found for platform \${cliPlatformSuffix}\`);
|
|
console.error(\`Expected: \${clineHostSource}\`);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Create symlinks or copies to the generic names
|
|
const clineTarget = path.join(binDir, 'cline');
|
|
const clineHostTarget = path.join(binDir, 'cline-host');
|
|
|
|
// Remove existing files if they exist
|
|
[clineTarget, clineHostTarget].forEach(target => {
|
|
if (fs.existsSync(target)) {
|
|
try {
|
|
fs.unlinkSync(target);
|
|
} catch (e) {
|
|
console.warn(\`Warning: Could not remove existing file \${target}: \${e.message}\`);
|
|
}
|
|
}
|
|
});
|
|
|
|
// On Unix, create symlinks; on Windows, copy files
|
|
if (platform === 'win32') {
|
|
// Windows: copy files
|
|
fs.copyFileSync(clineSource, clineTarget);
|
|
fs.copyFileSync(clineHostSource, clineHostTarget);
|
|
console.log('✓ Copied platform-specific CLI binaries');
|
|
} else {
|
|
// Unix: create symlinks
|
|
fs.symlinkSync(path.basename(clineSource), clineTarget);
|
|
fs.symlinkSync(path.basename(clineHostSource), clineHostTarget);
|
|
console.log('✓ Created symlinks to platform-specific CLI binaries');
|
|
|
|
// Make binaries executable
|
|
try {
|
|
fs.chmodSync(clineSource, 0o755);
|
|
fs.chmodSync(clineHostSource, 0o755);
|
|
fs.chmodSync(clineTarget, 0o755);
|
|
fs.chmodSync(clineHostTarget, 0o755);
|
|
} catch (error) {
|
|
console.warn(\`Warning: Could not set executable permissions: \${error.message}\`);
|
|
}
|
|
}
|
|
|
|
// Setup ripgrep binary
|
|
console.log(\`Setting up ripgrep for \${rgPlatformSuffix}...\`);
|
|
|
|
const ripgrepDir = path.join(__dirname, 'ripgrep');
|
|
const rgSource = path.join(ripgrepDir, \`rg-\${rgPlatformSuffix}\`);
|
|
const rgTarget = path.join(__dirname, 'rg');
|
|
|
|
if (!fs.existsSync(rgSource)) {
|
|
console.error(\`Error: ripgrep binary not found for platform \${rgPlatformSuffix}\`);
|
|
console.error(\`Expected: \${rgSource}\`);
|
|
console.error(\`Supported platforms: darwin-arm64, darwin-x64, linux-x64, linux-arm64\`);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Remove existing rg if it exists
|
|
if (fs.existsSync(rgTarget)) {
|
|
try {
|
|
fs.unlinkSync(rgTarget);
|
|
} catch (e) {
|
|
console.warn(\`Warning: Could not remove existing ripgrep: \${e.message}\`);
|
|
}
|
|
}
|
|
|
|
// Copy ripgrep binary to root (where cline-core expects it)
|
|
fs.copyFileSync(rgSource, rgTarget);
|
|
|
|
// Make ripgrep executable (Unix only)
|
|
if (platform !== 'win32') {
|
|
try {
|
|
fs.chmodSync(rgTarget, 0o755);
|
|
} catch (error) {
|
|
console.warn(\`Warning: Could not set ripgrep executable permissions: \${error.message}\`);
|
|
}
|
|
}
|
|
console.log('✓ Copied platform-specific ripgrep binary');
|
|
|
|
console.log('✓ Cline CLI installation complete');
|
|
console.log('');
|
|
console.log('Usage:');
|
|
console.log(' cline - Start Cline CLI');
|
|
console.log(' cline-host - Start Cline host service');
|
|
console.log('');
|
|
console.log('Documentation: https://docs.cline.bot');
|
|
}
|
|
|
|
try {
|
|
setupBinaries();
|
|
} catch (error) {
|
|
console.error(\`Installation failed: \${error.message}\`);
|
|
console.error('Please report this issue at: https://github.com/cline/cline/issues');
|
|
process.exit(1);
|
|
}
|
|
`
|
|
|
|
const postinstallPath = path.join(BUILD_DIR, "postinstall.js")
|
|
fs.writeFileSync(postinstallPath, postinstallScript)
|
|
fs.chmodSync(postinstallPath, 0o755)
|
|
|
|
console.log(`✓ postinstall.js created`)
|
|
}
|
|
|
|
/* cp -r */
|
|
async function cpr(source, dest) {
|
|
log_verbose(`Copying ${source} -> ${dest}`)
|
|
await cp(source, dest, {
|
|
recursive: true,
|
|
preserveTimestamps: true,
|
|
dereference: false, // preserve symlinks instead of following them
|
|
})
|
|
}
|
|
|
|
/* rm -rf */
|
|
async function rmrf(dir) {
|
|
if (fs.existsSync(dir)) {
|
|
log_verbose(`Removing ${dir}`)
|
|
fs.rmSync(dir, { recursive: true, force: true })
|
|
}
|
|
}
|
|
|
|
function log_verbose(...args) {
|
|
if (IS_VERBOSE) {
|
|
console.log(...args)
|
|
}
|
|
}
|
|
|
|
await main()
|