Split workspace into electron and ui packages

This commit is contained in:
Shantur Rathore
2025-11-17 12:06:58 +00:00
parent aa77ca2931
commit 89bd32814f
137 changed files with 407 additions and 1371 deletions

View File

@@ -79,10 +79,10 @@ jobs:
cache: npm cache: npm
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci --workspaces
- name: Build macOS binaries - name: Build macOS binaries
run: npm run build:mac run: npm run build:mac --workspace @codenomad/electron-app
- name: Upload release assets - name: Upload release assets
env: env:
@@ -91,7 +91,7 @@ jobs:
run: | run: |
set -euo pipefail set -euo pipefail
shopt -s nullglob shopt -s nullglob
for file in release/*; do for file in packages/electron-app/release/*; do
[ -f "$file" ] || continue [ -f "$file" ] || continue
case "$file" in case "$file" in
*.dmg|*.zip) *.dmg|*.zip)
@@ -119,10 +119,10 @@ jobs:
cache: npm cache: npm
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci --workspaces
- name: Build Windows binaries - name: Build Windows binaries
run: npm run build:win run: npm run build:win --workspace @codenomad/electron-app
- name: Upload release assets - name: Upload release assets
shell: pwsh shell: pwsh
@@ -130,7 +130,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.prepare-release.outputs.tag }} TAG: ${{ needs.prepare-release.outputs.tag }}
run: | run: |
Get-ChildItem -Path "release" -File | Where-Object { Get-ChildItem -Path "packages/electron-app/release" -File | Where-Object {
$_.Name -match '\.(exe|zip)$' $_.Name -match '\.(exe|zip)$'
} | ForEach-Object { } | ForEach-Object {
gh release upload $env:TAG $_.FullName --clobber gh release upload $env:TAG $_.FullName --clobber
@@ -152,10 +152,10 @@ jobs:
cache: npm cache: npm
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci --workspaces
- name: Build Linux binaries - name: Build Linux binaries
run: npm run build:linux run: npm run build:linux --workspace @codenomad/electron-app
- name: Upload release assets - name: Upload release assets
env: env:
@@ -164,7 +164,7 @@ jobs:
run: | run: |
set -euo pipefail set -euo pipefail
shopt -s nullglob shopt -s nullglob
for file in release/*; do for file in packages/electron-app/release/*; do
[ -f "$file" ] || continue [ -f "$file" ] || continue
case "$file" in case "$file" in
*.AppImage|*.deb|*.tar.gz) *.AppImage|*.deb|*.tar.gz)
@@ -198,10 +198,10 @@ jobs:
sudo gem install --no-document fpm sudo gem install --no-document fpm
- name: Install project dependencies - name: Install project dependencies
run: npm ci run: npm ci --workspaces
- name: Build Linux RPM binaries - name: Build Linux RPM binaries
run: npm run build:linux-rpm run: npm run build:linux-rpm --workspace @codenomad/electron-app
- name: Upload RPM release assets - name: Upload RPM release assets
env: env:
@@ -210,7 +210,7 @@ jobs:
run: | run: |
set -euo pipefail set -euo pipefail
shopt -s nullglob shopt -s nullglob
for file in release/*.rpm; do for file in packages/electron-app/release/*.rpm; do
[ -f "$file" ] || continue [ -f "$file" ] || continue
gh release upload "$TAG" "$file" --clobber gh release upload "$TAG" "$file" --clobber
done done

View File

@@ -10,6 +10,12 @@ This guide explains how to build distributable binaries for CodeNomad.
## Quick Start ## Quick Start
All commands now run inside the workspace packages. From the repo root you can target the Electron app package directly:
```bash
npm run build --workspace @codenomad/electron-app
```
### Build for Current Platform (macOS default) ### Build for Current Platform (macOS default)
```bash ```bash

View File

@@ -22,6 +22,15 @@ CodeNomad is built for people who live inside OpenCode for hours on end and need
- [OpenCode CLI](https://opencode.ai) installed and available in your `PATH`, or point CodeNomad to a local binary through Advanced Settings. - [OpenCode CLI](https://opencode.ai) installed and available in your `PATH`, or point CodeNomad to a local binary through Advanced Settings.
## Repository Layout
CodeNomad now ships as a small workspace with two packages:
- `packages/ui` — SolidJS renderer, Tailwind styles, and standalone Vite configuration for building the UI bundle independently.
- `packages/electron-app` — Electron main/preload processes plus packaging scripts. It consumes the UI package during development/build via `electron-vite`.
Use `npm run dev --workspace @codenomad/electron-app` for the Electron shell and `npm run dev --workspace @codenomad/ui` for UI-only work. Working with the workspace requires Node.js 18+ with npm 7 or newer so the workspace protocol is available.
## Downloads ## Downloads
Grab the latest build for macOS, Windows, and Linux from the [GitHub Releases page](https://github.com/shantur/CodeNomad/releases). Grab the latest build for macOS, Windows, and Linux from the [GitHub Releases page](https://github.com/shantur/CodeNomad/releases).

1256
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,169 +1,24 @@
{ {
"name": "@shantur/codenomad", "name": "codenomad-workspace",
"version": "0.1.2", "version": "0.1.2",
"description": "CodeNomad - AI coding assistant", "private": true,
"author": { "description": "CodeNomad monorepo workspace",
"name": "Shantur Rathore", "workspaces": {
"email": "codenomad@shantur.com" "packages": [
"packages/*"
]
}, },
"type": "module",
"main": "dist/main/main.js",
"scripts": { "scripts": {
"dev": "electron-vite dev", "dev": "npm run dev --workspace @codenomad/electron-app",
"dev:electron": "NODE_ENV=development electron .", "dev:electron": "npm run dev --workspace @codenomad/electron-app",
"build": "electron-vite build", "dev:ui": "npm run dev --workspace @codenomad/ui",
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.node.json", "build": "npm run build --workspace @codenomad/electron-app",
"preview": "electron-vite preview", "build:ui": "npm run build --workspace @codenomad/ui",
"build:binaries": "node scripts/build.js", "build:mac-x64": "npm run build:mac-x64 --workspace @codenomad/electron-app",
"build:mac": "node scripts/build.js mac", "build:binaries": "npm run build:binaries --workspace @codenomad/electron-app",
"build:mac-x64": "node scripts/build.js mac-x64", "typecheck": "npm run typecheck --workspace @codenomad/ui && npm run typecheck --workspace @codenomad/electron-app"
"build:mac-arm64": "node scripts/build.js mac-arm64",
"build:win": "node scripts/build.js win",
"build:win-arm64": "node scripts/build.js win-arm64",
"build:linux": "node scripts/build.js linux",
"build:linux-arm64": "node scripts/build.js linux-arm64",
"build:linux-rpm": "node scripts/build.js linux-rpm",
"build:all": "node scripts/build.js all",
"package:mac": "electron-builder --mac",
"package:win": "electron-builder --win",
"package:linux": "electron-builder --linux"
}, },
"dependencies": { "dependencies": {
"@git-diff-view/solid": "^0.0.8", "7zip-bin": "^5.2.0"
"@kobalte/core": "0.13.11", }
"@opencode-ai/sdk": "1.0.68",
"@solidjs/router": "^0.13.0",
"github-markdown-css": "^5.8.1",
"ignore": "7.0.5",
"lucide-solid": "^0.300.0",
"marked": "^12.0.0",
"shiki": "^3.13.0",
"solid-js": "^1.8.0",
"solid-toast": "^0.5.0"
},
"devDependencies": {
"@tsconfig/bun": "^1.0.9",
"autoprefixer": "10.4.21",
"electron": "39.0.0",
"png2icons": "^2.0.1",
"pngjs": "^7.0.0",
"electron-builder": "^24.0.0",
"electron-vite": "4.0.1",
"postcss": "8.5.6",
"tailwindcss": "3",
"typescript": "^5.3.0",
"vite": "^5.0.0",
"vite-plugin-solid": "^2.10.0"
},
"build": {
"appId": "ai.opencode.client",
"productName": "CodeNomad",
"directories": {
"output": "release",
"buildResources": "electron/resources"
},
"files": [
"dist/**/*",
"package.json"
],
"mac": {
"category": "public.app-category.developer-tools",
"target": [
{
"target": "dmg",
"arch": [
"x64",
"arm64",
"universal"
]
},
{
"target": "zip",
"arch": [
"x64",
"arm64",
"universal"
]
}
],
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}",
"icon": "electron/resources/icon.icns"
},
"dmg": {
"contents": [
{
"x": 130,
"y": 220
},
{
"x": 410,
"y": 220,
"type": "link",
"path": "/Applications"
}
]
},
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64",
"arm64"
]
},
{
"target": "zip",
"arch": [
"x64",
"arm64"
]
}
],
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}",
"icon": "electron/resources/icon.ico"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true
},
"linux": {
"target": [
{
"target": "AppImage",
"arch": [
"x64",
"arm64"
]
},
{
"target": "deb",
"arch": [
"x64",
"arm64"
]
},
{
"target": "rpm",
"arch": [
"x64",
"arm64"
]
},
{
"target": "tar.gz",
"arch": [
"x64",
"arm64"
]
}
],
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}",
"category": "Development",
"icon": "electron/resources/icon.png"
}
},
"private": true
} }

