@anaclumos/taal 1.1.2
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.
- package/.claude/settings.json +15 -0
- package/.github/workflows/ci.yml +28 -0
- package/.github/workflows/publish.yml +42 -0
- package/AGENTS.md +123 -0
- package/README.md +568 -0
- package/biome.jsonc +24 -0
- package/lefthook.yml +12 -0
- package/package.json +52 -0
- package/src/commands/collect.ts +172 -0
- package/src/commands/diff.ts +127 -0
- package/src/commands/init.ts +66 -0
- package/src/commands/list.ts +80 -0
- package/src/commands/providers.ts +46 -0
- package/src/commands/sync.ts +111 -0
- package/src/commands/validate.ts +17 -0
- package/src/config/env.ts +49 -0
- package/src/config/loader.ts +88 -0
- package/src/config/parser.ts +44 -0
- package/src/config/schema.ts +67 -0
- package/src/errors/index.ts +43 -0
- package/src/index.ts +301 -0
- package/src/providers/antigravity.ts +24 -0
- package/src/providers/base.ts +70 -0
- package/src/providers/claude-code.ts +12 -0
- package/src/providers/claude-desktop.ts +19 -0
- package/src/providers/codex.ts +61 -0
- package/src/providers/continue.ts +62 -0
- package/src/providers/cursor.ts +25 -0
- package/src/providers/index.ts +34 -0
- package/src/providers/opencode.ts +42 -0
- package/src/providers/registry.ts +74 -0
- package/src/providers/types.ts +99 -0
- package/src/providers/utils.ts +106 -0
- package/src/providers/windsurf.ts +50 -0
- package/src/providers/zed.ts +35 -0
- package/src/scripts/generate-schema.ts +17 -0
- package/src/skills/copy.ts +58 -0
- package/src/skills/discovery.ts +87 -0
- package/src/skills/validator.ts +95 -0
- package/src/utils/atomic-write.ts +35 -0
- package/src/utils/backup.ts +27 -0
- package/taal.schema.json +91 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import type { McpServer } from "../config/schema.js";
|
|
3
|
+
import { initializeProviders, registry } from "../providers/index.js";
|
|
4
|
+
|
|
5
|
+
export interface CollectConflict {
|
|
6
|
+
serverName: string;
|
|
7
|
+
providers: string[];
|
|
8
|
+
configs: unknown[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CollectResult {
|
|
12
|
+
servers: Record<string, McpServer>;
|
|
13
|
+
conflicts: CollectConflict[];
|
|
14
|
+
summary: {
|
|
15
|
+
totalServers: number;
|
|
16
|
+
providersScanned: number;
|
|
17
|
+
providersWithConfigs: number;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Collect MCP server configs from all installed providers
|
|
23
|
+
*/
|
|
24
|
+
export async function collect(baseDir?: string): Promise<CollectResult> {
|
|
25
|
+
initializeProviders();
|
|
26
|
+
const home = baseDir || homedir();
|
|
27
|
+
const servers: Record<string, McpServer> = {};
|
|
28
|
+
const conflicts: CollectConflict[] = [];
|
|
29
|
+
const serverSources = new Map<
|
|
30
|
+
string,
|
|
31
|
+
{ provider: string; config: unknown }[]
|
|
32
|
+
>();
|
|
33
|
+
|
|
34
|
+
let providersScanned = 0;
|
|
35
|
+
let providersWithConfigs = 0;
|
|
36
|
+
|
|
37
|
+
const allProviders = registry.getAll();
|
|
38
|
+
|
|
39
|
+
for (const provider of allProviders) {
|
|
40
|
+
providersScanned++;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// Check if provider is installed
|
|
44
|
+
const isInstalled = await provider.isInstalled(home);
|
|
45
|
+
if (!isInstalled) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Read provider config
|
|
50
|
+
const config = await provider.readConfig(home);
|
|
51
|
+
if (!config || typeof config !== "object") {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Extract MCP servers from config
|
|
56
|
+
const mcpServers = (config as Record<string, unknown>)[provider.mcpKey];
|
|
57
|
+
if (!mcpServers || typeof mcpServers !== "object") {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
providersWithConfigs++;
|
|
62
|
+
|
|
63
|
+
// Process each server
|
|
64
|
+
for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
|
|
65
|
+
// Track source for conflict detection
|
|
66
|
+
if (!serverSources.has(serverName)) {
|
|
67
|
+
serverSources.set(serverName, []);
|
|
68
|
+
}
|
|
69
|
+
serverSources.get(serverName)?.push({
|
|
70
|
+
provider: provider.name,
|
|
71
|
+
config: serverConfig,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Convert to TAAL format
|
|
75
|
+
const taalServer = convertToTaalFormat(serverConfig);
|
|
76
|
+
if (taalServer) {
|
|
77
|
+
servers[serverName] = taalServer;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
// Skip providers that fail to read
|
|
82
|
+
console.warn(
|
|
83
|
+
`Warning: Failed to read config from ${provider.name}:`,
|
|
84
|
+
error
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Detect conflicts
|
|
90
|
+
for (const [serverName, sources] of serverSources.entries()) {
|
|
91
|
+
if (sources.length > 1) {
|
|
92
|
+
// Check if configs are actually different
|
|
93
|
+
const configStrings = sources.map((s) => JSON.stringify(s.config));
|
|
94
|
+
const uniqueConfigs = new Set(configStrings);
|
|
95
|
+
|
|
96
|
+
if (uniqueConfigs.size > 1) {
|
|
97
|
+
conflicts.push({
|
|
98
|
+
serverName,
|
|
99
|
+
providers: sources.map((s) => s.provider),
|
|
100
|
+
configs: sources.map((s) => s.config),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
servers,
|
|
108
|
+
conflicts,
|
|
109
|
+
summary: {
|
|
110
|
+
totalServers: Object.keys(servers).length,
|
|
111
|
+
providersScanned,
|
|
112
|
+
providersWithConfigs,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Convert provider-specific server config to TAAL format
|
|
119
|
+
*/
|
|
120
|
+
function convertToTaalFormat(config: unknown): McpServer | null {
|
|
121
|
+
if (!config || typeof config !== "object") {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const obj = config as Record<string, unknown>;
|
|
126
|
+
|
|
127
|
+
// HTTP server
|
|
128
|
+
if (obj.url && typeof obj.url === "string") {
|
|
129
|
+
return {
|
|
130
|
+
url: obj.url,
|
|
131
|
+
headers:
|
|
132
|
+
(obj.headers as Record<string, string>) ||
|
|
133
|
+
(obj.http_headers as Record<string, string>),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Stdio server
|
|
138
|
+
if (obj.command && typeof obj.command === "string") {
|
|
139
|
+
const server: McpServer = {
|
|
140
|
+
command: obj.command,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
if (Array.isArray(obj.args)) {
|
|
144
|
+
server.args = obj.args as string[];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (obj.env && typeof obj.env === "object") {
|
|
148
|
+
server.env = obj.env as Record<string, string>;
|
|
149
|
+
} else if (obj.environment && typeof obj.environment === "object") {
|
|
150
|
+
server.env = obj.environment as Record<string, string>;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return server;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// OpenCode format with command array
|
|
157
|
+
if (Array.isArray(obj.command) && obj.command.length > 0) {
|
|
158
|
+
const [cmd, ...args] = obj.command as string[];
|
|
159
|
+
const server: McpServer = {
|
|
160
|
+
command: cmd,
|
|
161
|
+
args,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
if (obj.environment && typeof obj.environment === "object") {
|
|
165
|
+
server.env = obj.environment as Record<string, string>;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return server;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { loadTaalConfig } from "../config/loader.js";
|
|
3
|
+
import { initializeProviders, registry } from "../providers/index.js";
|
|
4
|
+
|
|
5
|
+
export interface DiffChange {
|
|
6
|
+
type: "add" | "remove" | "modify";
|
|
7
|
+
serverName: string;
|
|
8
|
+
provider: string;
|
|
9
|
+
oldValue?: unknown;
|
|
10
|
+
newValue?: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface DiffResult {
|
|
14
|
+
hasChanges: boolean;
|
|
15
|
+
changes: DiffChange[];
|
|
16
|
+
provider?: string;
|
|
17
|
+
error?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function diff(
|
|
21
|
+
baseDir?: string,
|
|
22
|
+
providerName?: string
|
|
23
|
+
): Promise<DiffResult> {
|
|
24
|
+
initializeProviders();
|
|
25
|
+
const home = baseDir || homedir();
|
|
26
|
+
|
|
27
|
+
const result = await loadTaalConfig(baseDir);
|
|
28
|
+
|
|
29
|
+
if (!result.config) {
|
|
30
|
+
return {
|
|
31
|
+
hasChanges: false,
|
|
32
|
+
changes: [],
|
|
33
|
+
error: result.errors[0] || "Config file not found",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const config = result.config;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const changes: DiffChange[] = [];
|
|
41
|
+
|
|
42
|
+
const providers = providerName
|
|
43
|
+
? [registry.get(providerName)].filter(Boolean)
|
|
44
|
+
: registry.getAll();
|
|
45
|
+
|
|
46
|
+
for (const provider of providers) {
|
|
47
|
+
if (!provider) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const isInstalled = await provider.isInstalled(home);
|
|
52
|
+
if (!isInstalled) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const enabledProviders = config.providers?.enabled || [];
|
|
57
|
+
if (!enabledProviders.includes(provider.name)) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const currentConfig = await provider.readConfig(home);
|
|
63
|
+
const currentServers: Record<string, unknown> =
|
|
64
|
+
((currentConfig as Record<string, unknown>)?.[
|
|
65
|
+
provider.mcpKey
|
|
66
|
+
] as Record<string, unknown>) || {};
|
|
67
|
+
|
|
68
|
+
const taalServers = config.mcp || {};
|
|
69
|
+
const transformedServers = provider.transformMcpServers(taalServers);
|
|
70
|
+
|
|
71
|
+
const currentKeys = new Set(Object.keys(currentServers));
|
|
72
|
+
const taalKeys = new Set(Object.keys(transformedServers as object));
|
|
73
|
+
|
|
74
|
+
for (const key of taalKeys) {
|
|
75
|
+
if (currentKeys.has(key)) {
|
|
76
|
+
const currentValue = JSON.stringify(currentServers[key]);
|
|
77
|
+
const newValue = JSON.stringify(
|
|
78
|
+
(transformedServers as Record<string, unknown>)[key]
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (currentValue !== newValue) {
|
|
82
|
+
changes.push({
|
|
83
|
+
type: "modify",
|
|
84
|
+
serverName: key,
|
|
85
|
+
provider: provider.name,
|
|
86
|
+
oldValue: currentServers[key],
|
|
87
|
+
newValue: (transformedServers as Record<string, unknown>)[key],
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
changes.push({
|
|
92
|
+
type: "add",
|
|
93
|
+
serverName: key,
|
|
94
|
+
provider: provider.name,
|
|
95
|
+
newValue: (transformedServers as Record<string, unknown>)[key],
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const key of currentKeys) {
|
|
101
|
+
if (!taalKeys.has(key)) {
|
|
102
|
+
changes.push({
|
|
103
|
+
type: "remove",
|
|
104
|
+
serverName: key,
|
|
105
|
+
provider: provider.name,
|
|
106
|
+
oldValue: currentServers[key],
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch (_error) {
|
|
111
|
+
// Ignore provider read errors - continue with other providers
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
hasChanges: changes.length > 0,
|
|
117
|
+
changes,
|
|
118
|
+
provider: providerName,
|
|
119
|
+
};
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return {
|
|
122
|
+
hasChanges: false,
|
|
123
|
+
changes: [],
|
|
124
|
+
error: error instanceof Error ? error.message : String(error),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { exists, mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const SAMPLE_CONFIG = `# TAAL Configuration
|
|
6
|
+
# https://github.com/user/taal
|
|
7
|
+
|
|
8
|
+
version: "1"
|
|
9
|
+
|
|
10
|
+
mcp: {}
|
|
11
|
+
# Example stdio server
|
|
12
|
+
# example-server:
|
|
13
|
+
# command: npx
|
|
14
|
+
# args: ["-y", "@example/mcp-server"]
|
|
15
|
+
# env:
|
|
16
|
+
# API_KEY: "\${API_KEY}"
|
|
17
|
+
|
|
18
|
+
# Example HTTP server
|
|
19
|
+
# context7:
|
|
20
|
+
# url: https://mcp.context7.com/mcp
|
|
21
|
+
# headers:
|
|
22
|
+
# CONTEXT7_API_KEY: "\${CONTEXT7_API_KEY}"
|
|
23
|
+
|
|
24
|
+
skills:
|
|
25
|
+
paths:
|
|
26
|
+
- ~/.taal/skills
|
|
27
|
+
|
|
28
|
+
providers:
|
|
29
|
+
enabled:
|
|
30
|
+
- claude-desktop
|
|
31
|
+
- claude-code
|
|
32
|
+
- cursor
|
|
33
|
+
- continue
|
|
34
|
+
- zed
|
|
35
|
+
- opencode
|
|
36
|
+
- codex
|
|
37
|
+
- windsurf
|
|
38
|
+
- antigravity
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
export interface InitOptions {
|
|
42
|
+
force?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function init(
|
|
46
|
+
baseDir?: string,
|
|
47
|
+
options: InitOptions = {}
|
|
48
|
+
): Promise<void> {
|
|
49
|
+
const taalDir = join(baseDir || homedir(), ".taal");
|
|
50
|
+
const configPath = join(taalDir, "config.yaml");
|
|
51
|
+
const skillsDir = join(taalDir, "skills");
|
|
52
|
+
const backupsDir = join(taalDir, "backups");
|
|
53
|
+
|
|
54
|
+
// Check if already initialized
|
|
55
|
+
if ((await exists(configPath)) && !options.force) {
|
|
56
|
+
throw new Error("TAAL is already initialized. Use --force to overwrite.");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Create directories
|
|
60
|
+
await mkdir(taalDir, { recursive: true });
|
|
61
|
+
await mkdir(skillsDir, { recursive: true });
|
|
62
|
+
await mkdir(backupsDir, { recursive: true });
|
|
63
|
+
|
|
64
|
+
// Write sample config
|
|
65
|
+
await writeFile(configPath, SAMPLE_CONFIG, "utf-8");
|
|
66
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { loadTaalConfig } from "../config/loader.js";
|
|
3
|
+
import { discoverSkills } from "../skills/discovery.js";
|
|
4
|
+
|
|
5
|
+
export interface ServerInfo {
|
|
6
|
+
name: string;
|
|
7
|
+
type: "stdio" | "http";
|
|
8
|
+
command?: string;
|
|
9
|
+
url?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SkillInfo {
|
|
13
|
+
name: string;
|
|
14
|
+
path: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ListResult {
|
|
18
|
+
servers: ServerInfo[];
|
|
19
|
+
skills: SkillInfo[];
|
|
20
|
+
enabledProviders: string[];
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function list(baseDir?: string): Promise<ListResult> {
|
|
25
|
+
const home = baseDir || homedir();
|
|
26
|
+
const result = await loadTaalConfig(baseDir);
|
|
27
|
+
|
|
28
|
+
if (!result.config) {
|
|
29
|
+
return {
|
|
30
|
+
servers: [],
|
|
31
|
+
skills: [],
|
|
32
|
+
enabledProviders: [],
|
|
33
|
+
error: result.errors[0] || "Config file not found",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const config = result.config;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const servers: ServerInfo[] = [];
|
|
41
|
+
for (const [name, server] of Object.entries(config.mcp || {})) {
|
|
42
|
+
if (server.url) {
|
|
43
|
+
servers.push({
|
|
44
|
+
name,
|
|
45
|
+
type: "http",
|
|
46
|
+
url: server.url,
|
|
47
|
+
});
|
|
48
|
+
} else if (server.command) {
|
|
49
|
+
servers.push({
|
|
50
|
+
name,
|
|
51
|
+
type: "stdio",
|
|
52
|
+
command: server.command,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const skillPaths = config.skills?.paths || [];
|
|
58
|
+
const discoveredSkills = await discoverSkills(skillPaths, home);
|
|
59
|
+
|
|
60
|
+
const skills: SkillInfo[] = discoveredSkills.map((skill) => ({
|
|
61
|
+
name: skill.name,
|
|
62
|
+
path: skill.path,
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
const enabledProviders = config.providers?.enabled || [];
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
servers,
|
|
69
|
+
skills,
|
|
70
|
+
enabledProviders,
|
|
71
|
+
};
|
|
72
|
+
} catch (error) {
|
|
73
|
+
return {
|
|
74
|
+
servers: [],
|
|
75
|
+
skills: [],
|
|
76
|
+
enabledProviders: [],
|
|
77
|
+
error: error instanceof Error ? error.message : String(error),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { loadTaalConfig } from "../config/loader.js";
|
|
3
|
+
import { initializeProviders, registry } from "../providers/index.js";
|
|
4
|
+
|
|
5
|
+
export interface ProviderInfo {
|
|
6
|
+
name: string;
|
|
7
|
+
configPath: string;
|
|
8
|
+
format: string;
|
|
9
|
+
installed: boolean;
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ProvidersResult {
|
|
14
|
+
providers: ProviderInfo[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function providers(baseDir?: string): Promise<ProvidersResult> {
|
|
18
|
+
initializeProviders();
|
|
19
|
+
const home = baseDir || homedir();
|
|
20
|
+
|
|
21
|
+
const result = await loadTaalConfig(baseDir);
|
|
22
|
+
const enabledProviders = result.config?.providers?.enabled || [];
|
|
23
|
+
|
|
24
|
+
const allProviders = registry.getAll();
|
|
25
|
+
const providerInfos: ProviderInfo[] = [];
|
|
26
|
+
|
|
27
|
+
for (const provider of allProviders) {
|
|
28
|
+
const installed = await provider.isInstalled(home);
|
|
29
|
+
const configPathResolved =
|
|
30
|
+
typeof provider.configPath === "function"
|
|
31
|
+
? provider.configPath(home)
|
|
32
|
+
: provider.configPath;
|
|
33
|
+
|
|
34
|
+
providerInfos.push({
|
|
35
|
+
name: provider.name,
|
|
36
|
+
configPath: configPathResolved,
|
|
37
|
+
format: provider.format,
|
|
38
|
+
installed,
|
|
39
|
+
enabled: enabledProviders.includes(provider.name),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
providers: providerInfos,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { loadTaalConfig } from "../config/loader.js";
|
|
3
|
+
import { initializeProviders, registry } from "../providers/index.js";
|
|
4
|
+
import { copySkillsToProvider } from "../skills/copy.js";
|
|
5
|
+
import { discoverSkills } from "../skills/discovery.js";
|
|
6
|
+
import { backupConfig } from "../utils/backup.js";
|
|
7
|
+
|
|
8
|
+
export interface SyncResult {
|
|
9
|
+
success: boolean;
|
|
10
|
+
synced: string[];
|
|
11
|
+
failed: Array<{ provider: string; error: string }>;
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function sync(
|
|
16
|
+
baseDir?: string,
|
|
17
|
+
providerName?: string
|
|
18
|
+
): Promise<SyncResult> {
|
|
19
|
+
initializeProviders();
|
|
20
|
+
const home = baseDir || homedir();
|
|
21
|
+
|
|
22
|
+
const result = await loadTaalConfig(baseDir);
|
|
23
|
+
|
|
24
|
+
if (!result.config) {
|
|
25
|
+
return {
|
|
26
|
+
success: false,
|
|
27
|
+
synced: [],
|
|
28
|
+
failed: [],
|
|
29
|
+
error: result.errors[0] || "Config file not found",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const config = result.config;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const synced: string[] = [];
|
|
37
|
+
const failed: Array<{ provider: string; error: string }> = [];
|
|
38
|
+
|
|
39
|
+
const providers = providerName
|
|
40
|
+
? [registry.get(providerName)].filter(Boolean)
|
|
41
|
+
: registry.getAll();
|
|
42
|
+
|
|
43
|
+
const enabledProviders = config.providers?.enabled || [];
|
|
44
|
+
|
|
45
|
+
for (const provider of providers) {
|
|
46
|
+
if (!provider) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!enabledProviders.includes(provider.name)) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const isInstalled = await provider.isInstalled(home);
|
|
56
|
+
if (!isInstalled) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const currentConfig = await provider.readConfig(home);
|
|
61
|
+
|
|
62
|
+
await backupConfig(
|
|
63
|
+
typeof provider.configPath === "function"
|
|
64
|
+
? provider.configPath(home)
|
|
65
|
+
: provider.configPath,
|
|
66
|
+
home
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const taalServers = config.mcp || {};
|
|
70
|
+
const transformedServers = provider.transformMcpServers(taalServers);
|
|
71
|
+
|
|
72
|
+
const newConfig = {
|
|
73
|
+
...(currentConfig as object),
|
|
74
|
+
[provider.mcpKey]: transformedServers,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
await provider.writeConfig(newConfig, home);
|
|
78
|
+
|
|
79
|
+
if (provider.skillsPath && config.skills?.paths) {
|
|
80
|
+
const allSkills = await discoverSkills(config.skills.paths, home);
|
|
81
|
+
const skillsPath =
|
|
82
|
+
typeof provider.skillsPath === "function"
|
|
83
|
+
? provider.skillsPath(home)
|
|
84
|
+
: provider.skillsPath;
|
|
85
|
+
|
|
86
|
+
await copySkillsToProvider(allSkills, skillsPath);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
synced.push(provider.name);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
failed.push({
|
|
92
|
+
provider: provider.name,
|
|
93
|
+
error: error instanceof Error ? error.message : String(error),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
success: failed.length === 0,
|
|
100
|
+
synced,
|
|
101
|
+
failed,
|
|
102
|
+
};
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return {
|
|
105
|
+
success: false,
|
|
106
|
+
synced: [],
|
|
107
|
+
failed: [],
|
|
108
|
+
error: error instanceof Error ? error.message : String(error),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { loadTaalConfig } from "../config/loader.js";
|
|
2
|
+
|
|
3
|
+
export interface ValidationResult {
|
|
4
|
+
valid: boolean;
|
|
5
|
+
errors: string[];
|
|
6
|
+
warnings: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function validate(baseDir?: string): Promise<ValidationResult> {
|
|
10
|
+
const result = await loadTaalConfig(baseDir);
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
valid: result.config !== null && result.errors.length === 0,
|
|
14
|
+
errors: result.errors,
|
|
15
|
+
warnings: result.warnings,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export function findEnvVarReferences(value: unknown): string[] {
|
|
2
|
+
const refs = new Set<string>();
|
|
3
|
+
|
|
4
|
+
if (typeof value === "string") {
|
|
5
|
+
const matches = value.matchAll(/\$\{([^}]+)\}/g);
|
|
6
|
+
for (const match of matches) {
|
|
7
|
+
refs.add(match[1]);
|
|
8
|
+
}
|
|
9
|
+
} else if (Array.isArray(value)) {
|
|
10
|
+
for (const item of value) {
|
|
11
|
+
for (const ref of findEnvVarReferences(item)) {
|
|
12
|
+
refs.add(ref);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
} else if (value && typeof value === "object") {
|
|
16
|
+
for (const v of Object.values(value)) {
|
|
17
|
+
for (const ref of findEnvVarReferences(v)) {
|
|
18
|
+
refs.add(ref);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return Array.from(refs);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function substituteEnvVars(value: unknown): unknown {
|
|
27
|
+
if (typeof value === "string") {
|
|
28
|
+
return value.replace(/\$\{([^}]+)\}/g, (match, varName) => {
|
|
29
|
+
const envValue = process.env[varName];
|
|
30
|
+
if (envValue === undefined) {
|
|
31
|
+
console.warn(`Warning: Environment variable ${varName} is not set`);
|
|
32
|
+
return match;
|
|
33
|
+
}
|
|
34
|
+
return envValue;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (Array.isArray(value)) {
|
|
39
|
+
return value.map(substituteEnvVars);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (value && typeof value === "object") {
|
|
43
|
+
return Object.fromEntries(
|
|
44
|
+
Object.entries(value).map(([k, v]) => [k, substituteEnvVars(v)])
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return value;
|
|
49
|
+
}
|