feat(server): add CLI upgrade command (#374)
## Summary - Adds a `--upgrade [version]` CLI flag that upgrades the global CodeNomad CLI server package and exits. - Uses `bun add --global` for the package upgrade path and includes server-side tests. - Rebased onto the latest `dev` because we do not have permission to push to the original fork branch. ## Credits - Original PR: #363 - Original author: Pascal André (@pascalandr) ## Testing - Not run; this PR only recreates the rebased branch from #363. --------- Co-authored-by: Pascal André <pascalandr@gmail.com>
This commit is contained in:
39
packages/server/src/cli-upgrade.test.ts
Normal file
39
packages/server/src/cli-upgrade.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import assert from "node:assert/strict"
|
||||
import { describe, it } from "node:test"
|
||||
import { buildUpgradeCommand, detectPackageManager, formatUpgradeCommand } from "./cli-upgrade"
|
||||
|
||||
describe("cli upgrade", () => {
|
||||
it("defaults to npm when no package manager can be detected", () => {
|
||||
assert.equal(detectPackageManager({}), "npm")
|
||||
})
|
||||
|
||||
it("detects package managers from npm user agent", () => {
|
||||
assert.equal(detectPackageManager({ npm_config_user_agent: "pnpm/9.0.0 node/v22" }), "pnpm")
|
||||
assert.equal(detectPackageManager({ npm_config_user_agent: "bun/1.0.0" }), "bun")
|
||||
assert.equal(detectPackageManager({ npm_config_user_agent: "npm/10.0.0 node/v22" }), "npm")
|
||||
})
|
||||
|
||||
it("builds latest upgrade command by default", () => {
|
||||
const command = buildUpgradeCommand(undefined, "npm")
|
||||
|
||||
assert.equal(command.packageSpec, "@neuralnomads/codenomad@latest")
|
||||
assert.deepEqual(command.args, ["install", "-g", "@neuralnomads/codenomad@latest"])
|
||||
assert.equal(formatUpgradeCommand(command), "npm install -g @neuralnomads/codenomad@latest")
|
||||
})
|
||||
|
||||
it("builds a versioned upgrade command", () => {
|
||||
const command = buildUpgradeCommand("0.10.5", "pnpm")
|
||||
|
||||
assert.equal(command.packageSpec, "@neuralnomads/codenomad@0.10.5")
|
||||
assert.deepEqual(command.args, ["install", "-g", "@neuralnomads/codenomad@0.10.5"])
|
||||
assert.equal(formatUpgradeCommand(command), "pnpm install -g @neuralnomads/codenomad@0.10.5")
|
||||
})
|
||||
|
||||
it("uses bun add for Bun installs", () => {
|
||||
const command = buildUpgradeCommand("0.10.5", "bun")
|
||||
|
||||
assert.equal(command.packageSpec, "@neuralnomads/codenomad@0.10.5")
|
||||
assert.deepEqual(command.args, ["add", "-g", "@neuralnomads/codenomad@0.10.5"])
|
||||
assert.equal(formatUpgradeCommand(command), "bun add -g @neuralnomads/codenomad@0.10.5")
|
||||
})
|
||||
})
|
||||
70
packages/server/src/cli-upgrade.ts
Normal file
70
packages/server/src/cli-upgrade.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { spawn } from "child_process"
|
||||
|
||||
const CODENOMAD_PACKAGE_NAME = "@neuralnomads/codenomad"
|
||||
|
||||
export type SupportedPackageManager = "npm" | "pnpm" | "bun"
|
||||
|
||||
export interface UpgradeCommand {
|
||||
command: SupportedPackageManager
|
||||
args: string[]
|
||||
packageSpec: string
|
||||
}
|
||||
|
||||
function detectFromText(value: string | undefined): SupportedPackageManager | null {
|
||||
const lower = (value ?? "").toLowerCase()
|
||||
if (!lower) return null
|
||||
if (lower.includes("pnpm")) return "pnpm"
|
||||
if (lower.includes("bun")) return "bun"
|
||||
if (lower.includes("npm")) return "npm"
|
||||
return null
|
||||
}
|
||||
|
||||
export function detectPackageManager(env: NodeJS.ProcessEnv = process.env): SupportedPackageManager {
|
||||
return detectFromText(env.npm_config_user_agent) ?? detectFromText(env.npm_execpath) ?? "npm"
|
||||
}
|
||||
|
||||
export function buildUpgradeCommand(
|
||||
version?: string,
|
||||
packageManager: SupportedPackageManager = detectPackageManager(),
|
||||
): UpgradeCommand {
|
||||
const targetVersion = (version ?? "").trim() || "latest"
|
||||
const packageSpec = `${CODENOMAD_PACKAGE_NAME}@${targetVersion}`
|
||||
const args = packageManager === "bun" ? ["add", "-g", packageSpec] : ["install", "-g", packageSpec]
|
||||
|
||||
return {
|
||||
command: packageManager,
|
||||
args,
|
||||
packageSpec,
|
||||
}
|
||||
}
|
||||
|
||||
export function formatUpgradeCommand(command: UpgradeCommand): string {
|
||||
return [command.command, ...command.args].join(" ")
|
||||
}
|
||||
|
||||
export function runCliUpgrade(version?: string, env: NodeJS.ProcessEnv = process.env): Promise<number> {
|
||||
const upgrade = buildUpgradeCommand(version, detectPackageManager(env))
|
||||
console.log(`Upgrading CodeNomad with: ${formatUpgradeCommand(upgrade)}`)
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(upgrade.command, upgrade.args, {
|
||||
env,
|
||||
shell: process.platform === "win32",
|
||||
stdio: "inherit",
|
||||
})
|
||||
|
||||
child.on("exit", (code, signal) => {
|
||||
if (signal) {
|
||||
console.error(`Upgrade command stopped by signal ${signal}`)
|
||||
resolve(1)
|
||||
return
|
||||
}
|
||||
resolve(code ?? 0)
|
||||
})
|
||||
|
||||
child.on("error", (error) => {
|
||||
console.error("Failed to launch upgrade command", error)
|
||||
resolve(1)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import { SideCarManager } from "./sidecars/manager"
|
||||
import { ClientConnectionManager } from "./clients/connection-manager"
|
||||
import { PluginChannelManager } from "./plugins/channel"
|
||||
import { VoiceModeManager } from "./plugins/voice-mode"
|
||||
import { runCliUpgrade } from "./cli-upgrade"
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
@@ -63,6 +64,7 @@ interface CliOptions {
|
||||
authCookieName: string
|
||||
generateToken: boolean
|
||||
dangerouslySkipAuth: boolean
|
||||
upgrade?: string | boolean
|
||||
}
|
||||
|
||||
const DEFAULT_HOST = "127.0.0.1"
|
||||
@@ -124,6 +126,7 @@ function parseCliOptions(argv: string[]): CliOptions {
|
||||
.env("CODENOMAD_SKIP_AUTH")
|
||||
.default(false),
|
||||
)
|
||||
.addOption(new Option("--upgrade [version]", "Upgrade the global CodeNomad CLI server package and exit"))
|
||||
|
||||
program.parse(argv, { from: "user" })
|
||||
const parsed = program.opts<{
|
||||
@@ -153,8 +156,10 @@ function parseCliOptions(argv: string[]): CliOptions {
|
||||
authCookieName: string
|
||||
generateToken?: boolean
|
||||
dangerouslySkipAuth?: boolean
|
||||
upgrade?: string | boolean
|
||||
}>()
|
||||
|
||||
const upgrade = parsed.upgrade
|
||||
const parseBooleanEnv = (value: string | undefined): boolean => {
|
||||
const normalized = (value ?? "").trim().toLowerCase()
|
||||
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "y" || normalized === "on"
|
||||
@@ -170,7 +175,7 @@ function parseCliOptions(argv: string[]): CliOptions {
|
||||
const httpsEnabled = parseBooleanEnv(parsed.https)
|
||||
const httpEnabled = parseBooleanEnv(parsed.http)
|
||||
|
||||
if (!httpsEnabled && !httpEnabled) {
|
||||
if (upgrade === undefined && !httpsEnabled && !httpEnabled) {
|
||||
throw new InvalidArgumentError("At least one listener must be enabled (--https or --http)")
|
||||
}
|
||||
|
||||
@@ -200,6 +205,7 @@ function parseCliOptions(argv: string[]): CliOptions {
|
||||
authCookieName: parsed.authCookieName,
|
||||
generateToken: Boolean(parsed.generateToken),
|
||||
dangerouslySkipAuth: Boolean(parsed.dangerouslySkipAuth),
|
||||
upgrade,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,6 +238,12 @@ function programHasArg(argv: string[], flag: string): boolean {
|
||||
|
||||
async function main() {
|
||||
const options = parseCliOptions(process.argv.slice(2))
|
||||
if (options.upgrade !== undefined) {
|
||||
const version = typeof options.upgrade === "string" ? options.upgrade : undefined
|
||||
process.exitCode = await runCliUpgrade(version)
|
||||
return
|
||||
}
|
||||
|
||||
const logger = createLogger({ level: options.logLevel, destination: options.logDestination, component: "app" })
|
||||
const workspaceLogger = logger.child({ component: "workspace" })
|
||||
const configLogger = logger.child({ component: "config" })
|
||||
|
||||
Reference in New Issue
Block a user