@lnai/core 0.6.5 → 0.6.7

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 CHANGED
@@ -283,7 +283,7 @@ declare const claudeCodePlugin: Plugin;
283
283
  * Output structure:
284
284
  * - AGENTS.md (symlink -> .ai/AGENTS.md) [at project root]
285
285
  * - <dir>/AGENTS.md (generated from .ai/rules/*.md, per glob directory)
286
- * - .codex/skills/<name>/ (symlink -> ../../.ai/skills/<name>)
286
+ * - .agents/skills/<name>/ (symlink -> ../../.ai/skills/<name>)
287
287
  * - .codex/config.toml (generated from settings.mcpServers)
288
288
  * - .codex/<path> (symlink -> ../.ai/.codex/<path>) for override files
289
289
  */
@@ -295,7 +295,7 @@ declare const codexPlugin: Plugin;
295
295
  * Output structure:
296
296
  * - AGENTS.md (symlink -> .ai/AGENTS.md) [at project root]
297
297
  * - .opencode/rules/ (symlink -> ../.ai/rules)
298
- * - .opencode/skills/<name>/ (symlink -> ../../.ai/skills/<name>)
298
+ * - .agents/skills/<name>/ (symlink -> ../../.ai/skills/<name>)
299
299
  * - opencode.json (generated config merged with .ai/.opencode/opencode.json)
300
300
  * - .opencode/<path> (symlink -> ../.ai/.opencode/<path>) for other override files
301
301
  */
@@ -343,7 +343,7 @@ declare function writeFiles(files: OutputFile[], options: WriterOptions): Promis
343
343
  /**
344
344
  * Update .gitignore with paths that should not be version controlled.
345
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).
346
+ * Replaces the managed section on each run so stale paths are removed.
347
347
  */
348
348
  declare function updateGitignore(rootDir: string, paths: string[]): Promise<void>;
349
349
 
package/dist/index.js CHANGED
@@ -25,6 +25,7 @@ var CONFIG_DIRS = {
25
25
  skills: "skills",
26
26
  subagents: "subagents"
27
27
  };
28
+ var CROSS_TOOL_SKILLS_DIR = ".agents";
28
29
  var TOOL_OUTPUT_DIRS = {
29
30
  claudeCode: ".claude",
30
31
  opencode: ".opencode",
@@ -413,6 +414,39 @@ function validateToolIds(tools) {
413
414
  }
414
415
  return { valid: true, errors: [], warnings: [], skipped: [] };
415
416
  }
417
+
418
+ // src/utils/agents.ts
419
+ function createRootAgentsMdSymlink(state) {
420
+ if (!state.agents) {
421
+ return null;
422
+ }
423
+ return {
424
+ path: "AGENTS.md",
425
+ type: "symlink",
426
+ target: `${UNIFIED_DIR}/AGENTS.md`
427
+ };
428
+ }
429
+ function createSkillSymlinks(state, outputDir) {
430
+ const depth = outputDir.split("/").length + 1;
431
+ const prefix = "../".repeat(depth);
432
+ return state.skills.map((skill) => ({
433
+ path: `${outputDir}/skills/${skill.path}`,
434
+ type: "symlink",
435
+ target: `${prefix}${UNIFIED_DIR}/skills/${skill.path}`
436
+ }));
437
+ }
438
+ function createNoAgentsMdWarning(outputDescription) {
439
+ return {
440
+ path: ["AGENTS.md"],
441
+ message: `No AGENTS.md found - ${outputDescription} will not be created`
442
+ };
443
+ }
444
+ function hasPermissionsConfigured(permissions) {
445
+ if (!permissions) {
446
+ return false;
447
+ }
448
+ return (permissions.allow?.length ?? 0) > 0 || (permissions.ask?.length ?? 0) > 0 || (permissions.deny?.length ?? 0) > 0;
449
+ }
416
450
  async function scanOverrideDirectory(rootDir, toolId) {
417
451
  const overrideDir = path.join(rootDir, UNIFIED_DIR, OVERRIDE_DIRS[toolId]);
418
452
  try {
@@ -457,6 +491,7 @@ async function applyFileOverrides(files, rootDir, toolId) {
457
491
  }
458
492
 
459
493
  // src/plugins/claude-code/index.ts
494
+ var OUTPUT_DIR = TOOL_OUTPUT_DIRS.claudeCode;
460
495
  var claudeCodePlugin = {
461
496
  id: "claudeCode",
462
497
  name: "Claude Code",
@@ -468,28 +503,21 @@ var claudeCodePlugin = {
468
503
  },
469
504
  async export(state, rootDir) {
470
505
  const files = [];
471
- const outputDir = TOOL_OUTPUT_DIRS.claudeCode;
472
506
  if (state.agents) {
473
507
  files.push({
474
- path: `${outputDir}/CLAUDE.md`,
508
+ path: `${OUTPUT_DIR}/CLAUDE.md`,
475
509
  type: "symlink",
476
510
  target: `../${UNIFIED_DIR}/AGENTS.md`
477
511
  });
478
512
  }
479
513
  if (state.rules.length > 0) {
480
514
  files.push({
481
- path: `${outputDir}/rules`,
515
+ path: `${OUTPUT_DIR}/rules`,
482
516
  type: "symlink",
483
517
  target: `../${UNIFIED_DIR}/rules`
484
518
  });
485
519
  }
486
- for (const skill of state.skills) {
487
- files.push({
488
- path: `${outputDir}/skills/${skill.path}`,
489
- type: "symlink",
490
- target: `../../${UNIFIED_DIR}/skills/${skill.path}`
491
- });
492
- }
520
+ files.push(...createSkillSymlinks(state, OUTPUT_DIR));
493
521
  const settings = {};
494
522
  if (state.settings?.permissions) {
495
523
  settings["permissions"] = state.settings.permissions;
@@ -499,7 +527,7 @@ var claudeCodePlugin = {
499
527
  }
500
528
  if (Object.keys(settings).length > 0) {
501
529
  files.push({
502
- path: `${outputDir}/settings.json`,
530
+ path: `${OUTPUT_DIR}/settings.json`,
503
531
  type: "json",
504
532
  content: settings
505
533
  });
@@ -509,10 +537,7 @@ var claudeCodePlugin = {
509
537
  validate(state) {
510
538
  const warnings = [];
511
539
  if (!state.agents) {
512
- warnings.push({
513
- path: ["AGENTS.md"],
514
- message: "No AGENTS.md found - .claude/CLAUDE.md will not be created"
515
- });
540
+ warnings.push(createNoAgentsMdWarning(".claude/CLAUDE.md"));
516
541
  }
517
542
  return { valid: true, errors: [], warnings, skipped: [] };
518
543
  }
@@ -554,6 +579,7 @@ ${rule.content}
554
579
  }
555
580
 
556
581
  // src/plugins/codex/index.ts
582
+ var OUTPUT_DIR2 = TOOL_OUTPUT_DIRS.codex;
557
583
  var codexPlugin = {
558
584
  id: "codex",
559
585
  name: "Codex",
@@ -565,13 +591,9 @@ var codexPlugin = {
565
591
  },
566
592
  async export(state, rootDir) {
567
593
  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
- });
594
+ const agentsSymlink = createRootAgentsMdSymlink(state);
595
+ if (agentsSymlink) {
596
+ files.push(agentsSymlink);
575
597
  }
576
598
  const rulesMap = groupRulesByDirectory(state.rules);
577
599
  for (const [dir, contents] of rulesMap.entries()) {
@@ -585,17 +607,11 @@ var codexPlugin = {
585
607
  content: combinedContent
586
608
  });
587
609
  }
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
- }
610
+ files.push(...createSkillSymlinks(state, CROSS_TOOL_SKILLS_DIR));
595
611
  const configToml = buildCodexConfigToml(state.settings?.mcpServers);
596
612
  if (configToml) {
597
613
  files.push({
598
- path: `${outputDir}/config.toml`,
614
+ path: `${OUTPUT_DIR2}/config.toml`,
599
615
  type: "text",
600
616
  content: configToml
601
617
  });
@@ -606,10 +622,7 @@ var codexPlugin = {
606
622
  const warnings = [];
607
623
  const skipped = [];
608
624
  if (!state.agents) {
609
- warnings.push({
610
- path: ["AGENTS.md"],
611
- message: "No AGENTS.md found - root AGENTS.md will not be created"
612
- });
625
+ warnings.push(createNoAgentsMdWarning("root AGENTS.md"));
613
626
  }
614
627
  const rulesMap = groupRulesByDirectory(state.rules);
615
628
  if (rulesMap.has(".")) {
@@ -629,14 +642,11 @@ var codexPlugin = {
629
642
  }
630
643
  }
631
644
  }
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
- }
645
+ if (hasPermissionsConfigured(state.settings?.permissions)) {
646
+ skipped.push({
647
+ feature: "permissions",
648
+ reason: "Codex rules are not generated from LNAI permissions"
649
+ });
640
650
  }
641
651
  return { valid: true, errors: [], warnings, skipped };
642
652
  }
@@ -826,6 +836,7 @@ function transformMcpToCopilot(servers) {
826
836
  }
827
837
 
828
838
  // src/plugins/copilot/index.ts
839
+ var OUTPUT_DIR3 = TOOL_OUTPUT_DIRS.copilot;
829
840
  var copilotPlugin = {
830
841
  id: "copilot",
831
842
  name: "GitHub Copilot",
@@ -837,12 +848,9 @@ var copilotPlugin = {
837
848
  },
838
849
  async export(state, rootDir) {
839
850
  const files = [];
840
- if (state.agents) {
841
- files.push({
842
- path: ".github/copilot-instructions.md",
843
- type: "symlink",
844
- target: `../${UNIFIED_DIR}/AGENTS.md`
845
- });
851
+ const agentsSymlink = createRootAgentsMdSymlink(state);
852
+ if (agentsSymlink) {
853
+ files.push(agentsSymlink);
846
854
  }
847
855
  for (const rule of state.rules) {
848
856
  const transformed = transformRuleToCopilot(rule);
@@ -852,18 +860,12 @@ var copilotPlugin = {
852
860
  );
853
861
  const outputFilename = rule.path.replace(/\.md$/, ".instructions.md");
854
862
  files.push({
855
- path: `.github/instructions/${outputFilename}`,
863
+ path: `${OUTPUT_DIR3}/instructions/${outputFilename}`,
856
864
  type: "text",
857
865
  content: ruleContent
858
866
  });
859
867
  }
860
- for (const skill of state.skills) {
861
- files.push({
862
- path: `.github/skills/${skill.path}`,
863
- type: "symlink",
864
- target: `../../${UNIFIED_DIR}/skills/${skill.path}`
865
- });
866
- }
868
+ files.push(...createSkillSymlinks(state, OUTPUT_DIR3));
867
869
  const mcpConfig = transformMcpToCopilot(state.settings?.mcpServers);
868
870
  if (mcpConfig) {
869
871
  files.push({
@@ -878,14 +880,9 @@ var copilotPlugin = {
878
880
  const warnings = [];
879
881
  const skipped = [];
880
882
  if (!state.agents) {
881
- warnings.push({
882
- path: ["AGENTS.md"],
883
- message: "No AGENTS.md found - .github/copilot-instructions.md will not be created"
884
- });
883
+ warnings.push(createNoAgentsMdWarning("root AGENTS.md"));
885
884
  }
886
- const permissions = state.settings?.permissions;
887
- const hasPermissions = permissions && (permissions.allow && permissions.allow.length > 0 || permissions.ask && permissions.ask.length > 0 || permissions.deny && permissions.deny.length > 0);
888
- if (hasPermissions) {
885
+ if (hasPermissionsConfigured(state.settings?.permissions)) {
889
886
  skipped.push({
890
887
  feature: "permissions",
891
888
  reason: "GitHub Copilot does not support declarative permissions"
@@ -1017,6 +1014,7 @@ function transformPermissionRule(rule) {
1017
1014
  }
1018
1015
 
1019
1016
  // src/plugins/cursor/index.ts
1017
+ var OUTPUT_DIR4 = TOOL_OUTPUT_DIRS.cursor;
1020
1018
  var cursorPlugin = {
1021
1019
  id: "cursor",
1022
1020
  name: "Cursor",
@@ -1028,13 +1026,9 @@ var cursorPlugin = {
1028
1026
  },
1029
1027
  async export(state, rootDir) {
1030
1028
  const files = [];
1031
- const outputDir = TOOL_OUTPUT_DIRS.cursor;
1032
- if (state.agents) {
1033
- files.push({
1034
- path: "AGENTS.md",
1035
- type: "symlink",
1036
- target: `${UNIFIED_DIR}/AGENTS.md`
1037
- });
1029
+ const agentsSymlink = createRootAgentsMdSymlink(state);
1030
+ if (agentsSymlink) {
1031
+ files.push(agentsSymlink);
1038
1032
  }
1039
1033
  for (const rule of state.rules) {
1040
1034
  const transformed = transformRuleToCursor(rule);
@@ -1044,22 +1038,16 @@ var cursorPlugin = {
1044
1038
  );
1045
1039
  const outputFilename = rule.path.replace(/\.md$/, ".mdc");
1046
1040
  files.push({
1047
- path: `${outputDir}/rules/${outputFilename}`,
1041
+ path: `${OUTPUT_DIR4}/rules/${outputFilename}`,
1048
1042
  type: "text",
1049
1043
  content: ruleContent
1050
1044
  });
1051
1045
  }
1052
- for (const skill of state.skills) {
1053
- files.push({
1054
- path: `${outputDir}/skills/${skill.path}`,
1055
- type: "symlink",
1056
- target: `../../${UNIFIED_DIR}/skills/${skill.path}`
1057
- });
1058
- }
1046
+ files.push(...createSkillSymlinks(state, OUTPUT_DIR4));
1059
1047
  const mcpServers = transformMcpToCursor(state.settings?.mcpServers);
1060
1048
  if (mcpServers) {
1061
1049
  files.push({
1062
- path: `${outputDir}/mcp.json`,
1050
+ path: `${OUTPUT_DIR4}/mcp.json`,
1063
1051
  type: "json",
1064
1052
  content: { mcpServers }
1065
1053
  });
@@ -1067,7 +1055,7 @@ var cursorPlugin = {
1067
1055
  const cliContent = buildCliContent(state.settings?.permissions);
1068
1056
  if (cliContent) {
1069
1057
  files.push({
1070
- path: `${outputDir}/cli.json`,
1058
+ path: `${OUTPUT_DIR4}/cli.json`,
1071
1059
  type: "json",
1072
1060
  content: cliContent
1073
1061
  });
@@ -1077,10 +1065,7 @@ var cursorPlugin = {
1077
1065
  validate(state) {
1078
1066
  const warnings = [];
1079
1067
  if (!state.agents) {
1080
- warnings.push({
1081
- path: ["AGENTS.md"],
1082
- message: "No AGENTS.md found - root AGENTS.md will not be created"
1083
- });
1068
+ warnings.push(createNoAgentsMdWarning("root AGENTS.md"));
1084
1069
  }
1085
1070
  const permissionsResult = transformPermissionsToCursor(
1086
1071
  state.settings?.permissions
@@ -1131,6 +1116,7 @@ function transformMcpToGemini(mcpServers) {
1131
1116
  }
1132
1117
 
1133
1118
  // src/plugins/gemini/index.ts
1119
+ var OUTPUT_DIR5 = TOOL_OUTPUT_DIRS.gemini;
1134
1120
  var geminiPlugin = {
1135
1121
  id: "gemini",
1136
1122
  name: "Gemini CLI",
@@ -1142,13 +1128,9 @@ var geminiPlugin = {
1142
1128
  },
1143
1129
  async export(state, rootDir) {
1144
1130
  const files = [];
1145
- const outputDir = TOOL_OUTPUT_DIRS.gemini;
1146
- if (state.agents) {
1147
- files.push({
1148
- path: `${outputDir}/GEMINI.md`,
1149
- type: "symlink",
1150
- target: `../${UNIFIED_DIR}/AGENTS.md`
1151
- });
1131
+ const agentsSymlink = createRootAgentsMdSymlink(state);
1132
+ if (agentsSymlink) {
1133
+ files.push(agentsSymlink);
1152
1134
  }
1153
1135
  const rulesMap = groupRulesByDirectory(state.rules);
1154
1136
  for (const [dir, contents] of rulesMap.entries()) {
@@ -1160,19 +1142,21 @@ var geminiPlugin = {
1160
1142
  content: combinedContent
1161
1143
  });
1162
1144
  }
1163
- for (const skill of state.skills) {
1164
- files.push({
1165
- path: `${outputDir}/skills/${skill.path}`,
1166
- type: "symlink",
1167
- target: `../../${UNIFIED_DIR}/skills/${skill.path}`
1168
- });
1169
- }
1145
+ files.push(...createSkillSymlinks(state, OUTPUT_DIR5));
1170
1146
  const mcpServers = transformMcpToGemini(state.settings?.mcpServers);
1171
- if (mcpServers) {
1147
+ const hasAgents = !!state.agents;
1148
+ if (mcpServers || hasAgents) {
1149
+ const settingsContent = {};
1150
+ if (hasAgents) {
1151
+ settingsContent["context"] = { fileName: ["AGENTS.md"] };
1152
+ }
1153
+ if (mcpServers) {
1154
+ settingsContent["mcpServers"] = mcpServers;
1155
+ }
1172
1156
  files.push({
1173
- path: `${outputDir}/settings.json`,
1157
+ path: `${OUTPUT_DIR5}/settings.json`,
1174
1158
  type: "json",
1175
- content: { mcpServers }
1159
+ content: settingsContent
1176
1160
  });
1177
1161
  }
1178
1162
  return applyFileOverrides(files, rootDir, "gemini");
@@ -1181,19 +1165,13 @@ var geminiPlugin = {
1181
1165
  const warnings = [];
1182
1166
  const skipped = [];
1183
1167
  if (!state.agents) {
1184
- warnings.push({
1185
- path: ["AGENTS.md"],
1186
- message: "No AGENTS.md found - GEMINI.md will not be created"
1187
- });
1168
+ warnings.push(createNoAgentsMdWarning("root AGENTS.md"));
1188
1169
  }
1189
- if (state.settings?.permissions) {
1190
- const hasPermissions = (state.settings.permissions.allow?.length ?? 0) > 0 || (state.settings.permissions.ask?.length ?? 0) > 0 || (state.settings.permissions.deny?.length ?? 0) > 0;
1191
- if (hasPermissions) {
1192
- skipped.push({
1193
- feature: "permissions",
1194
- reason: "Gemini CLI does not support declarative permissions - permissions must be granted interactively"
1195
- });
1196
- }
1170
+ if (hasPermissionsConfigured(state.settings?.permissions)) {
1171
+ skipped.push({
1172
+ feature: "permissions",
1173
+ reason: "Gemini CLI does not support declarative permissions - permissions must be granted interactively"
1174
+ });
1197
1175
  }
1198
1176
  if (state.rules.length > 0) {
1199
1177
  warnings.push({
@@ -1278,6 +1256,7 @@ function parsePermissionRuleForOpenCode(rule) {
1278
1256
  }
1279
1257
 
1280
1258
  // src/plugins/opencode/index.ts
1259
+ var OUTPUT_DIR6 = TOOL_OUTPUT_DIRS.opencode;
1281
1260
  var opencodePlugin = {
1282
1261
  id: "opencode",
1283
1262
  name: "OpenCode",
@@ -1289,33 +1268,23 @@ var opencodePlugin = {
1289
1268
  },
1290
1269
  async export(state, rootDir) {
1291
1270
  const files = [];
1292
- const outputDir = TOOL_OUTPUT_DIRS.opencode;
1293
- if (state.agents) {
1294
- files.push({
1295
- path: "AGENTS.md",
1296
- type: "symlink",
1297
- target: `${UNIFIED_DIR}/AGENTS.md`
1298
- });
1271
+ const agentsSymlink = createRootAgentsMdSymlink(state);
1272
+ if (agentsSymlink) {
1273
+ files.push(agentsSymlink);
1299
1274
  }
1300
1275
  if (state.rules.length > 0) {
1301
1276
  files.push({
1302
- path: `${outputDir}/rules`,
1277
+ path: `${OUTPUT_DIR6}/rules`,
1303
1278
  type: "symlink",
1304
1279
  target: `../${UNIFIED_DIR}/rules`
1305
1280
  });
1306
1281
  }
1307
- for (const skill of state.skills) {
1308
- files.push({
1309
- path: `${outputDir}/skills/${skill.path}`,
1310
- type: "symlink",
1311
- target: `../../${UNIFIED_DIR}/skills/${skill.path}`
1312
- });
1313
- }
1282
+ files.push(...createSkillSymlinks(state, CROSS_TOOL_SKILLS_DIR));
1314
1283
  const config = {
1315
1284
  $schema: "https://opencode.ai/config.json"
1316
1285
  };
1317
1286
  if (state.rules.length > 0) {
1318
- config["instructions"] = [`${outputDir}/rules/*.md`];
1287
+ config["instructions"] = [`${OUTPUT_DIR6}/rules/*.md`];
1319
1288
  }
1320
1289
  const mcp = transformMcpToOpenCode(state.settings?.mcpServers);
1321
1290
  if (mcp) {
@@ -1337,10 +1306,7 @@ var opencodePlugin = {
1337
1306
  validate(state) {
1338
1307
  const warnings = [];
1339
1308
  if (!state.agents) {
1340
- warnings.push({
1341
- path: ["AGENTS.md"],
1342
- message: "No AGENTS.md found - root AGENTS.md will not be created"
1343
- });
1309
+ warnings.push(createNoAgentsMdWarning("root AGENTS.md"));
1344
1310
  }
1345
1311
  warnings.push(
1346
1312
  ...validateMcpServers(state.settings?.mcpServers, [
@@ -1403,6 +1369,7 @@ function serializeWindsurfRule(frontmatter, content) {
1403
1369
  }
1404
1370
 
1405
1371
  // src/plugins/windsurf/index.ts
1372
+ var OUTPUT_DIR7 = TOOL_OUTPUT_DIRS.windsurf;
1406
1373
  var windsurfPlugin = {
1407
1374
  id: "windsurf",
1408
1375
  name: "Windsurf",
@@ -1414,13 +1381,9 @@ var windsurfPlugin = {
1414
1381
  },
1415
1382
  async export(state, rootDir) {
1416
1383
  const files = [];
1417
- const outputDir = TOOL_OUTPUT_DIRS.windsurf;
1418
- if (state.agents) {
1419
- files.push({
1420
- path: "AGENTS.md",
1421
- type: "symlink",
1422
- target: `${UNIFIED_DIR}/AGENTS.md`
1423
- });
1384
+ const agentsSymlink = createRootAgentsMdSymlink(state);
1385
+ if (agentsSymlink) {
1386
+ files.push(agentsSymlink);
1424
1387
  }
1425
1388
  for (const rule of state.rules) {
1426
1389
  const transformed = transformRuleToWindsurf(rule);
@@ -1429,28 +1392,19 @@ var windsurfPlugin = {
1429
1392
  transformed.content
1430
1393
  );
1431
1394
  files.push({
1432
- path: `${outputDir}/rules/${rule.path}`,
1395
+ path: `${OUTPUT_DIR7}/rules/${rule.path}`,
1433
1396
  type: "text",
1434
1397
  content: ruleContent
1435
1398
  });
1436
1399
  }
1437
- for (const skill of state.skills) {
1438
- files.push({
1439
- path: `${outputDir}/skills/${skill.path}`,
1440
- type: "symlink",
1441
- target: `../../${UNIFIED_DIR}/skills/${skill.path}`
1442
- });
1443
- }
1400
+ files.push(...createSkillSymlinks(state, OUTPUT_DIR7));
1444
1401
  return applyFileOverrides(files, rootDir, "windsurf");
1445
1402
  },
1446
1403
  validate(state) {
1447
1404
  const warnings = [];
1448
1405
  const skipped = [];
1449
1406
  if (!state.agents) {
1450
- warnings.push({
1451
- path: ["AGENTS.md"],
1452
- message: "No AGENTS.md found - root AGENTS.md will not be created"
1453
- });
1407
+ warnings.push(createNoAgentsMdWarning("root AGENTS.md"));
1454
1408
  }
1455
1409
  if (state.rules.length > 0) {
1456
1410
  warnings.push({
@@ -1464,14 +1418,11 @@ var windsurfPlugin = {
1464
1418
  reason: "Windsurf uses global MCP config at ~/.codeium/windsurf/mcp_config.json - project-level MCP servers are not exported"
1465
1419
  });
1466
1420
  }
1467
- if (state.settings?.permissions) {
1468
- const hasPermissions = (state.settings.permissions.allow?.length ?? 0) > 0 || (state.settings.permissions.ask?.length ?? 0) > 0 || (state.settings.permissions.deny?.length ?? 0) > 0;
1469
- if (hasPermissions) {
1470
- skipped.push({
1471
- feature: "permissions",
1472
- reason: "Windsurf does not support declarative permissions"
1473
- });
1474
- }
1421
+ if (hasPermissionsConfigured(state.settings?.permissions)) {
1422
+ skipped.push({
1423
+ feature: "permissions",
1424
+ reason: "Windsurf does not support declarative permissions"
1425
+ });
1475
1426
  }
1476
1427
  return { valid: true, errors: [], warnings, skipped };
1477
1428
  }
@@ -1642,23 +1593,37 @@ async function writeFiles(files, options) {
1642
1593
  async function updateGitignore(rootDir, paths) {
1643
1594
  const gitignorePath = path.join(rootDir, ".gitignore");
1644
1595
  let content = "";
1596
+ let hasExistingFile = true;
1645
1597
  try {
1646
1598
  content = await fs4.readFile(gitignorePath, "utf-8");
1647
1599
  } catch {
1600
+ hasExistingFile = false;
1648
1601
  }
1649
1602
  const marker = "# lnai-generated";
1650
1603
  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("#"));
1655
1604
  const markerRegex = new RegExp(`${marker}[\\s\\S]*?${endMarker}\\n?`, "g");
1605
+ const hasManagedSection = new RegExp(`${marker}[\\s\\S]*?${endMarker}`).test(
1606
+ content
1607
+ );
1656
1608
  content = content.replace(markerRegex, "");
1657
- content = content.trimEnd();
1658
- const uniquePaths = [.../* @__PURE__ */ new Set([...existingPaths, ...paths])].sort();
1659
- const newSection = ["", marker, ...uniquePaths, endMarker, ""].join("\n");
1660
- content = content + newSection;
1661
- await fs4.writeFile(gitignorePath, content, "utf-8");
1609
+ const baseContent = content.trimEnd();
1610
+ const uniquePaths = [...new Set(paths)].sort();
1611
+ if (uniquePaths.length === 0) {
1612
+ if (!hasManagedSection && !hasExistingFile) {
1613
+ return;
1614
+ }
1615
+ const cleanedContent = baseContent.length > 0 ? `${baseContent}
1616
+ ` : baseContent;
1617
+ await fs4.writeFile(gitignorePath, cleanedContent, "utf-8");
1618
+ return;
1619
+ }
1620
+ const managedSection = [marker, ...uniquePaths, endMarker].join("\n");
1621
+ const nextContent = baseContent.length > 0 ? `${baseContent}
1622
+
1623
+ ${managedSection}
1624
+ ` : `${managedSection}
1625
+ `;
1626
+ await fs4.writeFile(gitignorePath, nextContent, "utf-8");
1662
1627
  }
1663
1628
 
1664
1629
  // src/manifest/index.ts
@@ -1726,23 +1691,6 @@ function createEmptyManifest() {
1726
1691
  }
1727
1692
 
1728
1693
  // src/pipeline/index.ts
1729
- function getToolsToSync(config, requestedTools) {
1730
- if (requestedTools && requestedTools.length > 0) {
1731
- return requestedTools.filter((tool) => pluginRegistry.has(tool));
1732
- }
1733
- const enabledTools = [];
1734
- if (config.tools) {
1735
- for (const [toolId, toolConfig] of Object.entries(config.tools)) {
1736
- if (toolConfig?.enabled && pluginRegistry.has(toolId)) {
1737
- enabledTools.push(toolId);
1738
- }
1739
- }
1740
- }
1741
- if (enabledTools.length === 0) {
1742
- return pluginRegistry.getIds();
1743
- }
1744
- return enabledTools;
1745
- }
1746
1694
  async function runSyncPipeline(options) {
1747
1695
  const {
1748
1696
  rootDir,
@@ -1777,7 +1725,6 @@ async function runSyncPipeline(options) {
1777
1725
  }
1778
1726
  let manifest = await readManifest(rootDir) ?? createEmptyManifest();
1779
1727
  const results = [];
1780
- const pathsToIgnore = [];
1781
1728
  for (const toolId of toolsToSync) {
1782
1729
  const plugin = pluginRegistry.get(toolId);
1783
1730
  if (!plugin) {
@@ -1803,19 +1750,57 @@ async function runSyncPipeline(options) {
1803
1750
  if (!dryRun) {
1804
1751
  manifest = updateToolManifest(manifest, toolId, outputFiles);
1805
1752
  }
1806
- const toolConfig = state.config.tools?.[toolId];
1807
- if (!toolConfig?.versionControl) {
1808
- pathsToIgnore.push(...outputFiles.map((f) => f.path));
1809
- }
1810
1753
  }
1811
1754
  if (!dryRun) {
1812
1755
  await writeManifest(rootDir, manifest);
1813
- }
1814
- if (pathsToIgnore.length > 0 && !dryRun) {
1756
+ const pathsToIgnore = computePathsToIgnore(manifest, state.config.tools);
1815
1757
  await updateGitignore(rootDir, pathsToIgnore);
1816
1758
  }
1817
1759
  return results;
1818
1760
  }
1761
+ function getToolsToSync(config, requestedTools) {
1762
+ if (requestedTools && requestedTools.length > 0) {
1763
+ return requestedTools.filter((tool) => pluginRegistry.has(tool));
1764
+ }
1765
+ const enabledTools = [];
1766
+ if (config.tools) {
1767
+ for (const [toolId, toolConfig] of Object.entries(config.tools)) {
1768
+ if (toolConfig?.enabled && pluginRegistry.has(toolId)) {
1769
+ enabledTools.push(toolId);
1770
+ }
1771
+ }
1772
+ }
1773
+ if (enabledTools.length === 0) {
1774
+ return pluginRegistry.getIds();
1775
+ }
1776
+ return enabledTools;
1777
+ }
1778
+ function computePathsToIgnore(manifest, toolConfigs) {
1779
+ const pathToTools = /* @__PURE__ */ new Map();
1780
+ for (const [toolId, toolManifest] of Object.entries(manifest.tools)) {
1781
+ if (!toolManifest?.files) {
1782
+ continue;
1783
+ }
1784
+ for (const file of toolManifest.files) {
1785
+ let tools = pathToTools.get(file.path);
1786
+ if (!tools) {
1787
+ tools = /* @__PURE__ */ new Set();
1788
+ pathToTools.set(file.path, tools);
1789
+ }
1790
+ tools.add(toolId);
1791
+ }
1792
+ }
1793
+ const result = [];
1794
+ for (const [filePath, toolIds] of pathToTools) {
1795
+ const anyVersionControlled = [...toolIds].some(
1796
+ (toolId) => toolConfigs?.[toolId]?.versionControl === true
1797
+ );
1798
+ if (!anyVersionControlled) {
1799
+ result.push(filePath);
1800
+ }
1801
+ }
1802
+ return result;
1803
+ }
1819
1804
  async function initUnifiedConfig(options) {
1820
1805
  const {
1821
1806
  rootDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lnai/core",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
4
4
  "description": "Core library for LNAI - unified AI config management",
5
5
  "type": "module",
6
6
  "license": "MIT",