4
packages/electron-app/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules/
dist/
release/
.vite/

View File

@@ -2,6 +2,11 @@ import { defineConfig, externalizeDepsPlugin } from "electron-vite"
import solid from "vite-plugin-solid" import solid from "vite-plugin-solid"
import { resolve } from "path" import { resolve } from "path"
const uiRoot = resolve(__dirname, "../ui")
const uiSrc = resolve(uiRoot, "src")
const uiRendererRoot = resolve(uiRoot, "src/renderer")
const uiRendererEntry = resolve(uiRendererRoot, "index.html")
export default defineConfig({ export default defineConfig({
main: { main: {
plugins: [externalizeDepsPlugin()], plugins: [externalizeDepsPlugin()],
@@ -33,21 +38,24 @@ export default defineConfig({
}, },
}, },
renderer: { renderer: {
root: "./src/renderer", root: uiRendererRoot,
plugins: [solid()], plugins: [solid()],
css: { css: {
postcss: "./postcss.config.js", postcss: resolve(uiRoot, "postcss.config.js"),
}, },
resolve: { resolve: {
alias: { alias: {
"@": resolve(__dirname, "./src"), "@": uiSrc,
}, },
}, },
server: { server: {
port: 3000, port: 3000,
}, },
build: { build: {
outDir: "dist/renderer", outDir: resolve(__dirname, "dist/renderer"),
rollupOptions: {
input: uiRendererEntry,
},
}, },
}, },
}) })

