@lnai/core 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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"];
4
+ declare const TOOL_IDS: readonly ["claudeCode", "opencode", "cursor", "copilot", "windsurf"];
5
5
  type ToolId = (typeof TOOL_IDS)[number];
6
6
  declare const CONFIG_FILES: {
7
7
  readonly config: "config.json";
@@ -66,6 +66,8 @@ declare const toolIdSchema: z.ZodEnum<{
66
66
  claudeCode: "claudeCode";
67
67
  opencode: "opencode";
68
68
  cursor: "cursor";
69
+ copilot: "copilot";
70
+ windsurf: "windsurf";
69
71
  }>;
70
72
  /** Settings configuration (Claude format as source of truth) */
71
73
  declare const settingsSchema: z.ZodObject<{
@@ -85,11 +87,6 @@ declare const settingsSchema: z.ZodObject<{
85
87
  url: z.ZodOptional<z.ZodString>;
86
88
  headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
87
89
  }, z.core.$strip>>>;
88
- overrides: z.ZodOptional<z.ZodObject<{
89
- claudeCode: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
90
- opencode: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
91
- cursor: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
92
- }, z.core.$strip>>;
93
90
  }, z.core.$strip>;
94
91
  /** Main config.json structure. Uses partial object to allow partial tool configs. */
95
92
  declare const configSchema: z.ZodObject<{
@@ -106,6 +103,14 @@ declare const configSchema: z.ZodObject<{
106
103
  enabled: z.ZodBoolean;
107
104
  versionControl: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
108
105
  }, z.core.$strip>>;
106
+ copilot: z.ZodOptional<z.ZodObject<{
107
+ enabled: z.ZodBoolean;
108
+ versionControl: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
109
+ }, z.core.$strip>>;
110
+ windsurf: z.ZodOptional<z.ZodObject<{
111
+ enabled: z.ZodBoolean;
112
+ versionControl: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
113
+ }, z.core.$strip>>;
109
114
  }, z.core.$strip>>;
110
115
  }, z.core.$strip>;
111
116
  /** Skill frontmatter (name and description required) */
@@ -145,7 +150,6 @@ interface UnifiedState {
145
150
  settings: {
146
151
  permissions?: Permissions;
147
152
  mcpServers?: Record<string, McpServer>;
148
- overrides?: Partial<Record<ToolId, Record<string, unknown>>>;
149
153
  } | null;
150
154
  agents: string | null;
151
155
  rules: MarkdownFile<RuleFrontmatter>[];
@@ -302,12 +306,13 @@ interface InitOptions {
302
306
  tools?: ToolId[];
303
307
  minimal?: boolean;
304
308
  force?: boolean;
309
+ versionControl?: Record<ToolId, boolean>;
305
310
  }
306
311
  interface InitResult {
307
312
  created: string[];
308
313
  }
309
314
  declare function initUnifiedConfig(options: InitOptions): Promise<InitResult>;
310
315
  declare function hasUnifiedConfig(rootDir: string): Promise<boolean>;
311
- declare function generateDefaultConfig(tools?: ToolId[]): Config;
316
+ declare function generateDefaultConfig(tools?: ToolId[], versionControl?: Record<ToolId, boolean>): Config;
312
317
 
313
318
  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 };
package/dist/index.js CHANGED
@@ -2,12 +2,17 @@ import { z } from 'zod';
2
2
  import * as fs3 from 'fs/promises';
3
3
  import * as path from 'path';
4
4
  import matter from 'gray-matter';
5
- import deepmerge from 'deepmerge';
6
5
  import * as crypto from 'crypto';
7
6
 
8
7
  // src/constants.ts
9
8
  var UNIFIED_DIR = ".ai";
