@lnai/core 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/dist/index.d.ts +313 -0
- package/dist/index.js +915 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,915 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import * as fs3 from 'fs/promises';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import matter from 'gray-matter';
|
|
5
|
+
import deepmerge from 'deepmerge';
|
|
6
|
+
import * as crypto from 'crypto';
|
|
7
|
+
|
|
8
|
+
// src/constants.ts
|
|
9
|
+
var UNIFIED_DIR = ".ai";
|
|
10
|
+
var TOOL_IDS = ["claudeCode", "opencode"];
|
|
11
|
+
var CONFIG_FILES = {
|
|
12
|
+
config: "config.json",
|
|
13
|
+
settings: "settings.json",
|
|
14
|
+
agents: "AGENTS.md"
|
|
15
|
+
};
|
|
16
|
+
var CONFIG_DIRS = {
|
|
17
|
+
rules: "rules",
|
|
18
|
+
skills: "skills",
|
|
19
|
+
subagents: "subagents"
|
|
20
|
+
};
|
|
21
|
+
var TOOL_OUTPUT_DIRS = {
|
|
22
|
+
claudeCode: ".claude",
|
|
23
|
+
opencode: ".opencode"
|
|
24
|
+
};
|
|
25
|
+
var OVERRIDE_DIRS = {
|
|
26
|
+
claudeCode: ".claude",
|
|
27
|
+
opencode: ".opencode"
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// src/errors.ts
|
|
31
|
+
var LnaiError = class extends Error {
|
|
32
|
+
code;
|
|
33
|
+
constructor(message, code) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "LnaiError";
|
|
36
|
+
this.code = code;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var ParseError = class extends LnaiError {
|
|
40
|
+
filePath;
|
|
41
|
+
constructor(message, filePath, cause) {
|
|
42
|
+
super(message, "PARSE_ERROR");
|
|
43
|
+
this.name = "ParseError";
|
|
44
|
+
this.filePath = filePath;
|
|
45
|
+
if (cause) {
|
|
46
|
+
this.cause = cause;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var ValidationError = class extends LnaiError {
|
|
51
|
+
path;
|
|
52
|
+
value;
|
|
53
|
+
constructor(message, path7, value) {
|
|
54
|
+
super(message, "VALIDATION_ERROR");
|
|
55
|
+
this.name = "ValidationError";
|
|
56
|
+
this.path = path7;
|
|
57
|
+
this.value = value;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var FileNotFoundError = class extends LnaiError {
|
|
61
|
+
filePath;
|
|
62
|
+
constructor(message, filePath) {
|
|
63
|
+
super(message, "FILE_NOT_FOUND");
|
|
64
|
+
this.name = "FileNotFoundError";
|
|
65
|
+
this.filePath = filePath;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
var WriteError = class extends LnaiError {
|
|
69
|
+
filePath;
|
|
70
|
+
constructor(message, filePath, cause) {
|
|
71
|
+
super(message, "WRITE_ERROR");
|
|
72
|
+
this.name = "WriteError";
|
|
73
|
+
this.filePath = filePath;
|
|
74
|
+
if (cause) {
|
|
75
|
+
this.cause = cause;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
var PluginError = class extends LnaiError {
|
|
80
|
+
pluginId;
|
|
81
|
+
constructor(message, pluginId, cause) {
|
|
82
|
+
super(message, "PLUGIN_ERROR");
|
|
83
|
+
this.name = "PluginError";
|
|
84
|
+
this.pluginId = pluginId;
|
|
85
|
+
if (cause) {
|
|
86
|
+
this.cause = cause;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
var mcpServerSchema = z.object({
|
|
91
|
+
command: z.string().optional(),
|
|
92
|
+
args: z.array(z.string()).optional(),
|
|
93
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
94
|
+
type: z.enum(["http", "sse"]).optional(),
|
|
95
|
+
url: z.string().optional(),
|
|
96
|
+
headers: z.record(z.string(), z.string()).optional()
|
|
97
|
+
});
|
|
98
|
+
var permissionsSchema = z.object({
|
|
99
|
+
allow: z.array(z.string()).optional(),
|
|
100
|
+
ask: z.array(z.string()).optional(),
|
|
101
|
+
deny: z.array(z.string()).optional()
|
|
102
|
+
});
|
|
103
|
+
var toolConfigSchema = z.object({
|
|
104
|
+
enabled: z.boolean(),
|
|
105
|
+
versionControl: z.boolean().optional().default(false)
|
|
106
|
+
});
|
|
107
|
+
var toolIdSchema = z.enum(["claudeCode", "opencode"]);
|
|
108
|
+
var settingsSchema = z.object({
|
|
109
|
+
permissions: permissionsSchema.optional(),
|
|
110
|
+
mcpServers: z.record(z.string(), mcpServerSchema).optional(),
|
|
111
|
+
overrides: z.object({
|
|
112
|
+
claudeCode: z.record(z.string(), z.unknown()).optional(),
|
|
113
|
+
opencode: z.record(z.string(), z.unknown()).optional()
|
|
114
|
+
}).optional()
|
|
115
|
+
});
|
|
116
|
+
var configSchema = z.object({
|
|
117
|
+
tools: z.object({
|
|
118
|
+
claudeCode: toolConfigSchema,
|
|
119
|
+
opencode: toolConfigSchema
|
|
120
|
+
}).partial().optional()
|
|
121
|
+
});
|
|
122
|
+
var skillFrontmatterSchema = z.object({
|
|
123
|
+
name: z.string(),
|
|
124
|
+
description: z.string()
|
|
125
|
+
});
|
|
126
|
+
var ruleFrontmatterSchema = z.object({
|
|
127
|
+
paths: z.array(z.string()).min(1)
|
|
128
|
+
});
|
|
129
|
+
function parseFrontmatter(content) {
|
|
130
|
+
const parsed = matter(content);
|
|
131
|
+
return {
|
|
132
|
+
frontmatter: parsed.data,
|
|
133
|
+
content: parsed.content.trim()
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/parser/index.ts
|
|
138
|
+
async function parseUnifiedConfig(rootDir) {
|
|
139
|
+
const aiDir = path.join(rootDir, UNIFIED_DIR);
|
|
140
|
+
if (!await fileExists(aiDir)) {
|
|
141
|
+
throw new FileNotFoundError(
|
|
142
|
+
`Unified config directory not found: ${aiDir}`,
|
|
143
|
+
aiDir
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const configPath = path.join(aiDir, CONFIG_FILES.config);
|
|
147
|
+
let config;
|
|
148
|
+
try {
|
|
149
|
+
config = await readJsonFile(configPath);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (error instanceof FileNotFoundError) {
|
|
152
|
+
config = { tools: {} };
|
|
153
|
+
} else {
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const settingsPath = path.join(aiDir, CONFIG_FILES.settings);
|
|
158
|
+
let settings = null;
|
|
159
|
+
if (await fileExists(settingsPath)) {
|
|
160
|
+
settings = await readJsonFile(settingsPath);
|
|
161
|
+
}
|
|
162
|
+
const agentsPath = path.join(aiDir, CONFIG_FILES.agents);
|
|
163
|
+
let agents = null;
|
|
164
|
+
if (await fileExists(agentsPath)) {
|
|
165
|
+
agents = await readMarkdownFile(agentsPath);
|
|
166
|
+
}
|
|
167
|
+
const rulesDir = path.join(aiDir, CONFIG_DIRS.rules);
|
|
168
|
+
const rules = await readMarkdownDirectory(rulesDir);
|
|
169
|
+
const skillsDir = path.join(aiDir, CONFIG_DIRS.skills);
|
|
170
|
+
const skills = await readSkillsDirectory(skillsDir);
|
|
171
|
+
return {
|
|
172
|
+
config,
|
|
173
|
+
settings,
|
|
174
|
+
agents,
|
|
175
|
+
rules,
|
|
176
|
+
skills
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
async function fileExists(filePath) {
|
|
180
|
+
try {
|
|
181
|
+
await fs3.access(filePath);
|
|
182
|
+
return true;
|
|
183
|
+
} catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async function readJsonFile(filePath) {
|
|
188
|
+
try {
|
|
189
|
+
const content = await fs3.readFile(filePath, "utf-8");
|
|
190
|
+
return JSON.parse(content);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (error.code === "ENOENT") {
|
|
193
|
+
throw new FileNotFoundError(`File not found: ${filePath}`, filePath);
|
|
194
|
+
}
|
|
195
|
+
throw new ParseError(
|
|
196
|
+
`Failed to parse JSON: ${filePath}`,
|
|
197
|
+
filePath,
|
|
198
|
+
error
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function readMarkdownFile(filePath) {
|
|
203
|
+
try {
|
|
204
|
+
return await fs3.readFile(filePath, "utf-8");
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (error.code === "ENOENT") {
|
|
207
|
+
throw new FileNotFoundError(`File not found: ${filePath}`, filePath);
|
|
208
|
+
}
|
|
209
|
+
throw new ParseError(
|
|
210
|
+
`Failed to read markdown: ${filePath}`,
|
|
211
|
+
filePath,
|
|
212
|
+
error
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function readMarkdownDirectory(dirPath) {
|
|
217
|
+
const files = [];
|
|
218
|
+
if (!await fileExists(dirPath)) {
|
|
219
|
+
return files;
|
|
220
|
+
}
|
|
221
|
+
const entries = await fs3.readdir(dirPath, { withFileTypes: true });
|
|
222
|
+
for (const entry of entries) {
|
|
223
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
224
|
+
const filePath = path.join(dirPath, entry.name);
|
|
225
|
+
const content = await readMarkdownFile(filePath);
|
|
226
|
+
const parsed = parseFrontmatter(content);
|
|
227
|
+
files.push({
|
|
228
|
+
path: entry.name,
|
|
229
|
+
frontmatter: parsed.frontmatter,
|
|
230
|
+
content: parsed.content
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return files;
|
|
235
|
+
}
|
|
236
|
+
async function readSkillsDirectory(dirPath) {
|
|
237
|
+
const skills = [];
|
|
238
|
+
if (!await fileExists(dirPath)) {
|
|
239
|
+
return skills;
|
|
240
|
+
}
|
|
241
|
+
const entries = await fs3.readdir(dirPath, { withFileTypes: true });
|
|
242
|
+
for (const entry of entries) {
|
|
243
|
+
if (entry.isDirectory()) {
|
|
244
|
+
const skillFile = path.join(dirPath, entry.name, "SKILL.md");
|
|
245
|
+
if (await fileExists(skillFile)) {
|
|
246
|
+
const content = await readMarkdownFile(skillFile);
|
|
247
|
+
const parsed = parseFrontmatter(content);
|
|
248
|
+
skills.push({
|
|
249
|
+
path: entry.name,
|
|
250
|
+
frontmatter: parsed.frontmatter,
|
|
251
|
+
content: parsed.content
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return skills;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/validator/index.ts
|
|
260
|
+
function zodIssuesToErrors(issues, prefix = []) {
|
|
261
|
+
return issues.map((issue) => ({
|
|
262
|
+
path: [...prefix, ...issue.path.map(String)],
|
|
263
|
+
message: issue.message,
|
|
264
|
+
value: void 0
|
|
265
|
+
}));
|
|
266
|
+
}
|
|
267
|
+
function validateConfig(config) {
|
|
268
|
+
const result = configSchema.safeParse(config);
|
|
269
|
+
if (result.success) {
|
|
270
|
+
return {
|
|
271
|
+
valid: true,
|
|
272
|
+
errors: [],
|
|
273
|
+
warnings: [],
|
|
274
|
+
skipped: []
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
valid: false,
|
|
279
|
+
errors: zodIssuesToErrors(result.error.issues, ["config"]),
|
|
280
|
+
warnings: [],
|
|
281
|
+
skipped: []
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function validateSettings(settings) {
|
|
285
|
+
if (settings === null || settings === void 0) {
|
|
286
|
+
return {
|
|
287
|
+
valid: true,
|
|
288
|
+
errors: [],
|
|
289
|
+
warnings: [],
|
|
290
|
+
skipped: []
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
const result = settingsSchema.safeParse(settings);
|
|
294
|
+
if (result.success) {
|
|
295
|
+
return {
|
|
296
|
+
valid: true,
|
|
297
|
+
errors: [],
|
|
298
|
+
warnings: [],
|
|
299
|
+
skipped: []
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
valid: false,
|
|
304
|
+
errors: zodIssuesToErrors(result.error.issues, ["settings"]),
|
|
305
|
+
warnings: [],
|
|
306
|
+
skipped: []
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function validateSkillFrontmatter(frontmatter, skillPath) {
|
|
310
|
+
const result = skillFrontmatterSchema.safeParse(frontmatter);
|
|
311
|
+
if (result.success) {
|
|
312
|
+
return [];
|
|
313
|
+
}
|
|
314
|
+
return zodIssuesToErrors(result.error.issues, [
|
|
315
|
+
"skills",
|
|
316
|
+
skillPath,
|
|
317
|
+
"frontmatter"
|
|
318
|
+
]);
|
|
319
|
+
}
|
|
320
|
+
function validateRuleFrontmatter(frontmatter, rulePath) {
|
|
321
|
+
const result = ruleFrontmatterSchema.safeParse(frontmatter);
|
|
322
|
+
if (result.success) {
|
|
323
|
+
return [];
|
|
324
|
+
}
|
|
325
|
+
return zodIssuesToErrors(result.error.issues, [
|
|
326
|
+
"rules",
|
|
327
|
+
rulePath,
|
|
328
|
+
"frontmatter"
|
|
329
|
+
]);
|
|
330
|
+
}
|
|
331
|
+
function validateUnifiedState(state) {
|
|
332
|
+
const errors = [];
|
|
333
|
+
const warnings = [];
|
|
334
|
+
const configResult = validateConfig(state.config);
|
|
335
|
+
errors.push(...configResult.errors);
|
|
336
|
+
const settingsResult = validateSettings(state.settings);
|
|
337
|
+
errors.push(...settingsResult.errors);
|
|
338
|
+
for (const skill of state.skills) {
|
|
339
|
+
const skillErrors = validateSkillFrontmatter(skill.frontmatter, skill.path);
|
|
340
|
+
errors.push(...skillErrors);
|
|
341
|
+
}
|
|
342
|
+
for (const rule of state.rules) {
|
|
343
|
+
const ruleErrors = validateRuleFrontmatter(rule.frontmatter, rule.path);
|
|
344
|
+
errors.push(...ruleErrors);
|
|
345
|
+
}
|
|
346
|
+
if (!state.agents) {
|
|
347
|
+
warnings.push({
|
|
348
|
+
path: ["AGENTS.md"],
|
|
349
|
+
message: "AGENTS.md not found - no main instructions will be exported"
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
valid: errors.length === 0,
|
|
354
|
+
errors,
|
|
355
|
+
warnings,
|
|
356
|
+
skipped: []
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
async function scanOverrideDirectory(rootDir, toolId) {
|
|
360
|
+
const overrideDir = path.join(rootDir, UNIFIED_DIR, OVERRIDE_DIRS[toolId]);
|
|
361
|
+
try {
|
|
362
|
+
await fs3.access(overrideDir);
|
|
363
|
+
} catch {
|
|
364
|
+
return [];
|
|
365
|
+
}
|
|
366
|
+
const files = [];
|
|
367
|
+
await scanDir(overrideDir, overrideDir, files);
|
|
368
|
+
return files;
|
|
369
|
+
}
|
|
370
|
+
async function scanDir(baseDir, currentDir, files) {
|
|
371
|
+
const entries = await fs3.readdir(currentDir, { withFileTypes: true });
|
|
372
|
+
for (const entry of entries) {
|
|
373
|
+
const absolutePath = path.join(currentDir, entry.name);
|
|
374
|
+
if (entry.isDirectory()) {
|
|
375
|
+
await scanDir(baseDir, absolutePath, files);
|
|
376
|
+
} else if (entry.isFile()) {
|
|
377
|
+
const relativePath = path.relative(baseDir, absolutePath);
|
|
378
|
+
files.push({ relativePath, absolutePath });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function deepMergeConfigs(base, override) {
|
|
383
|
+
return deepmerge(base, override, {
|
|
384
|
+
arrayMerge: (target, source) => [.../* @__PURE__ */ new Set([...target, ...source])]
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
async function fileExists2(filePath) {
|
|
388
|
+
try {
|
|
389
|
+
await fs3.access(filePath);
|
|
390
|
+
return true;
|
|
391
|
+
} catch {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/plugins/claude-code/index.ts
|
|
397
|
+
var claudeCodePlugin = {
|
|
398
|
+
id: "claudeCode",
|
|
399
|
+
name: "Claude Code",
|
|
400
|
+
async detect(_rootDir) {
|
|
401
|
+
return false;
|
|
402
|
+
},
|
|
403
|
+
async import(_rootDir) {
|
|
404
|
+
return null;
|
|
405
|
+
},
|
|
406
|
+
async export(state, rootDir) {
|
|
407
|
+
const files = [];
|
|
408
|
+
const outputDir = TOOL_OUTPUT_DIRS.claudeCode;
|
|
409
|
+
if (state.agents) {
|
|
410
|
+
files.push({
|
|
411
|
+
path: `${outputDir}/CLAUDE.md`,
|
|
412
|
+
type: "symlink",
|
|
413
|
+
target: `../${UNIFIED_DIR}/AGENTS.md`
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
if (state.rules.length > 0) {
|
|
417
|
+
files.push({
|
|
418
|
+
path: `${outputDir}/rules`,
|
|
419
|
+
type: "symlink",
|
|
420
|
+
target: `../${UNIFIED_DIR}/rules`
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
for (const skill of state.skills) {
|
|
424
|
+
files.push({
|
|
425
|
+
path: `${outputDir}/skills/${skill.path}`,
|
|
426
|
+
type: "symlink",
|
|
427
|
+
target: `../../${UNIFIED_DIR}/skills/${skill.path}`
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
const baseSettings = {};
|
|
431
|
+
if (state.settings?.permissions) {
|
|
432
|
+
baseSettings["permissions"] = state.settings.permissions;
|
|
433
|
+
}
|
|
434
|
+
if (state.settings?.mcpServers) {
|
|
435
|
+
baseSettings["mcpServers"] = state.settings.mcpServers;
|
|
436
|
+
}
|
|
437
|
+
let finalSettings = baseSettings;
|
|
438
|
+
if (state.settings?.overrides?.claudeCode) {
|
|
439
|
+
finalSettings = deepMergeConfigs(
|
|
440
|
+
baseSettings,
|
|
441
|
+
state.settings.overrides.claudeCode
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
if (Object.keys(finalSettings).length > 0) {
|
|
445
|
+
files.push({
|
|
446
|
+
path: `${outputDir}/settings.json`,
|
|
447
|
+
type: "json",
|
|
448
|
+
content: finalSettings
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
const overrideFiles = await scanOverrideDirectory(rootDir, "claudeCode");
|
|
452
|
+
for (const overrideFile of overrideFiles) {
|
|
453
|
+
const targetPath = path.join(
|
|
454
|
+
rootDir,
|
|
455
|
+
outputDir,
|
|
456
|
+
overrideFile.relativePath
|
|
457
|
+
);
|
|
458
|
+
if (await fileExists2(targetPath)) {
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
files.push({
|
|
462
|
+
path: `${outputDir}/${overrideFile.relativePath}`,
|
|
463
|
+
type: "symlink",
|
|
464
|
+
target: `../${UNIFIED_DIR}/${OVERRIDE_DIRS.claudeCode}/${overrideFile.relativePath}`
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
return files;
|
|
468
|
+
},
|
|
469
|
+
validate(state) {
|
|
470
|
+
const warnings = [];
|
|
471
|
+
if (!state.agents) {
|
|
472
|
+
warnings.push({
|
|
473
|
+
path: ["AGENTS.md"],
|
|
474
|
+
message: "No AGENTS.md found - .claude/CLAUDE.md will not be created"
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
return { valid: true, errors: [], warnings, skipped: [] };
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
// src/plugins/opencode/transforms.ts
|
|
482
|
+
function transformMcpToOpenCode(servers) {
|
|
483
|
+
if (!servers || Object.keys(servers).length === 0) {
|
|
484
|
+
return void 0;
|
|
485
|
+
}
|
|
486
|
+
const result = {};
|
|
487
|
+
for (const [name, serverRaw] of Object.entries(servers)) {
|
|
488
|
+
const server = serverRaw;
|
|
489
|
+
if (server.type === "http" || server.type === "sse") {
|
|
490
|
+
const openCodeServer = {
|
|
491
|
+
type: "remote",
|
|
492
|
+
url: server.url
|
|
493
|
+
};
|
|
494
|
+
if (server.headers) {
|
|
495
|
+
openCodeServer.headers = server.headers;
|
|
496
|
+
}
|
|
497
|
+
result[name] = openCodeServer;
|
|
498
|
+
} else if (server.command) {
|
|
499
|
+
const command = [server.command, ...server.args || []];
|
|
500
|
+
const openCodeServer = {
|
|
501
|
+
type: "local",
|
|
502
|
+
command
|
|
503
|
+
};
|
|
504
|
+
if (server.env) {
|
|
505
|
+
openCodeServer.environment = transformEnvVars(server.env);
|
|
506
|
+
}
|
|
507
|
+
result[name] = openCodeServer;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
511
|
+
}
|
|
512
|
+
function transformPermissionsToOpenCode(permissions) {
|
|
513
|
+
if (!permissions) {
|
|
514
|
+
return void 0;
|
|
515
|
+
}
|
|
516
|
+
const result = {};
|
|
517
|
+
const processRules = (rules, level) => {
|
|
518
|
+
if (!rules) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
for (const rule of rules) {
|
|
522
|
+
const parsed = parsePermissionRule(rule);
|
|
523
|
+
if (!parsed) {
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
const { tool, pattern } = parsed;
|
|
527
|
+
if (!result[tool]) {
|
|
528
|
+
result[tool] = {};
|
|
529
|
+
}
|
|
530
|
+
result[tool][pattern] = level;
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
processRules(permissions.allow, "allow");
|
|
534
|
+
processRules(permissions.ask, "ask");
|
|
535
|
+
processRules(permissions.deny, "deny");
|
|
536
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
537
|
+
}
|
|
538
|
+
function transformEnvVar(value) {
|
|
539
|
+
return value.replace(/\$\{([^}:]+)(:-[^}]*)?\}/g, "{env:$1}");
|
|
540
|
+
}
|
|
541
|
+
function transformEnvVars(env) {
|
|
542
|
+
const result = {};
|
|
543
|
+
for (const [key, value] of Object.entries(env)) {
|
|
544
|
+
result[key] = transformEnvVar(value);
|
|
545
|
+
}
|
|
546
|
+
return result;
|
|
547
|
+
}
|
|
548
|
+
function parsePermissionRule(rule) {
|
|
549
|
+
const match = rule.match(/^(\w+)\(([^)]+)\)$/);
|
|
550
|
+
if (!match) {
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
const tool = match[1];
|
|
554
|
+
const pattern = match[2];
|
|
555
|
+
if (!tool || !pattern) {
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
const normalizedTool = tool.toLowerCase();
|
|
559
|
+
let normalizedPattern = pattern;
|
|
560
|
+
if (normalizedPattern.includes(":*")) {
|
|
561
|
+
normalizedPattern = normalizedPattern.replace(/:(\*)/g, " $1");
|
|
562
|
+
}
|
|
563
|
+
return {
|
|
564
|
+
tool: normalizedTool,
|
|
565
|
+
pattern: normalizedPattern
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/plugins/opencode/index.ts
|
|
570
|
+
var opencodePlugin = {
|
|
571
|
+
id: "opencode",
|
|
572
|
+
name: "OpenCode",
|
|
573
|
+
async detect(_rootDir) {
|
|
574
|
+
return false;
|
|
575
|
+
},
|
|
576
|
+
async import(_rootDir) {
|
|
577
|
+
return null;
|
|
578
|
+
},
|
|
579
|
+
async export(state, rootDir) {
|
|
580
|
+
const files = [];
|
|
581
|
+
const outputDir = TOOL_OUTPUT_DIRS.opencode;
|
|
582
|
+
if (state.agents) {
|
|
583
|
+
files.push({
|
|
584
|
+
path: `${outputDir}/AGENTS.md`,
|
|
585
|
+
type: "symlink",
|
|
586
|
+
target: `../${UNIFIED_DIR}/AGENTS.md`
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
if (state.rules.length > 0) {
|
|
590
|
+
files.push({
|
|
591
|
+
path: `${outputDir}/rules`,
|
|
592
|
+
type: "symlink",
|
|
593
|
+
target: `../${UNIFIED_DIR}/rules`
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
for (const skill of state.skills) {
|
|
597
|
+
files.push({
|
|
598
|
+
path: `${outputDir}/skills/${skill.path}`,
|
|
599
|
+
type: "symlink",
|
|
600
|
+
target: `../../${UNIFIED_DIR}/skills/${skill.path}`
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
const baseConfig = {
|
|
604
|
+
$schema: "https://opencode.ai/config.json"
|
|
605
|
+
};
|
|
606
|
+
if (state.rules.length > 0) {
|
|
607
|
+
baseConfig["instructions"] = [`${outputDir}/rules/*.md`];
|
|
608
|
+
}
|
|
609
|
+
const mcp = transformMcpToOpenCode(state.settings?.mcpServers);
|
|
610
|
+
if (mcp) {
|
|
611
|
+
baseConfig["mcp"] = mcp;
|
|
612
|
+
}
|
|
613
|
+
const permission = transformPermissionsToOpenCode(
|
|
614
|
+
state.settings?.permissions
|
|
615
|
+
);
|
|
616
|
+
if (permission) {
|
|
617
|
+
baseConfig["permission"] = permission;
|
|
618
|
+
}
|
|
619
|
+
let finalConfig = baseConfig;
|
|
620
|
+
if (state.settings?.overrides?.opencode) {
|
|
621
|
+
finalConfig = deepMergeConfigs(
|
|
622
|
+
baseConfig,
|
|
623
|
+
state.settings.overrides.opencode
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
files.push({
|
|
627
|
+
path: "opencode.json",
|
|
628
|
+
type: "json",
|
|
629
|
+
content: finalConfig
|
|
630
|
+
});
|
|
631
|
+
const overrideFiles = await scanOverrideDirectory(rootDir, "opencode");
|
|
632
|
+
for (const overrideFile of overrideFiles) {
|
|
633
|
+
const targetPath = path.join(
|
|
634
|
+
rootDir,
|
|
635
|
+
outputDir,
|
|
636
|
+
overrideFile.relativePath
|
|
637
|
+
);
|
|
638
|
+
if (await fileExists2(targetPath)) {
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
files.push({
|
|
642
|
+
path: `${outputDir}/${overrideFile.relativePath}`,
|
|
643
|
+
type: "symlink",
|
|
644
|
+
target: `../${UNIFIED_DIR}/${OVERRIDE_DIRS.opencode}/${overrideFile.relativePath}`
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
return files;
|
|
648
|
+
},
|
|
649
|
+
validate(state) {
|
|
650
|
+
const warnings = [];
|
|
651
|
+
if (!state.agents) {
|
|
652
|
+
warnings.push({
|
|
653
|
+
path: ["AGENTS.md"],
|
|
654
|
+
message: "No AGENTS.md found - .opencode/AGENTS.md will not be created"
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
return { valid: true, errors: [], warnings, skipped: [] };
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
// src/plugins/registry.ts
|
|
662
|
+
var PluginRegistry = class {
|
|
663
|
+
plugins = /* @__PURE__ */ new Map();
|
|
664
|
+
register(plugin) {
|
|
665
|
+
this.plugins.set(plugin.id, plugin);
|
|
666
|
+
}
|
|
667
|
+
get(id) {
|
|
668
|
+
return this.plugins.get(id);
|
|
669
|
+
}
|
|
670
|
+
getAll() {
|
|
671
|
+
return Array.from(this.plugins.values());
|
|
672
|
+
}
|
|
673
|
+
getIds() {
|
|
674
|
+
return Array.from(this.plugins.keys());
|
|
675
|
+
}
|
|
676
|
+
has(id) {
|
|
677
|
+
return this.plugins.has(id);
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
var pluginRegistry = new PluginRegistry();
|
|
681
|
+
|
|
682
|
+
// src/plugins/index.ts
|
|
683
|
+
pluginRegistry.register(claudeCodePlugin);
|
|
684
|
+
pluginRegistry.register(opencodePlugin);
|
|
685
|
+
function computeHash(content) {
|
|
686
|
+
return crypto.createHash("sha256").update(content, "utf-8").digest("hex");
|
|
687
|
+
}
|
|
688
|
+
async function readExistingFile(filePath) {
|
|
689
|
+
try {
|
|
690
|
+
return await fs3.readFile(filePath, "utf-8");
|
|
691
|
+
} catch {
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
async function getSymlinkTarget(filePath) {
|
|
696
|
+
try {
|
|
697
|
+
const stats = await fs3.lstat(filePath);
|
|
698
|
+
if (stats.isSymbolicLink()) {
|
|
699
|
+
return await fs3.readlink(filePath);
|
|
700
|
+
}
|
|
701
|
+
return null;
|
|
702
|
+
} catch {
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
async function ensureDir(dirPath) {
|
|
707
|
+
await fs3.mkdir(dirPath, { recursive: true });
|
|
708
|
+
}
|
|
709
|
+
async function removeIfExists(filePath) {
|
|
710
|
+
try {
|
|
711
|
+
const stats = await fs3.lstat(filePath);
|
|
712
|
+
if (stats.isDirectory() && !stats.isSymbolicLink()) {
|
|
713
|
+
await fs3.rm(filePath, { recursive: true, force: true });
|
|
714
|
+
} else {
|
|
715
|
+
await fs3.unlink(filePath);
|
|
716
|
+
}
|
|
717
|
+
} catch (error) {
|
|
718
|
+
if (error.code !== "ENOENT") {
|
|
719
|
+
throw error;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
async function writeSingleFile(file, rootDir, dryRun) {
|
|
724
|
+
const fullPath = path.join(rootDir, file.path);
|
|
725
|
+
const dirPath = path.dirname(fullPath);
|
|
726
|
+
if (file.type === "symlink") {
|
|
727
|
+
const target = file.target;
|
|
728
|
+
const existingTarget = await getSymlinkTarget(fullPath);
|
|
729
|
+
if (existingTarget === target) {
|
|
730
|
+
return {
|
|
731
|
+
path: file.path,
|
|
732
|
+
action: "unchanged"
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
if (!dryRun) {
|
|
736
|
+
await ensureDir(dirPath);
|
|
737
|
+
await removeIfExists(fullPath);
|
|
738
|
+
await fs3.symlink(target, fullPath);
|
|
739
|
+
}
|
|
740
|
+
return {
|
|
741
|
+
path: file.path,
|
|
742
|
+
action: existingTarget ? "update" : "create"
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
const content = file.type === "json" ? JSON.stringify(file.content, null, 2) + "\n" : String(file.content);
|
|
746
|
+
const newHash = computeHash(content);
|
|
747
|
+
const existingContent = await readExistingFile(fullPath);
|
|
748
|
+
const oldHash = existingContent ? computeHash(existingContent) : void 0;
|
|
749
|
+
if (oldHash === newHash) {
|
|
750
|
+
return {
|
|
751
|
+
path: file.path,
|
|
752
|
+
action: "unchanged",
|
|
753
|
+
oldHash,
|
|
754
|
+
newHash
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
if (!dryRun) {
|
|
758
|
+
await ensureDir(dirPath);
|
|
759
|
+
await fs3.writeFile(fullPath, content, "utf-8");
|
|
760
|
+
}
|
|
761
|
+
return {
|
|
762
|
+
path: file.path,
|
|
763
|
+
action: existingContent ? "update" : "create",
|
|
764
|
+
oldHash,
|
|
765
|
+
newHash
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
async function writeFiles(files, options) {
|
|
769
|
+
const { rootDir, dryRun = false } = options;
|
|
770
|
+
const results = [];
|
|
771
|
+
for (const file of files) {
|
|
772
|
+
try {
|
|
773
|
+
const result = await writeSingleFile(file, rootDir, dryRun);
|
|
774
|
+
results.push(result);
|
|
775
|
+
} catch (error) {
|
|
776
|
+
throw new WriteError(
|
|
777
|
+
`Failed to write file: ${file.path}`,
|
|
778
|
+
file.path,
|
|
779
|
+
error
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return results;
|
|
784
|
+
}
|
|
785
|
+
async function updateGitignore(rootDir, paths) {
|
|
786
|
+
const gitignorePath = path.join(rootDir, ".gitignore");
|
|
787
|
+
let content = "";
|
|
788
|
+
try {
|
|
789
|
+
content = await fs3.readFile(gitignorePath, "utf-8");
|
|
790
|
+
} catch {
|
|
791
|
+
}
|
|
792
|
+
const marker = "# lnai-generated";
|
|
793
|
+
const endMarker = "# end lnai-generated";
|
|
794
|
+
const markerRegex = new RegExp(`${marker}[\\s\\S]*?${endMarker}\\n?`, "g");
|
|
795
|
+
content = content.replace(markerRegex, "");
|
|
796
|
+
content = content.trimEnd();
|
|
797
|
+
const newSection = ["", marker, ...paths.map((p) => p), endMarker, ""].join(
|
|
798
|
+
"\n"
|
|
799
|
+
);
|
|
800
|
+
content = content + newSection;
|
|
801
|
+
await fs3.writeFile(gitignorePath, content, "utf-8");
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// src/pipeline/index.ts
|
|
805
|
+
function getToolsToSync(config, requestedTools) {
|
|
806
|
+
if (requestedTools && requestedTools.length > 0) {
|
|
807
|
+
return requestedTools.filter((tool) => pluginRegistry.has(tool));
|
|
808
|
+
}
|
|
809
|
+
const enabledTools = [];
|
|
810
|
+
if (config.tools) {
|
|
811
|
+
for (const [toolId, toolConfig] of Object.entries(config.tools)) {
|
|
812
|
+
if (toolConfig?.enabled && pluginRegistry.has(toolId)) {
|
|
813
|
+
enabledTools.push(toolId);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (enabledTools.length === 0) {
|
|
818
|
+
return pluginRegistry.getIds();
|
|
819
|
+
}
|
|
820
|
+
return enabledTools;
|
|
821
|
+
}
|
|
822
|
+
async function runSyncPipeline(options) {
|
|
823
|
+
const { rootDir, dryRun = false, tools: requestedTools } = options;
|
|
824
|
+
const state = await parseUnifiedConfig(rootDir);
|
|
825
|
+
const unifiedValidation = validateUnifiedState(state);
|
|
826
|
+
if (!unifiedValidation.valid) {
|
|
827
|
+
return [
|
|
828
|
+
{
|
|
829
|
+
tool: "claudeCode",
|
|
830
|
+
changes: [],
|
|
831
|
+
validation: unifiedValidation
|
|
832
|
+
}
|
|
833
|
+
];
|
|
834
|
+
}
|
|
835
|
+
const toolsToSync = getToolsToSync(state.config, requestedTools);
|
|
836
|
+
if (toolsToSync.length === 0) {
|
|
837
|
+
return [];
|
|
838
|
+
}
|
|
839
|
+
const results = [];
|
|
840
|
+
const pathsToIgnore = [];
|
|
841
|
+
for (const toolId of toolsToSync) {
|
|
842
|
+
const plugin = pluginRegistry.get(toolId);
|
|
843
|
+
if (!plugin) {
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
const validation = plugin.validate(state);
|
|
847
|
+
const outputFiles = await plugin.export(state, rootDir);
|
|
848
|
+
const changes = await writeFiles(outputFiles, { rootDir, dryRun });
|
|
849
|
+
results.push({
|
|
850
|
+
tool: toolId,
|
|
851
|
+
changes,
|
|
852
|
+
validation
|
|
853
|
+
});
|
|
854
|
+
const toolConfig = state.config.tools?.[toolId];
|
|
855
|
+
if (!toolConfig?.versionControl) {
|
|
856
|
+
pathsToIgnore.push(...outputFiles.map((f) => f.path));
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (pathsToIgnore.length > 0 && !dryRun) {
|
|
860
|
+
await updateGitignore(rootDir, pathsToIgnore);
|
|
861
|
+
}
|
|
862
|
+
return results;
|
|
863
|
+
}
|
|
864
|
+
async function hasUnifiedConfig(rootDir) {
|
|
865
|
+
const aiDir = path.join(rootDir, UNIFIED_DIR);
|
|
866
|
+
try {
|
|
867
|
+
const stats = await fs3.stat(aiDir);
|
|
868
|
+
return stats.isDirectory();
|
|
869
|
+
} catch {
|
|
870
|
+
return false;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
function generateDefaultConfig(tools) {
|
|
874
|
+
const enabledTools = tools ?? TOOL_IDS;
|
|
875
|
+
const toolsConfig = {};
|
|
876
|
+
for (const toolId of TOOL_IDS) {
|
|
877
|
+
toolsConfig[toolId] = {
|
|
878
|
+
enabled: enabledTools.includes(toolId),
|
|
879
|
+
versionControl: false
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
return {
|
|
883
|
+
tools: toolsConfig
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
async function initUnifiedConfig(options) {
|
|
887
|
+
const { rootDir, tools, minimal = false } = options;
|
|
888
|
+
const aiDir = path.join(rootDir, UNIFIED_DIR);
|
|
889
|
+
const created = [];
|
|
890
|
+
await fs3.mkdir(aiDir, { recursive: true });
|
|
891
|
+
created.push(UNIFIED_DIR);
|
|
892
|
+
const config = generateDefaultConfig(tools);
|
|
893
|
+
const configPath = path.join(aiDir, CONFIG_FILES.config);
|
|
894
|
+
await fs3.writeFile(
|
|
895
|
+
configPath,
|
|
896
|
+
JSON.stringify(config, null, 2) + "\n",
|
|
897
|
+
"utf-8"
|
|
898
|
+
);
|
|
899
|
+
created.push(path.join(UNIFIED_DIR, CONFIG_FILES.config));
|
|
900
|
+
if (!minimal) {
|
|
901
|
+
for (const dir of [CONFIG_DIRS.rules, CONFIG_DIRS.skills]) {
|
|
902
|
+
const dirPath = path.join(aiDir, dir);
|
|
903
|
+
await fs3.mkdir(dirPath, { recursive: true });
|
|
904
|
+
created.push(path.join(UNIFIED_DIR, dir));
|
|
905
|
+
const gitkeepPath = path.join(dirPath, ".gitkeep");
|
|
906
|
+
await fs3.writeFile(gitkeepPath, "", "utf-8");
|
|
907
|
+
created.push(path.join(UNIFIED_DIR, dir, ".gitkeep"));
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
return { created };
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
export { CONFIG_DIRS, CONFIG_FILES, FileNotFoundError, LnaiError, ParseError, PluginError, TOOL_IDS, TOOL_OUTPUT_DIRS, UNIFIED_DIR, ValidationError, WriteError, claudeCodePlugin, computeHash, configSchema, generateDefaultConfig, hasUnifiedConfig, initUnifiedConfig, mcpServerSchema, opencodePlugin, parseFrontmatter, parseUnifiedConfig, permissionsSchema, pluginRegistry, ruleFrontmatterSchema, runSyncPipeline, settingsSchema, skillFrontmatterSchema, toolConfigSchema, toolIdSchema, updateGitignore, validateConfig, validateSettings, validateUnifiedState, writeFiles };
|
|
914
|
+
//# sourceMappingURL=index.js.map
|
|
915
|
+
//# sourceMappingURL=index.js.map
|