View File

@@ -1,38 +1,5 @@
import { contextBridge, ipcRenderer } from "electron" import { contextBridge, ipcRenderer } from "electron"
import type { ElectronAPI } from "../../../ui/src/types/electron-api"
export interface ElectronAPI {
selectFolder: () => Promise<string | null>
createInstance: (
id: string,
folder: string,
binaryPath?: string,
environmentVariables?: Record<string, string>,
) => Promise<{ id: string; port: number; pid: number; binaryPath: string }>
stopInstance: (pid: number) => Promise<void>
onInstanceStarted: (callback: (data: { id: string; port: number; pid: number; binaryPath: string }) => void) => void
onInstanceError: (callback: (data: { id: string; error: string }) => void) => void
onInstanceStopped: (callback: (data: { id: string }) => void) => void
onInstanceLog: (
callback: (data: {
id: string
entry: { timestamp: number; level: "info" | "error" | "warn" | "debug"; message: string }
}) => void,
) => void
onNewInstance: (callback: () => void) => void
scanDirectory: (workspaceFolder: string) => Promise<string[]>
// OpenCode binary operations
selectOpenCodeBinary: () => Promise<string | null>
validateOpenCodeBinary: (path: string) => Promise<{ valid: boolean; version?: string; error?: string }>
// Storage operations
getConfigPath: () => Promise<string>
getInstancesDir: () => Promise<string>
readConfigFile: () => Promise<string>
writeConfigFile: (content: string) => Promise<void>
readInstanceFile: (instanceId: string) => Promise<string>
writeInstanceFile: (instanceId: string, content: string) => Promise<void>
deleteInstanceFile: (instanceId: string) => Promise<void>
onConfigChanged: (callback: () => void) => () => void
}
const electronAPI: ElectronAPI = { const electronAPI: ElectronAPI = {
selectFolder: () => ipcRenderer.invoke("dialog:selectFolder"), selectFolder: () => ipcRenderer.invoke("dialog:selectFolder"),

View File

Before

Width:  |  Height:  |  Size: 422 KiB

After

Width:  |  Height:  |  Size: 422 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -1,5 +1,5 @@
{ {
"extends": "../tsconfig.node.json", "extends": "../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"noEmit": true "noEmit": true
}, },

View File

@@ -0,0 +1,124 @@
{
"name": "@codenomad/electron-app",
"version": "0.1.2",
"description": "CodeNomad - AI coding assistant",
"author": {
"name": "Shantur Rathore",
"email": "codenomad@shantur.com"
},
"type": "module",
"main": "dist/main/main.js",
"scripts": {
"dev": "electron-vite dev",
"dev:electron": "NODE_ENV=development electron .",
"build": "electron-vite build",
"typecheck": "tsc --noEmit -p tsconfig.json",
"preview": "electron-vite preview",
"build:binaries": "node scripts/build.js",
"build:mac": "node scripts/build.js mac",
"build:mac-x64": "node scripts/build.js mac-x64",
"build:mac-arm64": "node scripts/build.js mac-arm64",
"build:win": "node scripts/build.js win",
"build:win-arm64": "node scripts/build.js win-arm64",
"build:linux": "node scripts/build.js linux",
"build:linux-arm64": "node scripts/build.js linux-arm64",
"build:linux-rpm": "node scripts/build.js linux-rpm",
"build:all": "node scripts/build.js all",
"package:mac": "electron-builder --mac",
"package:win": "electron-builder --win",
"package:linux": "electron-builder --linux"
},
"dependencies": {
"@codenomad/ui": "file:../ui",
"ignore": "7.0.5"
},
"devDependencies": {
"7zip-bin": "^5.2.0",
"app-builder-bin": "^4.2.0",
"electron": "39.0.0",
"electron-builder": "^24.0.0",
"electron-vite": "4.0.1",
"png2icons": "^2.0.1",
"pngjs": "^7.0.0",
"typescript": "^5.3.0",
"vite": "^5.0.0",
"vite-plugin-solid": "^2.10.0"
},
"build": {
"appId": "ai.opencode.client",
"productName": "CodeNomad",
"directories": {
"output": "release",
"buildResources": "electron/resources"
},
"files": [
"dist/**/*",
"package.json"
],
"mac": {
"category": "public.app-category.developer-tools",
"target": [
{
"target": "dmg",
"arch": ["x64", "arm64", "universal"]
},
{
"target": "zip",
"arch": ["x64", "arm64", "universal"]
}
],
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}",
"icon": "electron/resources/icon.icns"
},
"dmg": {
"contents": [
{ "x": 130, "y": 220 },
{ "x": 410, "y": 220, "type": "link", "path": "/Applications" }
]
},
"win": {
"target": [
{
"target": "nsis",
"arch": ["x64", "arm64"]
},
{
"target": "zip",
"arch": ["x64", "arm64"]
}
],
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}",
"icon": "electron/resources/icon.ico"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true
},
"linux": {
"target": [
{
"target": "AppImage",
"arch": ["x64", "arm64"]
},
{
"target": "deb",
"arch": ["x64", "arm64"]
},
{
"target": "rpm",
"arch": ["x64", "arm64"]
},
{
"target": "tar.gz",
"arch": ["x64", "arm64"]
}
],
"artifactName": "${productName}-${version}-${os}-${arch}.${ext}",
"category": "Development",
"icon": "electron/resources/icon.png"
}
},
"private": true
}

