@liendev/lien 0.19.2 → 0.19.4

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.js CHANGED
@@ -994,7 +994,7 @@ async function scanCodebaseWithFrameworks(rootDir, config) {
994
994
  const frameworkFiles = await scanFramework(rootDir, framework);
995
995
  allFiles.push(...frameworkFiles);
996
996
  }
997
- return allFiles;
997
+ return Array.from(new Set(allFiles));
998
998
  }
999
999
  async function scanFramework(rootDir, framework) {
1000
1000
  const frameworkPath = path13.join(rootDir, framework.path);
@@ -2766,90 +2766,89 @@ function findLiquidBlocks(content) {
2766
2766
  }
2767
2767
  return blocks.sort((a, b) => a.startLine - b.startLine);
2768
2768
  }
2769
- function chunkLiquidFile(filepath, content, chunkSize = 75, chunkOverlap = 10) {
2770
- const lines = content.split("\n");
2771
- const blocks = findLiquidBlocks(content);
2769
+ function createCodeChunk(content, startLine, endLine, filepath, type, options = {}) {
2770
+ return {
2771
+ content,
2772
+ metadata: {
2773
+ file: filepath,
2774
+ startLine,
2775
+ endLine,
2776
+ language: "liquid",
2777
+ type,
2778
+ symbolName: options.symbolName,
2779
+ symbolType: options.symbolType,
2780
+ imports: options.imports?.length ? options.imports : void 0
2781
+ }
2782
+ };
2783
+ }
2784
+ function splitLargeBlock(block, ctx, symbolName, imports) {
2772
2785
  const chunks = [];
2773
- const contentWithoutComments = removeComments(content);
2774
- const linesWithoutComments = contentWithoutComments.split("\n");
2775
- const coveredLines = /* @__PURE__ */ new Set();
2776
- for (const block of blocks) {
2777
- for (let i = block.startLine; i <= block.endLine; i++) {
2778
- coveredLines.add(i);
2779
- }
2780
- let symbolName;
2781
- if (block.type === "schema") {
2782
- symbolName = extractSchemaName(block.content);
2783
- }
2784
- const blockContentWithoutComments = linesWithoutComments.slice(block.startLine, block.endLine + 1).join("\n");
2785
- const imports = extractRenderTags(blockContentWithoutComments);
2786
- const blockLineCount = block.endLine - block.startLine + 1;
2787
- const maxBlockSize = chunkSize * 3;
2788
- if (blockLineCount <= maxBlockSize) {
2789
- chunks.push({
2790
- content: block.content,
2791
- metadata: {
2792
- file: filepath,
2793
- startLine: block.startLine + 1,
2794
- // 1-indexed
2795
- endLine: block.endLine + 1,
2796
- language: "liquid",
2797
- type: "block",
2798
- symbolName,
2799
- symbolType: block.type,
2800
- imports: imports.length > 0 ? imports : void 0
2801
- }
2802
- });
2803
- } else {
2804
- const blockLines = block.content.split("\n");
2805
- for (let offset = 0; offset < blockLines.length; offset += chunkSize - chunkOverlap) {
2806
- const endOffset = Math.min(offset + chunkSize, blockLines.length);
2807
- const chunkContent = blockLines.slice(offset, endOffset).join("\n");
2808
- if (chunkContent.trim().length > 0) {
2809
- chunks.push({
2810
- content: chunkContent,
2811
- metadata: {
2812
- file: filepath,
2813
- startLine: block.startLine + offset + 1,
2814
- // 1-indexed
2815
- endLine: block.startLine + endOffset,
2816
- // 1-indexed (endOffset already accounts for exclusivity)
2817
- language: "liquid",
2818
- type: "block",
2819
- symbolName,
2820
- // Preserve symbol name for all split chunks
2821
- symbolType: block.type,
2822
- imports: imports.length > 0 ? imports : void 0
2823
- }
2824
- });
2825
- }
2826
- if (endOffset >= blockLines.length) break;
2827
- }
2786
+ const blockLines = block.content.split("\n");
2787
+ const { chunkSize, chunkOverlap, filepath } = ctx.params;
2788
+ for (let offset = 0; offset < blockLines.length; offset += chunkSize - chunkOverlap) {
2789
+ const endOffset = Math.min(offset + chunkSize, blockLines.length);
2790
+ const chunkContent = blockLines.slice(offset, endOffset).join("\n");
2791
+ if (chunkContent.trim().length > 0) {
2792
+ chunks.push(createCodeChunk(
2793
+ chunkContent,
2794
+ block.startLine + offset + 1,
2795
+ block.startLine + endOffset,
2796
+ filepath,
2797
+ "block",
2798
+ { symbolName, symbolType: block.type, imports }
2799
+ ));
2828
2800
  }
2801
+ if (endOffset >= blockLines.length) break;
2829
2802
  }
2803
+ return chunks;
2804
+ }
2805
+ function processSpecialBlock(block, ctx, coveredLines) {
2806
+ for (let i = block.startLine; i <= block.endLine; i++) {
2807
+ coveredLines.add(i);
2808
+ }
2809
+ const symbolName = block.type === "schema" ? extractSchemaName(block.content) : void 0;
2810
+ const blockContentWithoutComments = ctx.linesWithoutComments.slice(block.startLine, block.endLine + 1).join("\n");
2811
+ const imports = extractRenderTags(blockContentWithoutComments);
2812
+ const blockLineCount = block.endLine - block.startLine + 1;
2813
+ const maxBlockSize = ctx.params.chunkSize * 3;
2814
+ if (blockLineCount <= maxBlockSize) {
2815
+ return [createCodeChunk(
2816
+ block.content,
2817
+ block.startLine + 1,
2818
+ block.endLine + 1,
2819
+ ctx.params.filepath,
2820
+ "block",
2821
+ { symbolName, symbolType: block.type, imports }
2822
+ )];
2823
+ }
2824
+ return splitLargeBlock(block, ctx, symbolName, imports);
2825
+ }
2826
+ function flushTemplateChunk(currentChunk, chunkStartLine, endLine, ctx) {
2827
+ if (currentChunk.length === 0) return null;
2828
+ const chunkContent = currentChunk.join("\n");
2829
+ if (chunkContent.trim().length === 0) return null;
2830
+ const cleanedChunk = ctx.linesWithoutComments.slice(chunkStartLine, endLine).join("\n");
2831
+ const imports = extractRenderTags(cleanedChunk);
2832
+ return createCodeChunk(
2833
+ chunkContent,
2834
+ chunkStartLine + 1,
2835
+ endLine,
2836
+ ctx.params.filepath,
2837
+ "template",
2838
+ { imports }
2839
+ );
2840
+ }
2841
+ function processTemplateContent(ctx, coveredLines) {
2842
+ const chunks = [];
2843
+ const { lines, params } = ctx;
2844
+ const { chunkSize, chunkOverlap } = params;
2830
2845
  let currentChunk = [];
2831
2846
  let chunkStartLine = 0;
2832
2847
  for (let i = 0; i < lines.length; i++) {
2833
2848
  if (coveredLines.has(i)) {
2834
- if (currentChunk.length > 0) {
2835
- const chunkContent = currentChunk.join("\n");
2836
- if (chunkContent.trim().length > 0) {
2837
- const cleanedChunk = linesWithoutComments.slice(chunkStartLine, i).join("\n");
2838
- const imports = extractRenderTags(cleanedChunk);
2839
- chunks.push({
2840
- content: chunkContent,
2841
- metadata: {
2842
- file: filepath,
2843
- startLine: chunkStartLine + 1,
2844
- endLine: i,
2845
- language: "liquid",
2846
- type: "template",
2847
- imports: imports.length > 0 ? imports : void 0
2848
- }
2849
- });
2850
- }
2851
- currentChunk = [];
2852
- }
2849
+ const chunk = flushTemplateChunk(currentChunk, chunkStartLine, i, ctx);
2850
+ if (chunk) chunks.push(chunk);
2851
+ currentChunk = [];
2853
2852
  continue;
2854
2853
  }
2855
2854
  if (currentChunk.length === 0) {
@@ -2857,46 +2856,28 @@ function chunkLiquidFile(filepath, content, chunkSize = 75, chunkOverlap = 10) {
2857
2856
  }
2858
2857
  currentChunk.push(lines[i]);
2859
2858
  if (currentChunk.length >= chunkSize) {
2860
- const chunkContent = currentChunk.join("\n");
2861
- if (chunkContent.trim().length > 0) {
2862
- const cleanedChunk = linesWithoutComments.slice(chunkStartLine, i + 1).join("\n");
2863
- const imports = extractRenderTags(cleanedChunk);
2864
- chunks.push({
2865
- content: chunkContent,
2866
- metadata: {
2867
- file: filepath,
2868
- startLine: chunkStartLine + 1,
2869
- endLine: i + 1,
2870
- language: "liquid",
2871
- type: "template",
2872
- imports: imports.length > 0 ? imports : void 0
2873
- }
2874
- });
2875
- }
2859
+ const chunk = flushTemplateChunk(currentChunk, chunkStartLine, i + 1, ctx);
2860
+ if (chunk) chunks.push(chunk);
2876
2861
  currentChunk = currentChunk.slice(-chunkOverlap);
2877
2862
  chunkStartLine = Math.max(0, i + 1 - chunkOverlap);
2878
2863
  }
2879
2864
  }
2880
- if (currentChunk.length > 0) {
2881
- const chunkContent = currentChunk.join("\n");
2882
- if (chunkContent.trim().length === 0) {
2883
- return chunks.sort((a, b) => a.metadata.startLine - b.metadata.startLine);
2884
- }
2885
- const cleanedChunk = linesWithoutComments.slice(chunkStartLine, lines.length).join("\n");
2886
- const imports = extractRenderTags(cleanedChunk);
2887
- chunks.push({
2888
- content: chunkContent,
2889
- metadata: {
2890
- file: filepath,
2891
- startLine: chunkStartLine + 1,
2892
- endLine: lines.length,
2893
- language: "liquid",
2894
- type: "template",
2895
- imports: imports.length > 0 ? imports : void 0
2896
- }
2897
- });
2898
- }
2899
- return chunks.sort((a, b) => a.metadata.startLine - b.metadata.startLine);
2865
+ const finalChunk = flushTemplateChunk(currentChunk, chunkStartLine, lines.length, ctx);
2866
+ if (finalChunk) chunks.push(finalChunk);
2867
+ return chunks;
2868
+ }
2869
+ function chunkLiquidFile(filepath, content, chunkSize = 75, chunkOverlap = 10) {
2870
+ const contentWithoutComments = removeComments(content);
2871
+ const ctx = {
2872
+ lines: content.split("\n"),
2873
+ linesWithoutComments: contentWithoutComments.split("\n"),
2874
+ params: { filepath, chunkSize, chunkOverlap }
2875
+ };
2876
+ const blocks = findLiquidBlocks(content);
2877
+ const coveredLines = /* @__PURE__ */ new Set();
2878
+ const blockChunks = blocks.flatMap((block) => processSpecialBlock(block, ctx, coveredLines));
2879
+ const templateChunks = processTemplateContent(ctx, coveredLines);
2880
+ return [...blockChunks, ...templateChunks].sort((a, b) => a.metadata.startLine - b.metadata.startLine);
2900
2881
  }
2901
2882
  var init_liquid_chunker = __esm({
2902
2883
  "src/indexer/liquid-chunker.ts"() {
@@ -6150,176 +6131,241 @@ async function initCommand(options = {}) {
6150
6131
  process.exit(1);
6151
6132
  }
6152
6133
  }
6153
- async function createNewConfig(rootDir, options) {
6154
- showCompactBanner();
6155
- console.log(chalk3.bold("Initializing Lien...\n"));
6156
- console.log(chalk3.dim("\u{1F50D} Detecting frameworks in"), chalk3.bold(rootDir));
6157
- const detections = await detectAllFrameworks(rootDir);
6158
- let frameworks = [];
6159
- if (detections.length === 0) {
6160
- console.log(chalk3.yellow("\n\u26A0\uFE0F No frameworks detected"));
6161
- if (!options.yes) {
6162
- const { useGeneric } = await inquirer.prompt([
6163
- {
6164
- type: "confirm",
6165
- name: "useGeneric",
6166
- message: "Create a generic config (index all supported file types)?",
6167
- default: true
6168
- }
6169
- ]);
6170
- if (!useGeneric) {
6171
- console.log(chalk3.dim("Aborted."));
6172
- return;
6173
- }
6134
+ function createGenericFramework() {
6135
+ return {
6136
+ name: "generic",
6137
+ path: ".",
6138
+ enabled: true,
6139
+ config: {
6140
+ include: ["**/*.{ts,tsx,js,jsx,py,php,go,rs,java,c,cpp,cs}"],
6141
+ exclude: [
6142
+ "**/node_modules/**",
6143
+ "**/dist/**",
6144
+ "**/build/**",
6145
+ "**/.git/**",
6146
+ "**/coverage/**",
6147
+ "**/.next/**",
6148
+ "**/.nuxt/**",
6149
+ "**/vendor/**"
6150
+ ]
6151
+ }
6152
+ };
6153
+ }
6154
+ async function handleNoFrameworksDetected(options) {
6155
+ console.log(chalk3.yellow("\n\u26A0\uFE0F No frameworks detected"));
6156
+ if (!options.yes) {
6157
+ const { useGeneric } = await inquirer.prompt([{
6158
+ type: "confirm",
6159
+ name: "useGeneric",
6160
+ message: "Create a generic config (index all supported file types)?",
6161
+ default: true
6162
+ }]);
6163
+ if (!useGeneric) {
6164
+ console.log(chalk3.dim("Aborted."));
6165
+ return null;
6174
6166
  }
6175
- frameworks.push({
6176
- name: "generic",
6177
- path: ".",
6178
- enabled: true,
6179
- config: {
6180
- include: ["**/*.{ts,tsx,js,jsx,py,php,go,rs,java,c,cpp,cs}"],
6181
- exclude: [
6182
- "**/node_modules/**",
6183
- "**/dist/**",
6184
- "**/build/**",
6185
- "**/.git/**",
6186
- "**/coverage/**",
6187
- "**/.next/**",
6188
- "**/.nuxt/**",
6189
- "**/vendor/**"
6190
- ]
6191
- }
6192
- });
6193
6167
  } else {
6194
- console.log(chalk3.green(`
6168
+ console.log(chalk3.dim("Creating generic config (no frameworks detected)..."));
6169
+ }
6170
+ return [createGenericFramework()];
6171
+ }
6172
+ function displayDetectedFrameworks(detections) {
6173
+ console.log(chalk3.green(`
6195
6174
  \u2713 Found ${detections.length} framework(s):
6196
6175
  `));
6197
- for (const det of detections) {
6198
- const pathDisplay = det.path === "." ? "root" : det.path;
6199
- console.log(chalk3.bold(` ${det.name}`), chalk3.dim(`(${det.confidence} confidence)`));
6200
- console.log(chalk3.dim(` Location: ${pathDisplay}`));
6201
- if (det.evidence.length > 0) {
6202
- det.evidence.forEach((e) => {
6203
- console.log(chalk3.dim(` \u2022 ${e}`));
6204
- });
6205
- }
6206
- console.log();
6207
- }
6208
- if (!options.yes) {
6209
- const { confirm } = await inquirer.prompt([
6210
- {
6211
- type: "confirm",
6212
- name: "confirm",
6213
- message: "Configure these frameworks?",
6214
- default: true
6215
- }
6216
- ]);
6217
- if (!confirm) {
6218
- console.log(chalk3.dim("Aborted."));
6219
- return;
6220
- }
6221
- }
6222
- for (const det of detections) {
6223
- const detector = getFrameworkDetector(det.name);
6224
- if (!detector) {
6225
- console.warn(chalk3.yellow(`\u26A0\uFE0F No detector found for ${det.name}, skipping`));
6226
- continue;
6227
- }
6228
- const frameworkConfig = await detector.generateConfig(rootDir, det.path);
6229
- let shouldCustomize = false;
6230
- if (!options.yes) {
6231
- const { customize } = await inquirer.prompt([
6232
- {
6233
- type: "confirm",
6234
- name: "customize",
6235
- message: `Customize ${det.name} settings?`,
6236
- default: false
6237
- }
6238
- ]);
6239
- shouldCustomize = customize;
6240
- }
6241
- let finalConfig = frameworkConfig;
6242
- if (shouldCustomize) {
6243
- const customized = await promptForCustomization(det.name, frameworkConfig);
6244
- finalConfig = { ...frameworkConfig, ...customized };
6245
- } else {
6246
- const pathDisplay = det.path === "." ? "root" : det.path;
6247
- console.log(chalk3.dim(` \u2192 Using defaults for ${det.name} at ${pathDisplay}`));
6248
- }
6249
- frameworks.push({
6250
- name: det.name,
6251
- path: det.path,
6252
- enabled: true,
6253
- config: finalConfig
6254
- });
6255
- }
6176
+ for (const det of detections) {
6177
+ const pathDisplay = det.path === "." ? "root" : det.path;
6178
+ console.log(chalk3.bold(` ${det.name}`), chalk3.dim(`(${det.confidence} confidence)`));
6179
+ console.log(chalk3.dim(` Location: ${pathDisplay}`));
6180
+ if (det.evidence.length > 0) {
6181
+ det.evidence.forEach((e) => console.log(chalk3.dim(` \u2022 ${e}`)));
6182
+ }
6183
+ console.log();
6184
+ }
6185
+ }
6186
+ async function confirmFrameworkConfiguration(options) {
6187
+ if (options.yes) return true;
6188
+ const { confirm } = await inquirer.prompt([{
6189
+ type: "confirm",
6190
+ name: "confirm",
6191
+ message: "Configure these frameworks?",
6192
+ default: true
6193
+ }]);
6194
+ return confirm;
6195
+ }
6196
+ async function generateSingleFrameworkConfig(det, rootDir, options) {
6197
+ const detector = getFrameworkDetector(det.name);
6198
+ if (!detector) {
6199
+ console.warn(chalk3.yellow(`\u26A0\uFE0F No detector found for ${det.name}, skipping`));
6200
+ return null;
6256
6201
  }
6202
+ const frameworkConfig = await detector.generateConfig(rootDir, det.path);
6203
+ let finalConfig = frameworkConfig;
6204
+ const pathDisplay = det.path === "." ? "root" : det.path;
6257
6205
  if (!options.yes) {
6258
- const { installCursorRules } = await inquirer.prompt([
6259
- {
6260
- type: "confirm",
6261
- name: "installCursorRules",
6262
- message: "Install recommended Cursor rules?",
6263
- default: true
6264
- }
6265
- ]);
6266
- if (installCursorRules) {
6206
+ const { customize } = await inquirer.prompt([{
6207
+ type: "confirm",
6208
+ name: "customize",
6209
+ message: `Customize ${det.name} settings?`,
6210
+ default: false
6211
+ }]);
6212
+ if (customize) {
6213
+ const customized = await promptForCustomization(det.name, frameworkConfig);
6214
+ finalConfig = { ...frameworkConfig, ...customized };
6215
+ } else {
6216
+ console.log(chalk3.dim(` \u2192 Using defaults for ${det.name} at ${pathDisplay}`));
6217
+ }
6218
+ } else {
6219
+ console.log(chalk3.dim(` \u2192 Using defaults for ${det.name} at ${pathDisplay}`));
6220
+ }
6221
+ return {
6222
+ name: det.name,
6223
+ path: det.path,
6224
+ enabled: true,
6225
+ config: finalConfig
6226
+ };
6227
+ }
6228
+ async function handleFrameworksDetected(detections, rootDir, options) {
6229
+ displayDetectedFrameworks(detections);
6230
+ if (!await confirmFrameworkConfiguration(options)) {
6231
+ console.log(chalk3.dim("Aborted."));
6232
+ return null;
6233
+ }
6234
+ const frameworks = [];
6235
+ for (const det of detections) {
6236
+ const framework = await generateSingleFrameworkConfig(det, rootDir, options);
6237
+ if (framework) frameworks.push(framework);
6238
+ }
6239
+ if (frameworks.length === 0) {
6240
+ console.log(chalk3.yellow("\n\u26A0\uFE0F No framework configs could be generated"));
6241
+ return null;
6242
+ }
6243
+ return frameworks;
6244
+ }
6245
+ async function getPathType(filepath) {
6246
+ try {
6247
+ const stats = await fs8.stat(filepath);
6248
+ if (stats.isDirectory()) return "directory";
6249
+ if (stats.isFile()) return "file";
6250
+ return "other";
6251
+ } catch {
6252
+ return "none";
6253
+ }
6254
+ }
6255
+ async function convertRulesFileToDirectory(rulesPath, templatePath) {
6256
+ const existingRules = await fs8.readFile(rulesPath, "utf-8");
6257
+ const parentDir = path8.dirname(rulesPath);
6258
+ const baseName = path8.basename(rulesPath);
6259
+ const tempDir = await fs8.mkdtemp(path8.join(parentDir, baseName + "_tmp_"));
6260
+ const backupPath = rulesPath + ".backup";
6261
+ try {
6262
+ await fs8.writeFile(path8.join(tempDir, "project.mdc"), existingRules);
6263
+ await fs8.copyFile(templatePath, path8.join(tempDir, "lien.mdc"));
6264
+ try {
6265
+ await fs8.unlink(backupPath);
6266
+ } catch {
6267
+ }
6268
+ await fs8.rename(rulesPath, backupPath);
6269
+ try {
6270
+ await fs8.rename(tempDir, rulesPath);
6267
6271
  try {
6268
- const cursorRulesDir = path8.join(rootDir, ".cursor");
6269
- await fs8.mkdir(cursorRulesDir, { recursive: true });
6270
- const templatePath = path8.join(__dirname3, "../CURSOR_RULES_TEMPLATE.md");
6271
- const rulesPath = path8.join(cursorRulesDir, "rules");
6272
- let targetPath;
6273
- let isDirectory = false;
6274
- let isFile = false;
6275
- try {
6276
- const stats = await fs8.stat(rulesPath);
6277
- isDirectory = stats.isDirectory();
6278
- isFile = stats.isFile();
6279
- } catch {
6280
- }
6281
- if (isDirectory) {
6282
- targetPath = path8.join(rulesPath, "lien.mdc");
6283
- await fs8.copyFile(templatePath, targetPath);
6284
- console.log(chalk3.green("\u2713 Installed Cursor rules as .cursor/rules/lien.mdc"));
6285
- } else if (isFile) {
6286
- const { convertToDir } = await inquirer.prompt([
6287
- {
6288
- type: "confirm",
6289
- name: "convertToDir",
6290
- message: "Existing .cursor/rules file found. Convert to directory and preserve your rules?",
6291
- default: true
6292
- }
6293
- ]);
6294
- if (convertToDir) {
6295
- const existingRules = await fs8.readFile(rulesPath, "utf-8");
6296
- await fs8.unlink(rulesPath);
6297
- await fs8.mkdir(rulesPath);
6298
- await fs8.writeFile(path8.join(rulesPath, "project.mdc"), existingRules);
6299
- await fs8.copyFile(templatePath, path8.join(rulesPath, "lien.mdc"));
6300
- console.log(chalk3.green("\u2713 Converted .cursor/rules to directory"));
6301
- console.log(chalk3.green(" - Your project rules: .cursor/rules/project.mdc"));
6302
- console.log(chalk3.green(" - Lien rules: .cursor/rules/lien.mdc"));
6303
- } else {
6304
- console.log(chalk3.dim("Skipped Cursor rules installation (preserving existing file)"));
6305
- }
6306
- } else {
6307
- await fs8.mkdir(rulesPath, { recursive: true });
6308
- targetPath = path8.join(rulesPath, "lien.mdc");
6309
- await fs8.copyFile(templatePath, targetPath);
6310
- console.log(chalk3.green("\u2713 Installed Cursor rules as .cursor/rules/lien.mdc"));
6311
- }
6312
- } catch (error) {
6313
- console.log(chalk3.yellow("\u26A0\uFE0F Could not install Cursor rules"));
6314
- console.log(chalk3.dim(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
6315
- console.log(chalk3.dim("You can manually copy CURSOR_RULES_TEMPLATE.md to .cursor/rules/lien.mdc"));
6272
+ await fs8.unlink(backupPath);
6273
+ } catch {
6274
+ console.log(chalk3.yellow("\u26A0\uFE0F Could not remove backup file, but conversion succeeded"));
6275
+ console.log(chalk3.dim(`Backup file: ${backupPath}`));
6316
6276
  }
6277
+ } catch (renameErr) {
6278
+ try {
6279
+ await fs8.rename(backupPath, rulesPath);
6280
+ } catch (restoreErr) {
6281
+ console.log(chalk3.red("\u274C Failed to restore original .cursor/rules from backup after failed conversion."));
6282
+ console.log(chalk3.red(` - Original error: ${renameErr instanceof Error ? renameErr.message : renameErr}`));
6283
+ console.log(chalk3.red(` - Restore error: ${restoreErr instanceof Error ? restoreErr.message : restoreErr}`));
6284
+ console.log(chalk3.red(` - Backup file location: ${backupPath}`));
6285
+ throw new Error("Failed to convert .cursor/rules to directory and failed to restore from backup. Manual recovery needed.");
6286
+ }
6287
+ throw renameErr;
6288
+ }
6289
+ console.log(chalk3.green("\u2713 Converted .cursor/rules to directory"));
6290
+ console.log(chalk3.green(" - Your project rules: .cursor/rules/project.mdc"));
6291
+ console.log(chalk3.green(" - Lien rules: .cursor/rules/lien.mdc"));
6292
+ } catch (err) {
6293
+ try {
6294
+ await fs8.rm(tempDir, { recursive: true, force: true });
6295
+ } catch {
6317
6296
  }
6297
+ throw err;
6298
+ }
6299
+ }
6300
+ async function handleExistingRulesDirectory(rulesPath, templatePath) {
6301
+ const targetPath = path8.join(rulesPath, "lien.mdc");
6302
+ try {
6303
+ await fs8.access(targetPath);
6304
+ console.log(chalk3.dim("lien.mdc already exists in .cursor/rules/, skipping..."));
6305
+ return;
6306
+ } catch {
6307
+ }
6308
+ await fs8.copyFile(templatePath, targetPath);
6309
+ console.log(chalk3.green("\u2713 Installed Cursor rules as .cursor/rules/lien.mdc"));
6310
+ }
6311
+ async function handleExistingRulesFile(rulesPath, templatePath, options) {
6312
+ if (options.yes) {
6313
+ console.log(chalk3.dim("Skipped Cursor rules installation (preserving existing .cursor/rules file)"));
6314
+ return;
6318
6315
  }
6319
- const config = {
6320
- ...defaultConfig,
6321
- frameworks
6316
+ const { convertToDir } = await inquirer.prompt([{
6317
+ type: "confirm",
6318
+ name: "convertToDir",
6319
+ message: "Existing .cursor/rules file found. Convert to directory and preserve your rules?",
6320
+ default: true
6321
+ }]);
6322
+ if (convertToDir) {
6323
+ await convertRulesFileToDirectory(rulesPath, templatePath);
6324
+ } else {
6325
+ console.log(chalk3.dim("Skipped Cursor rules installation (preserving existing file)"));
6326
+ }
6327
+ }
6328
+ async function handleInvalidRulesPath() {
6329
+ console.log(chalk3.yellow("\u26A0\uFE0F .cursor/rules exists but is not a regular file or directory"));
6330
+ console.log(chalk3.dim("Skipped Cursor rules installation"));
6331
+ }
6332
+ async function handleFreshRulesInstall(rulesPath, templatePath) {
6333
+ await fs8.mkdir(rulesPath, { recursive: true });
6334
+ await fs8.copyFile(templatePath, path8.join(rulesPath, "lien.mdc"));
6335
+ console.log(chalk3.green("\u2713 Installed Cursor rules as .cursor/rules/lien.mdc"));
6336
+ }
6337
+ async function installCursorRulesFiles(rootDir, options) {
6338
+ const cursorRulesDir = path8.join(rootDir, ".cursor");
6339
+ await fs8.mkdir(cursorRulesDir, { recursive: true });
6340
+ const templatePath = path8.join(__dirname3, "../CURSOR_RULES_TEMPLATE.md");
6341
+ const rulesPath = path8.join(cursorRulesDir, "rules");
6342
+ const pathType = await getPathType(rulesPath);
6343
+ const handlers = {
6344
+ directory: () => handleExistingRulesDirectory(rulesPath, templatePath),
6345
+ file: () => handleExistingRulesFile(rulesPath, templatePath, options),
6346
+ other: () => handleInvalidRulesPath(),
6347
+ none: () => handleFreshRulesInstall(rulesPath, templatePath)
6322
6348
  };
6349
+ await handlers[pathType]();
6350
+ }
6351
+ async function promptAndInstallCursorRules(rootDir, options) {
6352
+ const shouldInstall = options.yes || (await inquirer.prompt([{
6353
+ type: "confirm",
6354
+ name: "installCursorRules",
6355
+ message: "Install recommended Cursor rules?",
6356
+ default: true
6357
+ }])).installCursorRules;
6358
+ if (!shouldInstall) return;
6359
+ try {
6360
+ await installCursorRulesFiles(rootDir, options);
6361
+ } catch (error) {
6362
+ console.log(chalk3.yellow("\u26A0\uFE0F Could not install Cursor rules"));
6363
+ console.log(chalk3.dim(`Error: ${error instanceof Error ? error.message : "Unknown error"}`));
6364
+ console.log(chalk3.dim("You can manually copy CURSOR_RULES_TEMPLATE.md to .cursor/rules/lien.mdc"));
6365
+ }
6366
+ }
6367
+ async function writeConfigAndShowSuccess(rootDir, frameworks) {
6368
+ const config = { ...defaultConfig, frameworks };
6323
6369
  const configPath = path8.join(rootDir, ".lien.config.json");
6324
6370
  await fs8.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
6325
6371
  console.log(chalk3.green("\n\u2713 Created .lien.config.json"));
@@ -6329,6 +6375,16 @@ async function createNewConfig(rootDir, options) {
6329
6375
  console.log(chalk3.dim(" 2. Run"), chalk3.bold("lien serve"), chalk3.dim("to start the MCP server"));
6330
6376
  console.log(chalk3.dim(" 3. Configure Cursor to use the MCP server (see README.md)"));
6331
6377
  }
6378
+ async function createNewConfig(rootDir, options) {
6379
+ showCompactBanner();
6380
+ console.log(chalk3.bold("Initializing Lien...\n"));
6381
+ console.log(chalk3.dim("\u{1F50D} Detecting frameworks in"), chalk3.bold(rootDir));
6382
+ const detections = await detectAllFrameworks(rootDir);
6383
+ const frameworks = detections.length === 0 ? await handleNoFrameworksDetected(options) : await handleFrameworksDetected(detections, rootDir, options);
6384
+ if (!frameworks) return;
6385
+ await promptAndInstallCursorRules(rootDir, options);
6386
+ await writeConfigAndShowSuccess(rootDir, frameworks);
6387
+ }
6332
6388
  async function promptForCustomization(frameworkName, config) {
6333
6389
  console.log(chalk3.bold(`
6334
6390
  Customizing ${frameworkName} settings:`));
@@ -6480,7 +6536,7 @@ async function indexCommand(options) {
6480
6536
  // src/cli/serve.ts
6481
6537
  import chalk7 from "chalk";
6482
6538
  import fs20 from "fs/promises";
6483
- import path21 from "path";
6539
+ import path22 from "path";
6484
6540
 
6485
6541
  // src/mcp/server.ts
6486
6542
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -6793,8 +6849,8 @@ async function handleFindSimilar(args, ctx) {
6793
6849
  }
6794
6850
 
6795
6851
  // src/mcp/utils/path-matching.ts
6796
- function normalizePath(path23, workspaceRoot) {
6797
- let normalized = path23.replace(/['"]/g, "").trim().replace(/\\/g, "/");
6852
+ function normalizePath(path24, workspaceRoot) {
6853
+ let normalized = path24.replace(/['"]/g, "").trim().replace(/\\/g, "/");
6798
6854
  normalized = normalized.replace(/\.(ts|tsx|js|jsx)$/, "");
6799
6855
  if (normalized.startsWith(workspaceRoot + "/")) {
6800
6856
  normalized = normalized.substring(workspaceRoot.length + 1);
@@ -6890,10 +6946,10 @@ async function handleGetFilesContext(args, ctx) {
6890
6946
  log(`Scanned ${SCAN_LIMIT} chunks (limit reached). Test associations may be incomplete for large codebases.`, "warning");
6891
6947
  }
6892
6948
  const pathCache = /* @__PURE__ */ new Map();
6893
- const normalizePathCached = (path23) => {
6894
- if (pathCache.has(path23)) return pathCache.get(path23);
6895
- const normalized = normalizePath(path23, workspaceRoot);
6896
- pathCache.set(path23, normalized);
6949
+ const normalizePathCached = (path24) => {
6950
+ if (pathCache.has(path24)) return pathCache.get(path24);
6951
+ const normalized = normalizePath(path24, workspaceRoot);
6952
+ pathCache.set(path24, normalized);
6897
6953
  return normalized;
6898
6954
  };
6899
6955
  const testAssociationsMap = filepaths.map((filepath) => {
@@ -7134,9 +7190,9 @@ async function handleGetDependents(args, ctx) {
7134
7190
  log(`Scanning ${allChunks.length} chunks for imports...`);
7135
7191
  const workspaceRoot = process.cwd().replace(/\\/g, "/");
7136
7192
  const pathCache = /* @__PURE__ */ new Map();
7137
- const normalizePathCached = (path23) => {
7138
- if (!pathCache.has(path23)) pathCache.set(path23, normalizePath(path23, workspaceRoot));
7139
- return pathCache.get(path23);
7193
+ const normalizePathCached = (path24) => {
7194
+ if (!pathCache.has(path24)) pathCache.set(path24, normalizePath(path24, workspaceRoot));
7195
+ return pathCache.get(path24);
7140
7196
  };
7141
7197
  const importIndex = buildImportIndex(allChunks, normalizePathCached);
7142
7198
  const normalizedTarget = normalizePathCached(validatedArgs.filepath);
@@ -7202,11 +7258,11 @@ var COMPLEXITY_THRESHOLDS2 = {
7202
7258
  };
7203
7259
  function createPathNormalizer(workspaceRoot) {
7204
7260
  const cache = /* @__PURE__ */ new Map();
7205
- return (path23) => {
7206
- const cached = cache.get(path23);
7261
+ return (path24) => {
7262
+ const cached = cache.get(path24);
7207
7263
  if (cached !== void 0) return cached;
7208
- const normalized = normalizePath(path23, workspaceRoot);
7209
- cache.set(path23, normalized);
7264
+ const normalized = normalizePath(path24, workspaceRoot);
7265
+ cache.set(path24, normalized);
7210
7266
  return normalized;
7211
7267
  };
7212
7268
  }
@@ -7717,6 +7773,7 @@ init_utils();
7717
7773
  // src/watcher/index.ts
7718
7774
  init_schema();
7719
7775
  import chokidar from "chokidar";
7776
+ import path21 from "path";
7720
7777
  var FileWatcher = class {
7721
7778
  watcher = null;
7722
7779
  debounceTimers = /* @__PURE__ */ new Map();
@@ -7755,9 +7812,12 @@ var FileWatcher = class {
7755
7812
  persistent: true,
7756
7813
  ignoreInitial: true,
7757
7814
  // Don't trigger for existing files
7815
+ // Handle atomic saves from modern editors (VS Code, Sublime, etc.)
7816
+ // Editors write to temp file then rename - without this, we get unlink+add instead of change
7817
+ atomic: true,
7758
7818
  awaitWriteFinish: {
7759
- stabilityThreshold: 500,
7760
- // Wait 500ms for file to stop changing
7819
+ stabilityThreshold: 300,
7820
+ // Reduced from 500ms for faster detection
7761
7821
  pollInterval: 100
7762
7822
  },
7763
7823
  // Performance optimizations
@@ -7786,7 +7846,7 @@ var FileWatcher = class {
7786
7846
  const timer = setTimeout(() => {
7787
7847
  this.debounceTimers.delete(filepath);
7788
7848
  if (this.onChangeHandler) {
7789
- const absolutePath = filepath.startsWith("/") ? filepath : `${this.rootDir}/${filepath}`;
7849
+ const absolutePath = path21.isAbsolute(filepath) ? filepath : path21.join(this.rootDir, filepath);
7790
7850
  try {
7791
7851
  const result = this.onChangeHandler({
7792
7852
  type,
@@ -8061,7 +8121,7 @@ async function startMCPServer(options) {
8061
8121
 
8062
8122
  // src/cli/serve.ts
8063
8123
  async function serveCommand(options) {
8064
- const rootDir = options.root ? path21.resolve(options.root) : process.cwd();
8124
+ const rootDir = options.root ? path22.resolve(options.root) : process.cwd();
8065
8125
  try {
8066
8126
  if (options.root) {
8067
8127
  try {
@@ -8109,7 +8169,7 @@ init_lancedb();
8109
8169
  init_service();
8110
8170
  import chalk9 from "chalk";
8111
8171
  import fs21 from "fs";
8112
- import path22 from "path";
8172
+ import path23 from "path";
8113
8173
 
8114
8174
  // src/insights/formatters/text.ts
8115
8175
  import chalk8 from "chalk";
@@ -8384,7 +8444,7 @@ function validateFormat(format) {
8384
8444
  function validateFilesExist(files, rootDir) {
8385
8445
  if (!files || files.length === 0) return;
8386
8446
  const missingFiles = files.filter((file) => {
8387
- const fullPath = path22.isAbsolute(file) ? file : path22.join(rootDir, file);
8447
+ const fullPath = path23.isAbsolute(file) ? file : path23.join(rootDir, file);
8388
8448
  return !fs21.existsSync(fullPath);
8389
8449
  });
8390
8450
  if (missingFiles.length > 0) {