10
- var TOOL_IDS = ["claudeCode", "opencode", "cursor"];
9
+ var TOOL_IDS = [
10
+ "claudeCode",
11
+ "opencode",
12
+ "cursor",
13
+ "copilot",
14
+ "windsurf"
15
+ ];
11
16
  var CONFIG_FILES = {
12
17
  config: "config.json",
13
18
  settings: "settings.json",
@@ -21,12 +26,16 @@ var CONFIG_DIRS = {
21
26
  var TOOL_OUTPUT_DIRS = {
22
27
  claudeCode: ".claude",
23
28
  opencode: ".opencode",
24
- cursor: ".cursor"
29
+ cursor: ".cursor",
30
+ copilot: ".github",
31
+ windsurf: ".windsurf"
25
32
  };
26
33
  var OVERRIDE_DIRS = {
27
34
  claudeCode: ".claude",
28
35
  opencode: ".opencode",
29
- cursor: ".cursor"
36
+ cursor: ".cursor",
37
+ copilot: ".copilot",
38
+ windsurf: ".windsurf"
30
39
  };
31
40
 
32
41
  // src/errors.ts
@@ -106,21 +115,24 @@ var toolConfigSchema = z.object({
106
115
  enabled: z.boolean(),
107
116
  versionControl: z.boolean().optional().default(false)
108
117
  });
109
- var toolIdSchema = z.enum(["claudeCode", "opencode", "cursor"]);
118
+ var toolIdSchema = z.enum([
119
+ "claudeCode",
120
+ "opencode",
121
+ "cursor",
122
+ "copilot",
123
+ "windsurf"
124
+ ]);
110
125
  var settingsSchema = z.object({
111
126
  permissions: permissionsSchema.optional(),
112
- mcpServers: z.record(z.string(), mcpServerSchema).optional(),
113
- overrides: z.object({
114
- claudeCode: z.record(z.string(), z.unknown()).optional(),
115
- opencode: z.record(z.string(), z.unknown()).optional(),
116
- cursor: z.record(z.string(), z.unknown()).optional()
117
- }).optional()
127
+ mcpServers: z.record(z.string(), mcpServerSchema).optional()
118
128
  });
119
129
  var configSchema = z.object({
120
130
  tools: z.object({
121
131
  claudeCode: toolConfigSchema,
122
132
  opencode: toolConfigSchema,
123
- cursor: toolConfigSchema
133
+ cursor: toolConfigSchema,
134
+ copilot: toolConfigSchema,
135
+ windsurf: toolConfigSchema
124
136
  }).partial().optional()
125
137
  });
126
138
  var skillFrontmatterSchema = z.object({
@@ -403,38 +415,24 @@ async function scanDir(baseDir, currentDir, files) {
403
415
  }
404
416
  }
405
417
  }
406
- function deepMergeConfigs(base, override) {
407
- return deepmerge(base, override, {
408
- arrayMerge: (target, source) => [.../* @__PURE__ */ new Set([...target, ...source])]
409
- });
410
- }
411
- async function fileExists2(filePath) {
412
- try {
413
- await fs3.access(filePath);
414
- return true;
415
- } catch {
416
- return false;
417
- }
418
- }
419
- async function getOverrideOutputFiles(rootDir, toolId) {
418
+ async function applyFileOverrides(files, rootDir, toolId) {
420
419
  const outputDir = TOOL_OUTPUT_DIRS[toolId];
421
420
  const overrideFiles = await scanOverrideDirectory(rootDir, toolId);
422
- const result = [];
421
+ const overridePaths = /* @__PURE__ */ new Set();
422
+ const overrideOutputFiles = [];
423
423
  for (const overrideFile of overrideFiles) {
424
- const targetPath = path.join(rootDir, outputDir, overrideFile.relativePath);
425
- if (await fileExists2(targetPath)) {
426
- continue;
427
- }
428
- const symlinkPath = `${outputDir}/${overrideFile.relativePath}`;
429
- const symlinkDir = path.dirname(symlinkPath);
424
+ const outputPath = `${outputDir}/${overrideFile.relativePath}`;
425
+ overridePaths.add(outputPath);
426
+ const symlinkDir = path.dirname(outputPath);
430
427
  const sourcePath = `${UNIFIED_DIR}/${OVERRIDE_DIRS[toolId]}/${overrideFile.relativePath}`;
431
- result.push({
432
- path: symlinkPath,
428
+ overrideOutputFiles.push({
429
+ path: outputPath,
433
430
  type: "symlink",
434
431
  target: path.relative(symlinkDir, sourcePath)
435
432
  });
436
433
  }
437
- return result;
434
+ const filteredFiles = files.filter((file) => !overridePaths.has(file.path));
435
+ return [...filteredFiles, ...overrideOutputFiles];
438
436
  }
439
437
 
440
438
  // src/plugins/claude-code/index.ts
@@ -471,30 +469,21 @@ var claudeCodePlugin = {
471
469
  target: `../../${UNIFIED_DIR}/skills/${skill.path}`
472
470
  });
473
471
  }
474
- const baseSettings = {};
472
+ const settings = {};
475
473
  if (state.settings?.permissions) {
476
- baseSettings["permissions"] = state.settings.permissions;
474
+ settings["permissions"] = state.settings.permissions;
477
475
  }
478
476
  if (state.settings?.mcpServers) {
479
- baseSettings["mcpServers"] = state.settings.mcpServers;
480
- }
481
- let finalSettings = baseSettings;
482
- if (state.settings?.overrides?.claudeCode) {
483
- finalSettings = deepMergeConfigs(
484
- baseSettings,
485
- state.settings.overrides.claudeCode
486
- );
477
+ settings["mcpServers"] = state.settings.mcpServers;
487
478
  }
488
- if (Object.keys(finalSettings).length > 0) {
479
+ if (Object.keys(settings).length > 0) {
489
480
  files.push({
490
481
  path: `${outputDir}/settings.json`,
491
482
  type: "json",
492
- content: finalSettings
483
+ content: settings
493
484
  });
494
485
  }
495
- const overrideFiles = await getOverrideOutputFiles(rootDir, "claudeCode");
496
- files.push(...overrideFiles);
497
- return files;
486
+ return applyFileOverrides(files, rootDir, "claudeCode");
498
487
  },
499
488
  validate(state) {
500
489
  const warnings = [];
@@ -508,6 +497,197 @@ var claudeCodePlugin = {
508
497
  }
509
498
  };
510
499
 
500
+ // src/utils/transforms.ts
501
+ var ENV_VAR_PATTERN = /\$\{([^}:]+)(:-[^}]*)?\}/g;
502
+ function transformEnvVar(value, format) {
503
+ if (format === "opencode") {
504
+ return value.replace(ENV_VAR_PATTERN, "{env:$1}");
505
+ }
506
+ return value.replace(ENV_VAR_PATTERN, "${env:$1}");
507
+ }
508
+ function transformEnvVars(env, format) {
509
+ const result = {};
510
+ for (const [key, value] of Object.entries(env)) {
511
+ result[key] = transformEnvVar(value, format);
512
+ }
513
+ return result;
514
+ }
515
+ function parsePermissionRule(rule) {
516
+ const match = rule.match(/^(\w+)\(([^)]+)\)$/);
517
+ if (!match) {
518
+ return null;
519
+ }
520
+ const tool = match[1];
521
+ const pattern = match[2];
522
+ if (!tool || !pattern) {
523
+ return null;
524
+ }
525
+ return { tool, pattern };
526
+ }
527
+ function deriveDescription(filename, content) {
528
+ const headingMatch = content.match(/^#\s+(.+)$/m);
529
+ if (headingMatch && headingMatch[1]) {
530
+ return headingMatch[1];
531
+ }
532
+ const baseName = filename.replace(/\.md$/, "");
533
+ return baseName.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
534
+ }
535
+
536
+ // src/plugins/copilot/transforms.ts
537
+ function transformRuleToCopilot(rule) {
538
+ const description = deriveDescription(rule.path, rule.content);
539
+ const paths = rule.frontmatter.paths || [];
540
+ const frontmatter = {
541
+ description
542
+ };
543
+ if (paths.length > 0) {
544
+ frontmatter.applyTo = paths.join(",");
545
+ }
546
+ return {
547
+ frontmatter,
548
+ content: rule.content
549
+ };
550
+ }
551
+ function serializeCopilotInstruction(frontmatter, content) {
552
+ const lines = ["---"];
553
+ if (frontmatter.applyTo) {
554
+ lines.push(`applyTo: ${JSON.stringify(frontmatter.applyTo)}`);
555
+ }
556
+ lines.push(`description: ${JSON.stringify(frontmatter.description)}`);
557
+ lines.push("---");
558
+ lines.push("");
559
+ lines.push(content);
560
+ return lines.join("\n");
561
+ }
562
+ function transformMcpToCopilot(servers) {
563
+ if (!servers || Object.keys(servers).length === 0) {
564
+ return void 0;
565
+ }
566
+ const result = {};
567
+ for (const [name, server] of Object.entries(servers)) {
568
+ if (server.type === "http" || server.type === "sse") {
569
+ if (!server.url) {
570
+ continue;
571
+ }
572
+ const copilotServer = {
573
+ url: server.url
574
+ };
575
+ if (server.headers && Object.keys(server.headers).length > 0) {
576
+ copilotServer.requestInit = {
577
+ headers: transformEnvVars(server.headers, "copilot")
578
+ };
579
+ }
580
+ result[name] = copilotServer;
581
+ } else if (server.command) {
582
+ const copilotServer = {
583
+ type: "stdio",
584
+ command: server.command
585
+ };
586
+ if (server.args && server.args.length > 0) {
587
+ copilotServer.args = server.args;
588
+ }
589
+ if (server.env && Object.keys(server.env).length > 0) {
590
+ copilotServer.env = transformEnvVars(server.env, "copilot");
591
+ }
592
+ result[name] = copilotServer;
593
+ }
594
+ }
595
+ if (Object.keys(result).length === 0) {
596
+ return void 0;
597
+ }
598
+ return {
599
+ inputs: [],
600
+ servers: result
601
+ };
602
+ }
603
+
604
+ // src/plugins/copilot/index.ts
605
+ var copilotPlugin = {
606
+ id: "copilot",
607
+ name: "GitHub Copilot",
608
+ async detect(_rootDir) {
609
+ return false;
610
+ },
611
+ async import(_rootDir) {
612
+ return null;
613
+ },
614
+ async export(state, rootDir) {
615
+ const files = [];
616
+ if (state.agents) {
617
+ files.push({
618
+ path: ".github/copilot-instructions.md",
619
+ type: "symlink",
620
+ target: `../${UNIFIED_DIR}/AGENTS.md`
621
+ });
622
+ }
623
+ for (const rule of state.rules) {
624
+ const transformed = transformRuleToCopilot(rule);
625
+ const ruleContent = serializeCopilotInstruction(
626
+ transformed.frontmatter,
627
+ transformed.content
628
+ );
629
+ const outputFilename = rule.path.replace(/\.md$/, ".instructions.md");
630
+ files.push({
631
+ path: `.github/instructions/${outputFilename}`,
632
+ type: "text",
633
+ content: ruleContent
634
+ });
635
+ }
636
+ for (const skill of state.skills) {
637
+ files.push({
638
+ path: `.github/skills/${skill.path}`,
639
+ type: "symlink",
640
+ target: `../../${UNIFIED_DIR}/skills/${skill.path}`
641
+ });
642
+ }
643
+ const mcpConfig = transformMcpToCopilot(state.settings?.mcpServers);
644
+ if (mcpConfig) {
645
+ files.push({
646
+ path: ".vscode/mcp.json",
647
+ type: "json",
648
+ content: { inputs: mcpConfig.inputs, servers: mcpConfig.servers }
649
+ });
650
+ }
651
+ return applyFileOverrides(files, rootDir, "copilot");
652
+ },
653
+ validate(state) {
654
+ const warnings = [];
655
+ if (!state.agents) {
656
+ warnings.push({
657
+ path: ["AGENTS.md"],
658
+ message: "No AGENTS.md found - .github/copilot-instructions.md will not be created"
659
+ });
660
+ }
661
+ const permissions = state.settings?.permissions;
662
+ const hasPermissions = permissions && (permissions.allow && permissions.allow.length > 0 || permissions.ask && permissions.ask.length > 0 || permissions.deny && permissions.deny.length > 0);
663
+ if (hasPermissions) {
664
+ warnings.push({
665
+ path: ["settings", "permissions"],
666
+ message: "GitHub Copilot does not support permissions - they will be ignored"
667
+ });
668
+ }
669
+ const mcpServers = state.settings?.mcpServers;
670
+ if (mcpServers) {
671
+ for (const [name, server] of Object.entries(mcpServers)) {
672
+ const isRemote = server.type === "http" || server.type === "sse";
673
+ const hasCommand = !!server.command;
674
+ if (isRemote && !server.url) {
675
+ warnings.push({
676
+ path: ["settings", "mcpServers", name],
677
+ message: `MCP server "${name}" is type "${server.type}" but has no url - it will be skipped`
678
+ });
679
+ } else if (!isRemote && !hasCommand) {
680
+ warnings.push({
681
+ path: ["settings", "mcpServers", name],
682
+ message: `MCP server "${name}" has no command or type - it will be skipped`
683
+ });
684
+ }
685
+ }
686
+ }
687
+ return { valid: true, errors: [], warnings, skipped: [] };
688
+ }
689
+ };
690
+
511
691
  // src/plugins/cursor/transforms.ts
512
692
  function transformRuleToCursor(rule) {
513
693
  const description = deriveDescription(rule.path, rule.content);
@@ -522,14 +702,6 @@ function transformRuleToCursor(rule) {
522
702
  content: rule.content
523
703
  };
524
704
  }
525
- function deriveDescription(filename, content) {
526
- const headingMatch = content.match(/^#\s+(.+)$/m);
527
- if (headingMatch && headingMatch[1]) {
528
- return headingMatch[1];
529
- }
530
- const baseName = filename.replace(/\.md$/, "");
531
- return baseName.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
532
- }
533
705
  function serializeCursorRule(frontmatter, content) {
534
706
  const lines = [
535
707
  "---",
@@ -556,7 +728,7 @@ function transformMcpToCursor(servers) {
556
728
  url: server.url
557
729
  };
558
730
  if (server.headers) {
559
- cursorServer.headers = transformEnvVarsToCursor(server.headers);
731
+ cursorServer.headers = transformEnvVars(server.headers, "cursor");
560
732
  }
561
733
  result[name] = cursorServer;
562
734
  } else if (server.command) {
@@ -567,23 +739,13 @@ function transformMcpToCursor(servers) {
567
739
  cursorServer.args = server.args;
568
740
  }
569
741
  if (server.env) {
570
- cursorServer.env = transformEnvVarsToCursor(server.env);
742
+ cursorServer.env = transformEnvVars(server.env, "cursor");
571
743
  }
572
744
  result[name] = cursorServer;
573
745
  }
574
746
  }
575
747
  return Object.keys(result).length > 0 ? result : void 0;
576
748
  }
577
- function transformEnvVarToCursor(value) {
578
- return value.replace(/\$\{([^}:]+)(:-[^}]*)?\}/g, "${env:$1}");
579
- }
580
- function transformEnvVarsToCursor(env) {
581
- const result = {};
582
- for (const [key, value] of Object.entries(env)) {
583
- result[key] = transformEnvVarToCursor(value);
584
- }
585
- return result;
586
- }
587
749
  function transformPermissionsToCursor(permissions) {
588
750
  if (!permissions) {
589
751
  return { permissions: void 0, hasAskPermissions: false };
@@ -682,47 +844,22 @@ var cursorPlugin = {
682
844
  });
683
845
  }