3
packages/ui/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules/
dist/
.vite/

32
packages/ui/package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "@codenomad/ui",
"version": "0.1.2",
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit -p tsconfig.json"
},
"dependencies": {
"@git-diff-view/solid": "^0.0.8",
"@kobalte/core": "0.13.11",
"@opencode-ai/sdk": "1.0.68",
"@solidjs/router": "^0.13.0",
"github-markdown-css": "^5.8.1",
"lucide-solid": "^0.300.0",
"marked": "^12.0.0",
"shiki": "^3.13.0",
"solid-js": "^1.8.0",
"solid-toast": "^0.5.0"
},
"devDependencies": {
"autoprefixer": "10.4.21",
"postcss": "8.5.6",
"tailwindcss": "3",
"typescript": "^5.3.0",
"vite": "^5.0.0",
"vite-plugin-solid": "^2.10.0"
}
}

View File

@@ -0,0 +1,11 @@
import { fileURLToPath } from "url"
import { dirname, resolve } from "path"
const __dirname = dirname(fileURLToPath(import.meta.url))
export default {
plugins: {
tailwindcss: { config: resolve(__dirname, "tailwind.config.js") },
autoprefixer: {},
},
}

View File

@@ -1,7 +1,7 @@
import { Component } from "solid-js" import { Component } from "solid-js"
import { Loader2 } from "lucide-solid" import { Loader2 } from "lucide-solid"
const codeNomadIcon = new URL("../../images/CodeNomad-Icon.png", import.meta.url).href const codeNomadIcon = new URL("../images/CodeNomad-Icon.png", import.meta.url).href
interface EmptyStateProps { interface EmptyStateProps {
onSelectFolder: () => void onSelectFolder: () => void

View File

@@ -4,7 +4,7 @@ import { useConfig } from "../stores/preferences"
import AdvancedSettingsModal from "./advanced-settings-modal" import AdvancedSettingsModal from "./advanced-settings-modal"
import Kbd from "./kbd" import Kbd from "./kbd"
const codeNomadLogo = new URL("../../images/CodeNomad-Icon.png", import.meta.url).href const codeNomadLogo = new URL("../images/CodeNomad-Icon.png", import.meta.url).href
interface FolderSelectionViewProps { interface FolderSelectionViewProps {
onSelectFolder: (folder?: string, binaryPath?: string) => void onSelectFolder: (folder?: string, binaryPath?: string) => void

View File

@@ -34,7 +34,7 @@ import { useConfig } from "../stores/preferences"
import { getSessionInfo, computeDisplayParts, sessions, setActiveSession, setActiveParentSession } from "../stores/sessions" import { getSessionInfo, computeDisplayParts, sessions, setActiveSession, setActiveParentSession } from "../stores/sessions"
import { setActiveInstanceId } from "../stores/instances" import { setActiveInstanceId } from "../stores/instances"
const codeNomadLogo = new URL("../../images/CodeNomad-Icon.png", import.meta.url).href const codeNomadLogo = new URL("../images/CodeNomad-Icon.png", import.meta.url).href
const SCROLL_OFFSET = 64 const SCROLL_OFFSET = 64
const SCROLL_DIRECTION_THRESHOLD = 10 const SCROLL_DIRECTION_THRESHOLD = 10

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Some files were not shown because too many files have changed in this diff Show More