Skip to content
Merged

Dev #97

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: Release

on:
push:
tags:
- 'v*'

permissions:
contents: write

jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
platform: win
- os: macos-latest
platform: mac
- os: ubuntu-latest
platform: linux
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Bundle backend
run: pnpm --filter @codingcode/desktop run bundle:backend

- name: Build Electron app
run: pnpm --filter @codingcode/desktop run build

- name: Package (no publish)
run: pnpm --filter @codingcode/desktop exec electron-builder --${{ matrix.platform }} --publish never

- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist-${{ matrix.platform }}
path: |
packages/desktop/dist/*.exe
packages/desktop/dist/*.exe.blockmap
packages/desktop/dist/*.yml
packages/desktop/dist/*.dmg
packages/desktop/dist/*.zip
packages/desktop/dist/*.AppImage
if-no-files-found: warn

release:
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: assets
merge-multiple: true

- name: Create GitHub Release and upload assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "${{ github.ref_name }}" \
--title "${{ github.ref_name }}" \
--generate-notes \
assets/*
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@
"lint:fix": "eslint \"packages/*/src/**/*.ts\" \"packages/*/src/**/*.tsx\" \"packages/*/test/**/*.ts\" --fix",
"format": "prettier --write \"packages/**/*.{ts,tsx,json}\" \"*.json\" \"*.ts\"",
"format:check": "prettier --check \"packages/**/*.{ts,tsx,json}\" \"*.json\" \"*.ts\"",
"debug": "tsx src/debug.ts"
"debug": "tsx src/debug.ts",
"dist": "pnpm --filter @codingcode/desktop run dist",
"dist:win": "pnpm --filter @codingcode/desktop run dist:win",
"dist:mac": "pnpm --filter @codingcode/desktop run dist:mac",
"dist:linux": "pnpm --filter @codingcode/desktop run dist:linux"
},
"dependencies": {
"@ai-sdk/deepseek": "^2.0.35",
Expand All @@ -43,6 +47,7 @@
"@eslint/js": "^10.0.1",
"@types/node": "^22.0.0",
"eslint": "^10.4.1",
"esbuild": "^0.25.0",
"playwright-core": "^1.60.0",
"prettier": "^3.8.3",
"tsx": "^4.19.0",
Expand Down
3 changes: 3 additions & 0 deletions packages/codingcode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"version": "0.1.0",
"type": "module",
"main": "./src/layer.ts",
"scripts": {
"bundle": "node scripts/bundle.mjs"
},
"exports": {
".": "./src/layer.ts",
"./agent/agent": "./src/agent/agent.ts",
Expand Down
29 changes: 29 additions & 0 deletions packages/codingcode/scripts/bundle.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { build } from 'esbuild';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const root = resolve(__dirname, '..');

await build({
entryPoints: [resolve(root, 'src/cli.ts')],
bundle: true,
platform: 'node',
format: 'cjs',
target: 'node20',
outfile: resolve(root, 'dist/cli.bundle.js'),
// pino-pretty 仅在开发模式使用,生产模式不会加载
external: ['pino-pretty'],
// 动态 import 的 TUI 路径无法静态解析,保持原样
splitting: false,
sourcemap: true,
minify: false,
define: {
'process.env.NODE_ENV': JSON.stringify('production'),
},
logOverride: {
'commonjs-variable-in-esm': 'silent',
},
});

console.log('Backend bundle written to dist/cli.bundle.js');
2 changes: 1 addition & 1 deletion packages/codingcode/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { serve } from '@hono/node-server';
import { LLMFactoryService } from './llm/factory.js';
import { createServer } from './server/index.js';
import { createAppRuntime } from './layer.js';
import { loadConfig, ensureUserConfig } from '../../infra/src/config.js';
import { loadConfig, ensureUserConfig } from '@codingcode/infra/config';
import { WorkspaceService, parseWorkspaceArgs } from './core/workspace.js';
import { findAvailablePort } from './server/port-discovery.js';
import { AgentError } from './core/error.js';
Expand Down
30 changes: 30 additions & 0 deletions packages/codingcode/test/ci/tooling-scripts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,41 @@
expect(content).toContain('build-desktop:');
});

