qwen-code/packages/desktop/scripts/dev-instance.ts

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 };
}