mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-10 01:29:17 +00:00
171 lines
4.7 KiB
TypeScript
171 lines
4.7 KiB
TypeScript
import { execFileSync } from 'child_process';
|
|
import { createHash } from 'crypto';
|
|
import { existsSync, statSync } from 'fs';
|
|
import { homedir } from 'os';
|
|
import { basename, dirname, join } from 'path';
|
|
|
|
export type DevInstance = {
|
|
kind: 'numbered' | 'git-worktree';
|
|
source: string;
|
|
instanceNumber: string;
|
|
label: string;
|
|
appName: string;
|
|
configDir?: string;
|
|
runtimeDir: string;
|
|
userDataDir: string;
|
|
serverLockFile: string;
|
|
deeplinkScheme: string;
|
|
resolvePort(defaultPort: number): number;
|
|
};
|
|
|
|
export type ResolvedDevPort = {
|
|
port: number;
|
|
source: 'env' | 'instance' | 'default';
|
|
instance: DevInstance | null;
|
|
};
|
|
|
|
export type ResolveDevPortOptions = {
|
|
allowZero?: boolean;
|
|
};
|
|
|
|
function hashNumber(value: string): number {
|
|
const hex = createHash('sha256').update(value).digest('hex').slice(0, 8);
|
|
return parseInt(hex, 16);
|
|
}
|
|
|
|
function sanitizeInstanceLabel(value: string): string {
|
|
return (
|
|
value
|
|
.trim()
|
|
.replace(/[^a-zA-Z0-9]+/g, '-')
|
|
.replace(/^-+|-+$/g, '')
|
|
.slice(0, 32)
|
|
.toLowerCase() || 'worktree'
|
|
);
|
|
}
|
|
|
|
function gitOutput(rootDir: string, args: string[]): string | undefined {
|
|
try {
|
|
return (
|
|
execFileSync('git', args, {
|
|
cwd: rootDir,
|
|
encoding: 'utf-8',
|
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
}).trim() || undefined
|
|
);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function isLinkedGitWorktree(rootDir: string): boolean {
|
|
const gitPath = join(rootDir, '.git');
|
|
if (!existsSync(gitPath)) return false;
|
|
try {
|
|
return !statSync(gitPath).isDirectory();
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function appPortOffset(defaultPort: number): number {
|
|
if (defaultPort >= 5173 && defaultPort <= 5272) {
|
|
return defaultPort - 5173;
|
|
}
|
|
if (defaultPort === 9100) {
|
|
return 100;
|
|
}
|
|
if (defaultPort === 3100) {
|
|
return 101;
|
|
}
|
|
return 100 + (Math.abs(defaultPort) % 900);
|
|
}
|
|
|
|
function numberedPort(instanceNumber: string, defaultPort: number): number {
|
|
const suffix = String(defaultPort).slice(-3).padStart(3, '0');
|
|
const port = Number(`${instanceNumber}${suffix}`);
|
|
return Number.isFinite(port) && port > 0 && port <= 65535
|
|
? port
|
|
: defaultPort + Number(instanceNumber) * 100;
|
|
}
|
|
|
|
export function detectDevInstance(rootDir: string): DevInstance | null {
|
|
const home = process.env.HOME || homedir() || '';
|
|
const folderName = basename(rootDir);
|
|
const numberedMatch = folderName.match(/-(\d+)$/);
|
|
|
|
if (numberedMatch) {
|
|
const instanceNumber = numberedMatch[1]!;
|
|
const configDir = join(home, `.craft-agent-${instanceNumber}`);
|
|
return {
|
|
kind: 'numbered',
|
|
source: 'Numbered dev instance',
|
|
instanceNumber,
|
|
label: instanceNumber,
|
|
appName: `Qwen Code Desktop [${instanceNumber}]`,
|
|
configDir,
|
|
runtimeDir: configDir,
|
|
userDataDir: join(configDir, 'electron-user-data'),
|
|
serverLockFile: join(configDir, '.server.lock'),
|
|
deeplinkScheme: `craftagents${instanceNumber}`,
|
|
resolvePort: (defaultPort) => numberedPort(instanceNumber, defaultPort),
|
|
};
|
|
}
|
|
|
|
if (!isLinkedGitWorktree(rootDir)) return null;
|
|
|
|
const hash = hashNumber(rootDir);
|
|
const branchName = gitOutput(rootDir, ['branch', '--show-current']);
|
|
const parentName = basename(dirname(rootDir));
|
|
const label = sanitizeInstanceLabel(branchName || parentName || rootDir);
|
|
const shortHash = hash.toString(36).slice(0, 6);
|
|
const basePort = 41_000 + (hash % 20_000);
|
|
const runtimeDir = join(home, '.craft-agent-dev', `${label}-${shortHash}`);
|
|
|
|
return {
|
|
kind: 'git-worktree',
|
|
source: 'Git worktree dev instance',
|
|
instanceNumber: String((hash % 90) + 10),
|
|
label,
|
|
appName: `Qwen Code Desktop [${label}]`,
|
|
runtimeDir,
|
|
userDataDir: join(runtimeDir, 'electron-user-data'),
|
|
serverLockFile: join(runtimeDir, '.server.lock'),
|
|
deeplinkScheme: `craftagents${shortHash}`,
|
|
resolvePort: (defaultPort) => basePort + appPortOffset(defaultPort),
|
|
};
|
|
}
|
|
|
|
export function resolveDevPort(
|
|
rootDir: string,
|
|
defaultPort: number,
|
|
envVar?: string,
|
|
options: ResolveDevPortOptions = {},
|
|
): ResolvedDevPort {
|
|
const envPort = envVar ? process.env[envVar] : undefined;
|
|
if (envPort) {
|
|
const parsed = Number(envPort);
|
|
const minPort = options.allowZero ? 0 : 1;
|
|
if (!Number.isInteger(parsed) || parsed < minPort || parsed > 65535) {
|
|
throw new Error(
|
|
`Invalid ${envVar}: expected a TCP port from ${minPort} to 65535.`,
|
|
);
|
|
}
|
|
return {
|
|
port: parsed,
|
|
source: 'env',
|
|
instance: detectDevInstance(rootDir),
|
|
};
|
|
}
|
|
|
|
const instance = detectDevInstance(rootDir);
|
|
if (instance) {
|
|
return {
|
|
port: instance.resolvePort(defaultPort),
|
|
source: 'instance',
|
|
instance,
|
|
};
|
|
}
|
|
|
|
return { port: defaultPort, source: 'default', instance: null };
|
|
}
|