it('GitHub Actions release workflow exists and is triggered by tags', () => {
const workflowPath = join(root, '.github/workflows/release.yml');
expect(existsSync(workflowPath)).toBe(true);
const content = readFileSync(workflowPath, 'utf8');
expect(content).toContain('tags:');
expect(content).toContain("- 'v*'");
expect(content).toContain('permissions:');
expect(content).toContain('contents: write');
expect(content).toContain('GH_TOKEN');
expect(content).toContain('--publish never');
expect(content).toContain('gh release create');
expect(content).toContain('needs: build');
});

it('electron-builder.yml has publish config for GitHub Releases', () => {
const configPath = join(root, 'packages/desktop/electron-builder.yml');
expect(existsSync(configPath)).toBe(true);
const content = readFileSync(configPath, 'utf8');
expect(content).toContain('publish:');
expect(content).toContain('provider: github');
expect(content).toContain('releaseType: draft');
});

it('desktop package.json has release script', () => {
const pkgPath = join(root, 'packages/desktop/package.json');
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
expect(pkg.scripts.release).toBeDefined();
expect(pkg.scripts.release).toContain('--publish always');
});

it('pnpm run lint exits successfully', () => {
expect(() => execSync('pnpm run lint', { cwd: root, stdio: 'pipe' })).not.toThrow();
}, 20000);

it('pnpm run format:check exits successfully', () => {
expect(() => execSync('pnpm run format:check', { cwd: root, stdio: 'pipe' })).not.toThrow();

Check failure on line 78 in packages/codingcode/test/ci/tooling-scripts.test.ts

View workflow job for this annotation

GitHub Actions / test

packages/codingcode/test/ci/tooling-scripts.test.ts > CI tooling configuration > pnpm run format:check exits successfully

AssertionError: expected [Function] to not throw an error but 'Error: Command failed: pnpm run forma…' was thrown - Expected: undefined + Received: "Error: Command failed: pnpm run format:check [WARN] The \"pnpm\" field in package.json is no longer read by pnpm. The following keys were ignored: \"pnpm.overrides\". See https://pnpm.io/settings for the new home of each setting. $ prettier --check \"packages/**/*.{ts,tsx,json}\" \"*.json\" \"*.ts\" [warn] packages/codingcode/src/session/store.ts [warn] packages/codingcode/test/server/settings-routes.test.ts [warn] packages/desktop/src/agent/MessageStream.tsx [warn] packages/desktop/src/settings/MemoryPanel.tsx [warn] packages/desktop/test/global-store.test.ts [warn] Code style issues found in 5 files. Run Prettier with --write to fix. " ❯ packages/codingcode/test/ci/tooling-scripts.test.ts:78:87
}, 20000);
});
50 changes: 50 additions & 0 deletions packages/desktop/electron-builder.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
appId: com.codingcode.desktop
productName: Coding Agent
copyright: Copyright © 2025

directories:
buildResources: resources
output: dist

files:
- out/**/*
- "!out/shots"

# 后端打包为单文件 bundle,通过 extraResources 放入安装包
extraResources:
- from: ../codingcode/dist/cli.bundle.js
to: backend/cli.bundle.js
- from: ../codingcode/dist/cli.bundle.js.map
to: backend/cli.bundle.js.map
- from: ../../config/models.json
to: config/models.json

asar: true

win:
target:
- target: nsis
arch: [x64]

mac:
target:
- target: dmg
arch: [x64, arm64]
category: public.app-category.developer-tools

linux:
target:
- target: AppImage
arch: [x64]
category: Development

nsis:
oneClick: false
allowToChangeInstallationDirectory: true
createDesktopShortcut: true

publish:
provider: github
owner: phantom5099
repo: coding-code
releaseType: draft
47 changes: 33 additions & 14 deletions packages/desktop/electron/core/child-process.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,60 @@
import { spawn, ChildProcess } from 'child_process';
import { resolve } from 'path';
import { resolve, join } from 'path';
import { app } from 'electron';

let child: ChildProcess | null = null;

