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

@@ -77,7 +77,7 @@ bun run build:all
The build script performs these steps:
1. **Build @codenomad/cli** → Produces the CLI `dist/` bundle (also rebuilds the UI assets it serves)
1. **Build @neuralnomads/codenomad** → Produces the CLI `dist/` bundle (also rebuilds the UI assets it serves)
2. **Compile TypeScript + bundle with Vite** → Electron main, preload, and renderer output in `dist/`
3. **Package with electron-builder** → Platform-specific binaries

View File

@@ -44,7 +44,7 @@ Grab the latest build for macOS, Windows, and Linux from the [GitHub Releases pa
## CLI Server Flags
The bundled CLI server (`@codenomad/cli`) controls which folders the UI can browse when you pick a workspace:
The bundled CLI server (`@neuralnomads/codenomad`) controls which folders the UI can browse when you pick a workspace:
- `--workspace-root <path>` (default: current working directory) scopes browsing to a safe subtree. The UI can only see folders beneath this root.
- `--unrestricted-root` explicitly allows full-machine browsing for the current process. In this mode the UI starts from the host home directory, adds a "parent" option so you can reach `/` on macOS/Linux, and lists drives/UNC paths on Windows. The flag is runtime-only—restart the CLI without it to go back to restricted mode.

16
package-lock.json generated
View File

@@ -313,8 +313,8 @@
"node": ">=6.9.0"
}
},
"node_modules/@codenomad/cli": {
"resolved": "packages/cli",
"node_modules/@neuralnomads/codenomad": {
"resolved": "packages/server",
"link": true
},
"node_modules/@codenomad/electron-app": {
@@ -8390,8 +8390,8 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"packages/cli": {
"name": "@codenomad/cli",
"packages/server": {
"name": "@neuralnomads/codenomad",
"version": "0.1.0",
"dependencies": {
"@fastify/cors": "^8.5.0",
@@ -8405,7 +8405,7 @@
"zod": "^3.23.8"
},
"bin": {
"codenomad-cli": "dist/bin.js"
"codenomad": "dist/bin.js"
},
"devDependencies": {
"cross-env": "^7.0.3",
@@ -8414,7 +8414,7 @@
"typescript": "^5.6.3"
}
},
"packages/cli/node_modules/commander": {
"packages/server/node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
@@ -8423,7 +8423,7 @@
"node": ">=18"
}
},
"packages/cli/node_modules/fuzzysort": {
"packages/server/node_modules/fuzzysort": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-2.0.4.tgz",
"integrity": "sha512-Api1mJL+Ad7W7vnDZnWq5pGaXJjyencT+iKGia2PlHUcSsSzWwIQ3S1isiMpwpavjYtGd2FzhUIhnnhOULZgDw==",
@@ -8433,7 +8433,7 @@
"name": "@codenomad/electron-app",
"version": "0.1.2",
"dependencies": {
"@codenomad/cli": "file:../cli",
"@neuralnomads/codenomad": "file:../server",
"@codenomad/ui": "file:../ui"
},
"devDependencies": {

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,