684
846
  const mcpServers = transformMcpToCursor(state.settings?.mcpServers);
685
- const cursorOverrides = state.settings?.overrides?.cursor;
686
- const hasMcpOverrides = cursorOverrides?.["mcpServers"] !== void 0;
687
- if (mcpServers || hasMcpOverrides) {
688
- let mcpContent = mcpServers ? { mcpServers } : {};
689
- if (cursorOverrides?.["mcpServers"]) {
690
- mcpContent = deepMergeConfigs(mcpContent, {
691
- mcpServers: cursorOverrides["mcpServers"]
692
- });
693
- }
847
+ if (mcpServers) {
694
848
  files.push({
695
849
  path: `${outputDir}/mcp.json`,
696
850
  type: "json",
697
- content: mcpContent
851
+ content: { mcpServers }
698
852
  });
699
853
  }
700
- const permissionsResult = transformPermissionsToCursor(
701
- state.settings?.permissions
702
- );
703
- const hasCliOverrides = cursorOverrides !== void 0 && Object.keys(cursorOverrides).some((key) => key !== "mcpServers");
704
- if (permissionsResult.permissions || hasCliOverrides) {
705
- let cliContent = permissionsResult.permissions ? { permissions: permissionsResult.permissions } : {};
706
- if (cursorOverrides) {
707
- const cliOverrides = {};
708
- for (const [key, value] of Object.entries(cursorOverrides)) {
709
- if (key !== "mcpServers") {
710
- cliOverrides[key] = value;
711
- }
712
- }
713
- if (Object.keys(cliOverrides).length > 0) {
714
- cliContent = deepMergeConfigs(cliContent, cliOverrides);
715
- }
716
- }
854
+ const cliContent = buildCliContent(state.settings?.permissions);
855
+ if (cliContent) {
717
856
  files.push({
718
857
  path: `${outputDir}/cli.json`,
719
858
  type: "json",
720
859
  content: cliContent
721
860
  });
722
861
  }
723
- const overrideFiles = await getOverrideOutputFiles(rootDir, "cursor");
724
- files.push(...overrideFiles);
725
- return files;
862
+ return applyFileOverrides(files, rootDir, "cursor");
726
863
  },
727
864
  validate(state) {
728
865
  const warnings = [];
@@ -757,6 +894,13 @@ var cursorPlugin = {
757
894
  return { valid: true, errors: [], warnings, skipped: [] };
758
895
  }
759
896
  };
897
+ function buildCliContent(permissions) {
898
+ const permissionsResult = transformPermissionsToCursor(permissions);
899
+ if (!permissionsResult.permissions) {
900
+ return void 0;
901
+ }
902
+ return { permissions: permissionsResult.permissions };
903
+ }
760
904
 
761
905
  // src/plugins/opencode/transforms.ts
762
906
  function transformMcpToOpenCode(servers) {
@@ -781,7 +925,7 @@ function transformMcpToOpenCode(servers) {
781
925
  command
782
926
  };
783
927
  if (server.env) {
784
- openCodeServer.environment = transformEnvVars(server.env);
928
+ openCodeServer.environment = transformEnvVars(server.env, "opencode");
785
929
  }
786
930
  result[name] = openCodeServer;
787
931
  }
@@ -798,7 +942,7 @@ function transformPermissionsToOpenCode(permissions) {
798
942
  return;
799
943
  }
800
944
  for (const rule of rules) {
801
- const parsed = parsePermissionRule(rule);
945
+ const parsed = parsePermissionRuleForOpenCode(rule);
802
946
  if (!parsed) {
803
947
  continue;
804
948
  }
@@ -814,28 +958,13 @@ function transformPermissionsToOpenCode(permissions) {
814
958
  processRules(permissions.deny, "deny");
815
959
  return Object.keys(result).length > 0 ? result : void 0;
816
960
  }
817
- function transformEnvVar(value) {
818
- return value.replace(/\$\{([^}:]+)(:-[^}]*)?\}/g, "{env:$1}");
819
- }
820
- function transformEnvVars(env) {
821
- const result = {};
822
- for (const [key, value] of Object.entries(env)) {
823
- result[key] = transformEnvVar(value);
824
- }
825
- return result;
826
- }
827
- function parsePermissionRule(rule) {
828
- const match = rule.match(/^(\w+)\(([^)]+)\)$/);
829
- if (!match) {
830
- return null;
831
- }
832
- const tool = match[1];
833
- const pattern = match[2];
834
- if (!tool || !pattern) {
961
+ function parsePermissionRuleForOpenCode(rule) {
962
+ const parsed = parsePermissionRule(rule);
963
+ if (!parsed) {
835
964
  return null;
836
965
  }
837
- const normalizedTool = tool.toLowerCase();
838
- let normalizedPattern = pattern;
966
+ const normalizedTool = parsed.tool.toLowerCase();
967
+ let normalizedPattern = parsed.pattern;
839
968
  if (normalizedPattern.includes(":*")) {
840
969
  normalizedPattern = normalizedPattern.replace(/:(\*)/g, " $1");
841
970
  }
@@ -879,37 +1008,28 @@ var opencodePlugin = {
879
1008
  target: `../../${UNIFIED_DIR}/skills/${skill.path}`
880
1009
  });
881
1010
  }
