Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
42f1bcc
feat(desktop): add Tauri shell
awsl233777 May 25, 2026
6e2d283
fix(desktop): include Tauri Cargo manifest
awsl233777 May 25, 2026
f15c332
fix(desktop): drop backend state lock before shutdown
awsl233777 May 25, 2026
04966dc
fix(desktop): hide backend console window on Windows
awsl233777 May 25, 2026
d69d704
fix(desktop): stage runtime resources before Tauri packaging
awsl233777 May 26, 2026
adccc52
chore(site): update logo
ymkiux May 26, 2026
e232564
chore(site): update logo and add logo-v variant
ymkiux May 26, 2026
dcb7205
chore(desktop): regenerate Tauri app icons
awsl233777 May 26, 2026
6242a2b
fix(desktop): address security review findings
awsl233777 May 27, 2026
bd6cd6b
fix(ci): use resolvable checkout action sha
awsl233777 May 27, 2026
af1e3b5
fix(desktop): restrict cli fallback to debug builds
awsl233777 May 27, 2026
b13f13a
ci(release): attach desktop installers to releases
awsl233777 May 27, 2026
948fdd3
feat(analytics): export usage data
awsl233777 May 27, 2026
c621391
fix(analytics): address usage export review feedback
awsl233777 May 27, 2026
e91b464
test(desktop): verify Tauri icon bundle contract
awsl233777 May 27, 2026
90cab7f
fix(desktop): expose startup diagnostics logs
awsl233777 May 28, 2026
89cfa3f
merge main into pr-168
awsl233777 May 28, 2026
b56e661
fix(desktop): write backend startup log
awsl233777 May 28, 2026
25cb231
fix(desktop): clear stale backend port
awsl233777 May 28, 2026
abcbbe0
fix(desktop): force clear occupied backend port
awsl233777 May 28, 2026
e60905b
fix(desktop): show occupied port guidance
awsl233777 May 28, 2026
77f6d43
fix(desktop): show native startup failures
awsl233777 May 28, 2026
21cc4d4
fix(desktop): elevate stale backend cleanup
awsl233777 May 29, 2026
4be79e9
fix(desktop): require admin via Windows manifest
awsl233777 May 29, 2026
4ebfd74
test(desktop): verify packaged UAC manifest
awsl233777 May 29, 2026
64b24bc
ci(desktop): upload verified Windows exe manifest
awsl233777 May 29, 2026
ba938f1
fix(desktop): bundle node runtime for packaged startup
awsl233777 May 29, 2026
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
97 changes: 97 additions & 0 deletions .github/workflows/desktop-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: desktop-build

on:
workflow_dispatch:
pull_request:
paths:
- 'src-tauri/**'
- 'tools/desktop/**'
- 'web-ui/**'
- 'cli.js'
- 'cli/**'
- 'lib/**'
- 'plugins/**'
- 'package.json'
- 'package-lock.json'
- '.github/workflows/desktop-build.yml'

permissions:
contents: read

jobs:
tauri:
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- name: macOS
os: macos-latest
- name: Windows
os: windows-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
persist-credentials: false

- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '22'
cache: npm

- name: Setup Rust
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8

- name: Install dependencies
run: npm ci

- name: Verify npm package payload
run: npm pack --dry-run --json

- name: Stage desktop runtime resources
run: npm run desktop:stage

- name: Build desktop app
run: npm run desktop:build

- name: Verify Windows app UAC manifest
if: matrix.name == 'Windows'
shell: pwsh
run: |
$exe = Join-Path $PWD 'src-tauri/target/release/codexmate-desktop.exe'
if (!(Test-Path $exe)) {
throw "Built app exe not found: $exe"
}

$mt = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin" -Recurse -Filter mt.exe |
Sort-Object FullName -Descending |
Select-Object -First 1
if (!$mt) {
throw 'Windows manifest tool mt.exe not found'
}

$manifest = Join-Path $env:RUNNER_TEMP 'codexmate-desktop.manifest.xml'
& $mt.FullName -nologo "-inputresource:$exe;#1" "-out:$manifest"
if ($LASTEXITCODE -ne 0) {
throw "mt.exe failed to extract manifest from $exe"
}

$text = Get-Content $manifest -Raw
Write-Host $text
if ($text -notmatch 'requestedExecutionLevel\s+level="requireAdministrator"\s+uiAccess="false"') {
throw 'Windows app manifest does not require administrator privileges'
}
Copy-Item $manifest (Join-Path (Split-Path $exe) 'codexmate-desktop.manifest.xml') -Force

