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 { ClientConnectionManager } from "./clients/connection-manager"
|
||||||
import { PluginChannelManager } from "./plugins/channel"
|
import { PluginChannelManager } from "./plugins/channel"
|
||||||
import { VoiceModeManager } from "./plugins/voice-mode"
|
import { VoiceModeManager } from "./plugins/voice-mode"
|
||||||
|
import { runCliUpgrade } from "./cli-upgrade"
|
||||||
|
|
||||||
const require = createRequire(import.meta.url)
|
const require = createRequire(import.meta.url)
|
||||||
|
|
||||||
@@ -63,6 +64,7 @@ interface CliOptions {
|
|||||||
authCookieName: string
|
authCookieName: string
|
||||||
generateToken: boolean
|
generateToken: boolean
|
||||||
dangerouslySkipAuth: boolean
|
dangerouslySkipAuth: boolean
|
||||||
|
upgrade?: string | boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_HOST = "127.0.0.1"
|
const DEFAULT_HOST = "127.0.0.1"
|
||||||
@@ -124,6 +126,7 @@ function parseCliOptions(argv: string[]): CliOptions {
|
|||||||
.env("CODENOMAD_SKIP_AUTH")
|
.env("CODENOMAD_SKIP_AUTH")
|
||||||
.default(false),
|
.default(false),
|
||||||
)
|
)
|
||||||
|
.addOption(new Option("--upgrade [version]", "Upgrade the global CodeNomad CLI server package and exit"))
|
||||||
|
|
||||||
program.parse(argv, { from: "user" })
|
program.parse(argv, { from: "user" })
|
||||||
const parsed = program.opts<{
|
const parsed = program.opts<{
|
||||||
@@ -153,8 +156,10 @@ function parseCliOptions(argv: string[]): CliOptions {
|
|||||||
authCookieName: string
|
authCookieName: string
|
||||||
generateToken?: boolean
|
generateToken?: boolean
|
||||||
dangerouslySkipAuth?: boolean
|
dangerouslySkipAuth?: boolean
|
||||||
|
upgrade?: string | boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const upgrade = parsed.upgrade
|
||||||
const parseBooleanEnv = (value: string | undefined): boolean => {
|
const parseBooleanEnv = (value: string | undefined): boolean => {
|
||||||
const normalized = (value ?? "").trim().toLowerCase()
|
const normalized = (value ?? "").trim().toLowerCase()
|
||||||
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "y" || normalized === "on"
|
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 httpsEnabled = parseBooleanEnv(parsed.https)
|
||||||
const httpEnabled = parseBooleanEnv(parsed.http)
|
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)")
|
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,
|
authCookieName: parsed.authCookieName,
|
||||||
generateToken: Boolean(parsed.generateToken),
|
generateToken: Boolean(parsed.generateToken),
|
||||||
dangerouslySkipAuth: Boolean(parsed.dangerouslySkipAuth),
|
dangerouslySkipAuth: Boolean(parsed.dangerouslySkipAuth),
|
||||||
|
upgrade,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,6 +238,12 @@ function programHasArg(argv: string[], flag: string): boolean {
|
|||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const options = parseCliOptions(process.argv.slice(2))
|
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 logger = createLogger({ level: options.logLevel, destination: options.logDestination, component: "app" })
|
||||||
const workspaceLogger = logger.child({ component: "workspace" })
|
const workspaceLogger = logger.child({ component: "workspace" })
|
||||||
const configLogger = logger.child({ component: "config" })
|
const configLogger = logger.child({ component: "config" })
|
||||||
|
|||||||
Reference in New Issue
Block a user