882
- const baseConfig = {
1011
+ const config = {
883
1012
  $schema: "https://opencode.ai/config.json"
884
1013
  };
885
1014
  if (state.rules.length > 0) {
886
- baseConfig["instructions"] = [`${outputDir}/rules/*.md`];
1015
+ config["instructions"] = [`${outputDir}/rules/*.md`];
887
1016
  }
888
1017
  const mcp = transformMcpToOpenCode(state.settings?.mcpServers);
889
1018
  if (mcp) {
890
- baseConfig["mcp"] = mcp;
1019
+ config["mcp"] = mcp;
891
1020
  }
892
1021
  const permission = transformPermissionsToOpenCode(
893
1022
  state.settings?.permissions
894
1023
  );
895
1024
  if (permission) {
896
- baseConfig["permission"] = permission;
897
- }
898
- let finalConfig = baseConfig;
899
- if (state.settings?.overrides?.opencode) {
900
- finalConfig = deepMergeConfigs(
901
- baseConfig,
902
- state.settings.overrides.opencode
903
- );
1025
+ config["permission"] = permission;
904
1026
  }
905
1027
  files.push({
906
1028
  path: "opencode.json",
907
1029
  type: "json",
908
- content: finalConfig
1030
+ content: config
909
1031
  });
910
- const overrideFiles = await getOverrideOutputFiles(rootDir, "opencode");
911
- files.push(...overrideFiles);
912
- return files;
1032
+ return applyFileOverrides(files, rootDir, "opencode");
913
1033
  },
