Skip to content
Merged
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
25 changes: 25 additions & 0 deletions .github/workflows/desktop-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ jobs:
env:
VIFORGE_POSTGRES_PLATFORM_ARCH: win32-x64
VIFORGE_REQUIRE_PGVECTOR: '1'
VIFORGE_RELEASE_COMMIT: ${{ github.sha }}
VIFORGE_POSTGRES_BUNDLE_RELEASE_REPO: YukeonWayne/pg_pgvector_binary
VIFORGE_POSTGRES_BUNDLE_RELEASE_TAG: ${{ inputs.bundle_release_tag || 'v18.4-pgvector0.8.3-win32-x64' }}
VIFORGE_POSTGRES_BUNDLE_ASSET_NAME: postgres-18.4-pgvector-0.8.3-win32-x64.zip
Expand All @@ -39,20 +40,44 @@ jobs:
node-version: 22
cache: pnpm

- name: Read release metadata
id: release_meta
run: node scripts/release-metadata.mjs --github-output

- name: Install dependencies
run: pnpm install --frozen-lockfile
env:
VIFORGE_RELEASE_VERSION: ${{ steps.release_meta.outputs.version }}
VIFORGE_RELEASE_TAG: ${{ steps.release_meta.outputs.tag }}
VIFORGE_RELEASE_CHANNEL: ${{ steps.release_meta.outputs.channel }}

- name: Verify or download PostgreSQL bundle
run: pnpm --filter @viforge/desktop prepare:postgres
env:
VIFORGE_RELEASE_VERSION: ${{ steps.release_meta.outputs.version }}
VIFORGE_RELEASE_TAG: ${{ steps.release_meta.outputs.tag }}
VIFORGE_RELEASE_CHANNEL: ${{ steps.release_meta.outputs.channel }}

- name: Typecheck API
run: pnpm --filter @viforge/api typecheck
env:
VIFORGE_RELEASE_VERSION: ${{ steps.release_meta.outputs.version }}
VIFORGE_RELEASE_TAG: ${{ steps.release_meta.outputs.tag }}
VIFORGE_RELEASE_CHANNEL: ${{ steps.release_meta.outputs.channel }}

- name: Typecheck web
run: pnpm --filter @viforge/web typecheck
env:
VIFORGE_RELEASE_VERSION: ${{ steps.release_meta.outputs.version }}
VIFORGE_RELEASE_TAG: ${{ steps.release_meta.outputs.tag }}
VIFORGE_RELEASE_CHANNEL: ${{ steps.release_meta.outputs.channel }}

- name: Build Windows installer
run: pnpm desktop:dist
env:
VIFORGE_RELEASE_VERSION: ${{ steps.release_meta.outputs.version }}
VIFORGE_RELEASE_TAG: ${{ steps.release_meta.outputs.tag }}
VIFORGE_RELEASE_CHANNEL: ${{ steps.release_meta.outputs.channel }}

- name: Upload Windows artifacts
uses: actions/upload-artifact@v7
Expand Down
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@viforge/api",
"version": "0.0.0",
"version": "0.1.0",
"private": true,
"license": "MIT",
"type": "module",
Expand Down
35 changes: 35 additions & 0 deletions apps/api/src/releaseInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import os from 'node:os';
import process from 'node:process';

import { RELEASE_CHANNEL, RELEASE_VERSION, normalizeReleaseTag, releaseManifest, type ReleaseInfo } from '@viforge/shared';

export function getReleaseInfo(): ReleaseInfo {
const version = process.env.VIFORGE_RELEASE_VERSION?.trim() || releaseManifest.version;
const tag = process.env.VIFORGE_RELEASE_TAG?.trim() || normalizeReleaseTag(version);
const channel = process.env.VIFORGE_RELEASE_CHANNEL?.trim() || RELEASE_CHANNEL;
const commit = process.env.VIFORGE_RELEASE_COMMIT?.trim() || process.env.GITHUB_SHA?.trim() || releaseManifest.commit;
const platform = detectPlatform();
const currentArtifact = releaseManifest.artifacts.find((artifact) => artifact.platform === platform);

return {
...releaseManifest,
version,
tag,
channel: channel === 'dev' || channel === 'beta' || channel === 'stable' ? channel : RELEASE_CHANNEL,
commit,
currentArtifact,
};
}

export { RELEASE_VERSION };

function detectPlatform(): ReleaseInfo['currentArtifact'] extends infer T
? T extends { platform: infer P }
? P
: never
: never {
if (process.platform === 'win32') return 'windows-x64';
if (process.platform === 'darwin') return process.arch === 'arm64' ? 'macos-arm64' : 'macos-x64';
if (process.platform === 'linux') return 'linux-x64';
return os.platform() === 'win32' ? 'windows-x64' : 'linux-x64';
}
18 changes: 17 additions & 1 deletion apps/api/src/routes/runtimeConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,23 @@ describe('runtime config routes', () => {
});
});

