tools-cc 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,12 @@
1
1
  import chalk from 'chalk';
2
2
  import inquirer from 'inquirer';
3
- import { useSource, unuseSource, listUsedSources, initProject } from '../core/project';
3
+ import { useSource, unuseSource, listUsedSources, initProject, importProjectConfig } from '../core/project';
4
4
  import { getSourcePath, listSources } from '../core/source';
5
+ import { scanSource } from '../core/manifest';
5
6
  import { createSymlink, isSymlink } from '../core/symlink';
6
7
  import { GLOBAL_CONFIG_DIR, getToolsccDir, getProjectConfigPath } from '../utils/path';
8
+ import { parseSourcePath, buildSelectionFromPaths } from '../utils/parsePath';
9
+ import type { SourceSelection } from '../types/config';
7
10
  import fs from 'fs-extra';
8
11
  import path from 'path';
9
12
 
@@ -11,69 +14,279 @@ const SUPPORTED_TOOLS: Record<string, string> = {
11
14
  iflow: '.iflow',
12
15
  claude: '.claude',
13
16
  codebuddy: '.codebuddy',
14
- opencode: '.opencode'
17
+ opencode: '.opencode',
18
+ codex: '.codex'
15
19
  };
16
20
 
21
+ /**
22
+ * use 命令选项
23
+ */
24
+ export interface UseOptions {
25
+ projects?: string[];
26
+ ls?: boolean;
27
+ config?: string;
28
+ }
29
+
30
+ /**
31
+ * 处理 use 命令
32
+ *
33
+ * 支持多种模式:
34
+ * 1. 配置导入模式: tools-cc use -c config.json
35
+ * 2. 交互选择模式: tools-cc use my-source --ls
36
+ * 3. 路径语法模式: tools-cc use my-source/skills/a-skill
37
+ * 4. 整体导入模式: tools-cc use my-source
38
+ * 5. 点模式: tools-cc use . (使用已配置源)
39
+ */
17
40
  export async function handleUse(
18
- sourceNames: string[],
19
- options: { projects?: string[] }
41
+ sourceSpecs: string[],
42
+ options: UseOptions
20
43
  ): Promise<void> {
21
44
  const projectDir = process.cwd();
22
-
23
- // 如果没有指定 source,进入交互模式
24
- if (sourceNames.length === 0) {
25
- const sources = await listSources(GLOBAL_CONFIG_DIR);
26
- const sourceList = Object.keys(sources);
27
-
28
- if (sourceList.length === 0) {
29
- console.log(chalk.yellow('No sources configured. Use `tools-cc -s add` to add one.'));
45
+ const toolsccDir = getToolsccDir(projectDir);
46
+ const configFile = getProjectConfigPath(projectDir);
47
+
48
+ // 1. 配置导入模式
49
+ if (options.config) {
50
+ await handleConfigImportMode(options.config, projectDir, options.projects);
51
+ return;
52
+ }
53
+
54
+ // 2. 点模式:使用当前项目已配置的源
55
+ if (sourceSpecs.length === 1 && sourceSpecs[0] === '.') {
56
+ await handleDotMode(projectDir, toolsccDir, configFile, options.projects);
57
+ return;
58
+ }
59
+
60
+ // 3. 交互选择模式:单个源 + --ls 选项
61
+ if (options.ls && sourceSpecs.length === 1) {
62
+ const parsed = parseSourcePath(sourceSpecs[0]);
63
+ // 只有源名称时才进入交互模式
64
+ if (!parsed.type && parsed.sourceName) {
65
+ await handleInteractiveMode(parsed.sourceName, projectDir, options.projects);
30
66
  return;
31
67
  }
32
-
33
- const answers = await inquirer.prompt([
34
- {
35
- type: 'checkbox',
36
- name: 'selectedSources',
37
- message: 'Select sources to use:',
38
- choices: sourceList
39
- }
40
- ]);
41
-
42
- sourceNames = answers.selectedSources;
43
68
  }
44
-
45
- if (sourceNames.length === 0) {
46
- console.log(chalk.gray('No sources selected.'));
47
- return;
69
+
70
+ // 4. 无参数时显示源选择列表
71
+ if (sourceSpecs.length === 0) {
72
+ sourceSpecs = await selectSourcesInteractively();
73
+ if (sourceSpecs.length === 0) {
74
+ console.log(chalk.gray('No sources selected.'));
75
+ return;
76
+ }
48
77
  }
49
-
78
+
79
+ // 5. 解析路径语法并构建选择配置
80
+ const selectionMap = buildSelectionFromPaths(sourceSpecs);
81
+
50
82
  // 初始化项目
51
83
  await initProject(projectDir);
52
-
53
- // 启用每个配置源
54
- for (const sourceName of sourceNames) {
84
+
85
+ // 应用每个源的选择配置
86
+ for (const [sourceName, selection] of Object.entries(selectionMap)) {
55
87
  try {
56
88
  const sourcePath = await getSourcePath(sourceName, GLOBAL_CONFIG_DIR);
57
- await useSource(sourceName, sourcePath, projectDir);
89
+ await useSource(sourceName, sourcePath, projectDir, selection);
58
90
  console.log(chalk.green(`✓ Using source: ${sourceName}`));
59
91
  } catch (error) {
60
92
  console.log(chalk.red(`✗ Failed to use ${sourceName}: ${error instanceof Error ? error.message : 'Unknown error'}`));
61
93
  }
62
94
  }
63
-
95
+
64
96
  // 创建符号链接
65
- const tools = options.projects || Object.keys(SUPPORTED_TOOLS);
66
- const toolsccDir = getToolsccDir(projectDir);
67
-
97
+ await createToolLinks(projectDir, toolsccDir, configFile, options.projects);
98
+ }
99
+
100
+ /**
101
+ * 配置导入模式
102
+ */
103
+ async function handleConfigImportMode(
104
+ configPath: string,
105
+ projectDir: string,
106
+ projects?: string[]
107
+ ): Promise<void> {
108
+ try {
109
+ const toolsccDir = getToolsccDir(projectDir);
110
+ const configFile = getProjectConfigPath(projectDir);
111
+
112
+ // 解析配置文件路径
113
+ const resolvedPath = path.resolve(configPath);
114
+
115
+ // 定义源路径解析函数
116
+ const resolveSourcePath = async (sourceName: string): Promise<string> => {
117
+ return await getSourcePath(sourceName, GLOBAL_CONFIG_DIR);
118
+ };
119
+
120
+ // 导入配置
121
+ await importProjectConfig(resolvedPath, projectDir, resolveSourcePath);
122
+ console.log(chalk.green(`✓ Imported config from: ${resolvedPath}`));
123
+
124
+ // 创建符号链接
125
+ await createToolLinks(projectDir, toolsccDir, configFile, projects);
126
+ } catch (error) {
127
+ console.log(chalk.red(`✗ Failed to import config: ${error instanceof Error ? error.message : 'Unknown error'}`));
128
+ }
129
+ }
130
+
131
+ /**
132
+ * 点模式:使用已配置源创建符号链接
133
+ */
134
+ async function handleDotMode(
135
+ projectDir: string,
136
+ toolsccDir: string,
137
+ configFile: string,
138
+ projects?: string[]
139
+ ): Promise<void> {
140
+ if (!(await fs.pathExists(configFile))) {
141
+ console.log(chalk.yellow('Project not initialized. Run `tools-cc use <source>` first.'));
142
+ return;
143
+ }
144
+
145
+ const config = await fs.readJson(configFile);
146
+ const configuredSources = Object.keys(config.sources || {});
147
+
148
+ if (configuredSources.length === 0) {
149
+ console.log(chalk.yellow('No sources configured in this project. Run `tools-cc use <source>` to add one.'));
150
+ return;
151
+ }
152
+
153
+ console.log(chalk.cyan(`Using existing sources: ${configuredSources.join(', ')}`));
154
+ await createToolLinks(projectDir, toolsccDir, configFile, projects);
155
+ }
156
+
157
+ /**
158
+ * 交互选择模式:显示技能/命令/代理选择列表
159
+ */
160
+ async function handleInteractiveMode(
161
+ sourceName: string,
162
+ projectDir: string,
163
+ projects?: string[]
164
+ ): Promise<void> {
165
+ try {
166
+ const sourcePath = await getSourcePath(sourceName, GLOBAL_CONFIG_DIR);
167
+ const manifest = await scanSource(sourcePath);
168
+
169
+ // 构建选项列表
170
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
171
+ const choices: any[] = [];
172
+
173
+ // Skills 区
174
+ if (manifest.skills && manifest.skills.length > 0) {
175
+ choices.push(new inquirer.Separator(`--- Skills (${manifest.skills.length}) ---`));
176
+ for (const skill of manifest.skills) {
177
+ choices.push({ name: skill, value: `skills/${skill}` });
178
+ }
179
+ }
180
+
181
+ // Commands 区
182
+ if (manifest.commands && manifest.commands.length > 0) {
183
+ choices.push(new inquirer.Separator(`--- Commands (${manifest.commands.length}) ---`));
184
+ for (const cmd of manifest.commands) {
185
+ choices.push({ name: cmd, value: `commands/${cmd}` });
186
+ }
187
+ }
188
+
189
+ // Agents 区
190
+ if (manifest.agents && manifest.agents.length > 0) {
191
+ choices.push(new inquirer.Separator(`--- Agents (${manifest.agents.length}) ---`));
192
+ for (const agent of manifest.agents) {
193
+ choices.push({ name: agent, value: `agents/${agent}` });
194
+ }
195
+ }
196
+
197
+ if (choices.length === 0) {
198
+ console.log(chalk.yellow(`No items found in source: ${sourceName}`));
199
+ return;
200
+ }
201
+
202
+ // 显示选择列表
203
+ const answers = await inquirer.prompt([
204
+ {
205
+ type: 'checkbox',
206
+ name: 'selectedItems',
207
+ message: `Select items from ${sourceName}:`,
208
+ choices,
209
+ pageSize: 15
210
+ }
211
+ ]);
212
+
213
+ if (answers.selectedItems.length === 0) {
214
+ console.log(chalk.gray('No items selected.'));
215
+ return;
216
+ }
217
+
218
+ // 将选择转换为 SourceSelection
219
+ const selection: SourceSelection = {
220
+ skills: [],
221
+ commands: [],
222
+ agents: []
223
+ };
224
+
225
+ for (const item of answers.selectedItems) {
226
+ const [type, name] = item.split('/');
227
+ if (type === 'skills') selection.skills.push(name);
228
+ else if (type === 'commands') selection.commands.push(name);
229
+ else if (type === 'agents') selection.agents.push(name);
230
+ }
231
+
232
+ // 初始化项目并应用选择
233
+ await initProject(projectDir);
234
+ await useSource(sourceName, sourcePath, projectDir, selection);
235
+ console.log(chalk.green(`✓ Using source: ${sourceName}`));
236
+
237
+ // 创建符号链接
238
+ const toolsccDir = getToolsccDir(projectDir);
239
+ const configFile = getProjectConfigPath(projectDir);
240
+ await createToolLinks(projectDir, toolsccDir, configFile, projects);
241
+ } catch (error) {
242
+ console.log(chalk.red(`✗ Failed to use ${sourceName}: ${error instanceof Error ? error.message : 'Unknown error'}`));
243
+ }
244
+ }
245
+
246
+ /**
247
+ * 显示源选择列表并返回用户选择
248
+ */
249
+ async function selectSourcesInteractively(): Promise<string[]> {
250
+ const sources = await listSources(GLOBAL_CONFIG_DIR);
251
+ const sourceList = Object.keys(sources);
252
+
253
+ if (sourceList.length === 0) {
254
+ console.log(chalk.yellow('No sources configured. Use `tools-cc -s add` to add one.'));
255
+ return [];
256
+ }
257
+
258
+ const answers = await inquirer.prompt([
259
+ {
260
+ type: 'checkbox',
261
+ name: 'selectedSources',
262
+ message: 'Select sources to use:',
263
+ choices: sourceList
264
+ }
265
+ ]);
266
+
267
+ return answers.selectedSources;
268
+ }
269
+
270
+ /**
271
+ * 创建工具符号链接
272
+ */
273
+ async function createToolLinks(
274
+ projectDir: string,
275
+ toolsccDir: string,
276
+ configFile: string,
277
+ projects?: string[]
278
+ ): Promise<void> {
279
+ const tools = projects || Object.keys(SUPPORTED_TOOLS);
280
+
68
281
  for (const tool of tools) {
69
282
  const linkName = SUPPORTED_TOOLS[tool];
70
283
  if (!linkName) {
71
284
  console.log(chalk.yellow(`Unknown tool: ${tool}`));
72
285
  continue;
73
286
  }
74
-
287
+
75
288
  const linkPath = path.join(projectDir, linkName);
76
-
289
+
77
290
  try {
78
291
  await createSymlink(toolsccDir, linkPath, true);
79
292
  console.log(chalk.green(`✓ Linked: ${linkName} -> .toolscc`));
@@ -81,9 +294,8 @@ export async function handleUse(
81
294
  console.log(chalk.red(`✗ Failed to link ${linkName}: ${error instanceof Error ? error.message : 'Unknown error'}`));
82
295
  }
83
296
  }
84
-
297
+
85
298
  // 更新项目配置
86
- const configFile = getProjectConfigPath(projectDir);
87
299
  const config = await fs.readJson(configFile);
88
300
  const existingLinks = config.links || [];
89
301
  config.links = [...new Set([...existingLinks, ...tools])];
@@ -157,9 +369,11 @@ export async function handleProjectUpdate(sourceNames?: string[]): Promise<void>
157
369
  }
158
370
 
159
371
  const config = await fs.readJson(configFile);
372
+ const configuredSources = Object.keys(config.sources || {});
373
+
160
374
  let sourcesToUpdate = sourceNames && sourceNames.length > 0
161
375
  ? sourceNames
162
- : config.sources || [];
376
+ : configuredSources;
163
377
 
164
378
  if (sourcesToUpdate.length === 0) {
165
379
  console.log(chalk.gray('No sources to update.'));
@@ -168,18 +382,20 @@ export async function handleProjectUpdate(sourceNames?: string[]): Promise<void>
168
382
 
169
383
  // 验证指定的源是否存在于项目配置中
170
384
  if (sourceNames && sourceNames.length > 0) {
171
- const invalidSources = sourceNames.filter((s: string) => !config.sources.includes(s));
385
+ const invalidSources = sourceNames.filter((s: string) => !configuredSources.includes(s));
172
386
  if (invalidSources.length > 0) {
173
387
  console.log(chalk.yellow(`Sources not in project: ${invalidSources.join(', ')}`));
174
388
  }
175
- sourcesToUpdate = sourcesToUpdate.filter((s: string) => config.sources.includes(s));
389
+ sourcesToUpdate = sourcesToUpdate.filter((s: string) => configuredSources.includes(s));
176
390
  }
177
391
 
178
392
  // 更新每个配置源
179
393
  for (const sourceName of sourcesToUpdate) {
180
394
  try {
181
395
  const sourcePath = await getSourcePath(sourceName, GLOBAL_CONFIG_DIR);
182
- await useSource(sourceName, sourcePath, projectDir);
396
+ // 使用保存的选择配置进行更新
397
+ const selection = config.sources[sourceName];
398
+ await useSource(sourceName, sourcePath, projectDir, selection);
183
399
  console.log(chalk.green(`✓ Updated source: ${sourceName}`));
184
400
  } catch (error) {
185
401
  console.log(chalk.red(`✗ Failed to update ${sourceName}: ${error instanceof Error ? error.message : 'Unknown error'}`));
@@ -187,4 +403,4 @@ export async function handleProjectUpdate(sourceNames?: string[]): Promise<void>
187
403
  }
188
404
 
189
405
  console.log(chalk.green(`\n✓ Project update complete`));
190
- }
406
+ }
@@ -1,9 +1,18 @@
1
1
  import fs from 'fs-extra';
2
2
  import path from 'path';
3
- import { ProjectConfig } from '../types';
3
+ import { ProjectConfig, LegacyProjectConfig, normalizeProjectConfig, SourceSelection, ExportConfig } from '../types';
4
4
  import { loadManifest } from './manifest';
5
5
  import { getToolsccDir, getProjectConfigPath } from '../utils/path';
6
6
 
7
+ /**
8
+ * 默认选择配置 - 导入所有内容
9
+ */
10
+ const DEFAULT_SELECTION: SourceSelection = {
11
+ skills: ['*'],
12
+ commands: ['*'],
13
+ agents: ['*']
14
+ };
15
+
7
16
  export async function initProject(projectDir: string): Promise<void> {
8
17
  const toolsccDir = getToolsccDir(projectDir);
9
18
  const configFile = getProjectConfigPath(projectDir);
@@ -16,17 +25,45 @@ export async function initProject(projectDir: string): Promise<void> {
16
25
  // Create project config if not exists
17
26
  if (!(await fs.pathExists(configFile))) {
18
27
  const config: ProjectConfig = {
19
- sources: [],
28
+ sources: {},
20
29
  links: []
21
30
  };
22
31
  await fs.writeJson(configFile, config, { spaces: 2 });
23
32
  }
24
33
  }
25
34
 
35
+ /**
36
+ * 读取项目配置,自动处理新旧格式
37
+ */
38
+ async function readProjectConfig(configFile: string): Promise<ProjectConfig> {
39
+ const rawConfig = await fs.readJson(configFile);
40
+ return normalizeProjectConfig(rawConfig);
41
+ }
42
+
43
+ /**
44
+ * 获取源名称列表(兼容新旧格式)
45
+ */
46
+ function getSourceNames(config: ProjectConfig): string[] {
47
+ return Object.keys(config.sources);
48
+ }
49
+
50
+ /**
51
+ * 检查是否应该复制某项(根据选择配置)
52
+ */
53
+ function shouldInclude(itemName: string, selection: string[]): boolean {
54
+ // 如果选择包含通配符,包含所有项
55
+ if (selection.includes('*')) {
56
+ return true;
57
+ }
58
+ // 否则检查是否在选择列表中
59
+ return selection.includes(itemName);
60
+ }
61
+
26
62
  export async function useSource(
27
63
  sourceName: string,
28
64
  sourceDir: string,
29
- projectDir: string
65
+ projectDir: string,
66
+ selection?: SourceSelection
30
67
  ): Promise<void> {
31
68
  // Input validation
32
69
  if (!sourceName || !sourceName.trim()) {
@@ -44,11 +81,19 @@ export async function useSource(
44
81
  // Ensure project is initialized
45
82
  await initProject(projectDir);
46
83
 
84
+ // 使用传入的选择配置或默认配置
85
+ const effectiveSelection: SourceSelection = selection ?? DEFAULT_SELECTION;
86
+
47
87
  // Copy/link skills (flattened with prefix)
48
88
  const sourceSkillsDir = path.join(sourceDir, 'skills');
49
89
  if (await fs.pathExists(sourceSkillsDir)) {
50
90
  const skills = await fs.readdir(sourceSkillsDir);
51
91
  for (const skill of skills) {
92
+ // 检查是否应该包含此 skill
93
+ if (!shouldInclude(skill, effectiveSelection.skills)) {
94
+ continue;
95
+ }
96
+
52
97
  const srcPath = path.join(sourceSkillsDir, skill);
53
98
  const name = `${sourceName}` == `${skill}` ? skill : `${sourceName}-${skill}`;
54
99
  const destPath = path.join(toolsccDir, 'skills', name);
@@ -64,25 +109,53 @@ export async function useSource(
64
109
  // Copy commands (in subdirectory by source name)
65
110
  const sourceCommandsDir = path.join(sourceDir, 'commands');
66
111
  if (await fs.pathExists(sourceCommandsDir)) {
67
- const destDir = path.join(toolsccDir, 'commands', sourceName);
68
- await fs.remove(destDir);
69
- await fs.copy(sourceCommandsDir, destDir);
112
+ // 检查是否有选择 commands
113
+ if (effectiveSelection.commands.includes('*')) {
114
+ // 复制所有 commands
115
+ const destDir = path.join(toolsccDir, 'commands', sourceName);
116
+ await fs.remove(destDir);
117
+ await fs.copy(sourceCommandsDir, destDir);
118
+ } else if (effectiveSelection.commands.length > 0) {
119
+ // 只复制选中的 commands
120
+ const destDir = path.join(toolsccDir, 'commands', sourceName);
121
+ await fs.ensureDir(destDir);
122
+
123
+ for (const cmdName of effectiveSelection.commands) {
124
+ const srcFile = path.join(sourceCommandsDir, `${cmdName}.md`);
125
+ if (await fs.pathExists(srcFile)) {
126
+ await fs.copy(srcFile, path.join(destDir, `${cmdName}.md`));
127
+ }
128
+ }
129
+ }
70
130
  }
71
131
 
72
132
  // Copy agents (in subdirectory by source name)
73
133
  const sourceAgentsDir = path.join(sourceDir, 'agents');
74
134
  if (await fs.pathExists(sourceAgentsDir)) {
75
- const destDir = path.join(toolsccDir, 'agents', sourceName);
76
- await fs.remove(destDir);
77
- await fs.copy(sourceAgentsDir, destDir);
135
+ // 检查是否有选择 agents
136
+ if (effectiveSelection.agents.includes('*')) {
137
+ // 复制所有 agents
138
+ const destDir = path.join(toolsccDir, 'agents', sourceName);
139
+ await fs.remove(destDir);
140
+ await fs.copy(sourceAgentsDir, destDir);
141
+ } else if (effectiveSelection.agents.length > 0) {
142
+ // 只复制选中的 agents
143
+ const destDir = path.join(toolsccDir, 'agents', sourceName);
144
+ await fs.ensureDir(destDir);
145
+
146
+ for (const agentName of effectiveSelection.agents) {
147
+ const srcFile = path.join(sourceAgentsDir, `${agentName}.md`);
148
+ if (await fs.pathExists(srcFile)) {
149
+ await fs.copy(srcFile, path.join(destDir, `${agentName}.md`));
150
+ }
151
+ }
152
+ }
78
153
  }
79
154
 
80
- // Update project config
155
+ // Update project config - 保存实际使用的选择配置
81
156
  const configFile = getProjectConfigPath(projectDir);
82
- const config: ProjectConfig = await fs.readJson(configFile);
83
- if (!config.sources.includes(sourceName)) {
84
- config.sources.push(sourceName);
85
- }
157
+ const config = await readProjectConfig(configFile);
158
+ config.sources[sourceName] = effectiveSelection;
86
159
  await fs.writeJson(configFile, config, { spaces: 2 });
87
160
  }
88
161
 
@@ -115,13 +188,13 @@ export async function unuseSource(sourceName: string, projectDir: string): Promi
115
188
  // Update project config with error handling
116
189
  let config: ProjectConfig;
117
190
  try {
118
- config = await fs.readJson(configFile);
191
+ config = await readProjectConfig(configFile);
119
192
  } catch (error) {
120
193
  // If config file doesn't exist or is invalid, nothing to update
121
194
  return;
122
195
  }
123
196
 
124
- config.sources = config.sources.filter(s => s !== sourceName);
197
+ delete config.sources[sourceName];
125
198
  await fs.writeJson(configFile, config, { spaces: 2 });
126
199
  }
127
200
 
@@ -132,6 +205,73 @@ export async function listUsedSources(projectDir: string): Promise<string[]> {
132
205
  return [];
133
206
  }
134
207
 
135
- const config: ProjectConfig = await fs.readJson(configFile);
136
- return config.sources;
208
+ const config = await readProjectConfig(configFile);
209
+ return getSourceNames(config);
137
210
  }
211
+
212
+ /**
213
+ * 导出项目配置到 JSON 文件
214
+ */
215
+ export async function exportProjectConfig(
216
+ projectDir: string,
217
+ outputPath: string
218
+ ): Promise<void> {
219
+ const configFile = getProjectConfigPath(projectDir);
220
+
221
+ if (!(await fs.pathExists(configFile))) {
222
+ throw new Error('Project not initialized. Use `tools-cc use <source>` to get started.');
223
+ }
224
+
225
+ const config = await readProjectConfig(configFile);
226
+
227
+ const exportConfig: ExportConfig = {
228
+ version: '1.0',
229
+ type: 'project',
230
+ config,
231
+ exportedAt: new Date().toISOString()
232
+ };
233
+
234
+ await fs.writeJson(outputPath, exportConfig, { spaces: 2 });
235
+ }
236
+
237
+ /**
238
+ * 从 JSON 文件导入项目配置
239
+ */
240
+ export async function importProjectConfig(
241
+ configPath: string,
242
+ projectDir: string,
243
+ resolveSourcePath: (sourceName: string) => Promise<string>
244
+ ): Promise<void> {
245
+ if (!(await fs.pathExists(configPath))) {
246
+ throw new Error(`Config file not found: ${configPath}`);
247
+ }
248
+
249
+ const exportConfig: ExportConfig = await fs.readJson(configPath);
250
+
251
+ // Validate version
252
+ if (exportConfig.version !== '1.0') {
253
+ throw new Error(`Unsupported config version: ${exportConfig.version}`);
254
+ }
255
+
256
+ // Validate type
257
+ if (exportConfig.type !== 'project') {
258
+ throw new Error(`Invalid config type: ${exportConfig.type}. Expected 'project'.`);
259
+ }
260
+
261
+ // Initialize project
262
+ await initProject(projectDir);
263
+
264
+ // Apply each source
265
+ for (const [sourceName, selection] of Object.entries(exportConfig.config.sources)) {
266
+ const sourceDir = await resolveSourcePath(sourceName);
267
+ await useSource(sourceName, sourceDir, projectDir, selection);
268
+ }
269
+
270
+ // Update links if present
271
+ if (exportConfig.config.links && exportConfig.config.links.length > 0) {
272
+ const configFile = getProjectConfigPath(projectDir);
273
+ const config = await readProjectConfig(configFile);
274
+ config.links = exportConfig.config.links;
275
+ await fs.writeJson(configFile, config, { spaces: 2 });
276
+ }
277
+ }