@lnai/core 0.6.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 +67 -3
- package/dist/index.js +231 -64
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -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<{
|
|
@@ -202,6 +206,25 @@ interface SyncResult {
|
|
|
202
206
|
changes: ChangeResult[];
|
|
203
207
|
validation: ValidationResult;
|
|
204
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
|
+
}
|
|
205
228
|
|
|
206
229
|
declare function parseFrontmatter(content: string): {
|
|
207
230
|
frontmatter: Record<string, unknown>;
|
|
@@ -298,8 +321,8 @@ interface SyncOptions {
|
|
|
298
321
|
tools?: ToolId[];
|
|
299
322
|
/** Preview changes without writing files */
|
|
300
323
|
dryRun?: boolean;
|
|
301
|
-
/**
|
|
302
|
-
|
|
324
|
+
/** Skip cleanup of orphaned files */
|
|
325
|
+
skipCleanup?: boolean;
|
|
303
326
|
}
|
|
304
327
|
/**
|
|
305
328
|
* Run the sync pipeline to export .ai/ config to native tool formats.
|
|
@@ -320,9 +343,50 @@ declare function writeFiles(files: OutputFile[], options: WriterOptions): Promis
|
|
|
320
343
|
/**
|
|
321
344
|
* Update .gitignore with paths that should not be version controlled.
|
|
322
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).
|
|
323
347
|
*/
|
|
324
348
|
declare function updateGitignore(rootDir: string, paths: string[]): Promise<void>;
|
|
325
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
|
+
|
|
326
390
|
interface InitOptions {
|
|
327
391
|
rootDir: string;
|
|
328
392
|
tools?: ToolId[];
|
|
@@ -337,4 +401,4 @@ declare function initUnifiedConfig(options: InitOptions): Promise<InitResult>;
|
|
|
337
401
|
declare function hasUnifiedConfig(rootDir: string): Promise<boolean>;
|
|
338
402
|
declare function generateDefaultConfig(tools?: ToolId[], versionControl?: Record<ToolId, boolean>): Config;
|
|
339
403
|
|
|
340
|
-
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, codexPlugin, 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';
|
|
@@ -67,10 +67,10 @@ var ParseError = class extends LnaiError {
|
|
|
67
67
|
var ValidationError = class extends LnaiError {
|
|
68
68
|
path;
|
|
69
69
|
value;
|
|
70
|
-
constructor(message,
|
|
70
|
+
constructor(message, path8, value) {
|
|
71
71
|
super(message, "VALIDATION_ERROR");
|
|
72
72
|
this.name = "ValidationError";
|
|
73
|
-
this.path =
|
|
73
|
+
this.path = path8;
|
|
74
74
|
this.value = value;
|
|
75
75
|
}
|
|
76
76
|
};
|
|
@@ -104,6 +104,17 @@ var PluginError = class extends LnaiError {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
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
|
+
};
|
|
107
118
|
var mcpServerSchema = z.object({
|
|
108
119
|
command: z.string().optional(),
|
|
109
120
|
args: z.array(z.string()).optional(),
|
|
@@ -204,7 +215,7 @@ async function parseUnifiedConfig(rootDir) {
|
|
|
204
215
|
}
|
|
205
216
|
async function fileExists(filePath) {
|
|
206
217
|
try {
|
|
207
|
-
await
|
|
218
|
+
await fs4.access(filePath);
|
|
208
219
|
return true;
|
|
209
220
|
} catch {
|
|
210
221
|
return false;
|
|
@@ -212,7 +223,7 @@ async function fileExists(filePath) {
|
|
|
212
223
|
}
|
|
213
224
|
async function readJsonFile(filePath) {
|
|
214
225
|
try {
|
|
215
|
-
const content = await
|
|
226
|
+
const content = await fs4.readFile(filePath, "utf-8");
|
|
216
227
|
return JSON.parse(content);
|
|
217
228
|
} catch (error) {
|
|
218
229
|
if (error.code === "ENOENT") {
|
|
@@ -227,7 +238,7 @@ async function readJsonFile(filePath) {
|
|
|
227
238
|
}
|
|
228
239
|
async function readMarkdownFile(filePath) {
|
|
229
240
|
try {
|
|
230
|
-
return await
|
|
241
|
+
return await fs4.readFile(filePath, "utf-8");
|
|
231
242
|
} catch (error) {
|
|
232
243
|
if (error.code === "ENOENT") {
|
|
233
244
|
throw new FileNotFoundError(`File not found: ${filePath}`, filePath);
|
|
@@ -244,7 +255,7 @@ async function readMarkdownDirectory(dirPath) {
|
|
|
244
255
|
if (!await fileExists(dirPath)) {
|
|
245
256
|
return files;
|
|
246
257
|
}
|
|
247
|
-
const entries = await
|
|
258
|
+
const entries = await fs4.readdir(dirPath, { withFileTypes: true });
|
|
248
259
|
for (const entry of entries) {
|
|
249
260
|
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
250
261
|
const filePath = path.join(dirPath, entry.name);
|
|
@@ -264,7 +275,7 @@ async function readSkillsDirectory(dirPath) {
|
|
|
264
275
|
if (!await fileExists(dirPath)) {
|
|
265
276
|
return skills;
|
|
266
277
|
}
|
|
267
|
-
const entries = await
|
|
278
|
+
const entries = await fs4.readdir(dirPath, { withFileTypes: true });
|
|
268
279
|
for (const entry of entries) {
|
|
269
280
|
if (entry.isDirectory()) {
|
|
270
281
|
const skillFile = path.join(dirPath, entry.name, "SKILL.md");
|
|
@@ -405,7 +416,7 @@ function validateToolIds(tools) {
|
|
|
405
416
|
async function scanOverrideDirectory(rootDir, toolId) {
|
|
406
417
|
const overrideDir = path.join(rootDir, UNIFIED_DIR, OVERRIDE_DIRS[toolId]);
|
|
407
418
|
try {
|
|
408
|
-
await
|
|
419
|
+
await fs4.access(overrideDir);
|
|
409
420
|
} catch {
|
|
410
421
|
return [];
|
|
411
422
|
}
|
|
@@ -414,7 +425,7 @@ async function scanOverrideDirectory(rootDir, toolId) {
|
|
|
414
425
|
return files;
|
|
415
426
|
}
|
|
416
427
|
async function scanDir(baseDir, currentDir, files) {
|
|
417
|
-
const entries = await
|
|
428
|
+
const entries = await fs4.readdir(currentDir, { withFileTypes: true });
|
|
418
429
|
for (const entry of entries) {
|
|
419
430
|
const absolutePath = path.join(currentDir, entry.name);
|
|
420
431
|
if (entry.isDirectory()) {
|
|
@@ -510,8 +521,8 @@ function getDirFromGlob(glob) {
|
|
|
510
521
|
const cleanPath = glob.replace(/(\*\*|\*|\{.*,.*\}).*$/, "");
|
|
511
522
|
const dir = cleanPath.replace(/\/$/, "");
|
|
512
523
|
if (dir === glob) {
|
|
513
|
-
const
|
|
514
|
-
return
|
|
524
|
+
const dirname5 = path.dirname(dir);
|
|
525
|
+
return dirname5 === "." && !dir.includes("/") ? "." : dirname5;
|
|
515
526
|
}
|
|
516
527
|
if (!dir) {
|
|
517
528
|
return ".";
|
|
@@ -684,6 +695,32 @@ function formatTomlInlineTable(values) {
|
|
|
684
695
|
return `{ ${entries.join(", ")} }`;
|
|
685
696
|
}
|
|
686
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
|
+
}
|
|
723
|
+
|
|
687
724
|
// src/utils/transforms.ts
|
|
688
725
|
var ENV_VAR_PATTERN = /\$\{([^}:]+)(:-[^}]*)?\}/g;
|
|
689
726
|
function transformEnvVar(value, format) {
|
|
@@ -854,24 +891,12 @@ var copilotPlugin = {
|
|
|
854
891
|
reason: "GitHub Copilot does not support declarative permissions"
|
|
855
892
|
});
|
|
856
893
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
warnings.push({
|
|
864
|
-
path: ["settings", "mcpServers", name],
|
|
865
|
-
message: `MCP server "${name}" is type "${server.type}" but has no url - it will be skipped`
|
|
866
|
-
});
|
|
867
|
-
} else if (!isRemote && !hasCommand) {
|
|
868
|
-
warnings.push({
|
|
869
|
-
path: ["settings", "mcpServers", name],
|
|
870
|
-
message: `MCP server "${name}" has no command or type - it will be skipped`
|
|
871
|
-
});
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
}
|
|
894
|
+
warnings.push(
|
|
895
|
+
...validateMcpServers(state.settings?.mcpServers, [
|
|
896
|
+
"settings",
|
|
897
|
+
"mcpServers"
|
|
898
|
+
])
|
|
899
|
+
);
|
|
875
900
|
return { valid: true, errors: [], warnings, skipped };
|
|
876
901
|
}
|
|
877
902
|
};
|
|
@@ -1066,19 +1091,12 @@ var cursorPlugin = {
|
|
|
1066
1091
|
message: 'Cursor does not support "ask" permission level - these rules will be treated as "allow"'
|
|
1067
1092
|
});
|
|
1068
1093
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
warnings.push({
|
|
1076
|
-
path: ["settings", "mcpServers", name],
|
|
1077
|
-
message: `MCP server "${name}" has no command or type - it will be skipped`
|
|
1078
|
-
});
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1094
|
+
warnings.push(
|
|
1095
|
+
...validateMcpServers(state.settings?.mcpServers, [
|
|
1096
|
+
"settings",
|
|
1097
|
+
"mcpServers"
|
|
1098
|
+
])
|
|
1099
|
+
);
|
|
1082
1100
|
return { valid: true, errors: [], warnings, skipped: [] };
|
|
1083
1101
|
}
|
|
1084
1102
|
};
|
|
@@ -1324,6 +1342,12 @@ var opencodePlugin = {
|
|
|
1324
1342
|
message: "No AGENTS.md found - root AGENTS.md will not be created"
|
|
1325
1343
|
});
|
|
1326
1344
|
}
|
|
1345
|
+
warnings.push(
|
|
1346
|
+
...validateMcpServers(state.settings?.mcpServers, [
|
|
1347
|
+
"settings",
|
|
1348
|
+
"mcpServers"
|
|
1349
|
+
])
|
|
1350
|
+
);
|
|
1327
1351
|
return { valid: true, errors: [], warnings, skipped: [] };
|
|
1328
1352
|
}
|
|
1329
1353
|
};
|
|
@@ -1461,21 +1485,65 @@ pluginRegistry.register(codexPlugin);
|
|
|
1461
1485
|
pluginRegistry.register(opencodePlugin);
|
|
1462
1486
|
pluginRegistry.register(windsurfPlugin);
|
|
1463
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
|
+
}
|
|
1464
1532
|
function computeHash(content) {
|
|
1465
1533
|
return crypto.createHash("sha256").update(content, "utf-8").digest("hex");
|
|
1466
1534
|
}
|
|
1467
1535
|
async function readExistingFile(filePath) {
|
|
1468
1536
|
try {
|
|
1469
|
-
return await
|
|
1537
|
+
return await fs4.readFile(filePath, "utf-8");
|
|
1470
1538
|
} catch {
|
|
1471
1539
|
return null;
|
|
1472
1540
|
}
|
|
1473
1541
|
}
|
|
1474
1542
|
async function getSymlinkTarget(filePath) {
|
|
1475
1543
|
try {
|
|
1476
|
-
const stats = await
|
|
1544
|
+
const stats = await fs4.lstat(filePath);
|
|
1477
1545
|
if (stats.isSymbolicLink()) {
|
|
1478
|
-
return await
|
|
1546
|
+
return await fs4.readlink(filePath);
|
|
1479
1547
|
}
|
|
1480
1548
|
return null;
|
|
1481
1549
|
} catch {
|
|
@@ -1483,15 +1551,15 @@ async function getSymlinkTarget(filePath) {
|
|
|
1483
1551
|
}
|
|
1484
1552
|
}
|
|
1485
1553
|
async function ensureDir(dirPath) {
|
|
1486
|
-
await
|
|
1554
|
+
await fs4.mkdir(dirPath, { recursive: true });
|
|
1487
1555
|
}
|
|
1488
1556
|
async function removeIfExists(filePath) {
|
|
1489
1557
|
try {
|
|
1490
|
-
const stats = await
|
|
1558
|
+
const stats = await fs4.lstat(filePath);
|
|
1491
1559
|
if (stats.isDirectory() && !stats.isSymbolicLink()) {
|
|
1492
|
-
await
|
|
1560
|
+
await fs4.rm(filePath, { recursive: true, force: true });
|
|
1493
1561
|
} else {
|
|
1494
|
-
await
|
|
1562
|
+
await fs4.unlink(filePath);
|
|
1495
1563
|
}
|
|
1496
1564
|
} catch (error) {
|
|
1497
1565
|
if (error.code !== "ENOENT") {
|
|
@@ -1503,6 +1571,12 @@ async function writeSingleFile(file, rootDir, dryRun) {
|
|
|
1503
1571
|
const fullPath = path.join(rootDir, file.path);
|
|
1504
1572
|
const dirPath = path.dirname(fullPath);
|
|
1505
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
|
+
}
|
|
1506
1580
|
const target = file.target;
|
|
1507
1581
|
const existingTarget = await getSymlinkTarget(fullPath);
|
|
1508
1582
|
if (existingTarget === target) {
|
|
@@ -1514,7 +1588,7 @@ async function writeSingleFile(file, rootDir, dryRun) {
|
|
|
1514
1588
|
if (!dryRun) {
|
|
1515
1589
|
await ensureDir(dirPath);
|
|
1516
1590
|
await removeIfExists(fullPath);
|
|
1517
|
-
await
|
|
1591
|
+
await fs4.symlink(target, fullPath);
|
|
1518
1592
|
}
|
|
1519
1593
|
return {
|
|
1520
1594
|
path: file.path,
|
|
@@ -1535,7 +1609,11 @@ async function writeSingleFile(file, rootDir, dryRun) {
|
|
|
1535
1609
|
}
|
|
1536
1610
|
if (!dryRun) {
|
|
1537
1611
|
await ensureDir(dirPath);
|
|
1538
|
-
await
|
|
1612
|
+
const existingSymlink = await getSymlinkTarget(fullPath);
|
|
1613
|
+
if (existingSymlink !== null) {
|
|
1614
|
+
await removeIfExists(fullPath);
|
|
1615
|
+
}
|
|
1616
|
+
await fs4.writeFile(fullPath, content, "utf-8");
|
|
1539
1617
|
}
|
|
1540
1618
|
return {
|
|
1541
1619
|
path: file.path,
|
|
@@ -1565,18 +1643,86 @@ async function updateGitignore(rootDir, paths) {
|
|
|
1565
1643
|
const gitignorePath = path.join(rootDir, ".gitignore");
|
|
1566
1644
|
let content = "";
|
|
1567
1645
|
try {
|
|
1568
|
-
content = await
|
|
1646
|
+
content = await fs4.readFile(gitignorePath, "utf-8");
|
|
1569
1647
|
} catch {
|
|
1570
1648
|
}
|
|
1571
1649
|
const marker = "# lnai-generated";
|
|
1572
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("#"));
|
|
1573
1655
|
const markerRegex = new RegExp(`${marker}[\\s\\S]*?${endMarker}\\n?`, "g");
|
|
1574
1656
|
content = content.replace(markerRegex, "");
|
|
1575
1657
|
content = content.trimEnd();
|
|
1576
|
-
const uniquePaths = [
|
|
1658
|
+
const uniquePaths = [.../* @__PURE__ */ new Set([...existingPaths, ...paths])].sort();
|
|
1577
1659
|
const newSection = ["", marker, ...uniquePaths, endMarker, ""].join("\n");
|
|
1578
1660
|
content = content + newSection;
|
|
1579
|
-
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
|
+
};
|
|
1580
1726
|
}
|
|
1581
1727
|
|
|
1582
1728
|
// src/pipeline/index.ts
|
|
@@ -1598,7 +1744,12 @@ function getToolsToSync(config, requestedTools) {
|
|
|
1598
1744
|
return enabledTools;
|
|
1599
1745
|
}
|
|
1600
1746
|
async function runSyncPipeline(options) {
|
|
1601
|
-
const {
|
|
1747
|
+
const {
|
|
1748
|
+
rootDir,
|
|
1749
|
+
dryRun = false,
|
|
1750
|
+
skipCleanup = false,
|
|
1751
|
+
tools: requestedTools
|
|
1752
|
+
} = options;
|
|
1602
1753
|
if (requestedTools && requestedTools.length > 0) {
|
|
1603
1754
|
const toolValidation = validateToolIds(requestedTools);
|
|
1604
1755
|
if (!toolValidation.valid) {
|
|
@@ -1624,6 +1775,7 @@ async function runSyncPipeline(options) {
|
|
|
1624
1775
|
if (toolsToSync.length === 0) {
|
|
1625
1776
|
return [];
|
|
1626
1777
|
}
|
|
1778
|
+
let manifest = await readManifest(rootDir) ?? createEmptyManifest();
|
|
1627
1779
|
const results = [];
|
|
1628
1780
|
const pathsToIgnore = [];
|
|
1629
1781
|
for (const toolId of toolsToSync) {
|
|
@@ -1633,17 +1785,32 @@ async function runSyncPipeline(options) {
|
|
|
1633
1785
|
}
|
|
1634
1786
|
const validation = plugin.validate(state);
|
|
1635
1787
|
const outputFiles = await plugin.export(state, rootDir);
|
|
1636
|
-
|
|
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];
|
|
1637
1798
|
results.push({
|
|
1638
1799
|
tool: toolId,
|
|
1639
1800
|
changes,
|
|
1640
1801
|
validation
|
|
1641
1802
|
});
|
|
1803
|
+
if (!dryRun) {
|
|
1804
|
+
manifest = updateToolManifest(manifest, toolId, outputFiles);
|
|
1805
|
+
}
|
|
1642
1806
|
const toolConfig = state.config.tools?.[toolId];
|
|
1643
1807
|
if (!toolConfig?.versionControl) {
|
|
1644
1808
|
pathsToIgnore.push(...outputFiles.map((f) => f.path));
|
|
1645
1809
|
}
|
|
1646
1810
|
}
|
|
1811
|
+
if (!dryRun) {
|
|
1812
|
+
await writeManifest(rootDir, manifest);
|
|
1813
|
+
}
|
|
1647
1814
|
if (pathsToIgnore.length > 0 && !dryRun) {
|
|
1648
1815
|
await updateGitignore(rootDir, pathsToIgnore);
|
|
1649
1816
|
}
|
|
@@ -1669,12 +1836,12 @@ async function initUnifiedConfig(options) {
|
|
|
1669
1836
|
);
|
|
1670
1837
|
}
|
|
1671
1838
|
if (exists) {
|
|
1672
|
-
await
|
|
1839
|
+
await fs4.rm(aiDir, { recursive: true, force: true });
|
|
1673
1840
|
}
|
|
1674
|
-
await
|
|
1841
|
+
await fs4.mkdir(aiDir, { recursive: true });
|
|
1675
1842
|
created.push(UNIFIED_DIR);
|
|
1676
1843
|
const configPath = path.join(aiDir, CONFIG_FILES.config);
|
|
1677
|
-
await
|
|
1844
|
+
await fs4.writeFile(
|
|
1678
1845
|
configPath,
|
|
1679
1846
|
JSON.stringify(config, null, 2) + "\n",
|
|
1680
1847
|
"utf-8"
|
|
@@ -1683,10 +1850,10 @@ async function initUnifiedConfig(options) {
|
|
|
1683
1850
|
if (!minimal) {
|
|
1684
1851
|
for (const dir of [CONFIG_DIRS.rules, CONFIG_DIRS.skills]) {
|
|
1685
1852
|
const dirPath = path.join(aiDir, dir);
|
|
1686
|
-
await
|
|
1853
|
+
await fs4.mkdir(dirPath, { recursive: true });
|
|
1687
1854
|
created.push(path.join(UNIFIED_DIR, dir));
|
|
1688
1855
|
const gitkeepPath = path.join(dirPath, ".gitkeep");
|
|
1689
|
-
await
|
|
1856
|
+
await fs4.writeFile(gitkeepPath, "", "utf-8");
|
|
1690
1857
|
created.push(path.join(UNIFIED_DIR, dir, ".gitkeep"));
|
|
1691
1858
|
}
|
|
1692
1859
|
}
|
|
@@ -1695,7 +1862,7 @@ async function initUnifiedConfig(options) {
|
|
|
1695
1862
|
async function hasUnifiedConfig(rootDir) {
|
|
1696
1863
|
const aiDir = path.join(rootDir, UNIFIED_DIR);
|
|
1697
1864
|
try {
|
|
1698
|
-
const stats = await
|
|
1865
|
+
const stats = await fs4.stat(aiDir);
|
|
1699
1866
|
return stats.isDirectory();
|
|
1700
1867
|
} catch {
|
|
1701
1868
|
return false;
|
|
@@ -1724,4 +1891,4 @@ function generateDefaultConfig(tools, versionControl) {
|
|
|
1724
1891
|
return { tools: toolsConfig };
|
|
1725
1892
|
}
|
|
1726
1893
|
|
|
1727
|
-
export { CONFIG_DIRS, CONFIG_FILES, FileNotFoundError, LnaiError, ParseError, PluginError, TOOL_IDS, TOOL_OUTPUT_DIRS, UNIFIED_DIR, ValidationError, WriteError, claudeCodePlugin, codexPlugin, 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 };
|