conflux-ai 0.1.0
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/LICENSE +21 -0
- package/README.md +203 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1667 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +1509 -0
- package/dist/index.js +294 -0
- package/dist/index.js.map +1 -0
- package/package.json +78 -0
- package/schemas/ai-config.schema.json +572 -0
|
@@ -0,0 +1,1667 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { dirname as dirname2, join as join7 } from "path";
|
|
8
|
+
|
|
9
|
+
// src/providers/registry.ts
|
|
10
|
+
var ProviderRegistry = class {
|
|
11
|
+
providers = /* @__PURE__ */ new Map();
|
|
12
|
+
/** Register a provider (built-in or external) */
|
|
13
|
+
register(provider) {
|
|
14
|
+
if (this.providers.has(provider.id)) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`Provider "${provider.id}" is already registered. Remove it first before re-registering.`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
this.providers.set(provider.id, provider);
|
|
20
|
+
}
|
|
21
|
+
/** Unregister a provider */
|
|
22
|
+
unregister(id) {
|
|
23
|
+
return this.providers.delete(id);
|
|
24
|
+
}
|
|
25
|
+
/** Get a provider by ID */
|
|
26
|
+
get(id) {
|
|
27
|
+
return this.providers.get(id);
|
|
28
|
+
}
|
|
29
|
+
/** List all registered providers */
|
|
30
|
+
list() {
|
|
31
|
+
return Array.from(this.providers.values());
|
|
32
|
+
}
|
|
33
|
+
/** Detect which providers apply to the given project */
|
|
34
|
+
detect(projectPath2) {
|
|
35
|
+
return this.list().filter((p) => p.detect(projectPath2));
|
|
36
|
+
}
|
|
37
|
+
/** Get providers that support a specific capability */
|
|
38
|
+
withCapability(capability) {
|
|
39
|
+
return this.list().filter((p) => p.capabilities.includes(capability));
|
|
40
|
+
}
|
|
41
|
+
/** Get count of registered providers */
|
|
42
|
+
get size() {
|
|
43
|
+
return this.providers.size;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var registry = new ProviderRegistry();
|
|
47
|
+
|
|
48
|
+
// src/core/types.ts
|
|
49
|
+
var ProviderCapability = /* @__PURE__ */ ((ProviderCapability2) => {
|
|
50
|
+
ProviderCapability2["INSTRUCTIONS"] = "instructions";
|
|
51
|
+
ProviderCapability2["HOOKS"] = "hooks";
|
|
52
|
+
ProviderCapability2["MCP"] = "mcp";
|
|
53
|
+
ProviderCapability2["PERMISSIONS"] = "permissions";
|
|
54
|
+
ProviderCapability2["SKILLS"] = "skills";
|
|
55
|
+
return ProviderCapability2;
|
|
56
|
+
})(ProviderCapability || {});
|
|
57
|
+
|
|
58
|
+
// src/providers/base.ts
|
|
59
|
+
var AIProvider = class {
|
|
60
|
+
// ─── Utility ────────────────────────────────────────────────
|
|
61
|
+
/** Check if this provider supports a specific capability */
|
|
62
|
+
hasCapability(cap) {
|
|
63
|
+
return this.capabilities.includes(cap);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// src/utils/hash.ts
|
|
68
|
+
import { createHash } from "crypto";
|
|
69
|
+
function hashContent(content) {
|
|
70
|
+
return createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/utils/fs.ts
|
|
74
|
+
import {
|
|
75
|
+
readFileSync,
|
|
76
|
+
writeFileSync,
|
|
77
|
+
existsSync,
|
|
78
|
+
mkdirSync,
|
|
79
|
+
readdirSync,
|
|
80
|
+
statSync,
|
|
81
|
+
symlinkSync,
|
|
82
|
+
unlinkSync,
|
|
83
|
+
rmSync
|
|
84
|
+
} from "fs";
|
|
85
|
+
import { dirname, join, relative } from "path";
|
|
86
|
+
function readFileIfExists(path) {
|
|
87
|
+
if (existsSync(path)) {
|
|
88
|
+
return readFileSync(path, "utf-8");
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
function writeFile(path, content) {
|
|
93
|
+
const dir = dirname(path);
|
|
94
|
+
if (!existsSync(dir)) {
|
|
95
|
+
mkdirSync(dir, { recursive: true });
|
|
96
|
+
}
|
|
97
|
+
writeFileSync(path, content, "utf-8");
|
|
98
|
+
}
|
|
99
|
+
function pathExists(path) {
|
|
100
|
+
return existsSync(path);
|
|
101
|
+
}
|
|
102
|
+
function projectPath(projectRoot, ...segments) {
|
|
103
|
+
return join(projectRoot, ...segments);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/generators/instructions.ts
|
|
107
|
+
function generatePrimaryInstructions(config) {
|
|
108
|
+
const lines = [
|
|
109
|
+
"# AGENTS.md",
|
|
110
|
+
"",
|
|
111
|
+
"> Auto-generated by [Conflux](https://github.com/conflux-ai/conflux).",
|
|
112
|
+
"> Edit `.ai/config.yaml` instead \u2014 changes to this file will be overwritten on next `conflux sync`.",
|
|
113
|
+
"",
|
|
114
|
+
"---",
|
|
115
|
+
""
|
|
116
|
+
];
|
|
117
|
+
const alwaysRules = config.rules?.filter((r) => r.alwaysApply !== false) ?? [];
|
|
118
|
+
if (alwaysRules.length > 0) {
|
|
119
|
+
for (const rule of alwaysRules) {
|
|
120
|
+
lines.push(`<!-- BEGIN: ${rule.path} -->`);
|
|
121
|
+
lines.push(`<!-- Source: ${rule.path} -->`);
|
|
122
|
+
lines.push(`@${rule.path}`);
|
|
123
|
+
lines.push(`<!-- END: ${rule.path} -->`);
|
|
124
|
+
lines.push("");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (config.references && config.references.length > 0) {
|
|
128
|
+
lines.push("## References");
|
|
129
|
+
lines.push("");
|
|
130
|
+
for (const ref of config.references) {
|
|
131
|
+
lines.push(`- [${ref.description ?? ref.path}](${ref.path})`);
|
|
132
|
+
}
|
|
133
|
+
lines.push("");
|
|
134
|
+
}
|
|
135
|
+
const content = lines.join("\n");
|
|
136
|
+
const hash = hashContent(content);
|
|
137
|
+
return {
|
|
138
|
+
path: config.primary,
|
|
139
|
+
content,
|
|
140
|
+
format: "markdown",
|
|
141
|
+
mode: "write",
|
|
142
|
+
hash
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function generateToolInstructions(config, toolPrimaryFile) {
|
|
146
|
+
const content = `<!-- Generated by Conflux \u2014 edit .ai/config.yaml instead -->
|
|
147
|
+
@${config.primary}
|
|
148
|
+
`;
|
|
149
|
+
return {
|
|
150
|
+
path: toolPrimaryFile,
|
|
151
|
+
content,
|
|
152
|
+
format: "markdown",
|
|
153
|
+
mode: "write",
|
|
154
|
+
hash: hashContent(content)
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/generators/mcp.ts
|
|
159
|
+
function generateMcpClaudeCode(config) {
|
|
160
|
+
const servers = {};
|
|
161
|
+
for (const [name, server] of Object.entries(config.servers)) {
|
|
162
|
+
const entry = { command: server.command };
|
|
163
|
+
if (server.args && server.args.length > 0) {
|
|
164
|
+
entry.args = server.args;
|
|
165
|
+
}
|
|
166
|
+
if (server.env && Object.keys(server.env).length > 0) {
|
|
167
|
+
entry.env = server.env;
|
|
168
|
+
}
|
|
169
|
+
servers[name] = entry;
|
|
170
|
+
}
|
|
171
|
+
const mcpConfig = { mcpServers: servers };
|
|
172
|
+
const content = JSON.stringify(mcpConfig, null, 2) + "\n";
|
|
173
|
+
return {
|
|
174
|
+
path: ".claude/settings.json",
|
|
175
|
+
content,
|
|
176
|
+
format: "json",
|
|
177
|
+
mode: "merge",
|
|
178
|
+
hash: hashContent(content)
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function generateMcpCodex(config) {
|
|
182
|
+
const lines = [
|
|
183
|
+
"# MCP Servers \u2014 Auto-generated by Conflux",
|
|
184
|
+
"# Edit .ai/config.yaml instead.",
|
|
185
|
+
""
|
|
186
|
+
];
|
|
187
|
+
if (config.env) {
|
|
188
|
+
for (const [key, value] of Object.entries(config.env)) {
|
|
189
|
+
lines.push(`[mcp_env]`);
|
|
190
|
+
lines.push(`${key} = "${value}"`);
|
|
191
|
+
lines.push("");
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
for (const [name, server] of Object.entries(config.servers)) {
|
|
196
|
+
lines.push("[[mcp_servers]]");
|
|
197
|
+
lines.push(`name = "${name}"`);
|
|
198
|
+
lines.push(`command = "${server.command}"`);
|
|
199
|
+
if (server.args && server.args.length > 0) {
|
|
200
|
+
const argsStr = server.args.map((a) => `"${a}"`).join(", ");
|
|
201
|
+
lines.push(`args = [${argsStr}]`);
|
|
202
|
+
}
|
|
203
|
+
if (server.env && Object.keys(server.env).length > 0) {
|
|
204
|
+
for (const [key, value] of Object.entries(server.env)) {
|
|
205
|
+
lines.push(`[mcp_servers.env]`);
|
|
206
|
+
lines.push(`${key} = "${value}"`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
lines.push("");
|
|
210
|
+
}
|
|
211
|
+
const content = lines.join("\n");
|
|
212
|
+
return {
|
|
213
|
+
path: ".codex/config.toml",
|
|
214
|
+
content,
|
|
215
|
+
format: "toml",
|
|
216
|
+
mode: "merge",
|
|
217
|
+
hash: hashContent(content)
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function generateMcpCursor(config) {
|
|
221
|
+
const servers = {};
|
|
222
|
+
for (const [name, server] of Object.entries(config.servers)) {
|
|
223
|
+
const entry = { command: server.command };
|
|
224
|
+
if (server.args && server.args.length > 0) {
|
|
225
|
+
entry.args = server.args;
|
|
226
|
+
}
|
|
227
|
+
if (server.env && Object.keys(server.env).length > 0) {
|
|
228
|
+
entry.env = server.env;
|
|
229
|
+
}
|
|
230
|
+
servers[name] = entry;
|
|
231
|
+
}
|
|
232
|
+
const mcpConfig = { mcpServers: servers };
|
|
233
|
+
const content = JSON.stringify(mcpConfig, null, 2) + "\n";
|
|
234
|
+
return {
|
|
235
|
+
path: ".cursor/mcp.json",
|
|
236
|
+
content,
|
|
237
|
+
format: "json",
|
|
238
|
+
mode: "write",
|
|
239
|
+
hash: hashContent(content)
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/providers/claude-code/index.ts
|
|
244
|
+
var ClaudeCodeProvider = class extends AIProvider {
|
|
245
|
+
id = "claude-code";
|
|
246
|
+
name = "Claude Code";
|
|
247
|
+
version = "1.0.0";
|
|
248
|
+
capabilities = [
|
|
249
|
+
"instructions" /* INSTRUCTIONS */,
|
|
250
|
+
"hooks" /* HOOKS */,
|
|
251
|
+
"mcp" /* MCP */,
|
|
252
|
+
"permissions" /* PERMISSIONS */,
|
|
253
|
+
"skills" /* SKILLS */
|
|
254
|
+
];
|
|
255
|
+
detect(projectPath2) {
|
|
256
|
+
return pathExists(projectPath(projectPath2, ".claude")) || pathExists(projectPath(projectPath2, "CLAUDE.md"));
|
|
257
|
+
}
|
|
258
|
+
discoverExisting(projectPath2) {
|
|
259
|
+
const result = {
|
|
260
|
+
instructions: [],
|
|
261
|
+
hooks: null,
|
|
262
|
+
mcp: null,
|
|
263
|
+
permissions: null,
|
|
264
|
+
skills: []
|
|
265
|
+
};
|
|
266
|
+
const claudeMd = projectPath(projectPath2, "CLAUDE.md");
|
|
267
|
+
if (pathExists(claudeMd)) {
|
|
268
|
+
result.instructions.push(claudeMd);
|
|
269
|
+
}
|
|
270
|
+
const settingsPath = projectPath(projectPath2, ".claude", "settings.json");
|
|
271
|
+
if (pathExists(settingsPath)) {
|
|
272
|
+
try {
|
|
273
|
+
const raw = JSON.parse(readFileIfExists(settingsPath) ?? "{}");
|
|
274
|
+
if (raw.hooks) result.hooks = raw.hooks;
|
|
275
|
+
if (raw.mcpServers) result.mcp = raw.mcpServers;
|
|
276
|
+
if (raw.permissions) result.permissions = raw.permissions;
|
|
277
|
+
} catch {
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const skillsDir = projectPath(projectPath2, ".claude", "skills");
|
|
281
|
+
if (pathExists(skillsDir)) {
|
|
282
|
+
result.skills.push(skillsDir);
|
|
283
|
+
}
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
async sync(projectPath2, config, options) {
|
|
287
|
+
const result = {
|
|
288
|
+
provider: this.id,
|
|
289
|
+
filesWritten: [],
|
|
290
|
+
filesRemoved: [],
|
|
291
|
+
errors: [],
|
|
292
|
+
warnings: []
|
|
293
|
+
};
|
|
294
|
+
const dryRun = options?.dryRun ?? false;
|
|
295
|
+
try {
|
|
296
|
+
if (config.instructions) {
|
|
297
|
+
const override = config.providers?.overrides?.["claude-code"];
|
|
298
|
+
const instructionsConfig = {
|
|
299
|
+
...config.instructions,
|
|
300
|
+
...override?.instructions
|
|
301
|
+
};
|
|
302
|
+
const files = this.generateInstructions(instructionsConfig);
|
|
303
|
+
for (const file of files) {
|
|
304
|
+
result.filesWritten.push(file);
|
|
305
|
+
if (!dryRun) {
|
|
306
|
+
writeFile(projectPath(projectPath2, file.path), file.content);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (config.hooks) {
|
|
311
|
+
const files = this.generateHooks(config.hooks);
|
|
312
|
+
for (const file of files) {
|
|
313
|
+
result.filesWritten.push(file);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (config.mcp) {
|
|
317
|
+
const files = this.generateMcp(config.mcp);
|
|
318
|
+
for (const file of files) {
|
|
319
|
+
result.filesWritten.push(file);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (config.permissions) {
|
|
323
|
+
const files = this.generatePermissions(config.permissions);
|
|
324
|
+
for (const file of files) {
|
|
325
|
+
result.filesWritten.push(file);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
this.writeSettingsJson(projectPath2, config, dryRun, result);
|
|
329
|
+
} catch (err) {
|
|
330
|
+
result.errors.push(`Sync failed: ${err.message}`);
|
|
331
|
+
}
|
|
332
|
+
return result;
|
|
333
|
+
}
|
|
334
|
+
async clean(projectPath2) {
|
|
335
|
+
const removed = [];
|
|
336
|
+
const files = [
|
|
337
|
+
"CLAUDE.md",
|
|
338
|
+
".claude/settings.json"
|
|
339
|
+
];
|
|
340
|
+
for (const f of files) {
|
|
341
|
+
const fullPath = projectPath(projectPath2, f);
|
|
342
|
+
if (pathExists(fullPath)) {
|
|
343
|
+
removed.push(f);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return removed;
|
|
347
|
+
}
|
|
348
|
+
async checkDrift(projectPath2, _config, state) {
|
|
349
|
+
const providerState = state.providers[this.id];
|
|
350
|
+
const report = {
|
|
351
|
+
provider: this.id,
|
|
352
|
+
inSync: true,
|
|
353
|
+
lastSyncTime: providerState?.lastSync ? new Date(providerState.lastSync) : null,
|
|
354
|
+
modifiedFiles: [],
|
|
355
|
+
missingFiles: [],
|
|
356
|
+
extraFiles: []
|
|
357
|
+
};
|
|
358
|
+
if (!providerState?.synced) {
|
|
359
|
+
report.inSync = false;
|
|
360
|
+
return report;
|
|
361
|
+
}
|
|
362
|
+
for (const [filePath, fileState] of Object.entries(providerState.files)) {
|
|
363
|
+
const fullPath = projectPath(projectPath2, filePath);
|
|
364
|
+
const content = readFileIfExists(fullPath);
|
|
365
|
+
if (content === null) {
|
|
366
|
+
report.missingFiles.push(filePath);
|
|
367
|
+
report.inSync = false;
|
|
368
|
+
} else {
|
|
369
|
+
const actualHash = hashContent(content);
|
|
370
|
+
if (actualHash !== fileState.hash) {
|
|
371
|
+
report.modifiedFiles.push({
|
|
372
|
+
path: filePath,
|
|
373
|
+
expectedHash: fileState.hash,
|
|
374
|
+
actualHash
|
|
375
|
+
});
|
|
376
|
+
report.inSync = false;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return report;
|
|
381
|
+
}
|
|
382
|
+
// ─── Dimension Generators ──────────────────────────────────────
|
|
383
|
+
generateInstructions(config) {
|
|
384
|
+
const files = [];
|
|
385
|
+
files.push(generateToolInstructions(config, "CLAUDE.md"));
|
|
386
|
+
return files;
|
|
387
|
+
}
|
|
388
|
+
generateHooks(config) {
|
|
389
|
+
const hooksJson = {};
|
|
390
|
+
for (const [event, entries] of Object.entries(config)) {
|
|
391
|
+
hooksJson[event] = entries.map((entry) => ({
|
|
392
|
+
matcher: entry.matcher ?? "",
|
|
393
|
+
hooks: [
|
|
394
|
+
{
|
|
395
|
+
type: "command",
|
|
396
|
+
command: entry.command,
|
|
397
|
+
timeout: entry.timeout ?? 30
|
|
398
|
+
}
|
|
399
|
+
]
|
|
400
|
+
}));
|
|
401
|
+
}
|
|
402
|
+
const content = JSON.stringify({ hooks: hooksJson }, null, 2) + "\n";
|
|
403
|
+
return [
|
|
404
|
+
{
|
|
405
|
+
path: ".claude/settings.json",
|
|
406
|
+
content,
|
|
407
|
+
format: "json",
|
|
408
|
+
mode: "merge",
|
|
409
|
+
hash: hashContent(content)
|
|
410
|
+
}
|
|
411
|
+
];
|
|
412
|
+
}
|
|
413
|
+
generateMcp(config) {
|
|
414
|
+
return [generateMcpClaudeCode(config)];
|
|
415
|
+
}
|
|
416
|
+
generatePermissions(config) {
|
|
417
|
+
const permConfig = {
|
|
418
|
+
permissions: {
|
|
419
|
+
default: config.default
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
if (config.allow) {
|
|
423
|
+
permConfig.permissions.allow = config.allow;
|
|
424
|
+
}
|
|
425
|
+
if (config.deny) {
|
|
426
|
+
permConfig.permissions.deny = config.deny;
|
|
427
|
+
}
|
|
428
|
+
const content = JSON.stringify(permConfig, null, 2) + "\n";
|
|
429
|
+
return [
|
|
430
|
+
{
|
|
431
|
+
path: ".claude/settings.json",
|
|
432
|
+
content,
|
|
433
|
+
format: "json",
|
|
434
|
+
mode: "merge",
|
|
435
|
+
hash: hashContent(content)
|
|
436
|
+
}
|
|
437
|
+
];
|
|
438
|
+
}
|
|
439
|
+
generateSkills(config, _projectPath) {
|
|
440
|
+
const files = [];
|
|
441
|
+
if (config.local) {
|
|
442
|
+
for (const skill of config.local) {
|
|
443
|
+
files.push({
|
|
444
|
+
path: `.claude/skills/${skill.name}`,
|
|
445
|
+
content: `../.agents/skills/${skill.name}`,
|
|
446
|
+
format: "text",
|
|
447
|
+
mode: "symlink",
|
|
448
|
+
hash: ""
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return files;
|
|
453
|
+
}
|
|
454
|
+
// ─── Helpers ───────────────────────────────────────────────────
|
|
455
|
+
writeSettingsJson(projectPath2, config, dryRun, result) {
|
|
456
|
+
const settingsPath = projectPath(projectPath2, ".claude", "settings.json");
|
|
457
|
+
let existing = {};
|
|
458
|
+
if (pathExists(settingsPath)) {
|
|
459
|
+
try {
|
|
460
|
+
existing = JSON.parse(readFileIfExists(settingsPath) ?? "{}");
|
|
461
|
+
} catch {
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (config.hooks) {
|
|
465
|
+
const hooksSection = {};
|
|
466
|
+
for (const [event, entries] of Object.entries(config.hooks)) {
|
|
467
|
+
hooksSection[event] = entries.map((entry) => ({
|
|
468
|
+
matcher: entry.matcher ?? "",
|
|
469
|
+
hooks: [
|
|
470
|
+
{
|
|
471
|
+
type: "command",
|
|
472
|
+
command: entry.command,
|
|
473
|
+
timeout: entry.timeout ?? 30
|
|
474
|
+
}
|
|
475
|
+
]
|
|
476
|
+
}));
|
|
477
|
+
}
|
|
478
|
+
existing.hooks = hooksSection;
|
|
479
|
+
}
|
|
480
|
+
if (config.mcp) {
|
|
481
|
+
const servers = {};
|
|
482
|
+
for (const [name, server] of Object.entries(config.mcp.servers)) {
|
|
483
|
+
const entry = { command: server.command };
|
|
484
|
+
if (server.args?.length) entry.args = server.args;
|
|
485
|
+
if (server.env && Object.keys(server.env).length > 0) entry.env = server.env;
|
|
486
|
+
servers[name] = entry;
|
|
487
|
+
}
|
|
488
|
+
existing.mcpServers = servers;
|
|
489
|
+
}
|
|
490
|
+
if (config.permissions) {
|
|
491
|
+
const perm = {
|
|
492
|
+
default: config.permissions.default
|
|
493
|
+
};
|
|
494
|
+
if (config.permissions.allow) perm.allow = config.permissions.allow;
|
|
495
|
+
if (config.permissions.deny) perm.deny = config.permissions.deny;
|
|
496
|
+
existing.permissions = perm;
|
|
497
|
+
}
|
|
498
|
+
const content = JSON.stringify(existing, null, 2) + "\n";
|
|
499
|
+
const file = {
|
|
500
|
+
path: ".claude/settings.json",
|
|
501
|
+
content,
|
|
502
|
+
format: "json",
|
|
503
|
+
mode: "write",
|
|
504
|
+
hash: hashContent(content)
|
|
505
|
+
};
|
|
506
|
+
result.filesWritten.push(file);
|
|
507
|
+
if (!dryRun) {
|
|
508
|
+
writeFile(settingsPath, content);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// src/providers/codex/index.ts
|
|
514
|
+
var CodexProvider = class extends AIProvider {
|
|
515
|
+
id = "codex";
|
|
516
|
+
name = "Codex CLI";
|
|
517
|
+
version = "1.0.0";
|
|
518
|
+
capabilities = [
|
|
519
|
+
"instructions" /* INSTRUCTIONS */,
|
|
520
|
+
"hooks" /* HOOKS */,
|
|
521
|
+
"mcp" /* MCP */,
|
|
522
|
+
"permissions" /* PERMISSIONS */,
|
|
523
|
+
"skills" /* SKILLS */
|
|
524
|
+
];
|
|
525
|
+
detect(projectPath2) {
|
|
526
|
+
return pathExists(projectPath(projectPath2, ".codex")) || pathExists(projectPath(projectPath2, "AGENTS.md"));
|
|
527
|
+
}
|
|
528
|
+
discoverExisting(projectPath2) {
|
|
529
|
+
const result = {
|
|
530
|
+
instructions: [],
|
|
531
|
+
hooks: null,
|
|
532
|
+
mcp: null,
|
|
533
|
+
permissions: null,
|
|
534
|
+
skills: []
|
|
535
|
+
};
|
|
536
|
+
const agentsMd = projectPath(projectPath2, "AGENTS.md");
|
|
537
|
+
if (pathExists(agentsMd)) {
|
|
538
|
+
result.instructions.push(agentsMd);
|
|
539
|
+
}
|
|
540
|
+
const hooksPath = projectPath(projectPath2, ".codex", "hooks.json");
|
|
541
|
+
if (pathExists(hooksPath)) {
|
|
542
|
+
try {
|
|
543
|
+
result.hooks = JSON.parse(readFileIfExists(hooksPath) ?? "{}");
|
|
544
|
+
} catch {
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const configPath = projectPath(projectPath2, ".codex", "config.toml");
|
|
548
|
+
if (pathExists(configPath)) {
|
|
549
|
+
const raw = readFileIfExists(configPath);
|
|
550
|
+
if (raw?.includes("[[mcp_servers]]")) {
|
|
551
|
+
result.mcp = { _fromToml: true };
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return result;
|
|
555
|
+
}
|
|
556
|
+
async sync(projectPath2, config, options) {
|
|
557
|
+
const result = {
|
|
558
|
+
provider: this.id,
|
|
559
|
+
filesWritten: [],
|
|
560
|
+
filesRemoved: [],
|
|
561
|
+
errors: [],
|
|
562
|
+
warnings: []
|
|
563
|
+
};
|
|
564
|
+
const dryRun = options?.dryRun ?? false;
|
|
565
|
+
try {
|
|
566
|
+
if (config.instructions) {
|
|
567
|
+
const files = this.generateInstructions(config.instructions);
|
|
568
|
+
for (const file of files) {
|
|
569
|
+
result.filesWritten.push(file);
|
|
570
|
+
if (!dryRun) {
|
|
571
|
+
writeFile(projectPath(projectPath2, file.path), file.content);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (config.hooks) {
|
|
576
|
+
const files = this.generateHooks(config.hooks);
|
|
577
|
+
for (const file of files) {
|
|
578
|
+
result.filesWritten.push(file);
|
|
579
|
+
if (!dryRun) {
|
|
580
|
+
writeFile(projectPath(projectPath2, file.path), file.content);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (config.mcp) {
|
|
585
|
+
const mcpFile = generateMcpCodex(config.mcp);
|
|
586
|
+
result.filesWritten.push(mcpFile);
|
|
587
|
+
if (!dryRun) {
|
|
588
|
+
writeFile(projectPath(projectPath2, mcpFile.path), mcpFile.content);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
} catch (err) {
|
|
592
|
+
result.errors.push(`Sync failed: ${err.message}`);
|
|
593
|
+
}
|
|
594
|
+
return result;
|
|
595
|
+
}
|
|
596
|
+
async clean(projectPath2) {
|
|
597
|
+
const removed = [];
|
|
598
|
+
const files = [
|
|
599
|
+
"AGENTS.md",
|
|
600
|
+
".codex/hooks.json",
|
|
601
|
+
".codex/config.toml"
|
|
602
|
+
];
|
|
603
|
+
for (const f of files) {
|
|
604
|
+
if (pathExists(projectPath(projectPath2, f))) {
|
|
605
|
+
removed.push(f);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return removed;
|
|
609
|
+
}
|
|
610
|
+
async checkDrift(projectPath2, _config, state) {
|
|
611
|
+
const providerState = state.providers[this.id];
|
|
612
|
+
const report = {
|
|
613
|
+
provider: this.id,
|
|
614
|
+
inSync: true,
|
|
615
|
+
lastSyncTime: providerState?.lastSync ? new Date(providerState.lastSync) : null,
|
|
616
|
+
modifiedFiles: [],
|
|
617
|
+
missingFiles: [],
|
|
618
|
+
extraFiles: []
|
|
619
|
+
};
|
|
620
|
+
if (!providerState?.synced) {
|
|
621
|
+
report.inSync = false;
|
|
622
|
+
return report;
|
|
623
|
+
}
|
|
624
|
+
for (const [filePath, fileState] of Object.entries(providerState.files)) {
|
|
625
|
+
const fullPath = projectPath(projectPath2, filePath);
|
|
626
|
+
const content = readFileIfExists(fullPath);
|
|
627
|
+
if (content === null) {
|
|
628
|
+
report.missingFiles.push(filePath);
|
|
629
|
+
report.inSync = false;
|
|
630
|
+
} else if (hashContent(content) !== fileState.hash) {
|
|
631
|
+
report.modifiedFiles.push({
|
|
632
|
+
path: filePath,
|
|
633
|
+
expectedHash: fileState.hash,
|
|
634
|
+
actualHash: hashContent(content)
|
|
635
|
+
});
|
|
636
|
+
report.inSync = false;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return report;
|
|
640
|
+
}
|
|
641
|
+
// ─── Dimension Generators ──────────────────────────────────────
|
|
642
|
+
generateInstructions(config) {
|
|
643
|
+
return [generatePrimaryInstructions(config)];
|
|
644
|
+
}
|
|
645
|
+
generateHooks(config) {
|
|
646
|
+
const hooksData = { hooks: {} };
|
|
647
|
+
for (const [event, entries] of Object.entries(config)) {
|
|
648
|
+
hooksData.hooks[event] = entries.map((entry) => ({
|
|
649
|
+
matcher: entry.matcher ?? "",
|
|
650
|
+
hooks: [
|
|
651
|
+
{
|
|
652
|
+
type: "command",
|
|
653
|
+
command: entry.command,
|
|
654
|
+
timeout: entry.timeout ?? 30
|
|
655
|
+
}
|
|
656
|
+
]
|
|
657
|
+
}));
|
|
658
|
+
}
|
|
659
|
+
const content = JSON.stringify(hooksData, null, 2) + "\n";
|
|
660
|
+
return [
|
|
661
|
+
{
|
|
662
|
+
path: ".codex/hooks.json",
|
|
663
|
+
content,
|
|
664
|
+
format: "json",
|
|
665
|
+
mode: "write",
|
|
666
|
+
hash: hashContent(content)
|
|
667
|
+
}
|
|
668
|
+
];
|
|
669
|
+
}
|
|
670
|
+
generateMcp(config) {
|
|
671
|
+
return [generateMcpCodex(config)];
|
|
672
|
+
}
|
|
673
|
+
generatePermissions(config) {
|
|
674
|
+
const lines = [
|
|
675
|
+
"# Permissions \u2014 Auto-generated by Conflux",
|
|
676
|
+
"[permissions]",
|
|
677
|
+
`default = "${config.default}"`
|
|
678
|
+
];
|
|
679
|
+
if (config.allow) {
|
|
680
|
+
lines.push("allow = [");
|
|
681
|
+
for (const rule of config.allow) {
|
|
682
|
+
lines.push(` "${rule}",`);
|
|
683
|
+
}
|
|
684
|
+
lines.push("]");
|
|
685
|
+
}
|
|
686
|
+
if (config.deny) {
|
|
687
|
+
lines.push("deny = [");
|
|
688
|
+
for (const rule of config.deny) {
|
|
689
|
+
lines.push(` "${rule}",`);
|
|
690
|
+
}
|
|
691
|
+
lines.push("]");
|
|
692
|
+
}
|
|
693
|
+
const content = lines.join("\n") + "\n";
|
|
694
|
+
return [
|
|
695
|
+
{
|
|
696
|
+
path: ".codex/config.toml",
|
|
697
|
+
content,
|
|
698
|
+
format: "toml",
|
|
699
|
+
mode: "merge",
|
|
700
|
+
hash: hashContent(content)
|
|
701
|
+
}
|
|
702
|
+
];
|
|
703
|
+
}
|
|
704
|
+
generateSkills(config, _projectPath) {
|
|
705
|
+
return (config.local ?? []).map((skill) => ({
|
|
706
|
+
path: `.codex/skills/${skill.name}`,
|
|
707
|
+
content: `../.agents/skills/${skill.name}`,
|
|
708
|
+
format: "text",
|
|
709
|
+
mode: "symlink",
|
|
710
|
+
hash: ""
|
|
711
|
+
}));
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
// src/providers/cursor/index.ts
|
|
716
|
+
var CursorProvider = class extends AIProvider {
|
|
717
|
+
id = "cursor";
|
|
718
|
+
name = "Cursor";
|
|
719
|
+
version = "1.0.0";
|
|
720
|
+
capabilities = [
|
|
721
|
+
"instructions" /* INSTRUCTIONS */,
|
|
722
|
+
"mcp" /* MCP */,
|
|
723
|
+
"skills" /* SKILLS */
|
|
724
|
+
// Cursor does NOT support hooks or permissions natively
|
|
725
|
+
];
|
|
726
|
+
detect(projectPath2) {
|
|
727
|
+
return pathExists(projectPath(projectPath2, ".cursor")) || pathExists(projectPath(projectPath2, ".cursorrules"));
|
|
728
|
+
}
|
|
729
|
+
discoverExisting(projectPath2) {
|
|
730
|
+
const result = {
|
|
731
|
+
instructions: [],
|
|
732
|
+
hooks: null,
|
|
733
|
+
mcp: null,
|
|
734
|
+
permissions: null,
|
|
735
|
+
skills: []
|
|
736
|
+
};
|
|
737
|
+
const legacyRules = projectPath(projectPath2, ".cursorrules");
|
|
738
|
+
if (pathExists(legacyRules)) {
|
|
739
|
+
result.instructions.push(legacyRules);
|
|
740
|
+
}
|
|
741
|
+
const rulesDir = projectPath(projectPath2, ".cursor", "rules");
|
|
742
|
+
if (pathExists(rulesDir)) {
|
|
743
|
+
result.instructions.push(rulesDir);
|
|
744
|
+
}
|
|
745
|
+
const mcpPath = projectPath(projectPath2, ".cursor", "mcp.json");
|
|
746
|
+
if (pathExists(mcpPath)) {
|
|
747
|
+
try {
|
|
748
|
+
result.mcp = JSON.parse(readFileIfExists(mcpPath) ?? "{}");
|
|
749
|
+
} catch {
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return result;
|
|
753
|
+
}
|
|
754
|
+
async sync(projectPath2, config, options) {
|
|
755
|
+
const result = {
|
|
756
|
+
provider: this.id,
|
|
757
|
+
filesWritten: [],
|
|
758
|
+
filesRemoved: [],
|
|
759
|
+
errors: [],
|
|
760
|
+
warnings: []
|
|
761
|
+
};
|
|
762
|
+
const dryRun = options?.dryRun ?? false;
|
|
763
|
+
try {
|
|
764
|
+
if (config.instructions) {
|
|
765
|
+
const override = config.providers?.overrides?.cursor;
|
|
766
|
+
const instructionsConfig = {
|
|
767
|
+
...config.instructions,
|
|
768
|
+
...override?.instructions
|
|
769
|
+
};
|
|
770
|
+
const files = this.generateInstructions(instructionsConfig);
|
|
771
|
+
for (const file of files) {
|
|
772
|
+
result.filesWritten.push(file);
|
|
773
|
+
if (!dryRun) {
|
|
774
|
+
writeFile(projectPath(projectPath2, file.path), file.content);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (config.mcp) {
|
|
779
|
+
const files = this.generateMcp(config.mcp);
|
|
780
|
+
for (const file of files) {
|
|
781
|
+
result.filesWritten.push(file);
|
|
782
|
+
if (!dryRun) {
|
|
783
|
+
writeFile(projectPath(projectPath2, file.path), file.content);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
if (config.hooks) {
|
|
788
|
+
result.warnings.push("Cursor does not natively support lifecycle hooks \u2014 hooks skipped");
|
|
789
|
+
}
|
|
790
|
+
if (config.permissions) {
|
|
791
|
+
result.warnings.push("Cursor has limited permission support (settings UI only) \u2014 permissions skipped");
|
|
792
|
+
}
|
|
793
|
+
} catch (err) {
|
|
794
|
+
result.errors.push(`Sync failed: ${err.message}`);
|
|
795
|
+
}
|
|
796
|
+
return result;
|
|
797
|
+
}
|
|
798
|
+
async clean(projectPath2) {
|
|
799
|
+
const removed = [];
|
|
800
|
+
const files = [
|
|
801
|
+
".cursor/mcp.json"
|
|
802
|
+
];
|
|
803
|
+
for (const f of files) {
|
|
804
|
+
if (pathExists(projectPath(projectPath2, f))) {
|
|
805
|
+
removed.push(f);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return removed;
|
|
809
|
+
}
|
|
810
|
+
async checkDrift(projectPath2, _config, state) {
|
|
811
|
+
const providerState = state.providers[this.id];
|
|
812
|
+
const report = {
|
|
813
|
+
provider: this.id,
|
|
814
|
+
inSync: true,
|
|
815
|
+
lastSyncTime: providerState?.lastSync ? new Date(providerState.lastSync) : null,
|
|
816
|
+
modifiedFiles: [],
|
|
817
|
+
missingFiles: [],
|
|
818
|
+
extraFiles: []
|
|
819
|
+
};
|
|
820
|
+
if (!providerState?.synced) {
|
|
821
|
+
report.inSync = false;
|
|
822
|
+
return report;
|
|
823
|
+
}
|
|
824
|
+
for (const [filePath, fileState] of Object.entries(providerState.files)) {
|
|
825
|
+
const fullPath = projectPath(projectPath2, filePath);
|
|
826
|
+
const content = readFileIfExists(fullPath);
|
|
827
|
+
if (content === null) {
|
|
828
|
+
report.missingFiles.push(filePath);
|
|
829
|
+
report.inSync = false;
|
|
830
|
+
} else if (hashContent(content) !== fileState.hash) {
|
|
831
|
+
report.modifiedFiles.push({
|
|
832
|
+
path: filePath,
|
|
833
|
+
expectedHash: fileState.hash,
|
|
834
|
+
actualHash: hashContent(content)
|
|
835
|
+
});
|
|
836
|
+
report.inSync = false;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
return report;
|
|
840
|
+
}
|
|
841
|
+
// ─── Dimension Generators ──────────────────────────────────────
|
|
842
|
+
generateInstructions(config) {
|
|
843
|
+
const files = [];
|
|
844
|
+
files.push(generatePrimaryInstructions(config));
|
|
845
|
+
if (config.rules) {
|
|
846
|
+
for (const rule of config.rules) {
|
|
847
|
+
const mdcContent = [
|
|
848
|
+
"---",
|
|
849
|
+
`description: "${rule.description ?? rule.path}"`,
|
|
850
|
+
...rule.globs ? [`globs: [${rule.globs.join(", ")}]`] : [],
|
|
851
|
+
`alwaysApply: ${rule.alwaysApply ?? true}`,
|
|
852
|
+
"---",
|
|
853
|
+
"",
|
|
854
|
+
`@${rule.path}`
|
|
855
|
+
].join("\n");
|
|
856
|
+
const fileName = rule.path.replace(/^rules\//, "").replace(/\.md$/, ".mdc");
|
|
857
|
+
files.push({
|
|
858
|
+
path: `.cursor/rules/${fileName}`,
|
|
859
|
+
content: mdcContent,
|
|
860
|
+
format: "markdown",
|
|
861
|
+
mode: "write",
|
|
862
|
+
hash: hashContent(mdcContent)
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
return files;
|
|
867
|
+
}
|
|
868
|
+
generateHooks(_config) {
|
|
869
|
+
return [];
|
|
870
|
+
}
|
|
871
|
+
generateMcp(config) {
|
|
872
|
+
return [generateMcpCursor(config)];
|
|
873
|
+
}
|
|
874
|
+
generatePermissions(_config) {
|
|
875
|
+
return [];
|
|
876
|
+
}
|
|
877
|
+
generateSkills(config, _projectPath) {
|
|
878
|
+
return (config.local ?? []).map((skill) => ({
|
|
879
|
+
path: `.cursor/skills/${skill.name}`,
|
|
880
|
+
content: `../.agents/skills/${skill.name}`,
|
|
881
|
+
format: "text",
|
|
882
|
+
mode: "symlink",
|
|
883
|
+
hash: ""
|
|
884
|
+
}));
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
// src/providers/index.ts
|
|
889
|
+
function registerAllProviders() {
|
|
890
|
+
const providers = [
|
|
891
|
+
new ClaudeCodeProvider(),
|
|
892
|
+
new CodexProvider(),
|
|
893
|
+
new CursorProvider()
|
|
894
|
+
];
|
|
895
|
+
for (const provider of providers) {
|
|
896
|
+
if (!registry.get(provider.id)) {
|
|
897
|
+
registry.register(provider);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// src/cli/commands/init.ts
|
|
903
|
+
import { join as join3 } from "path";
|
|
904
|
+
|
|
905
|
+
// src/detectors/index.ts
|
|
906
|
+
import { existsSync as existsSync2 } from "fs";
|
|
907
|
+
import { join as join2 } from "path";
|
|
908
|
+
|
|
909
|
+
// src/detectors/patterns.ts
|
|
910
|
+
var DETECTOR_PATTERNS = [
|
|
911
|
+
{
|
|
912
|
+
providerId: "claude-code",
|
|
913
|
+
markers: [".claude", "CLAUDE.md", ".claude/settings.json"],
|
|
914
|
+
configPatterns: [
|
|
915
|
+
".claude/**/*.json",
|
|
916
|
+
".claude/**/*.md",
|
|
917
|
+
"CLAUDE.md"
|
|
918
|
+
]
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
providerId: "codex",
|
|
922
|
+
markers: [".codex", "AGENTS.md", ".codex/config.toml"],
|
|
923
|
+
configPatterns: [
|
|
924
|
+
".codex/**/*.toml",
|
|
925
|
+
".codex/**/*.json",
|
|
926
|
+
".codex/**/*.md",
|
|
927
|
+
"AGENTS.md"
|
|
928
|
+
]
|
|
929
|
+
},
|
|
930
|
+
{
|
|
931
|
+
providerId: "cursor",
|
|
932
|
+
markers: [".cursor", ".cursorrules", ".cursor/rules"],
|
|
933
|
+
configPatterns: [
|
|
934
|
+
".cursor/**/*.mdc",
|
|
935
|
+
".cursor/**/*.json",
|
|
936
|
+
".cursorrules"
|
|
937
|
+
]
|
|
938
|
+
},
|
|
939
|
+
{
|
|
940
|
+
providerId: "windsurf",
|
|
941
|
+
markers: [".windsurf", ".windsurfrules", ".windsurf/rules"],
|
|
942
|
+
configPatterns: [
|
|
943
|
+
".windsurf/**/*",
|
|
944
|
+
".windsurfrules"
|
|
945
|
+
]
|
|
946
|
+
},
|
|
947
|
+
{
|
|
948
|
+
providerId: "copilot",
|
|
949
|
+
markers: [
|
|
950
|
+
".github/copilot-instructions.md",
|
|
951
|
+
".github/instructions"
|
|
952
|
+
],
|
|
953
|
+
configPatterns: [
|
|
954
|
+
".github/copilot-instructions.md",
|
|
955
|
+
".github/instructions/**/*.md"
|
|
956
|
+
]
|
|
957
|
+
},
|
|
958
|
+
{
|
|
959
|
+
providerId: "gemini-cli",
|
|
960
|
+
markers: [".gemini", "GEMINI.md"],
|
|
961
|
+
configPatterns: [
|
|
962
|
+
".gemini/**/*.json",
|
|
963
|
+
".gemini/**/*.toml",
|
|
964
|
+
"GEMINI.md"
|
|
965
|
+
]
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
providerId: "aider",
|
|
969
|
+
markers: [".aider.conf.yml", ".aider", "CONVENTIONS.md"],
|
|
970
|
+
configPatterns: [
|
|
971
|
+
".aider.conf.yml",
|
|
972
|
+
".aider/**/*.json",
|
|
973
|
+
"CONVENTIONS.md"
|
|
974
|
+
]
|
|
975
|
+
}
|
|
976
|
+
];
|
|
977
|
+
|
|
978
|
+
// src/detectors/index.ts
|
|
979
|
+
function detectTools(projectPath2) {
|
|
980
|
+
return DETECTOR_PATTERNS.filter(
|
|
981
|
+
(pattern) => pattern.markers.some((marker) => existsSync2(join2(projectPath2, marker)))
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
function detectToolIds(projectPath2) {
|
|
985
|
+
return detectTools(projectPath2).map((p) => p.providerId);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// src/utils/logger.ts
|
|
989
|
+
import chalk from "chalk";
|
|
990
|
+
var currentLevel = 1 /* INFO */;
|
|
991
|
+
var logger = {
|
|
992
|
+
debug(msg) {
|
|
993
|
+
if (currentLevel <= 0 /* DEBUG */) {
|
|
994
|
+
console.error(chalk.gray(` [debug] ${msg}`));
|
|
995
|
+
}
|
|
996
|
+
},
|
|
997
|
+
info(msg) {
|
|
998
|
+
if (currentLevel <= 1 /* INFO */) {
|
|
999
|
+
console.log(chalk.blue("\u2139"), msg);
|
|
1000
|
+
}
|
|
1001
|
+
},
|
|
1002
|
+
success(msg) {
|
|
1003
|
+
if (currentLevel <= 1 /* INFO */) {
|
|
1004
|
+
console.log(chalk.green("\u2713"), msg);
|
|
1005
|
+
}
|
|
1006
|
+
},
|
|
1007
|
+
warn(msg) {
|
|
1008
|
+
if (currentLevel <= 2 /* WARN */) {
|
|
1009
|
+
console.warn(chalk.yellow("\u26A0"), msg);
|
|
1010
|
+
}
|
|
1011
|
+
},
|
|
1012
|
+
error(msg) {
|
|
1013
|
+
if (currentLevel <= 3 /* ERROR */) {
|
|
1014
|
+
console.error(chalk.red("\u2717"), msg);
|
|
1015
|
+
}
|
|
1016
|
+
},
|
|
1017
|
+
heading(msg) {
|
|
1018
|
+
if (currentLevel <= 1 /* INFO */) {
|
|
1019
|
+
console.log();
|
|
1020
|
+
console.log(chalk.bold.cyan("\u25CF"), chalk.bold(msg));
|
|
1021
|
+
}
|
|
1022
|
+
},
|
|
1023
|
+
table(rows) {
|
|
1024
|
+
if (currentLevel > 1 /* INFO */) return;
|
|
1025
|
+
if (rows.length === 0) return;
|
|
1026
|
+
const keys = Object.keys(rows[0]);
|
|
1027
|
+
const colWidths = keys.map(
|
|
1028
|
+
(key) => Math.max(key.length, ...rows.map((r) => (r[key] ?? "").length))
|
|
1029
|
+
);
|
|
1030
|
+
const header = keys.map((k, i) => chalk.bold(k.padEnd(colWidths[i]))).join(" ");
|
|
1031
|
+
console.log(" " + header);
|
|
1032
|
+
const sep = colWidths.map((w) => "\u2500".repeat(w)).join(" ");
|
|
1033
|
+
console.log(" " + chalk.gray(sep));
|
|
1034
|
+
for (const row of rows) {
|
|
1035
|
+
const line = keys.map((k, i) => (row[k] ?? "").padEnd(colWidths[i])).join(" ");
|
|
1036
|
+
console.log(" " + line);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
// src/cli/commands/init.ts
|
|
1042
|
+
function registerInitCommand(program2) {
|
|
1043
|
+
program2.command("init").description(
|
|
1044
|
+
"Initialize .ai/config.yaml \u2014 scan existing configs and import them"
|
|
1045
|
+
).option("--template <name>", "Template: minimal, full, monorepo").option("--force", "Overwrite existing .ai/config.yaml").action(async (options, command) => {
|
|
1046
|
+
const globalOpts = command.parent?.opts() ?? {};
|
|
1047
|
+
const projectPath2 = globalOpts.project ?? process.cwd();
|
|
1048
|
+
const dryRun = globalOpts.dryRun ?? false;
|
|
1049
|
+
const force = options.force ?? false;
|
|
1050
|
+
const configDir = join3(projectPath2, ".ai");
|
|
1051
|
+
const configPath = join3(configDir, "config.yaml");
|
|
1052
|
+
if (pathExists(configPath) && !force) {
|
|
1053
|
+
logger.error(".ai/config.yaml already exists.");
|
|
1054
|
+
logger.info("Use --force to overwrite, or edit it directly.");
|
|
1055
|
+
process.exit(1);
|
|
1056
|
+
}
|
|
1057
|
+
logger.heading("Conflux Init");
|
|
1058
|
+
logger.info(`Project: ${projectPath2}`);
|
|
1059
|
+
const detectedPatterns = detectTools(projectPath2);
|
|
1060
|
+
const detectedIds = detectedPatterns.map((p) => p.providerId);
|
|
1061
|
+
if (detectedIds.length > 0) {
|
|
1062
|
+
logger.success(`Detected ${detectedIds.length} AI tool(s): ${detectedIds.join(", ")}`);
|
|
1063
|
+
} else {
|
|
1064
|
+
logger.info("No AI tools detected. A minimal config will be generated.");
|
|
1065
|
+
}
|
|
1066
|
+
const discovered = {};
|
|
1067
|
+
for (const id of detectedIds) {
|
|
1068
|
+
const provider = registry.get(id);
|
|
1069
|
+
if (provider) {
|
|
1070
|
+
try {
|
|
1071
|
+
discovered[id] = provider.discoverExisting(projectPath2);
|
|
1072
|
+
} catch {
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
const config = buildConfigFromDiscovered(detectedIds, discovered, options.template);
|
|
1077
|
+
if (dryRun) {
|
|
1078
|
+
logger.info("[DRY RUN] Would create .ai/config.yaml:");
|
|
1079
|
+
console.log("---");
|
|
1080
|
+
console.log(toYamlString(config));
|
|
1081
|
+
console.log("---");
|
|
1082
|
+
} else {
|
|
1083
|
+
writeFile(configPath, toYamlString(config));
|
|
1084
|
+
logger.success("Created .ai/config.yaml");
|
|
1085
|
+
}
|
|
1086
|
+
console.log();
|
|
1087
|
+
logger.heading("Next steps:");
|
|
1088
|
+
logger.info("1. Review .ai/config.yaml and adjust as needed");
|
|
1089
|
+
logger.info("2. Run 'conflux sync' to generate tool-specific configs");
|
|
1090
|
+
logger.info("3. Run 'conflux status' to check sync health");
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
function buildConfigFromDiscovered(detectedIds, discovered, _template) {
|
|
1094
|
+
const config = {
|
|
1095
|
+
version: 1
|
|
1096
|
+
};
|
|
1097
|
+
const allInstructions = /* @__PURE__ */ new Set();
|
|
1098
|
+
for (const [, d] of Object.entries(discovered)) {
|
|
1099
|
+
for (const path of d.instructions) {
|
|
1100
|
+
allInstructions.add(path);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
if (allInstructions.size > 0) {
|
|
1104
|
+
config.instructions = {
|
|
1105
|
+
primary: "AGENTS.md",
|
|
1106
|
+
references: Array.from(allInstructions).map((path) => ({
|
|
1107
|
+
path,
|
|
1108
|
+
description: `Existing instruction file (auto-discovered)`
|
|
1109
|
+
}))
|
|
1110
|
+
};
|
|
1111
|
+
} else {
|
|
1112
|
+
config.instructions = {
|
|
1113
|
+
primary: "AGENTS.md"
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
const allMcpServers = {};
|
|
1117
|
+
for (const [providerId, d] of Object.entries(discovered)) {
|
|
1118
|
+
if (d.mcp && typeof d.mcp === "object") {
|
|
1119
|
+
const servers = d.mcp.mcpServers ?? d.mcp;
|
|
1120
|
+
if (typeof servers === "object") {
|
|
1121
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
1122
|
+
if (typeof server === "object" && server !== null && !allMcpServers[name]) {
|
|
1123
|
+
const s = server;
|
|
1124
|
+
allMcpServers[name] = {
|
|
1125
|
+
command: String(s.command ?? ""),
|
|
1126
|
+
...s.args ? { args: s.args } : {},
|
|
1127
|
+
...s.env ? { env: s.env } : {}
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
if (Object.keys(allMcpServers).length > 0) {
|
|
1135
|
+
config.mcp = { servers: allMcpServers };
|
|
1136
|
+
}
|
|
1137
|
+
config.providers = {
|
|
1138
|
+
enabled: detectedIds
|
|
1139
|
+
};
|
|
1140
|
+
config.meta = {
|
|
1141
|
+
created: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
|
|
1142
|
+
description: "Auto-generated by conflux init"
|
|
1143
|
+
};
|
|
1144
|
+
return config;
|
|
1145
|
+
}
|
|
1146
|
+
function toYamlString(config) {
|
|
1147
|
+
const lines = [
|
|
1148
|
+
"# .ai/config.yaml \u2014 Unified AI Coding Tool Configuration",
|
|
1149
|
+
"# Generated by Conflux (https://github.com/conflux-ai/conflux)",
|
|
1150
|
+
"# Edit this file as the single source of truth for all AI tool configs.",
|
|
1151
|
+
"",
|
|
1152
|
+
`version: ${config.version}`,
|
|
1153
|
+
""
|
|
1154
|
+
];
|
|
1155
|
+
if (config.instructions) {
|
|
1156
|
+
lines.push("# \u2500\u2500\u2500 Project Instructions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1157
|
+
lines.push("instructions:");
|
|
1158
|
+
lines.push(` primary: ${config.instructions.primary}`);
|
|
1159
|
+
if (config.instructions.references?.length) {
|
|
1160
|
+
lines.push(" references:");
|
|
1161
|
+
for (const ref of config.instructions.references) {
|
|
1162
|
+
lines.push(` - path: ${ref.path}`);
|
|
1163
|
+
if (ref.description) {
|
|
1164
|
+
lines.push(` description: "${ref.description}"`);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
lines.push("");
|
|
1169
|
+
}
|
|
1170
|
+
if (config.mcp && Object.keys(config.mcp.servers).length > 0) {
|
|
1171
|
+
lines.push("# \u2500\u2500\u2500 MCP Servers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1172
|
+
lines.push("mcp:");
|
|
1173
|
+
lines.push(" servers:");
|
|
1174
|
+
for (const [name, server] of Object.entries(config.mcp.servers)) {
|
|
1175
|
+
lines.push(` ${name}:`);
|
|
1176
|
+
lines.push(` command: ${server.command}`);
|
|
1177
|
+
if (server.args?.length) {
|
|
1178
|
+
lines.push(` args: [${server.args.map((a) => `"${a}"`).join(", ")}]`);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
lines.push("");
|
|
1182
|
+
}
|
|
1183
|
+
if (config.providers?.enabled?.length) {
|
|
1184
|
+
lines.push("# \u2500\u2500\u2500 Providers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1185
|
+
lines.push("providers:");
|
|
1186
|
+
lines.push(" enabled:");
|
|
1187
|
+
for (const id of config.providers.enabled) {
|
|
1188
|
+
lines.push(` - ${id}`);
|
|
1189
|
+
}
|
|
1190
|
+
lines.push("");
|
|
1191
|
+
}
|
|
1192
|
+
if (config.meta) {
|
|
1193
|
+
lines.push("# \u2500\u2500\u2500 Metadata \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1194
|
+
lines.push("meta:");
|
|
1195
|
+
if (config.meta.description) {
|
|
1196
|
+
lines.push(` description: "${config.meta.description}"`);
|
|
1197
|
+
}
|
|
1198
|
+
lines.push(` created: "${config.meta.created ?? "unknown"}"`);
|
|
1199
|
+
lines.push("");
|
|
1200
|
+
}
|
|
1201
|
+
return lines.join("\n");
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// src/cli/commands/sync.ts
|
|
1205
|
+
import { join as join5 } from "path";
|
|
1206
|
+
|
|
1207
|
+
// src/core/config.ts
|
|
1208
|
+
import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
|
|
1209
|
+
import { join as join4 } from "path";
|
|
1210
|
+
import yaml from "js-yaml";
|
|
1211
|
+
|
|
1212
|
+
// src/core/schema.ts
|
|
1213
|
+
import { z } from "zod";
|
|
1214
|
+
var InstructionRuleSchema = z.object({
|
|
1215
|
+
path: z.string().describe("Path to the rule file (markdown)"),
|
|
1216
|
+
description: z.string().optional().describe("What this rule covers"),
|
|
1217
|
+
globs: z.array(z.string()).optional().describe("File globs this rule applies to"),
|
|
1218
|
+
alwaysApply: z.boolean().optional().default(true).describe("Always apply regardless of glob")
|
|
1219
|
+
});
|
|
1220
|
+
var InstructionReferenceSchema = z.object({
|
|
1221
|
+
path: z.string().describe("Path to the reference document"),
|
|
1222
|
+
description: z.string().optional().describe("What this document covers")
|
|
1223
|
+
});
|
|
1224
|
+
var InstructionsConfigSchema = z.object({
|
|
1225
|
+
primary: z.string().default("AGENTS.md").describe("Primary instruction file name"),
|
|
1226
|
+
references: z.array(InstructionReferenceSchema).optional().describe("Reference documents to include"),
|
|
1227
|
+
rules: z.array(InstructionRuleSchema).optional().describe("Granular rule files")
|
|
1228
|
+
});
|
|
1229
|
+
var HookEntrySchema = z.object({
|
|
1230
|
+
command: z.string().describe("Command to execute"),
|
|
1231
|
+
timeout: z.number().int().positive().optional().describe("Timeout in seconds"),
|
|
1232
|
+
description: z.string().optional().describe("What this hook does"),
|
|
1233
|
+
matcher: z.string().optional().describe("Regex matcher (e.g., tool name pattern)")
|
|
1234
|
+
});
|
|
1235
|
+
var HooksConfigSchema = z.record(
|
|
1236
|
+
z.string(),
|
|
1237
|
+
z.array(HookEntrySchema)
|
|
1238
|
+
).describe("Lifecycle event hooks keyed by event name");
|
|
1239
|
+
var McpServerSchema = z.object({
|
|
1240
|
+
command: z.string().describe("Command to run the MCP server"),
|
|
1241
|
+
args: z.array(z.string()).optional().describe("Command arguments"),
|
|
1242
|
+
env: z.record(z.string(), z.string()).optional().describe("Environment variables")
|
|
1243
|
+
});
|
|
1244
|
+
var McpConfigSchema = z.object({
|
|
1245
|
+
env: z.record(z.string(), z.string()).optional().describe("Global env vars for all MCP servers"),
|
|
1246
|
+
servers: z.record(z.string(), McpServerSchema).describe("MCP server configurations")
|
|
1247
|
+
});
|
|
1248
|
+
var PermissionsConfigSchema = z.object({
|
|
1249
|
+
default: z.enum(["prompt", "allow", "deny"]).default("prompt").describe("Default permission mode"),
|
|
1250
|
+
allow: z.array(z.string()).optional().describe("Globally allowed operations"),
|
|
1251
|
+
deny: z.array(z.string()).optional().describe("Globally denied operations")
|
|
1252
|
+
});
|
|
1253
|
+
var LocalSkillSchema = z.object({
|
|
1254
|
+
name: z.string().describe("Skill name (invoked by this name)"),
|
|
1255
|
+
path: z.string().describe("Path to SKILL.md file"),
|
|
1256
|
+
description: z.string().optional().describe("What this skill does"),
|
|
1257
|
+
tags: z.array(z.string()).optional().describe("Tags for categorization")
|
|
1258
|
+
});
|
|
1259
|
+
var ExternalSkillSchema = z.object({
|
|
1260
|
+
name: z.string().describe("Skill name"),
|
|
1261
|
+
source: z.string().describe("Source repo or package (e.g., 'org/repo')"),
|
|
1262
|
+
sourceType: z.enum(["github", "npm", "url"]).describe("Type of source"),
|
|
1263
|
+
skillPath: z.string().describe("Path to the skill within the source"),
|
|
1264
|
+
description: z.string().optional()
|
|
1265
|
+
});
|
|
1266
|
+
var SkillsConfigSchema = z.object({
|
|
1267
|
+
local: z.array(LocalSkillSchema).optional().describe("Locally defined skills"),
|
|
1268
|
+
external: z.array(ExternalSkillSchema).optional().describe("Skills from external sources")
|
|
1269
|
+
});
|
|
1270
|
+
var ProviderOverrideSchema = z.object({
|
|
1271
|
+
instructions: InstructionsConfigSchema.partial().optional(),
|
|
1272
|
+
hooks: HooksConfigSchema.optional(),
|
|
1273
|
+
mcp: McpConfigSchema.partial().optional(),
|
|
1274
|
+
permissions: PermissionsConfigSchema.partial().optional(),
|
|
1275
|
+
skills: SkillsConfigSchema.partial().optional()
|
|
1276
|
+
});
|
|
1277
|
+
var ProvidersConfigSchema = z.object({
|
|
1278
|
+
enabled: z.array(z.string()).optional().describe("Providers to enable (omitted = auto-detect)"),
|
|
1279
|
+
overrides: z.record(z.string(), ProviderOverrideSchema).optional().describe("Per-provider overrides")
|
|
1280
|
+
});
|
|
1281
|
+
var ConfluxMetaSchema = z.object({
|
|
1282
|
+
created: z.string().optional().describe("Creation date"),
|
|
1283
|
+
schema: z.string().optional().describe("Schema URL for validation"),
|
|
1284
|
+
description: z.string().optional().describe("Project description")
|
|
1285
|
+
});
|
|
1286
|
+
var UnifiedConfigSchema = z.object({
|
|
1287
|
+
version: z.literal(1).describe("Config schema version (must be 1)"),
|
|
1288
|
+
instructions: InstructionsConfigSchema.optional().describe("Project instructions for AI agents"),
|
|
1289
|
+
hooks: HooksConfigSchema.optional().describe("Lifecycle event hooks"),
|
|
1290
|
+
mcp: McpConfigSchema.optional().describe("MCP server configurations"),
|
|
1291
|
+
permissions: PermissionsConfigSchema.optional().describe("Permission rules"),
|
|
1292
|
+
skills: SkillsConfigSchema.optional().describe("Reusable skill/command definitions"),
|
|
1293
|
+
providers: ProvidersConfigSchema.optional().describe("Provider enablement and overrides"),
|
|
1294
|
+
meta: ConfluxMetaSchema.optional().describe("Metadata")
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
// src/core/config.ts
|
|
1298
|
+
import { ZodError } from "zod";
|
|
1299
|
+
function loadConfig(projectPath2) {
|
|
1300
|
+
const configPath = join4(projectPath2, ".ai", "config.yaml");
|
|
1301
|
+
if (!existsSync3(configPath)) {
|
|
1302
|
+
throw new ConfigNotFoundError(
|
|
1303
|
+
`No .ai/config.yaml found in ${projectPath2}. Run "conflux init" to create one.`
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
let raw;
|
|
1307
|
+
try {
|
|
1308
|
+
const content = readFileSync2(configPath, "utf-8");
|
|
1309
|
+
raw = yaml.load(content);
|
|
1310
|
+
} catch (err) {
|
|
1311
|
+
if (err instanceof ConfigNotFoundError) throw err;
|
|
1312
|
+
throw new ConfigParseError(
|
|
1313
|
+
`Failed to parse ${configPath}: ${err.message}`
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
return validateConfig(raw, configPath);
|
|
1317
|
+
}
|
|
1318
|
+
function validateConfig(raw, source) {
|
|
1319
|
+
try {
|
|
1320
|
+
return UnifiedConfigSchema.parse(raw);
|
|
1321
|
+
} catch (err) {
|
|
1322
|
+
if (err instanceof ZodError) {
|
|
1323
|
+
const issues = err.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
1324
|
+
const label = source ? ` in ${source}` : "";
|
|
1325
|
+
throw new ConfigValidationError(`Schema validation failed${label}:
|
|
1326
|
+
${issues}`);
|
|
1327
|
+
}
|
|
1328
|
+
throw err;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
function configExists(projectPath2) {
|
|
1332
|
+
return existsSync3(join4(projectPath2, ".ai", "config.yaml"));
|
|
1333
|
+
}
|
|
1334
|
+
var ConfigNotFoundError = class extends Error {
|
|
1335
|
+
constructor(message) {
|
|
1336
|
+
super(message);
|
|
1337
|
+
this.name = "ConfigNotFoundError";
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
var ConfigParseError = class extends Error {
|
|
1341
|
+
constructor(message) {
|
|
1342
|
+
super(message);
|
|
1343
|
+
this.name = "ConfigParseError";
|
|
1344
|
+
}
|
|
1345
|
+
};
|
|
1346
|
+
var ConfigValidationError = class extends Error {
|
|
1347
|
+
constructor(message) {
|
|
1348
|
+
super(message);
|
|
1349
|
+
this.name = "ConfigValidationError";
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1352
|
+
|
|
1353
|
+
// src/cli/commands/sync.ts
|
|
1354
|
+
var STATE_PATH = ".ai/.conflux-state.json";
|
|
1355
|
+
function registerSyncCommand(program2) {
|
|
1356
|
+
program2.command("sync").description("Sync .ai/config.yaml to all detected AI tools").option("--providers <list>", "Comma-separated list of providers to target").option("--no-detect", "Skip auto-detection, use --providers explicitly").option("--check", "Check if configs are in sync (for CI); exit 1 if stale").action(async (options, command) => {
|
|
1357
|
+
const globalOpts = command.parent?.opts() ?? {};
|
|
1358
|
+
const projectPath2 = globalOpts.project ?? process.cwd();
|
|
1359
|
+
const dryRun = globalOpts.dryRun ?? false;
|
|
1360
|
+
const verbose = globalOpts.verbose ?? false;
|
|
1361
|
+
if (verbose) {
|
|
1362
|
+
logger.info(`Project: ${projectPath2}`);
|
|
1363
|
+
logger.info(`Dry run: ${dryRun}`);
|
|
1364
|
+
}
|
|
1365
|
+
if (options.check) {
|
|
1366
|
+
const ok = await checkSync(projectPath2);
|
|
1367
|
+
if (!ok) {
|
|
1368
|
+
logger.error("Configuration is out of sync. Run 'conflux sync' to fix.");
|
|
1369
|
+
process.exit(1);
|
|
1370
|
+
}
|
|
1371
|
+
logger.success("All AI tool configs are in sync.");
|
|
1372
|
+
process.exit(0);
|
|
1373
|
+
}
|
|
1374
|
+
try {
|
|
1375
|
+
const config = loadConfig(projectPath2);
|
|
1376
|
+
if (verbose) {
|
|
1377
|
+
logger.info("Config loaded and validated successfully");
|
|
1378
|
+
}
|
|
1379
|
+
const enableList = options.providers ? options.providers.split(",").map((s) => s.trim()) : void 0;
|
|
1380
|
+
let providerIds;
|
|
1381
|
+
if (enableList) {
|
|
1382
|
+
providerIds = enableList;
|
|
1383
|
+
} else if (options.detect !== false) {
|
|
1384
|
+
providerIds = detectToolIds(projectPath2);
|
|
1385
|
+
} else {
|
|
1386
|
+
providerIds = registry.list().map((p) => p.id);
|
|
1387
|
+
}
|
|
1388
|
+
if (config.providers?.enabled) {
|
|
1389
|
+
providerIds = providerIds.filter(
|
|
1390
|
+
(id) => config.providers.enabled.includes(id)
|
|
1391
|
+
);
|
|
1392
|
+
}
|
|
1393
|
+
if (providerIds.length === 0) {
|
|
1394
|
+
logger.warn("No AI tools detected in this project.");
|
|
1395
|
+
logger.info(
|
|
1396
|
+
"Run 'conflux providers --detect' to see what's available, or use --providers to specify tools explicitly."
|
|
1397
|
+
);
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
logger.heading(`Syncing to ${providerIds.length} tool(s)`);
|
|
1401
|
+
const syncOpts = {
|
|
1402
|
+
dryRun,
|
|
1403
|
+
verbose,
|
|
1404
|
+
force: false
|
|
1405
|
+
};
|
|
1406
|
+
const results = [];
|
|
1407
|
+
const stateData = {
|
|
1408
|
+
version: 1,
|
|
1409
|
+
lastSync: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1410
|
+
configHash: hashContent(JSON.stringify(config)),
|
|
1411
|
+
providers: {}
|
|
1412
|
+
};
|
|
1413
|
+
for (const providerId of providerIds) {
|
|
1414
|
+
const provider = registry.get(providerId);
|
|
1415
|
+
if (!provider) {
|
|
1416
|
+
logger.warn(`Unknown provider: ${providerId} \u2014 skipping`);
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
logger.info(`Syncing ${provider.name}...`);
|
|
1420
|
+
try {
|
|
1421
|
+
const result = await provider.sync(projectPath2, config, syncOpts);
|
|
1422
|
+
stateData.providers[providerId] = {
|
|
1423
|
+
synced: result.errors.length === 0,
|
|
1424
|
+
lastSync: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1425
|
+
files: {}
|
|
1426
|
+
};
|
|
1427
|
+
for (const file of result.filesWritten) {
|
|
1428
|
+
stateData.providers[providerId].files[file.path] = {
|
|
1429
|
+
hash: file.hash ?? hashContent(file.content),
|
|
1430
|
+
generatedFrom: file.path,
|
|
1431
|
+
type: file.mode === "symlink" ? "symlink" : "file"
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
results.push(result);
|
|
1435
|
+
for (const file of result.filesWritten) {
|
|
1436
|
+
const prefix = dryRun ? " [DRY RUN]" : " \u2192";
|
|
1437
|
+
logger.success(`${prefix} ${file.path}`);
|
|
1438
|
+
}
|
|
1439
|
+
for (const warning of result.warnings) {
|
|
1440
|
+
logger.warn(warning);
|
|
1441
|
+
}
|
|
1442
|
+
for (const error of result.errors) {
|
|
1443
|
+
logger.error(error);
|
|
1444
|
+
}
|
|
1445
|
+
} catch (err) {
|
|
1446
|
+
logger.error(`${provider.name}: ${err.message}`);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
if (!dryRun) {
|
|
1450
|
+
const statePath = join5(projectPath2, STATE_PATH);
|
|
1451
|
+
writeFile(statePath, JSON.stringify(stateData, null, 2) + "\n");
|
|
1452
|
+
logger.success(`State written to ${STATE_PATH}`);
|
|
1453
|
+
}
|
|
1454
|
+
const totalFiles = results.reduce((sum, r) => sum + r.filesWritten.length, 0);
|
|
1455
|
+
const totalErrors = results.reduce((sum, r) => sum + r.errors.length, 0);
|
|
1456
|
+
const totalWarnings = results.reduce((sum, r) => sum + r.warnings.length, 0);
|
|
1457
|
+
console.log();
|
|
1458
|
+
if (dryRun) {
|
|
1459
|
+
logger.heading(
|
|
1460
|
+
`[DRY RUN] Would write ${totalFiles} file(s) across ${results.length} tool(s)`
|
|
1461
|
+
);
|
|
1462
|
+
} else {
|
|
1463
|
+
logger.heading(
|
|
1464
|
+
`Synced ${totalFiles} file(s) across ${results.length} tool(s)`
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
if (totalWarnings > 0) {
|
|
1468
|
+
logger.warn(`${totalWarnings} warning(s)`);
|
|
1469
|
+
}
|
|
1470
|
+
if (totalErrors > 0) {
|
|
1471
|
+
logger.error(`${totalErrors} error(s)`);
|
|
1472
|
+
}
|
|
1473
|
+
} catch (err) {
|
|
1474
|
+
logger.error(`Failed to load config: ${err.message}`);
|
|
1475
|
+
logger.info("Run 'conflux init' to create a .ai/config.yaml file.");
|
|
1476
|
+
process.exit(1);
|
|
1477
|
+
}
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
async function checkSync(projectPath2) {
|
|
1481
|
+
if (!configExists(projectPath2)) {
|
|
1482
|
+
logger.error("No .ai/config.yaml found. Run 'conflux init' first.");
|
|
1483
|
+
return false;
|
|
1484
|
+
}
|
|
1485
|
+
const statePath = join5(projectPath2, STATE_PATH);
|
|
1486
|
+
if (!pathExists(statePath)) {
|
|
1487
|
+
logger.error("No .ai/.conflux-state.json found. Run 'conflux sync' first.");
|
|
1488
|
+
return false;
|
|
1489
|
+
}
|
|
1490
|
+
let state;
|
|
1491
|
+
try {
|
|
1492
|
+
state = JSON.parse(readFileIfExists(statePath) ?? "{}");
|
|
1493
|
+
} catch {
|
|
1494
|
+
logger.error("Corrupted state file. Run 'conflux sync' to regenerate.");
|
|
1495
|
+
return false;
|
|
1496
|
+
}
|
|
1497
|
+
const config = loadConfig(projectPath2);
|
|
1498
|
+
const currentConfigHash = hashContent(JSON.stringify(config));
|
|
1499
|
+
if (currentConfigHash !== state.configHash) {
|
|
1500
|
+
logger.error("\u274C .ai/config.yaml was modified since last sync.");
|
|
1501
|
+
logger.info(" Run 'conflux sync' to update tool configs.");
|
|
1502
|
+
return false;
|
|
1503
|
+
}
|
|
1504
|
+
let allOk = true;
|
|
1505
|
+
for (const [providerId, providerState] of Object.entries(state.providers)) {
|
|
1506
|
+
if (!providerState?.synced) continue;
|
|
1507
|
+
for (const [filePath, fileState] of Object.entries(providerState.files)) {
|
|
1508
|
+
const fullPath = join5(projectPath2, filePath);
|
|
1509
|
+
const content = readFileIfExists(fullPath);
|
|
1510
|
+
if (content === null) {
|
|
1511
|
+
logger.error(`\u274C Missing: ${filePath} (${providerId})`);
|
|
1512
|
+
allOk = false;
|
|
1513
|
+
continue;
|
|
1514
|
+
}
|
|
1515
|
+
const actualHash = hashContent(content);
|
|
1516
|
+
if (actualHash !== fileState.hash) {
|
|
1517
|
+
logger.error(`\u274C Drifted: ${filePath} (${providerId}) \u2014 manually edited`);
|
|
1518
|
+
allOk = false;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
const detectedIds = detectToolIds(projectPath2);
|
|
1523
|
+
const enabledIds = config.providers?.enabled ?? detectedIds;
|
|
1524
|
+
const syncedIds = Object.keys(state.providers);
|
|
1525
|
+
for (const id of enabledIds) {
|
|
1526
|
+
if (detectedIds.includes(id) && !syncedIds.includes(id)) {
|
|
1527
|
+
logger.warn(`\u26A0 New tool detected: ${id} \u2014 run 'conflux sync' to generate configs`);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
return allOk;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// src/cli/commands/providers.ts
|
|
1534
|
+
var CAPABILITY_ICONS = {
|
|
1535
|
+
instructions: "\u{1F4CB}",
|
|
1536
|
+
hooks: "\u{1FA9D}",
|
|
1537
|
+
mcp: "\u{1F50C}",
|
|
1538
|
+
permissions: "\u{1F512}",
|
|
1539
|
+
skills: "\u26A1"
|
|
1540
|
+
};
|
|
1541
|
+
function registerProvidersCommand(program2) {
|
|
1542
|
+
program2.command("providers").description("List supported AI coding tools and their capabilities").option("--details", "Show detailed capability breakdown").option("--detect", "Show which tools are detected in the current project").action(async (options, command) => {
|
|
1543
|
+
const globalOpts = command.parent?.opts() ?? {};
|
|
1544
|
+
const projectPath2 = globalOpts.project ?? process.cwd();
|
|
1545
|
+
const providers = registry.list();
|
|
1546
|
+
if (providers.length === 0) {
|
|
1547
|
+
logger.warn("No providers registered. This is a bug \u2014 please report it.");
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
logger.heading(`AI Coding Tools (${providers.length} supported)`);
|
|
1551
|
+
const detectedPatterns = options.detect ? detectTools(projectPath2) : [];
|
|
1552
|
+
const detectedIds = new Set(detectedPatterns.map((p) => p.providerId));
|
|
1553
|
+
if (options.details) {
|
|
1554
|
+
const rows = providers.map((p) => {
|
|
1555
|
+
const capStr = p.capabilities.map((c) => CAPABILITY_ICONS[c] ?? "\u2753").join(" ");
|
|
1556
|
+
return {
|
|
1557
|
+
Provider: p.id,
|
|
1558
|
+
Name: p.name,
|
|
1559
|
+
Capabilities: capStr,
|
|
1560
|
+
Detected: detectedIds.has(p.id) ? "\u2713" : "-"
|
|
1561
|
+
};
|
|
1562
|
+
});
|
|
1563
|
+
logger.table(rows);
|
|
1564
|
+
} else {
|
|
1565
|
+
for (const p of providers) {
|
|
1566
|
+
const detected = detectedIds.has(p.id) ? " (detected)" : "";
|
|
1567
|
+
console.log(` ${p.id.padEnd(20)} ${p.name}${detected}`);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
if (options.details) {
|
|
1571
|
+
console.log();
|
|
1572
|
+
console.log(" Capability legend:");
|
|
1573
|
+
for (const cap of Object.values(ProviderCapability)) {
|
|
1574
|
+
console.log(` ${CAPABILITY_ICONS[cap] ?? "\u2753"} ${cap}`);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
// src/cli/commands/setup-git-hook.ts
|
|
1581
|
+
import { join as join6 } from "path";
|
|
1582
|
+
var HOOK_SCRIPT = `#!/bin/sh
|
|
1583
|
+
# ============================================================================
|
|
1584
|
+
# Conflux pre-commit hook
|
|
1585
|
+
# Ensures AI tool configs are in sync before every commit.
|
|
1586
|
+
# Installed by: conflux setup-git-hook
|
|
1587
|
+
# ============================================================================
|
|
1588
|
+
|
|
1589
|
+
echo "\u{1F50D} Conflux: checking AI config sync..."
|
|
1590
|
+
|
|
1591
|
+
if command -v conflux &> /dev/null; then
|
|
1592
|
+
conflux sync --check
|
|
1593
|
+
if [ $? -ne 0 ]; then
|
|
1594
|
+
echo ""
|
|
1595
|
+
echo "\u274C AI tool configs are out of sync with .ai/config.yaml."
|
|
1596
|
+
echo " Run 'conflux sync' to fix, then commit again."
|
|
1597
|
+
echo " Or use 'git commit --no-verify' to skip this check."
|
|
1598
|
+
exit 1
|
|
1599
|
+
fi
|
|
1600
|
+
echo "\u2705 AI configs in sync."
|
|
1601
|
+
else
|
|
1602
|
+
echo "\u26A0\uFE0F conflux not found in PATH \u2014 skipping sync check."
|
|
1603
|
+
echo " Install: npm install -g conflux-ai"
|
|
1604
|
+
fi
|
|
1605
|
+
`;
|
|
1606
|
+
function registerSetupGitHookCommand(program2) {
|
|
1607
|
+
program2.command("setup-git-hook").description("Install a pre-commit hook that auto-checks config sync").option("--force", "Overwrite existing pre-commit hook").action(async (options, command) => {
|
|
1608
|
+
const globalOpts = command.parent?.opts() ?? {};
|
|
1609
|
+
const projectPath2 = globalOpts.project ?? process.cwd();
|
|
1610
|
+
const gitDir = join6(projectPath2, ".git");
|
|
1611
|
+
if (!pathExists(gitDir)) {
|
|
1612
|
+
logger.error("Not a git repository. Run 'git init' first.");
|
|
1613
|
+
process.exit(1);
|
|
1614
|
+
}
|
|
1615
|
+
const hookPath = join6(gitDir, "hooks", "pre-commit");
|
|
1616
|
+
const existing = readFileIfExists(hookPath);
|
|
1617
|
+
if (existing && !options.force) {
|
|
1618
|
+
if (existing.includes("Conflux pre-commit hook")) {
|
|
1619
|
+
logger.info("Conflux pre-commit hook already installed.");
|
|
1620
|
+
logger.info("Use --force to reinstall.");
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
logger.warn("A pre-commit hook already exists.");
|
|
1624
|
+
logger.info(hookPath);
|
|
1625
|
+
logger.info("");
|
|
1626
|
+
logger.info("Options:");
|
|
1627
|
+
logger.info(" 1. Use --force to replace it (your existing hook will be lost)");
|
|
1628
|
+
logger.info(" 2. Manually add this line to your existing hook:");
|
|
1629
|
+
console.log("");
|
|
1630
|
+
console.log(" conflux sync --check");
|
|
1631
|
+
console.log("");
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
writeFile(hookPath, HOOK_SCRIPT);
|
|
1635
|
+
try {
|
|
1636
|
+
const { chmodSync } = await import("fs");
|
|
1637
|
+
chmodSync(hookPath, 493);
|
|
1638
|
+
} catch {
|
|
1639
|
+
}
|
|
1640
|
+
logger.success("Installed pre-commit hook:");
|
|
1641
|
+
logger.info(` ${hookPath}`);
|
|
1642
|
+
console.log("");
|
|
1643
|
+
logger.info("Now, before every 'git commit', Conflux will:");
|
|
1644
|
+
logger.info(" 1. Check if .ai/config.yaml changed since last sync");
|
|
1645
|
+
logger.info(" 2. Check if any generated files were manually edited");
|
|
1646
|
+
logger.info(" 3. Block the commit if configs are out of sync");
|
|
1647
|
+
console.log("");
|
|
1648
|
+
logger.info("To skip the check: git commit --no-verify");
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
// src/cli/index.ts
|
|
1653
|
+
registerAllProviders();
|
|
1654
|
+
var __filename2 = fileURLToPath(import.meta.url);
|
|
1655
|
+
var __dirname2 = dirname2(__filename2);
|
|
1656
|
+
var pkgPath = join7(__dirname2, "..", "..", "package.json");
|
|
1657
|
+
var pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
1658
|
+
var program = new Command();
|
|
1659
|
+
program.name("conflux").description(
|
|
1660
|
+
"Unified configuration manager for AI coding tools.\nDefine once in .ai/config.yaml \u2014 sync to Claude Code, Codex, Cursor, and more."
|
|
1661
|
+
).version(pkg.version, "-V, --version", "Show version number").option("-v, --verbose", "Verbose output").option("-d, --dry-run", "Preview changes without writing files").option("-p, --project <path>", "Project root directory", process.cwd()).option("--json", "Output in JSON format (for programmatic use)").option("-y, --yes", "Skip confirmation prompts");
|
|
1662
|
+
registerInitCommand(program);
|
|
1663
|
+
registerSyncCommand(program);
|
|
1664
|
+
registerProvidersCommand(program);
|
|
1665
|
+
registerSetupGitHookCommand(program);
|
|
1666
|
+
program.parse(process.argv);
|
|
1667
|
+
//# sourceMappingURL=index.js.map
|