diff --git a/src/__tests__/docs-destination.test.ts b/src/__tests__/docs-destination.test.ts new file mode 100644 index 000000000..2c72bdf06 --- /dev/null +++ b/src/__tests__/docs-destination.test.ts @@ -0,0 +1,49 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import os from 'node:os'; +import path from 'node:path'; +import { resolveDocsDestination } from '../resources/docs.js'; +import type { LocalConfig, TeamaiConfig } from '../types.js'; + +function teamConfig(localDir: string): TeamaiConfig { + return { sharing: { docs: { localDir } } } as TeamaiConfig; +} + +function localConfig(scope: 'user' | 'project', projectRoot?: string): LocalConfig { + return { scope, ...(projectRoot ? { projectRoot } : {}) } as LocalConfig; +} + +describe('resolveDocsDestination', () => { + const homeDir = path.join(os.tmpdir(), 'teamai-docs-destination-home'); + + beforeEach(() => { + vi.stubEnv('HOME', homeDir); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it('anchors a project-relative directory to the workspace root', () => { + const projectRoot = path.join(os.tmpdir(), 'teamai-docs-project'); + + expect( + resolveDocsDestination(teamConfig('./.teamai/docs'), localConfig('project', projectRoot)), + ).toBe(path.resolve(projectRoot, '.teamai/docs')); + }); + + it('anchors a user-relative directory to the user home', () => { + expect(resolveDocsDestination(teamConfig('./.teamai/docs'), localConfig('user'))).toBe( + path.resolve(homeDir, '.teamai/docs'), + ); + }); + + it('preserves project tilde and absolute destinations', () => { + const projectRoot = path.join(os.tmpdir(), 'teamai-docs-project'); + const absolute = path.resolve(os.tmpdir(), 'teamai-docs-absolute'); + + expect(resolveDocsDestination(teamConfig('~/team-docs'), localConfig('project', projectRoot))).toBe( + path.join(projectRoot, 'team-docs'), + ); + expect(resolveDocsDestination(teamConfig(absolute), localConfig('user'))).toBe(absolute); + }); +}); diff --git a/src/resources/docs.ts b/src/resources/docs.ts index fd9cb9bd6..4895c255e 100644 --- a/src/resources/docs.ts +++ b/src/resources/docs.ts @@ -1,7 +1,7 @@ import path from 'node:path'; import fse from 'fs-extra'; import { ResourceHandler } from './base.js'; -import type { ResourceItem, TeamaiConfig, LocalConfig } from '../types.js'; +import { resolveBaseDir, type ResourceItem, type TeamaiConfig, type LocalConfig } from '../types.js'; import { expandHome, listFilesRecursive } from '../utils/fs.js'; import { log } from '../utils/logger.js'; @@ -15,7 +15,8 @@ export function resolveDocsDestination(teamConfig: TeamaiConfig, localConfig: Lo if (localConfig.scope === 'project' && localConfig.projectRoot && localDir.startsWith('~/')) { return path.join(localConfig.projectRoot, localDir.substring(2)); } - return expandHome(localDir); + const expanded = expandHome(localDir); + return path.isAbsolute(expanded) ? expanded : path.resolve(resolveBaseDir(localConfig), expanded); } export class DocsHandler extends ResourceHandler { diff --git a/src/uninstall.ts b/src/uninstall.ts index f2e4da24b..f27e79f41 100644 --- a/src/uninstall.ts +++ b/src/uninstall.ts @@ -35,6 +35,7 @@ import { import { BUILTIN_RULE_NAMES } from './builtin-rules.js'; import { ruleStemFromFilename } from './resources/rule-format.js'; import { agentStemFromFilename } from './resources/agent-format.js'; +import { resolveDocsDestination } from './resources/docs.js'; import { listTeamAgentDirs } from './resources/agents.js'; import { BUILTIN_AGENT_NAMES } from './builtin-agents.js'; import { BUILTIN_SKILL_NAMES } from './builtin-skills.js'; @@ -528,15 +529,7 @@ async function buildRemovalPlan( } // (f) Docs directory - const docsLocalDir = teamConfig.sharing.docs.localDir; - let docsDir: string; - if (localConfig.scope === 'project' && localConfig.projectRoot) { - docsDir = docsLocalDir.startsWith('~/') - ? path.join(localConfig.projectRoot, docsLocalDir.substring(2)) - : expandHome(docsLocalDir); - } else { - docsDir = expandHome(docsLocalDir); - } + const docsDir = resolveDocsDestination(teamConfig, localConfig); if (await pathExists(docsDir)) { plan.docsDir = docsDir; }