function getCliPath(): string {
const root = resolve(app.getAppPath(), '../../');
return resolve(root, 'packages/codingcode/src/cli.ts');
function isDev(): boolean {
// electron-vite 开发模式下会设置此环境变量
return !!process.env.ELECTRON_RENDERER_URL;
}

function getResourcesDir(): string {
return process.platform === 'darwin'
? join(process.execPath, '../../Resources')
: join(process.execPath, '../resources');
}

function getBackendEntry(): string {
if (isDev()) {
return resolve(app.getAppPath(), '../../packages/codingcode/src/cli.ts');
}
// 生产模式:后端 bundle 通过 extraResources 放到 resources/backend/
return join(getResourcesDir(), 'backend', 'cli.bundle.js');
}

function getProjectRoot(): string {
return resolve(app.getAppPath(), '../../');
if (isDev()) {
return resolve(app.getAppPath(), '../../');
}
// 生产模式:config/models.json 通过 extraResources 放到 resources/config/
return getResourcesDir();
}

export async function startBackend(): Promise<number> {
return new Promise((resolvePromise, reject) => {
const cliPath = getCliPath();
const entry = getBackendEntry();
const root = getProjectRoot();
const isWin = process.platform === 'win32';

if (isWin) {
// On Windows, .cmd files require shell mode, but spawn with shell:true
// does not auto-quote arguments. Paths with spaces get split.
// Solution: construct the full command string and pass it directly.
if (isDev()) {
// 开发模式:tsx 运行 TypeScript 源码
const tsxPath = resolve(root, 'node_modules/.bin/tsx.cmd');
const cmd = `"${tsxPath}" "${cliPath}" serve`;
const cmd = `"${tsxPath}" "${entry}" serve`;
child = spawn(cmd, [], {
cwd: root,
stdio: ['ignore', 'pipe', 'pipe'],
shell: true,
});
} else {
// On Unix, no shell needed — spawn handles paths with spaces natively.
const tsxPath = resolve(root, 'node_modules/.bin/tsx');
child = spawn(tsxPath, [cliPath, 'serve'], {
// 生产模式:node 运行 esbuild 打包后的单文件
child = spawn('node', [entry, 'serve'], {
cwd: root,
stdio: ['ignore', 'pipe', 'pipe'],
env: {
...process.env,
NODE_ENV: 'production',
},
});
}

Expand Down
9 changes: 8 additions & 1 deletion packages/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@
"scripts": {
"dev": "node scripts/dev.mjs",
"build": "electron-vite build",
"bundle:backend": "pnpm --filter @codingcode/core run bundle",
"preview": "electron-vite preview",
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.node.json",
"verify": "electron-vite build && node scripts/verify.mjs",
"test": "vitest run"
"test": "vitest run",
"pack": "electron-builder --dir",
"dist": "pnpm run bundle:backend && pnpm run build && electron-builder",
"dist:win": "pnpm run bundle:backend && pnpm run build && electron-builder --win",
"dist:mac": "pnpm run bundle:backend && pnpm run build && electron-builder --mac",
"dist:linux": "pnpm run bundle:backend && pnpm run build && electron-builder --linux",
"release": "pnpm run bundle:backend && pnpm run build && electron-builder --publish always"
},
"dependencies": {
"@codingcode/core": "workspace:*",
Expand Down
9 changes: 5 additions & 4 deletions packages/desktop/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { useEffect } from 'react';
import { useGlobalStore } from './stores/global.store';
import { useUIStore } from './stores/ui.store';
import { useWorkspaceStore } from './stores/workspace.store';
import AgentLayout from './layouts/AgentLayout';
import IDELayout from './layouts/IDELayout';
import TitleBar from './TitleBar';
import ErrorBoundary from './shared/ErrorBoundary';

export default function App() {
const mode = useGlobalStore((s) => s.ui.mode);
const theme = useGlobalStore((s) => s.ui.theme);
const rootPath = useGlobalStore((s) => s.workspace.rootPath);
const mode = useUIStore((s) => s.mode);
const theme = useUIStore((s) => s.theme);
const rootPath = useWorkspaceStore((s) => s.rootPath);

// Sync workspace cwd to main process for git polling
useEffect(() => {
Expand Down
6 changes: 3 additions & 3 deletions packages/desktop/src/TitleBar.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { useGlobalStore } from './stores/global.store';
import { useUIStore } from './stores/ui.store';

const isWindows = window.electronAPI?.platform === 'win32';

export default function TitleBar() {
const mode = useGlobalStore((s) => s.ui.mode);
const setMode = useGlobalStore((s) => s.setMode);
const mode = useUIStore((s) => s.mode);
const setMode = useUIStore((s) => s.setMode);

if (!isWindows) return null;

Expand Down
Loading
Loading