Rename CLI package to @neuralnomads/codenomad and bin codenomad

This commit is contained in:
Shantur Rathore
2025-11-20 23:51:44 +00:00
parent 30b075e4ba
commit d6fdef68d9
45 changed files with 238 additions and 47 deletions

View File

@@ -264,9 +264,9 @@ export class CliProcessManager extends EventEmitter {
if (options.dev) {
const tsxPath = this.resolveTsx()
const sourceCandidates = [
path.resolve(app.getAppPath(), "..", "cli", "src", "index.ts"),
path.resolve(app.getAppPath(), "..", "packages", "cli", "src", "index.ts"),
path.resolve(process.cwd(), "packages", "cli", "src", "index.ts"),
path.resolve(app.getAppPath(), "..", "server", "src", "index.ts"),
path.resolve(app.getAppPath(), "..", "packages", "server", "src", "index.ts"),
path.resolve(process.cwd(), "packages", "server", "src", "index.ts"),
]
const sourceEntry = sourceCandidates.find((candidate) => existsSync(candidate))
if (tsxPath && sourceEntry) {
@@ -279,7 +279,7 @@ export class CliProcessManager extends EventEmitter {
return { entry: dist, runner: "node" }
}
throw new Error("Unable to locate CodeNomad CLI build (dist/bin.js). Please build @codenomad/cli.")
throw new Error("Unable to locate CodeNomad CLI build (dist/bin.js). Please build @neuralnomads/codenomad.")
}
private resolveTsx(): string | null {
@@ -296,12 +296,12 @@ export class CliProcessManager extends EventEmitter {
private tryResolveDist(): string | null {
const candidates: Array<string | (() => string)> = [
() => nodeRequire.resolve("@codenomad/cli/dist/bin.js"),
() => nodeRequire.resolve("@codenomad/cli/dist/bin.js", { paths: [app.getAppPath()] }),
path.join(app.getAppPath(), "node_modules", "@codenomad", "cli", "dist", "bin.js"),
path.resolve(app.getAppPath(), "..", "cli", "dist", "bin.js"),
path.resolve(app.getAppPath(), "..", "packages", "cli", "dist", "bin.js"),
path.join(process.resourcesPath, "app.asar.unpacked", "node_modules", "@codenomad", "cli", "dist", "bin.js"),
() => nodeRequire.resolve("@neuralnomads/codenomad/dist/bin.js"),
() => nodeRequire.resolve("@neuralnomads/codenomad/dist/bin.js", { paths: [app.getAppPath()] }),
path.join(app.getAppPath(), "node_modules", "@neuralnomads", "codenomad", "dist", "bin.js"),
path.resolve(app.getAppPath(), "..", "server", "dist", "bin.js"),
path.resolve(app.getAppPath(), "..", "packages", "server", "dist", "bin.js"),
path.join(process.resourcesPath, "app.asar.unpacked", "node_modules", "@neuralnomads", "codenomad", "dist", "bin.js"),
]
for (const candidate of candidates) {

View File

@@ -29,7 +29,7 @@
"package:linux": "electron-builder --linux"
},
"dependencies": {
"@codenomad/cli": "file:../cli",
"@neuralnomads/codenomad": "file:../server",
"@codenomad/ui": "file:../ui"
},
"devDependencies": {

View File

@@ -96,7 +96,7 @@ async function build(platform) {
try {
console.log("📦 Step 1/3: Building CLI dependency...\n")
await run(npmCmd, ["run", "build", "--workspace", "@codenomad/cli"], {
await run(npmCmd, ["run", "build", "--workspace", "@neuralnomads/codenomad"], {
cwd: workspaceRoot,
env: { NODE_PATH: workspaceNodeModulesPath },
})

View File

@@ -1,11 +1,11 @@
{
"name": "@codenomad/cli",
"name": "@neuralnomads/codenomad",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@codenomad/cli",
"name": "@neuralnomads/codenomad",
"version": "0.1.0",
"dependencies": {
"@fastify/cors": "^8.5.0",

View File

@@ -1,11 +1,11 @@
{
"name": "@codenomad/cli",
"name": "@neuralnomads/codenomad",
"version": "0.1.0",
"description": "CodeNomad CLI server for HTTP/SSE control plane",
"type": "module",
"main": "dist/index.js",
"bin": {
"codenomad-cli": "dist/bin.js"
"codenomad": "dist/bin.js"
},
"scripts": {
"build": "npm run build:ui && npm run prepare-ui && tsc -p tsconfig.json",

View File

@@ -15,6 +15,7 @@ import { EventBus } from "./events/bus"
import { ServerMeta } from "./api-types"
import { InstanceStore } from "./storage/instance-store"
import { createLogger } from "./logger"
import { launchInBrowser } from "./launcher"
const require = createRequire(import.meta.url)
const packageJson = require("../package.json") as { version: string }
@@ -32,6 +33,7 @@ interface CliOptions {
logDestination?: string
uiStaticDir: string
uiDevServer?: string
launch: boolean
}
const DEFAULT_PORT = 9898
@@ -40,7 +42,7 @@ const DEFAULT_CONFIG_PATH = "~/.config/codenomad/config.json"
function parseCliOptions(argv: string[]): CliOptions {
const program = new Command()
.name("codenomad-cli")
.name("codenomad")
.description("CodeNomad CLI server")
.version(packageJson.version, "-v, --version", "Show the CLI version")
.addOption(new Option("--host <host>", "Host interface to bind").env("CLI_HOST").default(DEFAULT_HOST))
@@ -57,6 +59,7 @@ function parseCliOptions(argv: string[]): CliOptions {
new Option("--ui-dir <path>", "Directory containing the built UI bundle").env("CLI_UI_DIR").default(DEFAULT_UI_STATIC_DIR),
)
.addOption(new Option("--ui-dev-server <url>", "Proxy UI requests to a running dev server").env("CLI_UI_DEV_SERVER"))
.addOption(new Option("--launch", "Launch the UI in a browser after start").env("CLI_LAUNCH").default(false))
program.parse(argv, { from: "user" })
const parsed = program.opts<{
@@ -70,6 +73,7 @@ function parseCliOptions(argv: string[]): CliOptions {
logDestination?: string
uiDir: string
uiDevServer?: string
launch?: boolean
}>()
const resolvedRoot = parsed.workspaceRoot ?? parsed.root ?? process.cwd()
@@ -84,6 +88,7 @@ function parseCliOptions(argv: string[]): CliOptions {
logDestination: parsed.logDestination,
uiStaticDir: parsed.uiDir,
uiDevServer: parsed.uiDevServer,
launch: Boolean(parsed.launch),
}
}
@@ -139,11 +144,13 @@ async function main() {
logger,
})
const startInfo = await server.start()
logger.info({ port: startInfo.port, host: options.host }, "HTTP server listening")
console.log(`CodeNomad Server is ready at ${startInfo.url}`)
await server.start()
logger.info({ port: options.port, host: options.host }, "HTTP server listening")
const displayHost = options.host === "127.0.0.1" || options.host === "0.0.0.0" ? "localhost" : options.host
console.log(`CodeNomad Server is ready at http://${displayHost}:${options.port}`)
if (options.launch) {
await launchInBrowser(startInfo.url, logger.child({ component: "launcher" }))
}
let shuttingDown = false

View File

@@ -0,0 +1,177 @@
import { spawn } from "child_process"
import os from "os"
import path from "path"
import type { Logger } from "./logger"
interface BrowserCandidate {
name: string
command: string
args: (url: string) => string[]
}
const APP_ARGS = (url: string) => [`--app=${url}`, "--new-window"]
export async function launchInBrowser(url: string, logger: Logger): Promise<boolean> {
const { platform, candidates, manualExamples } = buildPlatformCandidates(url)
console.log(`Attempting to launch browser (${platform}) using:`)
candidates.forEach((candidate) => console.log(` - ${candidate.name}: ${candidate.command}`))
for (const candidate of candidates) {
const success = await tryLaunch(candidate, url, logger)
if (success) {
return true
}
}
console.error(
"No supported browser found to launch. Run without --launch and use one of the commands below or install a compatible browser.",
)
if (manualExamples.length > 0) {
console.error("Manual launch commands:")
manualExamples.forEach((line) => console.error(` ${line}`))
}
return false
}
async function tryLaunch(candidate: BrowserCandidate, url: string, logger: Logger): Promise<boolean> {
return new Promise((resolve) => {
let resolved = false
try {
const args = candidate.args(url)
const child = spawn(candidate.command, args, { stdio: "ignore", detached: true })
child.once("error", (error) => {
if (resolved) return
resolved = true
logger.debug({ err: error, candidate: candidate.name, command: candidate.command, args }, "Browser launch failed")
resolve(false)
})
child.once("spawn", () => {
if (resolved) return
resolved = true
logger.info(
{
browser: candidate.name,
command: candidate.command,
args,
fullCommand: [candidate.command, ...args].join(" "),
},
"Launched browser in app mode",
)
child.unref()
resolve(true)
})
} catch (error) {
if (resolved) return
resolved = true
logger.debug({ err: error, candidate: candidate.name, command: candidate.command }, "Browser spawn threw")
resolve(false)
}
})
}
function buildPlatformCandidates(url: string) {
switch (os.platform()) {
case "darwin":
return {
platform: "macOS",
candidates: buildMacCandidates(),
manualExamples: buildMacManualExamples(url),
}
case "win32":
return {
platform: "Windows",
candidates: buildWindowsCandidates(),
manualExamples: buildWindowsManualExamples(url),
}
default:
return {
platform: "Linux",
candidates: buildLinuxCandidates(),
manualExamples: buildLinuxManualExamples(url),
}
}
}
function buildMacCandidates(): BrowserCandidate[] {
const apps = [
{ name: "Google Chrome", path: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" },
{ name: "Google Chrome Canary", path: "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary" },
{ name: "Microsoft Edge", path: "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge" },
{ name: "Brave Browser", path: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser" },
{ name: "Chromium", path: "/Applications/Chromium.app/Contents/MacOS/Chromium" },
{ name: "Vivaldi", path: "/Applications/Vivaldi.app/Contents/MacOS/Vivaldi" },
{ name: "Arc", path: "/Applications/Arc.app/Contents/MacOS/Arc" },
]
return apps.map((entry) => ({ name: entry.name, command: entry.path, args: APP_ARGS }))
}
function buildWindowsCandidates(): BrowserCandidate[] {
const programFiles = process.env["ProgramFiles"]
const programFilesX86 = process.env["ProgramFiles(x86)"]
const localAppData = process.env["LocalAppData"]
const paths = [
[programFiles, "Google/Chrome/Application/chrome.exe", "Google Chrome"],
[programFilesX86, "Google/Chrome/Application/chrome.exe", "Google Chrome (x86)"],
[localAppData, "Google/Chrome/Application/chrome.exe", "Google Chrome (User)"],
[programFiles, "Microsoft/Edge/Application/msedge.exe", "Microsoft Edge"],
[programFilesX86, "Microsoft/Edge/Application/msedge.exe", "Microsoft Edge (x86)"],
[localAppData, "Microsoft/Edge/Application/msedge.exe", "Microsoft Edge (User)"],
[programFiles, "BraveSoftware/Brave-Browser/Application/brave.exe", "Brave"],
[localAppData, "BraveSoftware/Brave-Browser/Application/brave.exe", "Brave (User)"],
[programFiles, "Chromium/Application/chromium.exe", "Chromium"],
] as const
return paths
.filter(([root]) => Boolean(root))
.map(([root, rel, name]) => ({
name,
command: path.join(root as string, rel),
args: APP_ARGS,
}))
}
function buildLinuxCandidates(): BrowserCandidate[] {
const names = [
"google-chrome",
"google-chrome-stable",
"chromium",
"chromium-browser",
"brave-browser",
"microsoft-edge",
"microsoft-edge-stable",
"vivaldi",
]
return names.map((name) => ({ name, command: name, args: APP_ARGS }))
}
function buildMacManualExamples(url: string) {
return [
`"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --app="${url}" --new-window`,
`"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge" --app="${url}" --new-window`,
`"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser" --app="${url}" --new-window`,
]
}
function buildWindowsManualExamples(url: string) {
return [
`"%ProgramFiles%\\Google\\Chrome\\Application\\chrome.exe" --app="${url}" --new-window`,
`"%ProgramFiles%\\Microsoft\\Edge\\Application\\msedge.exe" --app="${url}" --new-window`,
`"%ProgramFiles%\\BraveSoftware\\Brave-Browser\\Application\\brave.exe" --app="${url}" --new-window`,
]
}
function buildLinuxManualExamples(url: string) {
return [
`google-chrome --app="${url}" --new-window`,
`chromium --app="${url}" --new-window`,
`brave-browser --app="${url}" --new-window`,
`microsoft-edge --app="${url}" --new-window`,
]
}

View File

@@ -1,7 +1,7 @@
import Fastify, { type FastifyInstance, type FastifyReply, type FastifyRequest } from "fastify"
import cors from "@fastify/cors"
import fastifyStatic from "@fastify/static"
import replyFrom, { type FastifyReplyFromOptions } from "@fastify/reply-from"
import replyFrom from "@fastify/reply-from"
import fs from "fs"
import path from "path"
import { fetch } from "undici"
@@ -36,6 +36,11 @@ interface HttpServerDeps {
logger: Logger
}
interface HttpServerStartResult {
port: number
url: string
displayHost: string
}
export function createHttpServer(deps: HttpServerDeps) {
const app = Fastify({ logger: false })
@@ -83,8 +88,9 @@ export function createHttpServer(deps: HttpServerDeps) {
return {
instance: app,
start: async () => {
start: async (): Promise<HttpServerStartResult> => {
const addressInfo = await app.listen({ port: deps.port, host: deps.host })
let actualPort = deps.port
if (typeof addressInfo === "string") {
@@ -101,13 +107,14 @@ export function createHttpServer(deps: HttpServerDeps) {
}
}
const displayHost = deps.host === "0.0.0.0" ? "127.0.0.1" : deps.host
const displayHost = deps.host === "0.0.0.0" ? "127.0.0.1" : deps.host === "127.0.0.1" ? "localhost" : deps.host
const serverUrl = `http://${displayHost}:${actualPort}`
deps.serverMeta.httpBaseUrl = `http://${displayHost}:${actualPort}`
deps.serverMeta.httpBaseUrl = serverUrl
deps.logger.info({ port: actualPort, host: deps.host }, "HTTP server listening")
console.log(`CodeNomad Server is ready at http://${displayHost}:${actualPort}`)
console.log(`CodeNomad Server is ready at ${serverUrl}`)
return actualPort
return { port: actualPort, url: serverUrl, displayHost }
},
stop: () => {
closeSseClients()

View File

@@ -1,7 +1,7 @@
import { Component, Show, For, createSignal, createMemo, createEffect, onCleanup } from "solid-js"
import { ArrowUpLeft, Folder as FolderIcon, Loader2, X } from "lucide-solid"
import type { FileSystemEntry, FileSystemListingMetadata } from "../../../cli/src/api-types"
import { WINDOWS_DRIVES_ROOT } from "../../../cli/src/api-types"
import type { FileSystemEntry, FileSystemListingMetadata } from "../../../server/src/api-types"
import { WINDOWS_DRIVES_ROOT } from "../../../server/src/api-types"
import { cliApi } from "../lib/api-client"
function normalizePathKey(input?: string | null) {

View File

@@ -1,6 +1,6 @@
import { Component, Show, For, createSignal, createMemo, createEffect, onCleanup } from "solid-js"
import { Folder as FolderIcon, File as FileIcon, Loader2, Search, X, ArrowUpLeft } from "lucide-solid"
import type { FileSystemEntry, FileSystemListingMetadata } from "../../../cli/src/api-types"
import type { FileSystemEntry, FileSystemListingMetadata } from "../../../server/src/api-types"
import { cliApi } from "../lib/api-client"
const MAX_RESULTS = 200

View File

@@ -16,7 +16,7 @@ import type {
WorkspaceLogEntry,
WorkspaceEventPayload,
WorkspaceEventType,
} from "../../../cli/src/api-types"
} from "../../../server/src/api-types"
const FALLBACK_API_BASE = "http://127.0.0.1:9898"
const RUNTIME_BASE = typeof window !== "undefined" ? window.location?.origin : undefined

View File

@@ -1,4 +1,4 @@
import type { WorkspaceEventPayload, WorkspaceEventType } from "../../../cli/src/api-types"
import type { WorkspaceEventPayload, WorkspaceEventType } from "../../../server/src/api-types"
import { cliApi } from "./api-client"
const RETRY_BASE_DELAY = 1000

View File

@@ -1,4 +1,4 @@
import type { ServerMeta } from "../../../cli/src/api-types"
import type { ServerMeta } from "../../../server/src/api-types"
import { cliApi } from "./api-client"
let cachedMeta: ServerMeta | null = null

View File

@@ -1,4 +1,4 @@
import type { AppConfig, InstanceData } from "../../../cli/src/api-types"
import type { AppConfig, InstanceData } from "../../../server/src/api-types"
import { cliApi } from "./api-client"
import { cliEvents } from "./cli-events"

View File

@@ -1,5 +1,5 @@
import { createContext, createMemo, createSignal, onCleanup, type Accessor, type ParentComponent, useContext } from "solid-js"
import type { InstanceData } from "../../../cli/src/api-types"
import type { InstanceData } from "../../../server/src/api-types"
import { storage } from "../lib/storage"
const DEFAULT_INSTANCE_DATA: InstanceData = { messageHistory: [], agentModelSelections: {} }

View File

@@ -6,7 +6,7 @@ import { sdkManager } from "../lib/sdk-manager"
import { sseManager } from "../lib/sse-manager"
import { cliApi } from "../lib/api-client"
import { cliEvents } from "../lib/cli-events"
import type { WorkspaceDescriptor, WorkspaceEventPayload, WorkspaceLogEntry } from "../../../cli/src/api-types"
import type { WorkspaceDescriptor, WorkspaceEventPayload, WorkspaceLogEntry } from "../../../server/src/api-types"
import { ensureInstanceConfigLoaded } from "./instance-config"
import {
fetchSessions,

View File

@@ -1,4 +1,4 @@
import type { InstanceData } from "../../../cli/src/api-types"
import type { InstanceData } from "../../../server/src/api-types"
import {
ensureInstanceConfigLoaded,
getInstanceConfig,