it('returns canonical release info for product and artifact surfaces', async () => {
const root = await mkdtemp(path.join(os.tmpdir(), 'viforge-runtime-config-'));
tempDirs.push(root);
const app = createRuntimeConfigRoutes(createRuntimeConfigStore(path.join(root, 'runtime-config.json')));

const response = await app.request('/release-info');

expect(response.status).toBe(200);
expect(await response.json()).toMatchObject({
productName: 'ViForge',
version: '0.1.0',
tag: 'v0.1.0',
channel: 'beta',
updateHeadline: '建立统一版本管理链路',
});
});

it('forces embedded PostgreSQL in desktop mode even when legacy config points to an external database', async () => {
process.env.VIFORGE_DESKTOP = '1';
process.env.DATABASE_URL = 'postgresql://legacy:password@db.example.test:5432/viforge';
Expand Down Expand Up @@ -289,4 +306,3 @@ function restoreEnv(key: string, value: string | undefined): void {
}
process.env[key] = value;
}

8 changes: 6 additions & 2 deletions apps/api/src/routes/runtimeConfig.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Hono } from 'hono';
import { z } from 'zod';

import type { RuntimeConfig, RuntimeMemoryRebuildResponse, RuntimeModelTestResponse, UpdateRuntimeConfigInput } from '@viforge/shared';
import type { ReleaseInfo, RuntimeConfig, RuntimeMemoryRebuildResponse, RuntimeModelTestResponse, UpdateRuntimeConfigInput } from '@viforge/shared';

import { buildAigcHubHeaders } from '../aigcHubHeaders';

import type { RuntimeConfigStore } from '../runtimeConfigStore';
import type { WorkspaceStore } from '../storage/workspaceStore';
import { MemoryEmbeddingIndexUnavailableError, MemoryEmbeddingRebuildInProgressError, reindexProjectMemories } from '../runs/langGraphAgents';
import { getReleaseInfo } from '../releaseInfo';