- name: Upload desktop bundles
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: codexmate-desktop-${{ matrix.name }}
path: |
src-tauri/target/release/bundle/**
src-tauri/target/release/codexmate-desktop.exe
src-tauri/target/release/codexmate-desktop.manifest.xml
if-no-files-found: error
135 changes: 107 additions & 28 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: release
name: release
run-name: "${{ github.event.repository.name }} ${{ inputs.tag || 'auto' }}"
on:
workflow_dispatch:
Expand All @@ -11,17 +11,28 @@ permissions:
contents: write

jobs:
release:
resolve:
runs-on: ubuntu-latest
outputs:
release_tag: ${{ steps.resolve.outputs.release_tag }}
release_version: ${{ steps.resolve.outputs.release_version }}
release_mode: ${{ steps.resolve.outputs.release_mode }}
latest_tag: ${{ steps.resolve.outputs.latest_tag }}
package_version: ${{ steps.resolve.outputs.package_version }}
base_version: ${{ steps.resolve.outputs.base_version }}
base_source: ${{ steps.resolve.outputs.base_source }}
tag_exists: ${{ steps.resolve.outputs.tag_exists }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
fetch-depth: 0
fetch-tags: true
persist-credentials: false
- name: Fetch tags
run: git fetch --tags --force
- uses: actions/setup-node@v4
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '18'
cache: 'npm'
Expand Down Expand Up @@ -113,18 +124,6 @@ jobs:
baseSource = tagExists ? 'package_tag' : 'package_version';
}

const envLines = [
`RELEASE_TAG=${resolvedTag}`,
`RELEASE_VERSION=${expectedVersion}`,
`RELEASE_MODE=${mode}`,
`LATEST_TAG=${latestTag}`,
`PACKAGE_VERSION=${pkgVersion}`,
`BASE_VERSION=${baseVersion}`,
`BASE_SOURCE=${baseSource}`,
`TAG_EXISTS=${tagExists ? 'true' : 'false'}`
].join('\n') + '\n';
fs.appendFileSync(process.env.GITHUB_ENV, envLines);

const outputLines = [
`release_tag=${resolvedTag}`,
`release_version=${expectedVersion}`,
Expand Down Expand Up @@ -152,28 +151,99 @@ jobs:
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summaryLines + '\n');
console.log(`::notice title=Resolved Tag::${resolvedTag}`);
NODE

desktop:
needs: resolve
name: desktop-${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- name: macOS
os: macos-latest
- name: Windows
os: windows-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
fetch-depth: 0
fetch-tags: true
persist-credentials: false
- name: Fetch tags
run: git fetch --tags --force
- name: Checkout target tag
if: ${{ steps.resolve.outputs.tag_exists == 'true' }}
if: ${{ needs.resolve.outputs.tag_exists == 'true' }}
env:
RELEASE_TAG: ${{ steps.resolve.outputs.release_tag }}
RELEASE_TAG: ${{ needs.resolve.outputs.release_tag }}
run: |
git rev-parse "refs/tags/${RELEASE_TAG}" >/dev/null 2>&1
git checkout "${RELEASE_TAG}"
- name: Verify tag matches package.json version
if: ${{ steps.resolve.outputs.tag_exists == 'true' }}
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '22'
cache: npm
- name: Verify package.json matches release tag
env:
RELEASE_TAG: ${{ steps.resolve.outputs.release_tag }}
RELEASE_TAG: ${{ needs.resolve.outputs.release_tag }}
run: |
node -e "const pkg=require('./package.json'); const tag=process.env.RELEASE_TAG; const expected='v'+pkg.version; if(tag!==expected){ console.error('Tag '+tag+' does not match package.json version '+expected); process.exit(1);} console.log('Tag matches '+expected);"
node -e "const pkg=require('./package.json'); const tag=process.env.RELEASE_TAG; const expected='v'+pkg.version; if(tag!==expected){ console.error('package.json '+expected+' does not match resolved release tag '+tag); process.exit(1);} console.log('Package matches '+expected);"
- name: Setup Rust
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8
- name: Install dependencies
run: npm ci
- name: Verify npm package payload
run: npm pack --dry-run --json
- name: Stage desktop runtime resources
run: npm run desktop:stage
- name: Build desktop app
run: npm run desktop:build
- name: Upload desktop release assets
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: codexmate-desktop-${{ matrix.name }}
path: |
src-tauri/target/release/bundle/dmg/*.dmg
src-tauri/target/release/bundle/msi/*.msi
src-tauri/target/release/bundle/nsis/*.exe
if-no-files-found: error

release:
needs:
- resolve
- desktop
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
fetch-depth: 0
fetch-tags: true
persist-credentials: false
- name: Fetch tags
run: git fetch --tags --force
- name: Checkout target tag
if: ${{ needs.resolve.outputs.tag_exists == 'true' }}
env:
RELEASE_TAG: ${{ needs.resolve.outputs.release_tag }}
run: |
git rev-parse "refs/tags/${RELEASE_TAG}" >/dev/null 2>&1
git checkout "${RELEASE_TAG}"
- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '18'
cache: 'npm'
Comment on lines +235 to +238
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== setup-node usage and cache configuration =="
rg -n --iglob '*.yml' -C2 "uses:\\s*actions/setup-node@|cache:\\s*['\"]?npm['\"]?" .github/workflows

echo
echo "== workflows with pull_request-style triggers =="
rg -n --iglob '*.yml' -C3 "pull_request|pull_request_target" .github/workflows

Repository: SakuraByteCore/codexmate

Length of output: 5620


Disable npm cache for the Node 18 setup-node step in release.yml.

.github/workflows/release.yml configures actions/setup-node@... with node-version: '18' and cache: 'npm'; omitting the npm cache in the release job reduces cross-run cache-contamination/cache-poisoning risk.

🔒 Suggested change
       - name: Setup Node
         uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
         with:
           node-version: '18'
-          cache: 'npm'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '18'
cache: 'npm'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '18'
🧰 Tools
🪛 zizmor (1.25.2)

[error] 235-235: runtime artifacts potentially vulnerable to a cache poisoning attack (cache-poisoning): this step

(cache-poisoning)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 235 - 238, The release workflow's
Node setup step (uses:
actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020) currently includes
cache: 'npm' which enables npm caching; remove the cache: 'npm' input (or set it
to an empty/disabled value) from that step so the Node 18 setup (node-version:
'18') does not use the npm cache and avoids cross-run cache contamination.

- name: Verify package.json matches release tag
if: ${{ steps.resolve.outputs.tag_exists != 'true' }}
env:
RELEASE_TAG: ${{ steps.resolve.outputs.release_tag }}
RELEASE_TAG: ${{ needs.resolve.outputs.release_tag }}
run: |
node -e "const pkg=require('./package.json'); const tag=process.env.RELEASE_TAG; const expected='v'+pkg.version; if(tag!==expected){ console.error('Current commit package.json '+expected+' does not match resolved release tag '+tag); process.exit(1);} console.log('Current package matches '+expected);"
node -e "const pkg=require('./package.json'); const tag=process.env.RELEASE_TAG; const expected='v'+pkg.version; if(tag!==expected){ console.error('package.json '+expected+' does not match resolved release tag '+tag); process.exit(1);} console.log('Package matches '+expected);"
- name: Compute release name
env:
RELEASE_TAG: ${{ steps.resolve.outputs.release_tag }}
RELEASE_TAG: ${{ needs.resolve.outputs.release_tag }}
run: |
node -e "const p=require('./package.json'); const tag=process.env.RELEASE_TAG; const name=p.name.includes('/')? p.name.split('/')[1]: p.name; const value=name+' '+tag; console.log('RELEASE_NAME='+value);" >> "$GITHUB_ENV"
- name: Pack npm artifact
Expand All @@ -191,15 +261,24 @@ jobs:
cli.js cli/ lib/ plugins/ web-ui.html web-ui/ \
node_modules/ package.json LICENSE README.md README.zh.md
echo "STANDALONE_TGZ=$name" >> "$GITHUB_ENV"
- name: Download desktop release assets
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
pattern: codexmate-desktop-*
path: desktop-release-assets
merge-multiple: true
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65
with:
tag_name: ${{ steps.resolve.outputs.release_tag }}
tag_name: ${{ needs.resolve.outputs.release_tag }}
target_commitish: ${{ github.sha }}
name: ${{ env.RELEASE_NAME }}
prerelease: false
draft: false
files: |
${{ env.PACKAGE_TGZ }}
${{ env.STANDALONE_TGZ }}
desktop-release-assets/**/*.dmg
desktop-release-assets/**/*.msi
desktop-release-assets/**/*.exe
generate_release_notes: true
66 changes: 65 additions & 1 deletion cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ const {
extractSessionDetailPreviewFromTailText,
extractSessionDetailPreviewFromFileFast
} = require('./lib/cli-sessions');
const { listSessionUsageCore } = require('./cli/session-usage');
const { listSessionUsageCore, exportSessionUsageCore } = require('./cli/session-usage');
const { parseAnalyticsExportArgs } = require('./cli/analytics-export-args');
const {
readBundledWebUiCss,
readBundledWebUiHtml,
Expand Down Expand Up @@ -5204,6 +5205,12 @@ async function listSessionUsage(params = {}) {
});
}

