diff --git a/packages/channels/base/src/index.ts b/packages/channels/base/src/index.ts index 177718aae00..b93c8e3e9bf 100644 --- a/packages/channels/base/src/index.ts +++ b/packages/channels/base/src/index.ts @@ -1,4 +1,4 @@ -export { getGlobalQwenDir } from './paths.js'; +export { getGlobalQwenDir, resolvePath } from './paths.js'; export { AcpBridge } from './AcpBridge.js'; export type { AcpBridgeOptions, diff --git a/packages/channels/base/src/paths.test.ts b/packages/channels/base/src/paths.test.ts index 92fe86f8f23..b024ac47551 100644 --- a/packages/channels/base/src/paths.test.ts +++ b/packages/channels/base/src/paths.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, afterEach } from 'vitest'; import * as path from 'node:path'; import * as os from 'node:os'; -import { getGlobalQwenDir } from './paths.js'; +import { getGlobalQwenDir, resolvePath } from './paths.js'; describe('channels/base paths – getGlobalQwenDir', () => { const originalEnv = process.env['QWEN_HOME']; @@ -45,3 +45,26 @@ describe('channels/base paths – getGlobalQwenDir', () => { expect(getGlobalQwenDir()).toBe(os.homedir()); }); }); + +describe('channels/base paths – resolvePath', () => { + it('returns absolute paths unchanged', () => { + const abs = path.resolve('/tmp/x'); + expect(resolvePath(abs)).toBe(abs); + }); + + it('expands bare tilde (~) to home directory', () => { + expect(resolvePath('~')).toBe(os.homedir()); + }); + + it('expands POSIX-style tilde (~/x)', () => { + expect(resolvePath('~/xomo')).toBe(path.join(os.homedir(), 'xomo')); + }); + + it('expands Windows-style tilde (~\\x)', () => { + expect(resolvePath('~\\xomo')).toBe(path.join(os.homedir(), 'xomo')); + }); + + it('resolves relative paths against process.cwd', () => { + expect(resolvePath('relative/dir')).toBe(path.resolve('relative/dir')); + }); +}); diff --git a/packages/channels/base/src/paths.ts b/packages/channels/base/src/paths.ts index 7e444944874..bd24d943cde 100644 --- a/packages/channels/base/src/paths.ts +++ b/packages/channels/base/src/paths.ts @@ -5,7 +5,7 @@ import * as os from 'node:os'; * Expands tilde and resolves relative paths to absolute. * Mirrors Storage.resolvePath() in packages/core. */ -function resolvePath(dir: string): string { +export function resolvePath(dir: string): string { let resolved = dir; if ( resolved === '~' || diff --git a/packages/cli/src/commands/channel/config-utils.test.ts b/packages/cli/src/commands/channel/config-utils.test.ts index f6a50cd187e..d73dedcb685 100644 --- a/packages/cli/src/commands/channel/config-utils.test.ts +++ b/packages/cli/src/commands/channel/config-utils.test.ts @@ -1,4 +1,6 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; +import * as path from 'node:path'; +import * as os from 'node:os'; import { resolveEnvVars, parseChannelConfig } from './config-utils.js'; // Mock the channel-registry so we don't pull in real plugins @@ -137,4 +139,37 @@ describe('parseChannelConfig', () => { }); expect((result as Record)['customField']).toBe(42); }); + + it('expands tilde in cwd (~/x → $HOME/x)', async () => { + const result = await parseChannelConfig('bot', { + type: 'bare', + cwd: '~/xomo', + }); + expect(result.cwd).toBe(path.join(os.homedir(), 'xomo')); + }); + + it('expands bare tilde (~) in cwd to home directory', async () => { + const result = await parseChannelConfig('bot', { + type: 'bare', + cwd: '~', + }); + expect(result.cwd).toBe(os.homedir()); + }); + + it('resolves relative cwd against process.cwd', async () => { + const result = await parseChannelConfig('bot', { + type: 'bare', + cwd: 'relative/dir', + }); + expect(result.cwd).toBe(path.resolve('relative/dir')); + }); + + it('leaves absolute cwd unchanged', async () => { + const abs = path.resolve('/custom'); + const result = await parseChannelConfig('bot', { + type: 'bare', + cwd: abs, + }); + expect(result.cwd).toBe(abs); + }); }); diff --git a/packages/cli/src/commands/channel/config-utils.ts b/packages/cli/src/commands/channel/config-utils.ts index 4248ad4b80a..99407be132f 100644 --- a/packages/cli/src/commands/channel/config-utils.ts +++ b/packages/cli/src/commands/channel/config-utils.ts @@ -1,4 +1,5 @@ import type { ChannelConfig } from '@qwen-code/channel-base'; +import { resolvePath } from '@qwen-code/channel-base'; import * as path from 'node:path'; import { getPlugin, supportedTypes } from './channel-registry.js'; @@ -73,7 +74,7 @@ export async function parseChannelConfig( allowedUsers: (rawConfig['allowedUsers'] as string[]) || [], sessionScope: (rawConfig['sessionScope'] as ChannelConfig['sessionScope']) || 'user', - cwd: (rawConfig['cwd'] as string) || process.cwd(), + cwd: resolvePath((rawConfig['cwd'] as string) || process.cwd()), approvalMode: rawConfig['approvalMode'] as string | undefined, instructions: rawConfig['instructions'] as string | undefined, model: rawConfig['model'] as string | undefined,