Skip to content

Commit e62799a

Browse files
committed
feat: add --scope flag for project vs user skill installation
1 parent 045fde4 commit e62799a

File tree

6 files changed

+56
-12
lines changed

6 files changed

+56
-12
lines changed

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@yavydev/cli",
3-
"version": "0.2.1",
3+
"version": "0.2.2",
44
"description": "Search and manage your AI-ready documentation on Yavy",
55
"type": "module",
66
"bin": {

src/commands/init.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ import { getAccessToken } from '@/auth/store';
66
import { error, warn } from '@/utils';
77
import { configureTool, type ConfigureResult } from '@/commands/init/configure-tool';
88
import { resolveToolFromFlag, scanForTools } from '@/commands/init/scan-tools';
9-
import { AiTool, TOOL_CONFIGS, type InitOptions } from '@/commands/init/types';
9+
import { AiTool, type Scope, TOOL_CONFIGS, type InitOptions } from '@/commands/init/types';
1010

1111
export function initCommand(): Command {
1212
return new Command('init')
1313
.description('Set up Yavy for your AI tools (skills + MCP config)')
1414
.option('--tool <name>', 'Configure a specific tool only')
1515
.option('--projects <slugs>', 'Comma-separated project slugs to configure (skips interactive selection)')
16+
.option('--scope <scope>', 'Install scope: "project" (default) or "user" (global, applies to all projects)')
1617
.option('--yes', 'Non-interactive mode: configure all detected tools + all projects')
1718
.action(async (options: InitOptions) => {
1819
try {
@@ -51,13 +52,15 @@ async function runInit(options: InitOptions): Promise<void> {
5152
process.exit(0);
5253
}
5354

55+
const scope = resolveScope(options, selectedTools);
56+
5457
const s = p.spinner();
5558
s.start('Configuring tools...');
5659

5760
const results: ConfigureResult[] = [];
5861
for (const tool of selectedTools) {
5962
try {
60-
results.push(configureTool(tool, selectedProjects, process.cwd()));
63+
results.push(configureTool(tool, selectedProjects, process.cwd(), scope));
6164
} catch (err) {
6265
warn(`Failed to configure ${TOOL_CONFIGS[tool].name}: ${err instanceof Error ? err.message : String(err)}`);
6366
}
@@ -70,7 +73,8 @@ async function runInit(options: InitOptions): Promise<void> {
7073
.map((r) => {
7174
const config = TOOL_CONFIGS[r.tool];
7275
const mcp = r.mcpConfigured ? ` + MCP` : '';
73-
return `${chalk.bold(config.name)}: ${r.skillPath} (${r.projectFiles.length} project files${mcp})`;
76+
const scopeLabel = r.scope === 'user' ? ' [user]' : ' [project]';
77+
return `${chalk.bold(config.name)}: ${r.skillPath}${scopeLabel} (${r.projectFiles.length} project files${mcp})`;
7478
})
7579
.join('\n'),
7680
'Summary',
@@ -180,6 +184,27 @@ async function selectProjects(projects: ApiProject[], options: InitOptions): Pro
180184
return projects.filter((proj) => selectedSlugs.has(proj.slug));
181185
}
182186

187+
function resolveScope(options: InitOptions, tools: AiTool[]): Scope {
188+
if (options.scope) {
189+
const normalized = options.scope.toLowerCase() as Scope;
190+
if (normalized !== 'project' && normalized !== 'user') {
191+
p.log.error(`Invalid scope: ${options.scope}. Use "project" or "user".`);
192+
process.exit(1);
193+
}
194+
195+
if (normalized === 'user') {
196+
const unsupported = tools.filter((t) => !TOOL_CONFIGS[t].userSkillDir);
197+
if (unsupported.length > 0) {
198+
warn(`User scope not supported for: ${unsupported.map((t) => TOOL_CONFIGS[t].name).join(', ')}. Using project scope for those.`);
199+
}
200+
}
201+
202+
return normalized;
203+
}
204+
205+
return 'project';
206+
}
207+
183208
async function fetchProjects(client: YavyApiClient): Promise<ApiProject[]> {
184209
const s = p.spinner();
185210
s.start('Fetching projects...');

src/commands/init/configure-tool.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,31 @@
11
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
22
import { dirname, join } from 'node:path';
3+
import { homedir } from 'node:os';
34
import type { ApiProject } from '@/api/client';
45
import { YAVY_BASE_URL } from '@/config';
56
import { generateProjectContent, generateSkillContent, slugify } from '@/commands/init/generate-skill';
6-
import { type AiTool, TOOL_CONFIGS } from '@/commands/init/types';
7+
import { type AiTool, type Scope, TOOL_CONFIGS } from '@/commands/init/types';
78
import { warn } from '@/utils';
89

910
export interface ConfigureResult {
1011
tool: AiTool;
1112
skillPath: string;
13+
scope: Scope;
1214
mcpConfigured: boolean;
1315
projectFiles: string[];
1416
}
1517

16-
export function configureTool(tool: AiTool, projects: ApiProject[], cwd: string): ConfigureResult {
18+
export function configureTool(tool: AiTool, projects: ApiProject[], cwd: string, scope: Scope = 'project'): ConfigureResult {
1719
const config = TOOL_CONFIGS[tool];
18-
const skillDir = join(cwd, config.skillDir);
20+
21+
let skillDir: string;
22+
let effectiveScope: Scope = scope;
23+
if (scope === 'user' && config.userSkillDir) {
24+
skillDir = join(homedir(), config.userSkillDir);
25+
} else {
26+
skillDir = join(cwd, config.skillDir);
27+
effectiveScope = 'project';
28+
}
1929
const projectsDir = join(skillDir, 'projects');
2030

2131
mkdirSync(projectsDir, { recursive: true });
@@ -44,7 +54,7 @@ export function configureTool(tool: AiTool, projects: ApiProject[], cwd: string)
4454
}
4555
}
4656

47-
return { tool, skillPath, mcpConfigured, projectFiles };
57+
return { tool, skillPath, scope: effectiveScope, mcpConfigured, projectFiles };
4858
}
4959

5060
function buildMcpUrl(projects: ApiProject[]): string {

src/commands/init/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@ export enum AiTool {
66
OpenCode = 'opencode',
77
}
88

9+
export type Scope = 'project' | 'user';
10+
911
export interface ToolConfig {
1012
name: string;
1113
detectDir: string;
1214
skillDir: string;
15+
userSkillDir: string | null;
1316
mcpConfigPath: string | null;
1417
mcpFormat: 'json' | 'embedded' | null;
1518
mcpServerKey?: string;
@@ -18,6 +21,7 @@ export interface ToolConfig {
1821
export interface InitOptions {
1922
tool?: string;
2023
projects?: string;
24+
scope?: Scope;
2125
yes?: boolean;
2226
}
2327

@@ -26,34 +30,39 @@ export const TOOL_CONFIGS: Record<AiTool, ToolConfig> = {
2630
name: 'Claude Code',
2731
detectDir: '.claude',
2832
skillDir: '.claude/skills/yavy',
33+
userSkillDir: '.claude/skills/yavy',
2934
mcpConfigPath: null,
3035
mcpFormat: null,
3136
},
3237
[AiTool.Cursor]: {
3338
name: 'Cursor',
3439
detectDir: '.cursor',
3540
skillDir: '.cursor/rules/yavy',
41+
userSkillDir: null, // Cursor has no filesystem-based global rules path
3642
mcpConfigPath: '.cursor/mcp.json',
3743
mcpFormat: 'json',
3844
},
3945
[AiTool.Vscode]: {
4046
name: 'VS Code',
4147
detectDir: '.vscode',
4248
skillDir: '.github/instructions/yavy',
49+
userSkillDir: '.copilot/instructions/yavy',
4350
mcpConfigPath: '.vscode/mcp.json',
4451
mcpFormat: 'json',
4552
},
4653
[AiTool.Windsurf]: {
4754
name: 'Windsurf',
4855
detectDir: '.windsurf',
4956
skillDir: '.windsurf/rules/yavy',
57+
userSkillDir: null, // Windsurf uses a single global file, not a directory
5058
mcpConfigPath: null,
5159
mcpFormat: null,
5260
},
5361
[AiTool.OpenCode]: {
5462
name: 'OpenCode',
5563
detectDir: '.opencode',
5664
skillDir: '.opencode/skills/yavy',
65+
userSkillDir: '.config/opencode/skills/yavy',
5766
mcpConfigPath: 'opencode.json',
5867
mcpFormat: 'embedded',
5968
mcpServerKey: 'mcp',

tests/commands/init.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ describe('initCommand', () => {
125125
await run(['--tool', 'claude-code']);
126126

127127
expect(resolveToolFromFlag).toHaveBeenCalledWith('claude-code');
128-
expect(configureTool).toHaveBeenCalledWith(AiTool.ClaudeCode, expect.any(Array), '/project');
128+
expect(configureTool).toHaveBeenCalledWith(AiTool.ClaudeCode, expect.any(Array), '/project', 'project');
129129
});
130130

131131
it('exits with error for unknown --tool', async () => {

0 commit comments

Comments
 (0)