async function exportSessionUsage(params = {}) {
return exportSessionUsageCore(params, {
listSessionUsage
});
}

function listSessionPaths(params = {}) {
const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
if (source && source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'all') {
Expand Down Expand Up @@ -9796,6 +9803,47 @@ async function cmdExportSession(args = []) {
console.log();
}

function printAnalyticsUsage() {
console.log('\n用法:');
console.log(' codexmate analytics export [--format csv|json] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--model <MODEL>] [--source <codex|claude|gemini|codebuddy|all>] [--output <PATH|->] [-o <PATH|->]');
console.log('');
}

async function cmdAnalytics(args = []) {
const subcommand = args[0];
if (subcommand !== 'export') {
printAnalyticsUsage();
process.exit(subcommand ? 1 : 0);
}
const parsed = parseAnalyticsExportArgs(args.slice(1));
if (parsed.options.help) {
printAnalyticsUsage();
process.exit(0);
}
if (parsed.error) {
console.error('错误:', parsed.error);
printAnalyticsUsage();
process.exit(1);
}

const result = await exportSessionUsage(parsed.options);
if (result && result.error) {
console.error('导出失败:', result.error);
process.exit(1);
}
const output = parsed.options.output || (result && result.fileName) || `usage-export.${parsed.options.format}`;
if (output === '-') {
process.stdout.write(result && result.content ? result.content : '');
return;
}
const outputPath = path.resolve(process.cwd(), output);
ensureDir(path.dirname(outputPath));
fs.writeFileSync(outputPath, result && result.content ? result.content : '', 'utf-8');
console.log(`\n✓ Usage 已导出: ${outputPath}`);
console.log(` 格式: ${result.format}; rows: ${Array.isArray(result.rows) ? result.rows.length : 0}`);
console.log();
}

function parseStartOptions(args = []) {
const options = { host: '', noBrowser: false };
if (!Array.isArray(args)) {
Expand Down Expand Up @@ -11077,6 +11125,20 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
}
}
break;
case 'export-sessions-usage':
{
const usageParams = isPlainObject(params) ? params : {};
const source = typeof usageParams.source === 'string' ? usageParams.source.trim().toLowerCase() : '';
if (source && source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'all') {
result = { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, or all' };
} else {
result = await exportSessionUsage({
...usageParams,
source: source || 'all'
});
}
}
break;
case 'list-session-paths':
{
const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
Expand Down Expand Up @@ -15960,6 +16022,7 @@ function printMainHelp() {
console.log(' codexmate delete-model <模型> 删除模型');
console.log(' codexmate workflow <list|get|validate|run|runs> MCP 工作流中心');
console.log(' codexmate task <plan|run|runs|queue|retry|cancel|logs> 本地任务编排');
console.log(' codexmate analytics export [--format csv|json] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--model <MODEL>] [--output <PATH|->] [-o <PATH|->] 导出 Usage 数据');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add --source to the top-level analytics help line.

Line [16025] omits --source, but this option is supported and documented in printAnalyticsUsage(). Keeping both help surfaces aligned avoids CLI confusion.

Suggested patch
-    console.log('  codexmate analytics export [--format csv|json] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--model <MODEL>] [--output <PATH|->] [-o <PATH|->]  导出 Usage 数据');
+    console.log('  codexmate analytics export [--format csv|json] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--model <MODEL>] [--source <codex|claude|gemini|codebuddy|all>] [--output <PATH|->] [-o <PATH|->]  导出 Usage 数据');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.log(' codexmate analytics export [--format csv|json] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--model <MODEL>] [--output <PATH|->] [-o <PATH|->] 导出 Usage 数据');
console.log(' codexmate analytics export [--format csv|json] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--model <MODEL>] [--source <codex|claude|gemini|codebuddy|all>] [--output <PATH|->] [-o <PATH|->] 导出 Usage 数据');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli.js` at line 16025, Update the top-level analytics help string in cli.js
so it includes the --source option to match printAnalyticsUsage(); specifically
edit the console.log call that prints "codexmate analytics export ..." and add
the `--source <SOURCE>` token (or `--source` flag format used elsewhere) into
the same options list so both help surfaces stay consistent with the
printAnalyticsUsage() documentation.

console.log(' codexmate run [--host <HOST>] [--no-browser] 启动 Web 界面');
console.log(' codexmate update [--check] 检查并快速更新工具');
console.log(' codexmate codex [参数...] [--follow-up <文本>|--queued-follow-up <文本> 可重复] 等同于 codex --yolo');
Expand Down Expand Up @@ -16052,6 +16115,7 @@ async function main() {
case 'proxy': await cmdProxy(args.slice(1)); break;
case 'workflow': await cmdWorkflow(args.slice(1)); break;
case 'task': await cmdTask(args.slice(1)); break;
case 'analytics': await cmdAnalytics(args.slice(1)); break;
case 'run': cmdStart(parseStartOptions(args.slice(1))); break;
case 'update': await cmdToolUpdate(args.slice(1)); break;
case 'start':
Expand Down
Loading
Loading