@lnai/core 0.5.0 → 0.6.5
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/dist/index.d.ts +85 -4
- package/dist/index.js +417 -96
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
3
|
declare const UNIFIED_DIR = ".ai";
|
|
4
|
-
declare const TOOL_IDS: readonly ["claudeCode", "opencode", "cursor", "copilot", "windsurf", "gemini"];
|
|
4
|
+
declare const TOOL_IDS: readonly ["claudeCode", "opencode", "cursor", "copilot", "windsurf", "gemini", "codex"];
|
|
5
5
|
type ToolId = (typeof TOOL_IDS)[number];
|
|
6
6
|
declare const CONFIG_FILES: {
|
|
7
7
|
readonly config: "config.json";
|
|
@@ -40,6 +40,10 @@ declare class PluginError extends LnaiError {
|
|
|
40
40
|
readonly pluginId: string;
|
|
41
41
|
constructor(message: string, pluginId: string, cause?: Error);
|
|
42
42
|
}
|
|
43
|
+
declare class InvalidToolError extends LnaiError {
|
|
44
|
+
readonly invalidTools: string[];
|
|
45
|
+
constructor(invalidTools: string[], validTools: string[]);
|
|
46
|
+
}
|
|
43
47
|
|
|
44
48
|
/** MCP Server configuration (Claude format as source of truth) */
|
|
45
49
|
declare const mcpServerSchema: z.ZodObject<{
|
|
@@ -69,6 +73,7 @@ declare const toolIdSchema: z.ZodEnum<{
|
|
|
69
73
|
copilot: "copilot";
|
|
70
74
|
windsurf: "windsurf";
|
|
71
75
|
gemini: "gemini";
|
|
76
|
+
codex: "codex";
|
|
72
77
|
}>;
|
|
73
78
|
/** Settings configuration (Claude format as source of truth) */
|
|
74
79
|
declare const settingsSchema: z.ZodObject<{
|
|
@@ -116,6 +121,10 @@ declare const configSchema: z.ZodObject<{
|
|
|
116
121
|
enabled: z.ZodBoolean;
|
|
117
122
|
versionControl: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
118
123
|
}, z.core.$strip>>;
|
|
124
|
+
codex: z.ZodOptional<z.ZodObject<{
|
|
125
|
+
enabled: z.ZodBoolean;
|
|
126
|
+
versionControl: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
127
|
+
}, z.core.$strip>>;
|
|
119
128
|
}, z.core.$strip>>;
|
|
120
129
|
}, z.core.$strip>;
|
|
121
130
|
/** Skill frontmatter (name and description required) */
|
|
@@ -197,6 +206,25 @@ interface SyncResult {
|
|
|
197
206
|
changes: ChangeResult[];
|
|
198
207
|
validation: ValidationResult;
|
|
199
208
|
}
|
|
209
|
+
/** Entry for a single file in the manifest */
|
|
210
|
+
interface ManifestEntry {
|
|
211
|
+
path: string;
|
|
212
|
+
type: "json" | "text" | "symlink";
|
|
213
|
+
hash?: string;
|
|
214
|
+
target?: string;
|
|
215
|
+
}
|
|
216
|
+
/** Manifest for a single tool's generated files */
|
|
217
|
+
interface ToolManifest {
|
|
218
|
+
version: 1;
|
|
219
|
+
tool: ToolId;
|
|
220
|
+
generatedAt: string;
|
|
221
|
+
files: ManifestEntry[];
|
|
222
|
+
}
|
|
223
|
+
/** Root manifest tracking all LNAI-generated files */
|
|
224
|
+
interface LnaiManifest {
|
|
225
|
+
version: 1;
|
|
226
|
+
tools: Partial<Record<ToolId, ToolManifest>>;
|
|
227
|
+
}
|
|
200
228
|
|
|
201
229
|
declare function parseFrontmatter(content: string): {
|
|
202
230
|
frontmatter: Record<string, unknown>;
|
|
@@ -249,6 +277,18 @@ interface Plugin {
|
|
|
249
277
|
*/
|
|
250
278
|
declare const claudeCodePlugin: Plugin;
|
|
251
279
|
|
|
280
|
+
/**
|
|
281
|
+
* Codex plugin for exporting to .codex/ format
|
|
282
|
+
*
|
|
283
|
+
* Output structure:
|
|
284
|
+
* - AGENTS.md (symlink -> .ai/AGENTS.md) [at project root]
|
|
285
|
+
* - <dir>/AGENTS.md (generated from .ai/rules/*.md, per glob directory)
|
|
286
|
+
* - .codex/skills/<name>/ (symlink -> ../../.ai/skills/<name>)
|
|
287
|
+
* - .codex/config.toml (generated from settings.mcpServers)
|
|
288
|
+
* - .codex/<path> (symlink -> ../.ai/.codex/<path>) for override files
|
|
289
|
+
*/
|
|
290
|
+
declare const codexPlugin: Plugin;
|
|
291
|
+
|
|
252
292
|
/**
|
|
253
293
|
* OpenCode plugin for exporting to opencode.json format
|
|
254
294
|
*
|
|
@@ -281,8 +321,8 @@ interface SyncOptions {
|
|
|
281
321
|
tools?: ToolId[];
|
|
282
322
|
/** Preview changes without writing files */
|
|
283
323
|
dryRun?: boolean;
|
|
284
|
-
/**
|
|
285
|
-
|
|
324
|
+
/** Skip cleanup of orphaned files */
|
|
325
|
+
skipCleanup?: boolean;
|
|
286
326
|
}
|
|
287
327
|
/**
|
|
288
328
|
* Run the sync pipeline to export .ai/ config to native tool formats.
|
|
@@ -303,9 +343,50 @@ declare function writeFiles(files: OutputFile[], options: WriterOptions): Promis
|
|
|
303
343
|
/**
|
|
304
344
|
* Update .gitignore with paths that should not be version controlled.
|
|
305
345
|
* Manages a dedicated "lnai-generated" section to avoid conflicts with user entries.
|
|
346
|
+
* Merges new paths with existing ones to support partial syncs (e.g., syncing one tool).
|
|
306
347
|
*/
|
|
307
348
|
declare function updateGitignore(rootDir: string, paths: string[]): Promise<void>;
|
|
308
349
|
|
|
350
|
+
declare const MANIFEST_FILENAME = ".lnai-manifest.json";
|
|
351
|
+
/**
|
|
352
|
+
* Read the LNAI manifest from the .ai directory.
|
|
353
|
+
* Returns null if the manifest doesn't exist.
|
|
354
|
+
*/
|
|
355
|
+
declare function readManifest(rootDir: string): Promise<LnaiManifest | null>;
|
|
356
|
+
/**
|
|
357
|
+
* Write the LNAI manifest to the .ai directory.
|
|
358
|
+
*/
|
|
359
|
+
declare function writeManifest(rootDir: string, manifest: LnaiManifest): Promise<void>;
|
|
360
|
+
/**
|
|
361
|
+
* Build a tool manifest from output files.
|
|
362
|
+
*/
|
|
363
|
+
declare function buildToolManifest(toolId: ToolId, files: OutputFile[]): ToolManifest;
|
|
364
|
+
/**
|
|
365
|
+
* Update a single tool's manifest entry.
|
|
366
|
+
*/
|
|
367
|
+
declare function updateToolManifest(manifest: LnaiManifest, toolId: ToolId, files: OutputFile[]): LnaiManifest;
|
|
368
|
+
/**
|
|
369
|
+
* Create an empty manifest.
|
|
370
|
+
*/
|
|
371
|
+
declare function createEmptyManifest(): LnaiManifest;
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Compute which files should be deleted based on previous and current manifest entries.
|
|
375
|
+
* Returns paths that exist in previous but not in current.
|
|
376
|
+
*/
|
|
377
|
+
declare function computeFilesToDelete(previousFiles: ManifestEntry[], currentFiles: OutputFile[]): string[];
|
|
378
|
+
/**
|
|
379
|
+
* Delete files that are no longer in the manifest.
|
|
380
|
+
* Returns ChangeResult[] with action: "delete" for each deleted file.
|
|
381
|
+
* When dryRun is true, no files are actually deleted.
|
|
382
|
+
*/
|
|
383
|
+
declare function deleteFiles(paths: string[], rootDir: string, dryRun: boolean): Promise<ChangeResult[]>;
|
|
384
|
+
/**
|
|
385
|
+
* Clean up empty parent directories after file deletion.
|
|
386
|
+
* Stops at the project root directory.
|
|
387
|
+
*/
|
|
388
|
+
declare function cleanupEmptyParentDirs(filePath: string, rootDir: string): Promise<void>;
|
|
389
|
+
|
|
309
390
|
interface InitOptions {
|
|
310
391
|
rootDir: string;
|
|
311
392
|
tools?: ToolId[];
|
|
@@ -320,4 +401,4 @@ declare function initUnifiedConfig(options: InitOptions): Promise<InitResult>;
|
|
|
320
401
|
declare function hasUnifiedConfig(rootDir: string): Promise<boolean>;
|
|
321
402
|
declare function generateDefaultConfig(tools?: ToolId[], versionControl?: Record<ToolId, boolean>): Config;
|
|
322
403
|
|
|
323
|
-
export { CONFIG_DIRS, CONFIG_FILES, type ChangeResult, type Config, FileNotFoundError, type InitOptions, type InitResult, LnaiError, type MarkdownFile, type MarkdownFrontmatter, type McpServer, type OutputFile, ParseError, type PermissionLevel, type Permissions, type Plugin, PluginError, type RuleFrontmatter, type Settings, type SkillFrontmatter, type SkippedFeatureDetail, type SyncOptions, type SyncResult, TOOL_IDS, TOOL_OUTPUT_DIRS, type ToolConfig, type ToolId, UNIFIED_DIR, type UnifiedState, ValidationError, type ValidationErrorDetail, type ValidationResult, type ValidationWarningDetail, WriteError, type WriterOptions, claudeCodePlugin, computeHash, configSchema, generateDefaultConfig, hasUnifiedConfig, initUnifiedConfig, mcpServerSchema, opencodePlugin, parseFrontmatter, parseUnifiedConfig, permissionsSchema, pluginRegistry, ruleFrontmatterSchema, runSyncPipeline, settingsSchema, skillFrontmatterSchema, toolConfigSchema, toolIdSchema, updateGitignore, validateConfig, validateSettings, validateUnifiedState, writeFiles };
|
|
404
|
+
export { CONFIG_DIRS, CONFIG_FILES, type ChangeResult, type Config, FileNotFoundError, type InitOptions, type InitResult, InvalidToolError, LnaiError, type LnaiManifest, MANIFEST_FILENAME, type ManifestEntry, type MarkdownFile, type MarkdownFrontmatter, type McpServer, type OutputFile, ParseError, type PermissionLevel, type Permissions, type Plugin, PluginError, type RuleFrontmatter, type Settings, type SkillFrontmatter, type SkippedFeatureDetail, type SyncOptions, type SyncResult, TOOL_IDS, TOOL_OUTPUT_DIRS, type ToolConfig, type ToolId, type ToolManifest, UNIFIED_DIR, type UnifiedState, ValidationError, type ValidationErrorDetail, type ValidationResult, type ValidationWarningDetail, WriteError, type WriterOptions, buildToolManifest, claudeCodePlugin, cleanupEmptyParentDirs, codexPlugin, computeFilesToDelete, computeHash, configSchema, createEmptyManifest, deleteFiles, generateDefaultConfig, hasUnifiedConfig, initUnifiedConfig, mcpServerSchema, opencodePlugin, parseFrontmatter, parseUnifiedConfig, permissionsSchema, pluginRegistry, readManifest, ruleFrontmatterSchema, runSyncPipeline, settingsSchema, skillFrontmatterSchema, toolConfigSchema, toolIdSchema, updateGitignore, updateToolManifest, validateConfig, validateSettings, validateUnifiedState, writeFiles, writeManifest };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import * as
|
|
2
|
+
import * as fs4 from 'fs/promises';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import matter from 'gray-matter';
|
|
5
5
|
import * as crypto from 'crypto';
|
|
@@ -12,7 +12,8 @@ var TOOL_IDS = [
|
|
|
12
12
|
"cursor",
|
|
13
13
|
"copilot",
|
|
14
14
|
"windsurf",
|
|
15
|
-
"gemini"
|
|
15
|
+
"gemini",
|
|
16
|
+
"codex"
|
|
16
17
|
];
|
|
17
18
|
var CONFIG_FILES = {
|
|
18
19
|
config: "config.json",
|
|
@@ -30,7 +31,8 @@ var TOOL_OUTPUT_DIRS = {
|
|
|
30
31
|
cursor: ".cursor",
|
|
31
32
|
copilot: ".github",
|
|
32
33
|
windsurf: ".windsurf",
|
|
33
|
-
gemini: ".gemini"
|
|
34
|
+
gemini: ".gemini",
|
|
35
|
+
codex: ".codex"
|
|
34
36
|
};
|
|
35
37
|
var OVERRIDE_DIRS = {
|
|
36
38
|
claudeCode: ".claude",
|
|
@@ -38,7 +40,8 @@ var OVERRIDE_DIRS = {
|
|
|
38
40
|
cursor: ".cursor",
|
|
39
41
|
copilot: ".copilot",
|
|
40
42
|
windsurf: ".windsurf",
|
|
41
|
-
gemini: ".gemini"
|
|
43
|
+
gemini: ".gemini",
|
|
44
|
+
codex: ".codex"
|
|
42
45
|
};
|
|
43
46
|
|
|
44
47
|
// src/errors.ts
|
|
@@ -64,10 +67,10 @@ var ParseError = class extends LnaiError {
|
|
|
64
67
|
var ValidationError = class extends LnaiError {
|
|
65
68
|
path;
|
|
66
69
|
value;
|
|
67
|
-
constructor(message,
|
|
70
|
+
constructor(message, path8, value) {
|
|
68
71
|
super(message, "VALIDATION_ERROR");
|
|
69
72
|
this.name = "ValidationError";
|
|
70
|
-
this.path =
|
|
73
|
+
this.path = path8;
|
|
71
74
|
this.value = value;
|
|
72
75
|
}
|
|
73
76
|
};
|
|
@@ -101,6 +104,17 @@ var PluginError = class extends LnaiError {
|
|
|
101
104
|
}
|
|
102
105
|
}
|
|
103
106
|
};
|
|
107
|
+
var InvalidToolError = class extends LnaiError {
|
|
108
|
+
invalidTools;
|
|
109
|
+
constructor(invalidTools, validTools) {
|
|
110
|
+
super(
|
|
111
|
+
`Invalid tool(s): ${invalidTools.join(", ")}. Valid tools: ${validTools.join(", ")}`,
|
|
112
|
+
"INVALID_TOOL"
|
|
113
|
+
);
|
|
114
|
+
this.name = "InvalidToolError";
|
|
115
|
+
this.invalidTools = invalidTools;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
104
118
|
var mcpServerSchema = z.object({
|
|
105
119
|
command: z.string().optional(),
|
|
106
120
|
args: z.array(z.string()).optional(),
|
|
@@ -124,7 +138,8 @@ var toolIdSchema = z.enum([
|
|
|
124
138
|
"cursor",
|
|
125
139
|
"copilot",
|
|
126
140
|
"windsurf",
|
|
127
|
-
"gemini"
|
|
141
|
+
"gemini",
|
|
142
|
+
"codex"
|
|
128
143
|
]);
|
|
129
144
|
var settingsSchema = z.object({
|
|
130
145
|
permissions: permissionsSchema.optional(),
|
|
@@ -137,7 +152,8 @@ var configSchema = z.object({
|
|
|
137
152
|
cursor: toolConfigSchema,
|
|
138
153
|
copilot: toolConfigSchema,
|
|
139
154
|
windsurf: toolConfigSchema,
|
|
140
|
-
gemini: toolConfigSchema
|
|
155
|
+
gemini: toolConfigSchema,
|
|
156
|
+
codex: toolConfigSchema
|
|
141
157
|
}).partial().optional()
|
|
142
158
|
});
|
|
143
159
|
var skillFrontmatterSchema = z.object({
|
|
@@ -199,7 +215,7 @@ async function parseUnifiedConfig(rootDir) {
|
|
|
199
215
|
}
|
|
200
216
|
async function fileExists(filePath) {
|
|
201
217
|
try {
|
|
202
|
-
await
|
|
218
|
+
await fs4.access(filePath);
|
|
203
219
|
return true;
|
|
204
220
|
} catch {
|
|
205
221
|
return false;
|
|
@@ -207,7 +223,7 @@ async function fileExists(filePath) {
|
|
|
207
223
|
}
|
|
208
224
|
async function readJsonFile(filePath) {
|
|
209
225
|
try {
|
|
210
|
-
const content = await
|
|
226
|
+
const content = await fs4.readFile(filePath, "utf-8");
|
|
211
227
|
return JSON.parse(content);
|
|
212
228
|
} catch (error) {
|
|
213
229
|
if (error.code === "ENOENT") {
|
|
@@ -222,7 +238,7 @@ async function readJsonFile(filePath) {
|
|
|
222
238
|
}
|
|
223
239
|
async function readMarkdownFile(filePath) {
|
|
224
240
|
try {
|
|
225
|
-
return await
|
|
241
|
+
return await fs4.readFile(filePath, "utf-8");
|
|
226
242
|
} catch (error) {
|
|
227
243
|
if (error.code === "ENOENT") {
|
|
228
244
|
throw new FileNotFoundError(`File not found: ${filePath}`, filePath);
|
|
@@ -239,7 +255,7 @@ async function readMarkdownDirectory(dirPath) {
|
|
|
239
255
|
if (!await fileExists(dirPath)) {
|
|
240
256
|
return files;
|
|
241
257
|
}
|
|
242
|
-
const entries = await
|
|
258
|
+
const entries = await fs4.readdir(dirPath, { withFileTypes: true });
|
|
243
259
|
for (const entry of entries) {
|
|
244
260
|
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
245
261
|
const filePath = path.join(dirPath, entry.name);
|
|
@@ -259,7 +275,7 @@ async function readSkillsDirectory(dirPath) {
|
|
|
259
275
|
if (!await fileExists(dirPath)) {
|
|
260
276
|
return skills;
|
|
261
277
|
}
|
|
262
|
-
const entries = await
|
|
278
|
+
const entries = await fs4.readdir(dirPath, { withFileTypes: true });
|
|
263
279
|
for (const entry of entries) {
|
|
264
280
|
if (entry.isDirectory()) {
|
|
265
281
|
const skillFile = path.join(dirPath, entry.name, "SKILL.md");
|
|
@@ -400,7 +416,7 @@ function validateToolIds(tools) {
|
|
|
400
416
|
async function scanOverrideDirectory(rootDir, toolId) {
|
|
401
417
|
const overrideDir = path.join(rootDir, UNIFIED_DIR, OVERRIDE_DIRS[toolId]);
|
|
402
418
|
try {
|
|
403
|
-
await
|
|
419
|
+
await fs4.access(overrideDir);
|
|
404
420
|
} catch {
|
|
405
421
|
return [];
|
|
406
422
|
}
|
|
@@ -409,7 +425,7 @@ async function scanOverrideDirectory(rootDir, toolId) {
|
|
|
409
425
|
return files;
|
|
410
426
|
}
|
|
411
427
|
async function scanDir(baseDir, currentDir, files) {
|
|
412
|
-
const entries = await
|
|
428
|
+
const entries = await fs4.readdir(currentDir, { withFileTypes: true });
|
|
413
429
|
for (const entry of entries) {
|
|
414
430
|
const absolutePath = path.join(currentDir, entry.name);
|
|
415
431
|
if (entry.isDirectory()) {
|
|
@@ -501,6 +517,209 @@ var claudeCodePlugin = {
|
|
|
501
517
|
return { valid: true, errors: [], warnings, skipped: [] };
|
|
502
518
|
}
|
|
503
519
|
};
|
|
520
|
+
function getDirFromGlob(glob) {
|
|
521
|
+
const cleanPath = glob.replace(/(\*\*|\*|\{.*,.*\}).*$/, "");
|
|
522
|
+
const dir = cleanPath.replace(/\/$/, "");
|
|
523
|
+
if (dir === glob) {
|
|
524
|
+
const dirname5 = path.dirname(dir);
|
|
525
|
+
return dirname5 === "." && !dir.includes("/") ? "." : dirname5;
|
|
526
|
+
}
|
|
527
|
+
if (!dir) {
|
|
528
|
+
return ".";
|
|
529
|
+
}
|
|
530
|
+
return dir;
|
|
531
|
+
}
|
|
532
|
+
function groupRulesByDirectory(rules) {
|
|
533
|
+
const rulesMap = /* @__PURE__ */ new Map();
|
|
534
|
+
const addedRules = /* @__PURE__ */ new Map();
|
|
535
|
+
for (const rule of rules) {
|
|
536
|
+
for (const pathGlob of rule.frontmatter.paths) {
|
|
537
|
+
const dir = getDirFromGlob(pathGlob);
|
|
538
|
+
if (!rulesMap.has(dir)) {
|
|
539
|
+
rulesMap.set(dir, []);
|
|
540
|
+
addedRules.set(dir, /* @__PURE__ */ new Set());
|
|
541
|
+
}
|
|
542
|
+
if (addedRules.get(dir)?.has(rule.path)) {
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
addedRules.get(dir)?.add(rule.path);
|
|
546
|
+
const content = `## ${rule.path}
|
|
547
|
+
|
|
548
|
+
${rule.content}
|
|
549
|
+
`;
|
|
550
|
+
rulesMap.get(dir)?.push(content);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return rulesMap;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/plugins/codex/index.ts
|
|
557
|
+
var codexPlugin = {
|
|
558
|
+
id: "codex",
|
|
559
|
+
name: "Codex",
|
|
560
|
+
async detect(_rootDir) {
|
|
561
|
+
return false;
|
|
562
|
+
},
|
|
563
|
+
async import(_rootDir) {
|
|
564
|
+
return null;
|
|
565
|
+
},
|
|
566
|
+
async export(state, rootDir) {
|
|
567
|
+
const files = [];
|
|
568
|
+
const outputDir = TOOL_OUTPUT_DIRS.codex;
|
|
569
|
+
if (state.agents) {
|
|
570
|
+
files.push({
|
|
571
|
+
path: "AGENTS.md",
|
|
572
|
+
type: "symlink",
|
|
573
|
+
target: `${UNIFIED_DIR}/AGENTS.md`
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
const rulesMap = groupRulesByDirectory(state.rules);
|
|
577
|
+
for (const [dir, contents] of rulesMap.entries()) {
|
|
578
|
+
if (dir === ".") {
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
const combinedContent = contents.join("\n---\n\n");
|
|
582
|
+
files.push({
|
|
583
|
+
path: `${dir}/AGENTS.md`,
|
|
584
|
+
type: "text",
|
|
585
|
+
content: combinedContent
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
for (const skill of state.skills) {
|
|
589
|
+
files.push({
|
|
590
|
+
path: `${outputDir}/skills/${skill.path}`,
|
|
591
|
+
type: "symlink",
|
|
592
|
+
target: `../../${UNIFIED_DIR}/skills/${skill.path}`
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
const configToml = buildCodexConfigToml(state.settings?.mcpServers);
|
|
596
|
+
if (configToml) {
|
|
597
|
+
files.push({
|
|
598
|
+
path: `${outputDir}/config.toml`,
|
|
599
|
+
type: "text",
|
|
600
|
+
content: configToml
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
return applyFileOverrides(files, rootDir, "codex");
|
|
604
|
+
},
|
|
605
|
+
validate(state) {
|
|
606
|
+
const warnings = [];
|
|
607
|
+
const skipped = [];
|
|
608
|
+
if (!state.agents) {
|
|
609
|
+
warnings.push({
|
|
610
|
+
path: ["AGENTS.md"],
|
|
611
|
+
message: "No AGENTS.md found - root AGENTS.md will not be created"
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
const rulesMap = groupRulesByDirectory(state.rules);
|
|
615
|
+
if (rulesMap.has(".")) {
|
|
616
|
+
warnings.push({
|
|
617
|
+
path: ["rules"],
|
|
618
|
+
message: "Rules with root globs are not exported - Codex only receives subdirectory AGENTS.md files"
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
const mcpServers = state.settings?.mcpServers;
|
|
622
|
+
if (mcpServers) {
|
|
623
|
+
for (const [name, server] of Object.entries(mcpServers)) {
|
|
624
|
+
if (!server.command && !server.url) {
|
|
625
|
+
warnings.push({
|
|
626
|
+
path: ["settings", "mcpServers", name],
|
|
627
|
+
message: `MCP server "${name}" has no command or url - it will be skipped`
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
if (state.settings?.permissions) {
|
|
633
|
+
const hasPermissions = (state.settings.permissions.allow?.length ?? 0) > 0 || (state.settings.permissions.ask?.length ?? 0) > 0 || (state.settings.permissions.deny?.length ?? 0) > 0;
|
|
634
|
+
if (hasPermissions) {
|
|
635
|
+
skipped.push({
|
|
636
|
+
feature: "permissions",
|
|
637
|
+
reason: "Codex rules are not generated from LNAI permissions"
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return { valid: true, errors: [], warnings, skipped };
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
function buildCodexConfigToml(mcpServers) {
|
|
645
|
+
if (!mcpServers || Object.keys(mcpServers).length === 0) {
|
|
646
|
+
return void 0;
|
|
647
|
+
}
|
|
648
|
+
const lines = [];
|
|
649
|
+
for (const [name, server] of Object.entries(mcpServers)) {
|
|
650
|
+
const hasCommand = !!server.command;
|
|
651
|
+
const hasUrl = !!server.url;
|
|
652
|
+
if (!hasCommand && !hasUrl) {
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
lines.push(`[mcp_servers.${formatTomlKey(name)}]`);
|
|
656
|
+
if (server.command) {
|
|
657
|
+
lines.push(`command = ${formatTomlString(server.command)}`);
|
|
658
|
+
if (server.args && server.args.length > 0) {
|
|
659
|
+
lines.push(`args = ${formatTomlArray(server.args)}`);
|
|
660
|
+
}
|
|
661
|
+
if (server.env && Object.keys(server.env).length > 0) {
|
|
662
|
+
lines.push(`env = ${formatTomlInlineTable(server.env)}`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (server.url) {
|
|
666
|
+
lines.push(`url = ${formatTomlString(server.url)}`);
|
|
667
|
+
if (server.headers && Object.keys(server.headers).length > 0) {
|
|
668
|
+
lines.push(`http_headers = ${formatTomlInlineTable(server.headers)}`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
lines.push("");
|
|
672
|
+
}
|
|
673
|
+
if (lines.length === 0) {
|
|
674
|
+
return void 0;
|
|
675
|
+
}
|
|
676
|
+
return `${lines.join("\n").trimEnd()}
|
|
677
|
+
`;
|
|
678
|
+
}
|
|
679
|
+
function formatTomlString(value) {
|
|
680
|
+
return JSON.stringify(value);
|
|
681
|
+
}
|
|
682
|
+
function formatTomlArray(values) {
|
|
683
|
+
return `[${values.map(formatTomlString).join(", ")}]`;
|
|
684
|
+
}
|
|
685
|
+
function formatTomlKey(key) {
|
|
686
|
+
if (/^[A-Za-z0-9_-]+$/.test(key)) {
|
|
687
|
+
return key;
|
|
688
|
+
}
|
|
689
|
+
return JSON.stringify(key);
|
|
690
|
+
}
|
|
691
|
+
function formatTomlInlineTable(values) {
|
|
692
|
+
const entries = Object.entries(values).map(
|
|
693
|
+
([key, value]) => `${formatTomlKey(key)} = ${formatTomlString(value)}`
|
|
694
|
+
);
|
|
695
|
+
return `{ ${entries.join(", ")} }`;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// src/utils/mcp.ts
|
|
699
|
+
function validateMcpServers(mcpServers, pathPrefix) {
|
|
700
|
+
const warnings = [];
|
|
701
|
+
if (!mcpServers) {
|
|
702
|
+
return warnings;
|
|
703
|
+
}
|
|
704
|
+
for (const [name, server] of Object.entries(mcpServers)) {
|
|
705
|
+
const isRemote = server.type === "http" || server.type === "sse";
|
|
706
|
+
const hasCommand = !!server.command;
|
|
707
|
+
const hasUrl = !!server.url;
|
|
708
|
+
if (!isRemote && !hasCommand) {
|
|
709
|
+
warnings.push({
|
|
710
|
+
path: [...pathPrefix, name],
|
|
711
|
+
message: `MCP server "${name}" has no command or type - it will be skipped`
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
if (isRemote && !hasUrl) {
|
|
715
|
+
warnings.push({
|
|
716
|
+
path: [...pathPrefix, name],
|
|
717
|
+
message: `MCP server "${name}" is remote but has no URL - it will be skipped`
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return warnings;
|
|
722
|
+
}
|
|
504
723
|
|
|
505
724
|
// src/utils/transforms.ts
|
|
506
725
|
var ENV_VAR_PATTERN = /\$\{([^}:]+)(:-[^}]*)?\}/g;
|
|
@@ -672,24 +891,12 @@ var copilotPlugin = {
|
|
|
672
891
|
reason: "GitHub Copilot does not support declarative permissions"
|
|
673
892
|
});
|
|
674
893
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
warnings.push({
|
|
682
|
-
path: ["settings", "mcpServers", name],
|
|
683
|
-
message: `MCP server "${name}" is type "${server.type}" but has no url - it will be skipped`
|
|
684
|
-
});
|
|
685
|
-
} else if (!isRemote && !hasCommand) {
|
|
686
|
-
warnings.push({
|
|
687
|
-
path: ["settings", "mcpServers", name],
|
|
688
|
-
message: `MCP server "${name}" has no command or type - it will be skipped`
|
|
689
|
-
});
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
}
|
|
894
|
+
warnings.push(
|
|
895
|
+
...validateMcpServers(state.settings?.mcpServers, [
|
|
896
|
+
"settings",
|
|
897
|
+
"mcpServers"
|
|
898
|
+
])
|
|
899
|
+
);
|
|
693
900
|
return { valid: true, errors: [], warnings, skipped };
|
|
694
901
|
}
|
|
695
902
|
};
|
|
@@ -884,19 +1091,12 @@ var cursorPlugin = {
|
|
|
884
1091
|
message: 'Cursor does not support "ask" permission level - these rules will be treated as "allow"'
|
|
885
1092
|
});
|
|
886
1093
|
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
warnings.push({
|
|
894
|
-
path: ["settings", "mcpServers", name],
|
|
895
|
-
message: `MCP server "${name}" has no command or type - it will be skipped`
|
|
896
|
-
});
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
}
|
|
1094
|
+
warnings.push(
|
|
1095
|
+
...validateMcpServers(state.settings?.mcpServers, [
|
|
1096
|
+
"settings",
|
|
1097
|
+
"mcpServers"
|
|
1098
|
+
])
|
|
1099
|
+
);
|
|
900
1100
|
return { valid: true, errors: [], warnings, skipped: [] };
|
|
901
1101
|
}
|
|
902
1102
|
};
|
|
@@ -907,18 +1107,6 @@ function buildCliContent(permissions) {
|
|
|
907
1107
|
}
|
|
908
1108
|
return { permissions: permissionsResult.permissions };
|
|
909
1109
|
}
|
|
910
|
-
function getDirFromGlob(glob) {
|
|
911
|
-
const cleanPath = glob.replace(/(\*\*|\*|\{.*,.*\}).*$/, "");
|
|
912
|
-
const dir = cleanPath.replace(/\/$/, "");
|
|
913
|
-
if (dir === glob) {
|
|
914
|
-
const dirname4 = path.dirname(dir);
|
|
915
|
-
return dirname4 === "." && !dir.includes("/") ? "." : dirname4;
|
|
916
|
-
}
|
|
917
|
-
if (!dir) {
|
|
918
|
-
return ".";
|
|
919
|
-
}
|
|
920
|
-
return dir;
|
|
921
|
-
}
|
|
922
1110
|
|
|
923
1111
|
// src/plugins/gemini/transforms.ts
|
|
924
1112
|
function transformMcpToGemini(mcpServers) {
|
|
@@ -941,23 +1129,6 @@ function transformMcpToGemini(mcpServers) {
|
|
|
941
1129
|
}
|
|
942
1130
|
return Object.keys(geminiMcp).length > 0 ? geminiMcp : void 0;
|
|
943
1131
|
}
|
|
944
|
-
function groupRulesByDirectory(rules) {
|
|
945
|
-
const rulesMap = /* @__PURE__ */ new Map();
|
|
946
|
-
for (const rule of rules) {
|
|
947
|
-
for (const pathGlob of rule.frontmatter.paths) {
|
|
948
|
-
const dir = getDirFromGlob(pathGlob);
|
|
949
|
-
if (!rulesMap.has(dir)) {
|
|
950
|
-
rulesMap.set(dir, []);
|
|
951
|
-
}
|
|
952
|
-
const content = `## ${rule.path}
|
|
953
|
-
|
|
954
|
-
${rule.content}
|
|
955
|
-
`;
|
|
956
|
-
rulesMap.get(dir)?.push(content);
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
return rulesMap;
|
|
960
|
-
}
|
|
961
1132
|
|
|
962
1133
|
// src/plugins/gemini/index.ts
|
|
963
1134
|
var geminiPlugin = {
|
|
@@ -1171,6 +1342,12 @@ var opencodePlugin = {
|
|
|
1171
1342
|
message: "No AGENTS.md found - root AGENTS.md will not be created"
|
|
1172
1343
|
});
|
|
1173
1344
|
}
|
|
1345
|
+
warnings.push(
|
|
1346
|
+
...validateMcpServers(state.settings?.mcpServers, [
|
|
1347
|
+
"settings",
|
|
1348
|
+
"mcpServers"
|
|
1349
|
+
])
|
|
1350
|
+
);
|
|
1174
1351
|
return { valid: true, errors: [], warnings, skipped: [] };
|
|
1175
1352
|
}
|
|
1176
1353
|
};
|
|
@@ -1304,24 +1481,69 @@ var windsurfPlugin = {
|
|
|
1304
1481
|
pluginRegistry.register(claudeCodePlugin);
|
|
1305
1482
|
pluginRegistry.register(copilotPlugin);
|
|
1306
1483
|
pluginRegistry.register(cursorPlugin);
|
|
1484
|
+
pluginRegistry.register(codexPlugin);
|
|
1307
1485
|
pluginRegistry.register(opencodePlugin);
|
|
1308
1486
|
pluginRegistry.register(windsurfPlugin);
|
|
1309
1487
|
pluginRegistry.register(geminiPlugin);
|
|
1488
|
+
function computeFilesToDelete(previousFiles, currentFiles) {
|
|
1489
|
+
const currentPaths = new Set(currentFiles.map((f) => f.path));
|
|
1490
|
+
return previousFiles.map((f) => f.path).filter((p) => !currentPaths.has(p));
|
|
1491
|
+
}
|
|
1492
|
+
async function deleteFiles(paths, rootDir, dryRun) {
|
|
1493
|
+
const results = [];
|
|
1494
|
+
for (const relativePath of paths) {
|
|
1495
|
+
const fullPath = path.join(rootDir, relativePath);
|
|
1496
|
+
try {
|
|
1497
|
+
await fs4.lstat(fullPath);
|
|
1498
|
+
} catch (error) {
|
|
1499
|
+
if (error.code === "ENOENT") {
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
throw error;
|
|
1503
|
+
}
|
|
1504
|
+
if (!dryRun) {
|
|
1505
|
+
await fs4.unlink(fullPath);
|
|
1506
|
+
await cleanupEmptyParentDirs(fullPath, rootDir);
|
|
1507
|
+
}
|
|
1508
|
+
results.push({
|
|
1509
|
+
path: relativePath,
|
|
1510
|
+
action: "delete"
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
return results;
|
|
1514
|
+
}
|
|
1515
|
+
async function cleanupEmptyParentDirs(filePath, rootDir) {
|
|
1516
|
+
let dir = path.dirname(filePath);
|
|
1517
|
+
const normalizedRoot = path.normalize(rootDir);
|
|
1518
|
+
while (dir !== normalizedRoot && dir.startsWith(normalizedRoot + path.sep)) {
|
|
1519
|
+
try {
|
|
1520
|
+
const entries = await fs4.readdir(dir);
|
|
1521
|
+
if (entries.length === 0) {
|
|
1522
|
+
await fs4.rmdir(dir);
|
|
1523
|
+
dir = path.dirname(dir);
|
|
1524
|
+
} else {
|
|
1525
|
+
break;
|
|
1526
|
+
}
|
|
1527
|
+
} catch {
|
|
1528
|
+
break;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1310
1532
|
function computeHash(content) {
|
|
1311
1533
|
return crypto.createHash("sha256").update(content, "utf-8").digest("hex");
|
|
1312
1534
|
}
|
|
1313
1535
|
async function readExistingFile(filePath) {
|
|
1314
1536
|
try {
|
|
1315
|
-
return await
|
|
1537
|
+
return await fs4.readFile(filePath, "utf-8");
|
|
1316
1538
|
} catch {
|
|
1317
1539
|
return null;
|
|
1318
1540
|
}
|
|
1319
1541
|
}
|
|
1320
1542
|
async function getSymlinkTarget(filePath) {
|
|
1321
1543
|
try {
|
|
1322
|
-
const stats = await
|
|
1544
|
+
const stats = await fs4.lstat(filePath);
|
|
1323
1545
|
if (stats.isSymbolicLink()) {
|
|
1324
|
-
return await
|
|
1546
|
+
return await fs4.readlink(filePath);
|
|
1325
1547
|
}
|
|
1326
1548
|
return null;
|
|
1327
1549
|
} catch {
|
|
@@ -1329,15 +1551,15 @@ async function getSymlinkTarget(filePath) {
|
|
|
1329
1551
|
}
|
|
1330
1552
|
}
|
|
1331
1553
|
async function ensureDir(dirPath) {
|
|
1332
|
-
await
|
|
1554
|
+
await fs4.mkdir(dirPath, { recursive: true });
|
|
1333
1555
|
}
|
|
1334
1556
|
async function removeIfExists(filePath) {
|
|
1335
1557
|
try {
|
|
1336
|
-
const stats = await
|
|
1558
|
+
const stats = await fs4.lstat(filePath);
|
|
1337
1559
|
if (stats.isDirectory() && !stats.isSymbolicLink()) {
|
|
1338
|
-
await
|
|
1560
|
+
await fs4.rm(filePath, { recursive: true, force: true });
|
|
1339
1561
|
} else {
|
|
1340
|
-
await
|
|
1562
|
+
await fs4.unlink(filePath);
|
|
1341
1563
|
}
|
|
1342
1564
|
} catch (error) {
|
|
1343
1565
|
if (error.code !== "ENOENT") {
|
|
@@ -1349,6 +1571,12 @@ async function writeSingleFile(file, rootDir, dryRun) {
|
|
|
1349
1571
|
const fullPath = path.join(rootDir, file.path);
|
|
1350
1572
|
const dirPath = path.dirname(fullPath);
|
|
1351
1573
|
if (file.type === "symlink") {
|
|
1574
|
+
if (!file.target) {
|
|
1575
|
+
throw new WriteError(
|
|
1576
|
+
`Symlink file missing target: ${file.path}`,
|
|
1577
|
+
file.path
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1352
1580
|
const target = file.target;
|
|
1353
1581
|
const existingTarget = await getSymlinkTarget(fullPath);
|
|
1354
1582
|
if (existingTarget === target) {
|
|
@@ -1360,7 +1588,7 @@ async function writeSingleFile(file, rootDir, dryRun) {
|
|
|
1360
1588
|
if (!dryRun) {
|
|
1361
1589
|
await ensureDir(dirPath);
|
|
1362
1590
|
await removeIfExists(fullPath);
|
|
1363
|
-
await
|
|
1591
|
+
await fs4.symlink(target, fullPath);
|
|
1364
1592
|
}
|
|
1365
1593
|
return {
|
|
1366
1594
|
path: file.path,
|
|
@@ -1381,7 +1609,11 @@ async function writeSingleFile(file, rootDir, dryRun) {
|
|
|
1381
1609
|
}
|
|
1382
1610
|
if (!dryRun) {
|
|
1383
1611
|
await ensureDir(dirPath);
|
|
1384
|
-
await
|
|
1612
|
+
const existingSymlink = await getSymlinkTarget(fullPath);
|
|
1613
|
+
if (existingSymlink !== null) {
|
|
1614
|
+
await removeIfExists(fullPath);
|
|
1615
|
+
}
|
|
1616
|
+
await fs4.writeFile(fullPath, content, "utf-8");
|
|
1385
1617
|
}
|
|
1386
1618
|
return {
|
|
1387
1619
|
path: file.path,
|
|
@@ -1411,18 +1643,86 @@ async function updateGitignore(rootDir, paths) {
|
|
|
1411
1643
|
const gitignorePath = path.join(rootDir, ".gitignore");
|
|
1412
1644
|
let content = "";
|
|
1413
1645
|
try {
|
|
1414
|
-
content = await
|
|
1646
|
+
content = await fs4.readFile(gitignorePath, "utf-8");
|
|
1415
1647
|
} catch {
|
|
1416
1648
|
}
|
|
1417
1649
|
const marker = "# lnai-generated";
|
|
1418
1650
|
const endMarker = "# end lnai-generated";
|
|
1651
|
+
const extractRegex = new RegExp(`${marker}\\n([\\s\\S]*?)${endMarker}`);
|
|
1652
|
+
const match = extractRegex.exec(content);
|
|
1653
|
+
const existingSection = match?.[1] ?? "";
|
|
1654
|
+
const existingPaths = existingSection.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
1419
1655
|
const markerRegex = new RegExp(`${marker}[\\s\\S]*?${endMarker}\\n?`, "g");
|
|
1420
1656
|
content = content.replace(markerRegex, "");
|
|
1421
1657
|
content = content.trimEnd();
|
|
1422
|
-
const uniquePaths = [
|
|
1658
|
+
const uniquePaths = [.../* @__PURE__ */ new Set([...existingPaths, ...paths])].sort();
|
|
1423
1659
|
const newSection = ["", marker, ...uniquePaths, endMarker, ""].join("\n");
|
|
1424
1660
|
content = content + newSection;
|
|
1425
|
-
await
|
|
1661
|
+
await fs4.writeFile(gitignorePath, content, "utf-8");
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// src/manifest/index.ts
|
|
1665
|
+
var MANIFEST_FILENAME = ".lnai-manifest.json";
|
|
1666
|
+
async function readManifest(rootDir) {
|
|
1667
|
+
const manifestPath = path.join(rootDir, UNIFIED_DIR, MANIFEST_FILENAME);
|
|
1668
|
+
try {
|
|
1669
|
+
const content = await fs4.readFile(manifestPath, "utf-8");
|
|
1670
|
+
const manifest = JSON.parse(content);
|
|
1671
|
+
if (manifest.version !== 1) {
|
|
1672
|
+
console.warn(
|
|
1673
|
+
`[lnai] Unknown manifest version ${manifest.version}, skipping cleanup`
|
|
1674
|
+
);
|
|
1675
|
+
return null;
|
|
1676
|
+
}
|
|
1677
|
+
return manifest;
|
|
1678
|
+
} catch (error) {
|
|
1679
|
+
if (error.code === "ENOENT") {
|
|
1680
|
+
return null;
|
|
1681
|
+
}
|
|
1682
|
+
console.warn(`[lnai] Failed to read manifest: ${error.message}`);
|
|
1683
|
+
return null;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
async function writeManifest(rootDir, manifest) {
|
|
1687
|
+
const manifestPath = path.join(rootDir, UNIFIED_DIR, MANIFEST_FILENAME);
|
|
1688
|
+
const content = JSON.stringify(manifest, null, 2) + "\n";
|
|
1689
|
+
await fs4.writeFile(manifestPath, content, "utf-8");
|
|
1690
|
+
}
|
|
1691
|
+
function buildToolManifest(toolId, files) {
|
|
1692
|
+
const entries = files.map((file) => {
|
|
1693
|
+
const entry = {
|
|
1694
|
+
path: file.path,
|
|
1695
|
+
type: file.type
|
|
1696
|
+
};
|
|
1697
|
+
if (file.type === "symlink") {
|
|
1698
|
+
entry.target = file.target;
|
|
1699
|
+
} else {
|
|
1700
|
+
const content = file.type === "json" ? JSON.stringify(file.content, null, 2) + "\n" : String(file.content);
|
|
1701
|
+
entry.hash = computeHash(content);
|
|
1702
|
+
}
|
|
1703
|
+
return entry;
|
|
1704
|
+
});
|
|
1705
|
+
return {
|
|
1706
|
+
version: 1,
|
|
1707
|
+
tool: toolId,
|
|
1708
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1709
|
+
files: entries
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
function updateToolManifest(manifest, toolId, files) {
|
|
1713
|
+
return {
|
|
1714
|
+
...manifest,
|
|
1715
|
+
tools: {
|
|
1716
|
+
...manifest.tools,
|
|
1717
|
+
[toolId]: buildToolManifest(toolId, files)
|
|
1718
|
+
}
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
function createEmptyManifest() {
|
|
1722
|
+
return {
|
|
1723
|
+
version: 1,
|
|
1724
|
+
tools: {}
|
|
1725
|
+
};
|
|
1426
1726
|
}
|
|
1427
1727
|
|
|
1428
1728
|
// src/pipeline/index.ts
|
|
@@ -1444,7 +1744,12 @@ function getToolsToSync(config, requestedTools) {
|
|
|
1444
1744
|
return enabledTools;
|
|
1445
1745
|
}
|
|
1446
1746
|
async function runSyncPipeline(options) {
|
|
1447
|
-
const {
|
|
1747
|
+
const {
|
|
1748
|
+
rootDir,
|
|
1749
|
+
dryRun = false,
|
|
1750
|
+
skipCleanup = false,
|
|
1751
|
+
tools: requestedTools
|
|
1752
|
+
} = options;
|
|
1448
1753
|
if (requestedTools && requestedTools.length > 0) {
|
|
1449
1754
|
const toolValidation = validateToolIds(requestedTools);
|
|
1450
1755
|
if (!toolValidation.valid) {
|
|
@@ -1470,6 +1775,7 @@ async function runSyncPipeline(options) {
|
|
|
1470
1775
|
if (toolsToSync.length === 0) {
|
|
1471
1776
|
return [];
|
|
1472
1777
|
}
|
|
1778
|
+
let manifest = await readManifest(rootDir) ?? createEmptyManifest();
|
|
1473
1779
|
const results = [];
|
|
1474
1780
|
const pathsToIgnore = [];
|
|
1475
1781
|
for (const toolId of toolsToSync) {
|
|
@@ -1479,17 +1785,32 @@ async function runSyncPipeline(options) {
|
|
|
1479
1785
|
}
|
|
1480
1786
|
const validation = plugin.validate(state);
|
|
1481
1787
|
const outputFiles = await plugin.export(state, rootDir);
|
|
1482
|
-
|
|
1788
|
+
let deleteChanges = [];
|
|
1789
|
+
if (!skipCleanup && manifest.tools[toolId]) {
|
|
1790
|
+
const toDelete = computeFilesToDelete(
|
|
1791
|
+
manifest.tools[toolId].files,
|
|
1792
|
+
outputFiles
|
|
1793
|
+
);
|
|
1794
|
+
deleteChanges = await deleteFiles(toDelete, rootDir, dryRun);
|
|
1795
|
+
}
|
|
1796
|
+
const writeChanges = await writeFiles(outputFiles, { rootDir, dryRun });
|
|
1797
|
+
const changes = [...deleteChanges, ...writeChanges];
|
|
1483
1798
|
results.push({
|
|
1484
1799
|
tool: toolId,
|
|
1485
1800
|
changes,
|
|
1486
1801
|
validation
|
|
1487
1802
|
});
|
|
1803
|
+
if (!dryRun) {
|
|
1804
|
+
manifest = updateToolManifest(manifest, toolId, outputFiles);
|
|
1805
|
+
}
|
|
1488
1806
|
const toolConfig = state.config.tools?.[toolId];
|
|
1489
1807
|
if (!toolConfig?.versionControl) {
|
|
1490
1808
|
pathsToIgnore.push(...outputFiles.map((f) => f.path));
|
|
1491
1809
|
}
|
|
1492
1810
|
}
|
|
1811
|
+
if (!dryRun) {
|
|
1812
|
+
await writeManifest(rootDir, manifest);
|
|
1813
|
+
}
|
|
1493
1814
|
if (pathsToIgnore.length > 0 && !dryRun) {
|
|
1494
1815
|
await updateGitignore(rootDir, pathsToIgnore);
|
|
1495
1816
|
}
|
|
@@ -1515,12 +1836,12 @@ async function initUnifiedConfig(options) {
|
|
|
1515
1836
|
);
|
|
1516
1837
|
}
|
|
1517
1838
|
if (exists) {
|
|
1518
|
-
await
|
|
1839
|
+
await fs4.rm(aiDir, { recursive: true, force: true });
|
|
1519
1840
|
}
|
|
1520
|
-
await
|
|
1841
|
+
await fs4.mkdir(aiDir, { recursive: true });
|
|
1521
1842
|
created.push(UNIFIED_DIR);
|
|
1522
1843
|
const configPath = path.join(aiDir, CONFIG_FILES.config);
|
|
1523
|
-
await
|
|
1844
|
+
await fs4.writeFile(
|
|
1524
1845
|
configPath,
|
|
1525
1846
|
JSON.stringify(config, null, 2) + "\n",
|
|
1526
1847
|
"utf-8"
|
|
@@ -1529,10 +1850,10 @@ async function initUnifiedConfig(options) {
|
|
|
1529
1850
|
if (!minimal) {
|
|
1530
1851
|
for (const dir of [CONFIG_DIRS.rules, CONFIG_DIRS.skills]) {
|
|
1531
1852
|
const dirPath = path.join(aiDir, dir);
|
|
1532
|
-
await
|
|
1853
|
+
await fs4.mkdir(dirPath, { recursive: true });
|
|
1533
1854
|
created.push(path.join(UNIFIED_DIR, dir));
|
|
1534
1855
|
const gitkeepPath = path.join(dirPath, ".gitkeep");
|
|
1535
|
-
await
|
|
1856
|
+
await fs4.writeFile(gitkeepPath, "", "utf-8");
|
|
1536
1857
|
created.push(path.join(UNIFIED_DIR, dir, ".gitkeep"));
|
|
1537
1858
|
}
|
|
1538
1859
|
}
|
|
@@ -1541,7 +1862,7 @@ async function initUnifiedConfig(options) {
|
|
|
1541
1862
|
async function hasUnifiedConfig(rootDir) {
|
|
1542
1863
|
const aiDir = path.join(rootDir, UNIFIED_DIR);
|
|
1543
1864
|
try {
|
|
1544
|
-
const stats = await
|
|
1865
|
+
const stats = await fs4.stat(aiDir);
|
|
1545
1866
|
return stats.isDirectory();
|
|
1546
1867
|
} catch {
|
|
1547
1868
|
return false;
|
|
@@ -1570,4 +1891,4 @@ function generateDefaultConfig(tools, versionControl) {
|
|
|
1570
1891
|
return { tools: toolsConfig };
|
|
1571
1892
|
}
|
|
1572
1893
|
|
|
1573
|
-
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 };
|
|
1894
|
+
export { CONFIG_DIRS, CONFIG_FILES, FileNotFoundError, InvalidToolError, LnaiError, MANIFEST_FILENAME, ParseError, PluginError, TOOL_IDS, TOOL_OUTPUT_DIRS, UNIFIED_DIR, ValidationError, WriteError, buildToolManifest, claudeCodePlugin, cleanupEmptyParentDirs, codexPlugin, computeFilesToDelete, computeHash, configSchema, createEmptyManifest, deleteFiles, generateDefaultConfig, hasUnifiedConfig, initUnifiedConfig, mcpServerSchema, opencodePlugin, parseFrontmatter, parseUnifiedConfig, permissionsSchema, pluginRegistry, readManifest, ruleFrontmatterSchema, runSyncPipeline, settingsSchema, skillFrontmatterSchema, toolConfigSchema, toolIdSchema, updateGitignore, updateToolManifest, validateConfig, validateSettings, validateUnifiedState, writeFiles, writeManifest };
|