Skip to content
Closed
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
87 changes: 87 additions & 0 deletions src/__tests__/wiki-toggle-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { describe, it, expect } from 'vitest';
import { isWikiEnabledByConfig, LocalConfigSchema } from '../types.js';

describe('Wiki feature toggle via local config (Approach B)', () => {
const baseConfig = {
repo: { localPath: '/tmp/test-repo', remote: 'git@test:repo.git' },
username: 'test-user',
scope: 'user' as const,
additionalRoles: [],
};

describe('isWikiEnabledByConfig', () => {
it('returns true when wikiEnabled is true', () => {
const config = LocalConfigSchema.parse({ ...baseConfig, wikiEnabled: true });
expect(isWikiEnabledByConfig(config)).toBe(true);
});

it('returns false when wikiEnabled is false', () => {
const config = LocalConfigSchema.parse({ ...baseConfig, wikiEnabled: false });
expect(isWikiEnabledByConfig(config)).toBe(false);
});

it('returns true when wikiEnabled is not set (default)', () => {
const config = LocalConfigSchema.parse(baseConfig);
expect(isWikiEnabledByConfig(config)).toBe(true);
});
});

describe('LocalConfigSchema backward compatibility', () => {
it('parses config without wikiEnabled field (defaults to true)', () => {
const config = LocalConfigSchema.parse(baseConfig);
expect(config.wikiEnabled).toBeUndefined();
});

it('parses config with wikiEnabled: false', () => {
const config = LocalConfigSchema.parse({ ...baseConfig, wikiEnabled: false });
expect(config.wikiEnabled).toBe(false);
});

it('parses config with wikiEnabled: true', () => {
const config = LocalConfigSchema.parse({ ...baseConfig, wikiEnabled: true });
expect(config.wikiEnabled).toBe(true);
});

it('rejects non-boolean wikiEnabled', () => {
expect(() =>
LocalConfigSchema.parse({ ...baseConfig, wikiEnabled: 'yes' }),
).toThrow();
});
});

describe('Pull resource type filtering', () => {
it('includes wiki when enabled', () => {
const config = LocalConfigSchema.parse({ ...baseConfig, wikiEnabled: true });
const wikiEnabled = isWikiEnabledByConfig(config);
const types = wikiEnabled
? ['skills', 'rules', 'docs', 'env', 'wiki']
: ['skills', 'rules', 'docs', 'env'];
expect(types).toContain('wiki');
});

it('excludes wiki when disabled', () => {
const config = LocalConfigSchema.parse({ ...baseConfig, wikiEnabled: false });
const wikiEnabled = isWikiEnabledByConfig(config);
const types = wikiEnabled
? ['skills', 'rules', 'docs', 'env', 'wiki']
: ['skills', 'rules', 'docs', 'env'];
expect(types).not.toContain('wiki');
});
});

describe('Builtin skills skipWiki', () => {
it('filters teamai-wiki when skipWiki is true', () => {
const names = ['teamai-share-learnings', 'teamai-wiki'];
const opts = { skipWiki: true };
const filtered = names.filter(n => !(opts.skipWiki && n === 'teamai-wiki'));
expect(filtered).toEqual(['teamai-share-learnings']);
});

it('keeps teamai-wiki when skipWiki is false', () => {
const names = ['teamai-share-learnings', 'teamai-wiki'];
const opts = { skipWiki: false };
const filtered = names.filter(n => !(opts.skipWiki && n === 'teamai-wiki'));
expect(filtered).toEqual(['teamai-share-learnings', 'teamai-wiki']);
});
});
});
3 changes: 2 additions & 1 deletion src/builtin-skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const BUILTIN_SKILL_NAMES = new Set(['teamai-share-learnings', 'teamai-wi
* - Built-in skills directory doesn't exist (dev environment without build)
* - A tool's skills directory is not configured
*/
export async function deployBuiltinSkills(teamConfig: TeamaiConfig, localConfig?: LocalConfig): Promise<number> {
export async function deployBuiltinSkills(teamConfig: TeamaiConfig, localConfig?: LocalConfig, options?: { skipWiki?: boolean }): Promise<number> {
const builtinDir = getBuiltinSkillsDir();

if (!await pathExists(builtinDir)) {
Expand Down Expand Up @@ -91,6 +91,7 @@ export async function deployBuiltinSkills(teamConfig: TeamaiConfig, localConfig?
const targetSkillsDir = path.join(baseDir, toolPath.skills);

for (const skillName of skillNames) {
if (options?.skipWiki && skillName === 'teamai-wiki') continue;
const srcDir = path.join(builtinDir, skillName);
const destDir = path.join(targetSkillsDir, skillName);

Expand Down
8 changes: 6 additions & 2 deletions src/pull.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
TEAMAI_CLAUDEMD_END,
CultureFrontmatterSchema,
resolveBaseDir,
isWikiEnabledByConfig,
getTeamaiHome,
} from './types.js';
import type { CultureFrontmatter } from './types.js';
Expand Down Expand Up @@ -260,7 +261,10 @@ async function pullForScope(
const subscribedTags = localConfig.subscribedTags;

// Step 2: Sync each resource type
const resourceTypes: ResourceType[] = ['skills', 'rules', 'docs', 'env', 'wiki'];
const wikiEnabled = isWikiEnabledByConfig(localConfig);
const resourceTypes: ResourceType[] = wikiEnabled
? ['skills', 'rules', 'docs', 'env', 'wiki']
: ['skills', 'rules', 'docs', 'env'];
let totalSynced = 0;
let desiredSkillNames: Set<string> | null = null;
let knownRepoSkillNames: Set<string> | null = null;
Expand Down Expand Up @@ -555,7 +559,7 @@ async function pullForScope(
if (!options.dryRun) {
try {
const { deployBuiltinSkills } = await import('./builtin-skills.js');
const deployed = await deployBuiltinSkills(freshConfig, localConfig);
const deployed = await deployBuiltinSkills(freshConfig, localConfig, { skipWiki: !wikiEnabled });
if (deployed > 0) {
log.debug(`[${scopeLabel}] Deployed ${deployed} built-in skill(s)`);
}
Expand Down
5 changes: 4 additions & 1 deletion src/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { log, spinner } from './utils/logger.js';
import { getHandler } from './resources/index.js';
import { scanTeamRepoNamespaces } from './resources/skills.js';
import type { GlobalOptions, ResourceItem, ResourceType } from './types.js';
import { isWikiEnabledByConfig } from './types.js';
import { loadRolesManifest, resolveRoleResourceNamespaces } from './roles.js';
import { askQuestion, askSelection } from './utils/prompt.js';
import { pathExists } from './utils/fs.js';
Expand Down Expand Up @@ -137,7 +138,9 @@ export async function push(options: GlobalOptions & { all?: boolean; role?: stri

// Scan for pushable resources first, then resolve namespace for new skills only.
// Modified skills already carry their namespace from scanLocalForPush.
const pushableTypes: ResourceType[] = ['skills', 'rules', 'env', 'wiki'];
const pushableTypes: ResourceType[] = isWikiEnabledByConfig(localConfig)
? ['skills', 'rules', 'env', 'wiki']
: ['skills', 'rules', 'env'];
const allItems: ResourceItem[] = [];

for (const type of pushableTypes) {
Expand Down
7 changes: 7 additions & 0 deletions src/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
type AgentSkillsView,
} from './agent-skills.js';
import type { GlobalOptions, ResourceType } from './types.js';
import { isWikiEnabledByConfig } from './types.js';

export interface ListOptions extends GlobalOptions {
/** Where to look for resources: 'repo' (default for backwards compat),
Expand Down Expand Up @@ -100,6 +101,12 @@ export async function status(options: GlobalOptions): Promise<void> {
console.log(` ${type}: ${count}`);
}

// Wiki feature status
if (!isWikiEnabledByConfig(localConfig)) {
console.log('');
log.info('ℹ Wiki: disabled (wikiEnabled: false in ~/.teamai/config.yaml)');
}

// Local pushable items
console.log('');
log.info('Local resources not yet pushed:');
Expand Down
12 changes: 12 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ export const LocalConfigSchema = z.object({
projectRoot: z.string().optional(),
/** Tags the user has subscribed to. If empty/undefined, pull all resources. */
subscribedTags: z.array(z.string()).optional(),
/** Whether the built-in wiki feature is enabled. Set to false to disable
* wiki sync/push and avoid conflicts with external wiki plugins. */
wikiEnabled: z.boolean().optional(),
});

export type LocalConfig = z.infer<typeof LocalConfigSchema>;
Expand Down Expand Up @@ -534,3 +537,12 @@ export function getStatePath(scope: Scope, projectRoot?: string): string {
export function getPushignorePath(): string {
return path.join(process.env.HOME ?? '', '.teamai', 'pushignore');
}

/**
* Check if wiki feature is enabled based on local config.
* Returns false only when explicitly disabled by the user.
* Defaults to true for backward compatibility.
*/
export function isWikiEnabledByConfig(localConfig: LocalConfig): boolean {
return localConfig.wikiEnabled !== false;
}
Loading