914
1034
  validate(state) {
915
1035
  const warnings = [];
@@ -944,10 +1064,116 @@ var PluginRegistry = class {
944
1064
  };
945
1065
  var pluginRegistry = new PluginRegistry();
946
1066
 
1067
+ // src/plugins/windsurf/transforms.ts
1068
+ function transformRuleToWindsurf(rule) {
1069
+ const frontmatter = {
1070
+ trigger: "manual"
1071
+ };
1072
+ return {
1073
+ frontmatter,
1074
+ content: rule.content
1075
+ };
1076
+ }
1077
+ function serializeWindsurfRule(frontmatter, content) {
1078
+ const lines = ["---"];
1079
+ lines.push(`trigger: ${frontmatter.trigger}`);
1080
+ if (frontmatter.globs && frontmatter.globs.length > 0) {
1081
+ lines.push("globs:");
1082
+ for (const glob of frontmatter.globs) {
1083
+ lines.push(` - ${glob}`);
1084
+ }
1085
+ }
1086
+ if (frontmatter.description) {
1087
+ const escaped = frontmatter.description.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1088
+ lines.push(`description: "${escaped}"`);
1089
+ }
1090
+ lines.push("---");
1091
+ lines.push("");
1092
+ lines.push(content);
1093
+ return lines.join("\n");
1094
+ }
1095
+
1096
+ // src/plugins/windsurf/index.ts
1097
+ var windsurfPlugin = {
1098
+ id: "windsurf",
1099
+ name: "Windsurf",
1100
+ async detect(_rootDir) {
1101
+ return false;
1102
+ },
1103
+ async import(_rootDir) {
1104
+ return null;
1105
+ },
1106
+ async export(state, rootDir) {
1107
+ const files = [];
1108
+ const outputDir = TOOL_OUTPUT_DIRS.windsurf;
1109
+ if (state.agents) {
1110
+ files.push({
1111
+ path: "AGENTS.md",
1112
+ type: "symlink",
1113
+ target: `${UNIFIED_DIR}/AGENTS.md`
1114
+ });
1115
+ }
1116
+ for (const rule of state.rules) {
1117
+ const transformed = transformRuleToWindsurf(rule);
1118
+ const ruleContent = serializeWindsurfRule(
1119
+ transformed.frontmatter,
1120
+ transformed.content
1121
+ );
1122
+ files.push({
1123
+ path: `${outputDir}/rules/${rule.path}`,
1124
+ type: "text",
1125
+ content: ruleContent
1126
+ });
1127
+ }
1128
+ for (const skill of state.skills) {
1129
+ files.push({
1130
+ path: `${outputDir}/skills/${skill.path}`,
1131
+ type: "symlink",
1132
+ target: `../../${UNIFIED_DIR}/skills/${skill.path}`
1133
+ });
1134
+ }
1135
+ return applyFileOverrides(files, rootDir, "windsurf");
1136
+ },
1137
+ validate(state) {
1138
+ const warnings = [];
1139
+ const skipped = [];
1140
+ if (!state.agents) {
1141
+ warnings.push({
1142
+ path: ["AGENTS.md"],
1143
+ message: "No AGENTS.md found - root AGENTS.md will not be created"
1144
+ });
1145
+ }
1146
+ if (state.rules.length > 0) {
1147
+ warnings.push({
1148
+ path: [".windsurf/rules"],
1149
+ message: "Rules are exported with 'trigger: manual' and require explicit @mention to invoke"
1150
+ });
1151
+ }
1152
+ if (state.settings?.mcpServers && Object.keys(state.settings.mcpServers).length > 0) {
1153
+ skipped.push({
1154
+ feature: "mcpServers",
1155
+ reason: "Windsurf uses global MCP config at ~/.codeium/windsurf/mcp_config.json - project-level MCP servers are not exported"
1156
+ });
1157
+ }
1158
+ if (state.settings?.permissions) {
1159
+ const hasPermissions = (state.settings.permissions.allow?.length ?? 0) > 0 || (state.settings.permissions.ask?.length ?? 0) > 0 || (state.settings.permissions.deny?.length ?? 0) > 0;
1160
+ if (hasPermissions) {
1161
+ skipped.push({
1162
+ feature: "permissions",
1163
+ reason: "Windsurf does not support declarative permissions"
1164
+ });
1165
+ }
1166
+ }
1167
+ return { valid: true, errors: [], warnings, skipped };
1168
+ }
1169
+ };
1170
+
947
1171
  // src/plugins/index.ts
948
1172
  pluginRegistry.register(claudeCodePlugin);
1173
+ pluginRegistry.register(copilotPlugin);
949
1174
  pluginRegistry.register(cursorPlugin);
950
1175
  pluginRegistry.register(opencodePlugin);
1176
+ pluginRegistry.register(windsurfPlugin);
951
1177
  function computeHash(content) {
952
1178
  return crypto.createHash("sha256").update(content, "utf-8").digest("hex");
953
1179
  }
@@ -1060,9 +1286,8 @@ async function updateGitignore(rootDir, paths) {
1060
1286
  const markerRegex = new RegExp(`${marker}[\\s\\S]*?${endMarker}\\n?`, "g");
1061
1287
  content = content.replace(markerRegex, "");
1062
1288
  content = content.trimEnd();
1063
- const newSection = ["", marker, ...paths.map((p) => p), endMarker, ""].join(
1064
- "\n"
1065
- );
1289
+ const uniquePaths = [...new Set(paths)];
1290
+ const newSection = ["", marker, ...uniquePaths, endMarker, ""].join("\n");
1066
1291
  content = content + newSection;
1067
1292
  await fs3.writeFile(gitignorePath, content, "utf-8");
1068
1293
  }
@@ -1138,10 +1363,16 @@ async function runSyncPipeline(options) {
1138
1363
  return results;
1139
1364
  }
1140
1365
  async function initUnifiedConfig(options) {
1141
- const { rootDir, tools, minimal = false, force = false } = options;
1366
+ const {
1367
+ rootDir,
1368
+ tools,
1369
+ minimal = false,
1370
+ force = false,
1371
+ versionControl
1372
+ } = options;
1142
1373
  const aiDir = path.join(rootDir, UNIFIED_DIR);
1143
1374
  const created = [];
1144
- const config = generateDefaultConfig(tools);
1375
+ const config = generateDefaultConfig(tools, versionControl);
1145
1376
  const exists = await hasUnifiedConfig(rootDir);
1146
1377
  if (exists && !force) {
1147
1378
  throw new ValidationError(
@@ -1183,7 +1414,7 @@ async function hasUnifiedConfig(rootDir) {
1183
1414
  return false;
1184
1415
  }
1185
1416
  }
1186
- function generateDefaultConfig(tools) {
1417
+ function generateDefaultConfig(tools, versionControl) {
1187
1418
  if (tools) {
1188
1419
  const validation = validateToolIds(tools);
1189
1420
  if (!validation.valid) {
@@ -1200,7 +1431,7 @@ function generateDefaultConfig(tools) {
1200
1431
  for (const toolId of TOOL_IDS) {
1201
1432
  toolsConfig[toolId] = {
1202
1433
  enabled: enabledTools.includes(toolId),
1203
- versionControl: false
1434
+ versionControl: versionControl?.[toolId] ?? false
1204
1435
  };
1205
1436
  }
1206
1437
  return { tools: toolsConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lnai/core",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Core library for LNAI - unified AI config management",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -21,6 +21,9 @@
21
21
  "claude",
22
22
  "cursor",
23
23
  "opencode",
24
+ "copilot",
25
+ "github-copilot",
26
+ "windsurf",
24
27
  "cli",
25
28
  "ai-tools"
26
29
  ],
@@ -34,7 +37,6 @@
34
37
  "dist"
35
38
  ],
36
39
  "dependencies": {
37
- "deepmerge": "^4.3.1",
38
40
  "gray-matter": "^4.0.3",
39
41
  "zod": "^4.3.6"
40
42
  },