const updateRuntimeConfigSchema = z.object({
modelProvider: z.object({
Expand Down Expand Up @@ -40,6 +41,10 @@ export function createRuntimeConfigRoutes(store: RuntimeConfigStore, workspaceSt
return context.json(await store.getConfig() satisfies RuntimeConfig);
});

routes.get('/release-info', (context) => {
return context.json(getReleaseInfo() satisfies ReleaseInfo);
});

routes.put('/runtime-config', async (context) => {
const parsed = updateRuntimeConfigSchema.safeParse(await context.req.json());
if (!parsed.success) {
Expand Down Expand Up @@ -161,4 +166,3 @@ async function modelTestErrorMessage(response: Response): Promise<string> {
function trimTrailingSlashes(value: string): string {
return value.replace(/\/+$/, '');
}

96 changes: 96 additions & 0 deletions apps/desktop/electron-builder.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { readFileSync } from 'node:fs';
import path from 'node:path';

const manifestSource = readFileSync(path.resolve('..', '..', 'packages', 'shared', 'src', 'releaseManifest.ts'), 'utf8');
const productName = readConstString('RELEASE_PRODUCT_NAME');
const manifestVersion = readConstString('RELEASE_VERSION');
const manifestChannel = readConstString('RELEASE_CHANNEL');
const releaseVersion = process.env.VIFORGE_RELEASE_VERSION?.trim() || manifestVersion;
const releaseChannel = normalizeReleaseChannel(process.env.VIFORGE_RELEASE_CHANNEL?.trim()) || manifestChannel;

export default {
appId: 'cn.viforge.desktop',
productName: 'ViForge',
executableName: 'viforge',
npmRebuild: false,
directories: {
output: '../../release/desktop',
},
files: [
'dist/**/*',
'build/**/*',
'package.json',
],
extraResources: [
{
from: '../web/dist',
to: 'web',
},
{
from: 'dist/api',
to: 'api',
filter: ['**/*'],
},
{
from: 'resources/postgres',
to: 'postgres',
filter: ['**/*'],
},
{
from: '../../LICENSE',
to: 'LICENSE',
},
{
from: '../../NOTICE',
to: 'NOTICE',
},
{
from: '../../THIRD_PARTY_NOTICES.md',
to: 'THIRD_PARTY_NOTICES.md',
},
],
win: {
target: ['nsis'],
icon: 'build/icon.ico',
requestedExecutionLevel: 'asInvoker',
artifactName: buildReleaseArtifactFileName({
productName,
version: releaseVersion,
channel: releaseChannel,
platform: 'win32-x64',
qualifier: 'installer',
extension: 'exe',
}),
},
nsis: {
oneClick: false,
perMachine: false,
allowToChangeInstallationDirectory: true,
runAfterFinish: false,
include: 'installer.nsh',
},
mac: {
target: ['dmg'],
icon: 'build/icon.png',
},
linux: {
target: ['AppImage'],
icon: 'build/icon.png',
},
};

function readConstString(name) {
const pattern = new RegExp("export\\s+const\\s+" + name + "\\s*=\\s*['\"]([^'\"]+)['\"]");
const match = manifestSource.match(pattern);
if (!match) throw new Error("Unable to read " + name + " from releaseManifest.ts");
return match[1];
}

function buildReleaseArtifactFileName(input) {
const qualifier = input.qualifier ? '-' + input.qualifier : '';
return input.productName + '-' + input.version + '-' + input.channel + '-' + input.platform + qualifier + '.' + input.extension;
}

function normalizeReleaseChannel(value) {
return value === 'dev' || value === 'beta' || value === 'stable' ? value : undefined;
}
72 changes: 3 additions & 69 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"description": "ViForge standalone desktop app",
"author": "ViForge contributors",
"license": "MIT",
"version": "0.0.0",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "dist/main.js",
Expand All @@ -14,8 +14,8 @@
"build:postgres": "node scripts/build-postgres-from-source.mjs",
"build:pgvector": "node scripts/build-pgvector-from-source.mjs",
"prepare:postgres": "node scripts/prepare-postgres.mjs",
"pack": "pnpm --filter @viforge/web build && pnpm --filter @viforge/api build && pnpm build && pnpm prepare:postgres && electron-builder --dir",
"dist": "pnpm --filter @viforge/web build && pnpm --filter @viforge/api build && pnpm build && pnpm prepare:postgres && electron-builder"
"pack": "pnpm --filter @viforge/web build && pnpm --filter @viforge/api build && pnpm build && pnpm prepare:postgres && electron-builder --dir --config electron-builder.config.mjs",
"dist": "pnpm --filter @viforge/web build && pnpm --filter @viforge/api build && pnpm build && pnpm prepare:postgres && electron-builder --config electron-builder.config.mjs"
},
"dependencies": {
"electron-squirrel-startup": "^1.0.1"
Expand All @@ -27,71 +27,5 @@
"esbuild": "^0.21.5",
"sharp": "^0.34.5",
"typescript": "^5.5.3"
},
"build": {
"appId": "cn.viforge.desktop",
"productName": "ViForge",
"executableName": "viforge",
"npmRebuild": false,
"directories": {
"output": "../../release/desktop"
},
"files": [
"dist/**/*",
"build/**/*",
"package.json"
],
"extraResources": [
{
"from": "../web/dist",
"to": "web"
},
{
"from": "dist/api",
"to": "api",
"filter": [
"**/*"
]
},
{
"from": "resources/postgres",
"to": "postgres",
"filter": [
"**/*"
]
},
{
"from": "../../LICENSE",
"to": "LICENSE"
},
{
"from": "../../NOTICE",
"to": "NOTICE"
},
{
"from": "../../THIRD_PARTY_NOTICES.md",
"to": "THIRD_PARTY_NOTICES.md"
}
],
"win": {
"target": ["nsis"],
"icon": "build/icon.ico",
"requestedExecutionLevel": "asInvoker"
},
"nsis": {
"oneClick": false,
"perMachine": false,
"allowToChangeInstallationDirectory": true,
"runAfterFinish": false,
"include": "installer.nsh"
},
"mac": {
"target": ["dmg"],
"icon": "build/icon.png"
},
"linux": {
"target": ["AppImage"],
"icon": "build/icon.png"
}
}
}
3 changes: 2 additions & 1 deletion apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ ipcMain.handle('viforge:select-data-root', async () => {
};
});

ipcMain.handle('viforge:get-app-version', () => app.getVersion());

async function startDesktopApp(): Promise<void> {
if (startupPromise) return startupPromise;
startupPromise = startDesktopAppOnce().finally(() => {
Expand Down Expand Up @@ -689,4 +691,3 @@ app.on('before-quit', (event) => {
event.preventDefault();
quitApp();
});

1 change: 1 addition & 0 deletions apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ type SelectDataRootResult = {

contextBridge.exposeInMainWorld('viforgeDesktop', {
selectDataRoot: async (): Promise<SelectDataRootResult> => ipcRenderer.invoke('viforge:select-data-root') as Promise<SelectDataRootResult>,
getAppVersion: async (): Promise<string> => ipcRenderer.invoke('viforge:get-app-version') as Promise<string>,
});
1 change: 1 addition & 0 deletions apps/desktop/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ interface Window {
dataRoot?: string;
restartRequired?: boolean;
}>;
getAppVersion(): Promise<string>;
};
}
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@viforge/web",
"version": "0.0.0",
"version": "0.1.0",
"private": true,
"license": "MIT",
"type": "module",
Expand Down
Loading