@aramassa/ai-rules 0.4.0 → 0.4.3

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/cli.js CHANGED
@@ -17904,7 +17904,7 @@ class ContentTracker {
17904
17904
  * Generates a hash for the given content
17905
17905
  */
17906
17906
  generateContentHash(content) {
17907
- return createHash('sha256').update(content.trim()).digest('hex');
17907
+ return createHash("sha256").update(content.trim()).digest("hex");
17908
17908
  }
17909
17909
  /**
17910
17910
  * Checks if the same content has already been written to the file
@@ -17949,7 +17949,7 @@ function findPackageRoot() {
17949
17949
  let dir = path.dirname(currentFile);
17950
17950
  // Walk up directory tree looking for package.json
17951
17951
  while (dir !== path.dirname(dir)) {
17952
- const packageJsonPath = path.join(dir, 'package.json');
17952
+ const packageJsonPath = path.join(dir, "package.json");
17953
17953
  try {
17954
17954
  // Use synchronous check since this is initialization
17955
17955
  fsSync.accessSync(packageJsonPath);
@@ -17973,17 +17973,49 @@ function resolveDefaultSrcDir(providedSrc) {
17973
17973
  }
17974
17974
  // Find package root and return artifact/ directory
17975
17975
  const packageRoot = findPackageRoot();
17976
- return path.join(packageRoot, 'artifact');
17976
+ return path.join(packageRoot, "artifact");
17977
17977
  }
17978
17978
  /**
17979
- * Resolves source directory for a recipe item following priority order:
17979
+ * Resolves source directory for a recipe item following priority order (Issue #197):
17980
17980
  * 1. CLI --src option (highest priority)
17981
- * 2. Recipe item src field (medium priority)
17982
- * 3. Import-level src field (medium priority)
17981
+ * 2. Import-level src field (overrides item-level src)
17982
+ * 3. Recipe item src field (lower priority - resolved relative to recipe file or import src)
17983
17983
  * 4. Default package artifact/ directory (lowest priority)
17984
17984
  */
17985
- function resolveItemSrcDir(cliSrc, itemSrc, importSrc) {
17986
- return cliSrc || itemSrc || importSrc || resolveDefaultSrcDir();
17985
+ function resolveItemSrcDir(cliSrc, itemSrc, importSrc, recipePath) {
17986
+ if (cliSrc) {
17987
+ return cliSrc;
17988
+ }
17989
+ // Per Issue #197: import-level src should override item-level src
17990
+ if (importSrc) {
17991
+ // If itemSrc is specified and relative, resolve it relative to importSrc
17992
+ if (itemSrc && !path.isAbsolute(itemSrc)) {
17993
+ // If importSrc is absolute, resolve itemSrc relative to it
17994
+ if (path.isAbsolute(importSrc)) {
17995
+ return path.resolve(importSrc, itemSrc);
17996
+ }
17997
+ // If importSrc is also relative, we need to resolve it first
17998
+ if (recipePath) {
17999
+ const resolvedImportSrc = path.resolve(path.dirname(recipePath), importSrc);
18000
+ return path.resolve(resolvedImportSrc, itemSrc);
18001
+ }
18002
+ }
18003
+ // Use importSrc as-is (either no itemSrc, or itemSrc is absolute and ignored)
18004
+ return importSrc;
18005
+ }
18006
+ if (itemSrc) {
18007
+ // If itemSrc is absolute, use it as-is
18008
+ if (path.isAbsolute(itemSrc)) {
18009
+ return itemSrc;
18010
+ }
18011
+ // If itemSrc is relative and we have a recipe path, resolve relative to recipe file directory
18012
+ if (recipePath) {
18013
+ return path.resolve(path.dirname(recipePath), itemSrc);
18014
+ }
18015
+ // Fallback to current working directory (existing behavior)
18016
+ return itemSrc;
18017
+ }
18018
+ return resolveDefaultSrcDir();
17987
18019
  }
17988
18020
  /**
17989
18021
  * Resolves output path with baseDir support following priority order:
@@ -17995,7 +18027,7 @@ function resolveItemSrcDir(cliSrc, itemSrc, importSrc) {
17995
18027
  function resolveOutputPath(itemOut, cliBaseDir, importBaseDir, recipeBaseDir, recipePath) {
17996
18028
  // Check if the itemOut contains environment variables or tilde - if so, try to expand
17997
18029
  let processedItemOut = itemOut;
17998
- if (itemOut.includes('$') || itemOut.startsWith('~')) {
18030
+ if (itemOut.includes("$") || itemOut.startsWith("~")) {
17999
18031
  try {
18000
18032
  processedItemOut = resolvePath(itemOut);
18001
18033
  // If expansion resulted in an absolute path, use it as-is (ignore baseDir)
@@ -18021,10 +18053,10 @@ function resolveOutputPath(itemOut, cliBaseDir, importBaseDir, recipeBaseDir, re
18021
18053
  }
18022
18054
  // Existing behavior: resolve relative to recipe file directory or current directory
18023
18055
  // For preset files (located in presets/ directory), resolve relative to project root instead
18024
- let baseDirectory = '.';
18056
+ let baseDirectory = ".";
18025
18057
  if (recipePath) {
18026
18058
  const packageRoot = findPackageRoot();
18027
- const presetsDir = path.join(packageRoot, 'presets');
18059
+ const presetsDir = path.join(packageRoot, "presets");
18028
18060
  // If the recipe is in the presets directory, use project root as base
18029
18061
  if (path.dirname(recipePath) === presetsDir) {
18030
18062
  baseDirectory = process.cwd();
@@ -18038,10 +18070,10 @@ function resolveOutputPath(itemOut, cliBaseDir, importBaseDir, recipeBaseDir, re
18038
18070
  }
18039
18071
  function resolveRecipePath(recipePath) {
18040
18072
  // If recipe starts with ':', resolve to package preset
18041
- if (recipePath.startsWith(':')) {
18073
+ if (recipePath.startsWith(":")) {
18042
18074
  const presetName = recipePath.slice(1); // Remove the ':'
18043
18075
  const packageRoot = findPackageRoot();
18044
- return path.join(packageRoot, 'presets', `${presetName}.yaml`);
18076
+ return path.join(packageRoot, "presets", `${presetName}.yaml`);
18045
18077
  }
18046
18078
  // Otherwise, use the path as-is
18047
18079
  return recipePath;
@@ -18061,8 +18093,8 @@ async function validateRecipeFilesExist(recipePaths) {
18061
18093
  }
18062
18094
  }
18063
18095
  if (missingFiles.length > 0) {
18064
- const errorMessages = missingFiles.map(file => {
18065
- if (file.startsWith(':')) {
18096
+ const errorMessages = missingFiles.map((file) => {
18097
+ if (file.startsWith(":")) {
18066
18098
  const presetName = file.slice(1);
18067
18099
  return `Preset ':${presetName}' not found. Use 'ai-rules presets' to list available presets.`;
18068
18100
  }
@@ -18074,7 +18106,7 @@ async function validateRecipeFilesExist(recipePaths) {
18074
18106
  throw new Error(errorMessages[0]);
18075
18107
  }
18076
18108
  else {
18077
- throw new Error(`Multiple recipe files not found:\n${errorMessages.join('\n')}`);
18109
+ throw new Error(`Multiple recipe files not found:\n${errorMessages.join("\n")}`);
18078
18110
  }
18079
18111
  }
18080
18112
  }
@@ -18083,7 +18115,7 @@ async function validateRecipeFilesExist(recipePaths) {
18083
18115
  */
18084
18116
  function resolveImportPath(importPath, currentRecipePath) {
18085
18117
  // If import starts with ':', resolve to package preset
18086
- if (importPath.startsWith(':')) {
18118
+ if (importPath.startsWith(":")) {
18087
18119
  return resolveRecipePath(importPath);
18088
18120
  }
18089
18121
  // Otherwise, resolve relative to current recipe file directory
@@ -18109,8 +18141,8 @@ async function expandRecipeImports(items, currentPath, debugLogger, visited = ne
18109
18141
  // Check for circular imports
18110
18142
  if (visited.has(resolvedImportPath)) {
18111
18143
  const visitedArray = Array.from(visited);
18112
- debugLogger?.log(`Circular import detected in path: ${visitedArray.join(' -> ')} -> ${resolvedImportPath}`);
18113
- throw new Error(`Circular import detected: ${visitedArray.join(' -> ')} -> ${resolvedImportPath}`);
18144
+ debugLogger?.log(`Circular import detected in path: ${visitedArray.join(" -> ")} -> ${resolvedImportPath}`);
18145
+ throw new Error(`Circular import detected: ${visitedArray.join(" -> ")} -> ${resolvedImportPath}`);
18114
18146
  }
18115
18147
  // Check for duplicate imports and skip if already imported
18116
18148
  if (imported.has(resolvedImportPath)) {
@@ -18152,18 +18184,25 @@ async function expandRecipeImports(items, currentPath, debugLogger, visited = ne
18152
18184
  const expandedImported = await expandRecipeImports(importData.recipe, resolvedImportPath, debugLogger, newVisited, imported, depth + 1);
18153
18185
  debugLogger?.timeEnd(`Expanding nested imports in: ${resolvedImportPath}`);
18154
18186
  debugLogger?.log(`Nested expansion yielded ${expandedImported.length} items`);
18155
- // Apply import-level configurations to all expanded items
18187
+ // Apply import-level configurations to all expanded items
18188
+ expandedImported.forEach((expandedItem) => {
18189
+ // Ensure _recipePath is set for proper path resolution
18190
+ if (!expandedItem._recipePath) {
18191
+ expandedItem._recipePath = resolvedImportPath;
18192
+ debugLogger?.log(`Tagged item '${expandedItem.title || expandedItem.out || "untitled"}' with recipe path: ${resolvedImportPath}`);
18193
+ }
18194
+ });
18156
18195
  if (importLevelBaseDir || importLevelSrc || importLevelVariables) {
18157
- expandedImported.forEach(expandedItem => {
18196
+ expandedImported.forEach((expandedItem) => {
18158
18197
  // Apply baseDir if specified (only if not already set to preserve nested import priority)
18159
18198
  if (importLevelBaseDir && !expandedItem._importBaseDir) {
18160
18199
  expandedItem._importBaseDir = importLevelBaseDir;
18161
- debugLogger?.log(`Tagged item '${expandedItem.title || expandedItem.out || 'untitled'}' with import baseDir: ${importLevelBaseDir}`);
18200
+ debugLogger?.log(`Tagged item '${expandedItem.title || expandedItem.out || "untitled"}' with import baseDir: ${importLevelBaseDir}`);
18162
18201
  }
18163
18202
  // Apply src if specified (only if not already set to preserve nested import priority)
18164
18203
  if (importLevelSrc && !expandedItem._importSrc) {
18165
18204
  expandedItem._importSrc = importLevelSrc;
18166
- debugLogger?.log(`Tagged item '${expandedItem.title || expandedItem.out || 'untitled'}' with import src: ${importLevelSrc}`);
18205
+ debugLogger?.log(`Tagged item '${expandedItem.title || expandedItem.out || "untitled"}' with import src: ${importLevelSrc}`);
18167
18206
  }
18168
18207
  // Apply variables if specified (merge with existing variables, with import-level taking priority)
18169
18208
  if (importLevelVariables) {
@@ -18174,10 +18213,10 @@ async function expandRecipeImports(items, currentPath, debugLogger, visited = ne
18174
18213
  // Merge variables, with import-level taking priority over nested imports
18175
18214
  expandedItem._importVariables = {
18176
18215
  ...expandedItem._importVariables,
18177
- ...importLevelVariables
18216
+ ...importLevelVariables,
18178
18217
  };
18179
18218
  }
18180
- debugLogger?.log(`Tagged item '${expandedItem.title || expandedItem.out || 'untitled'}' with import variables:`, expandedItem._importVariables);
18219
+ debugLogger?.log(`Tagged item '${expandedItem.title || expandedItem.out || "untitled"}' with import variables:`, expandedItem._importVariables);
18181
18220
  }
18182
18221
  });
18183
18222
  }
@@ -18188,10 +18227,10 @@ async function expandRecipeImports(items, currentPath, debugLogger, visited = ne
18188
18227
  // Remove from imported set if there was an error to allow retry
18189
18228
  imported.delete(resolvedImportPath);
18190
18229
  debugLogger?.log(`Error processing import ${resolvedImportPath}:`, error);
18191
- if (item.import.startsWith(':')) {
18230
+ if (item.import.startsWith(":")) {
18192
18231
  const presetName = item.import.slice(1);
18193
18232
  // Only treat ENOENT errors as "preset not found"
18194
- if (error instanceof Error && error.message.includes('ENOENT')) {
18233
+ if (error instanceof Error && error.message.includes("ENOENT")) {
18195
18234
  throw new Error(`Preset ':${presetName}' not found. Use 'ai-rules presets' to list available presets.`);
18196
18235
  }
18197
18236
  // For other errors, add context but preserve the original error message
@@ -18200,7 +18239,7 @@ async function expandRecipeImports(items, currentPath, debugLogger, visited = ne
18200
18239
  }
18201
18240
  throw error;
18202
18241
  }
18203
- if (error instanceof Error && error.message.includes('ENOENT')) {
18242
+ if (error instanceof Error && error.message.includes("ENOENT")) {
18204
18243
  throw new Error(`Import file '${item.import}' not found (resolved to: ${resolvedImportPath})`);
18205
18244
  }
18206
18245
  throw error;
@@ -18208,8 +18247,13 @@ async function expandRecipeImports(items, currentPath, debugLogger, visited = ne
18208
18247
  }
18209
18248
  else {
18210
18249
  // This is a regular recipe item
18211
- debugLogger?.log(`Adding regular recipe item: ${item.title || 'untitled'}`);
18212
- expanded.push(item);
18250
+ debugLogger?.log(`Adding regular recipe item: ${item.title || "untitled"}`);
18251
+ // Tag the item with the recipe path it originated from
18252
+ const taggedItem = { ...item };
18253
+ if (!taggedItem._recipePath) {
18254
+ taggedItem._recipePath = currentPath;
18255
+ }
18256
+ expanded.push(taggedItem);
18213
18257
  }
18214
18258
  }
18215
18259
  debugLogger?.log(`Import expansion complete: ${expanded.length} total items after processing`);
@@ -18221,43 +18265,43 @@ async function expandRecipeImports(items, currentPath, debugLogger, visited = ne
18221
18265
  function setupProgram() {
18222
18266
  const program = new Command();
18223
18267
  program
18224
- .name('ai-rules')
18225
- .description('CLI for extracting and analyzing markdown files')
18226
- .version('0.0.1');
18268
+ .name("ai-rules")
18269
+ .description("CLI for extracting and analyzing markdown files")
18270
+ .version("0.0.1");
18227
18271
  // Extract command
18228
18272
  program
18229
- .command('extract')
18230
- .description('Extract and merge markdown files')
18231
- .option('--src <path>', 'Source directory')
18232
- .option('--out <path>', 'Output file')
18233
- .option('--type <types>', 'Filter by type (comma-separated)')
18234
- .option('--language <languages>', 'Filter by language (comma-separated)')
18235
- .option('--attr <attributes>', 'Filter by attributes (comma-separated)')
18236
- .option('--title <title>', 'Title for the output')
18237
- .option('--mode <mode>', 'Write mode: append, prepend, overwrite', 'overwrite')
18238
- .option('--recipe <path>', 'Recipe file path or package preset (e.g., :typescript). Can be specified multiple times or comma-separated.', collectRecipeOptions, [])
18239
- .option('--base-dir <path>', 'Base directory for output files (supports ~ and environment variable expansion)')
18240
- .option('--vars <variables>', 'Template variables in key=value format (comma-separated)')
18241
- .option('--env-file <path>', 'Path to .env file for template variables')
18242
- .option('--debug', 'Enable debug logging')
18243
- .option('--verbose', 'Enable verbose logging (alias for --debug)')
18244
- .option('--dry-run', 'Preview input/output files without writing')
18273
+ .command("extract")
18274
+ .description("Extract and merge markdown files")
18275
+ .option("--src <path>", "Source directory")
18276
+ .option("--out <path>", "Output file")
18277
+ .option("--type <types>", "Filter by type (comma-separated)")
18278
+ .option("--language <languages>", "Filter by language (comma-separated)")
18279
+ .option("--attr <attributes>", "Filter by attributes (comma-separated)")
18280
+ .option("--title <title>", "Title for the output")
18281
+ .option("--mode <mode>", "Write mode: append, prepend, overwrite", "overwrite")
18282
+ .option("--recipe <path>", "Recipe file path or package preset (e.g., :typescript). Can be specified multiple times or comma-separated.", collectRecipeOptions, [])
18283
+ .option("--base-dir <path>", "Base directory for output files (supports ~ and environment variable expansion)")
18284
+ .option("--vars <variables>", "Template variables in key=value format (comma-separated)")
18285
+ .option("--env-file <path>", "Path to .env file for template variables")
18286
+ .option("--debug", "Enable debug logging")
18287
+ .option("--verbose", "Enable verbose logging (alias for --debug)")
18288
+ .option("--dry-run", "Preview input/output files without writing")
18245
18289
  .action(async (options) => {
18246
18290
  await handleExtractCommand(options);
18247
18291
  });
18248
- // Stats command
18292
+ // Stats command
18249
18293
  program
18250
- .command('stats')
18251
- .description('Generate statistics from markdown files')
18252
- .option('--src <path>', 'Source directory')
18253
- .option('--variables', 'Show available template variables')
18294
+ .command("stats")
18295
+ .description("Generate statistics from markdown files")
18296
+ .option("--src <path>", "Source directory")
18297
+ .option("--variables", "Show available template variables")
18254
18298
  .action(async (options) => {
18255
18299
  await handleStatsCommand(options);
18256
18300
  });
18257
18301
  // Presets command
18258
18302
  program
18259
- .command('presets')
18260
- .description('List available package presets')
18303
+ .command("presets")
18304
+ .description("List available package presets")
18261
18305
  .action(async () => {
18262
18306
  await listPresets();
18263
18307
  });
@@ -18277,7 +18321,8 @@ async function findYamlFilesRecursively(dir, baseDir = dir) {
18277
18321
  const subFiles = await findYamlFilesRecursively(fullPath, baseDir);
18278
18322
  yamlFiles.push(...subFiles);
18279
18323
  }
18280
- else if (item.isFile() && (item.name.endsWith('.yaml') || item.name.endsWith('.yml'))) {
18324
+ else if (item.isFile() &&
18325
+ (item.name.endsWith(".yaml") || item.name.endsWith(".yml"))) {
18281
18326
  // Get relative path from base directory
18282
18327
  const relativePath = path.relative(baseDir, fullPath);
18283
18328
  yamlFiles.push(relativePath);
@@ -18295,25 +18340,25 @@ async function findYamlFilesRecursively(dir, baseDir = dir) {
18295
18340
  */
18296
18341
  async function listPresets() {
18297
18342
  const packageRoot = findPackageRoot();
18298
- const presetsDir = path.join(packageRoot, 'presets');
18343
+ const presetsDir = path.join(packageRoot, "presets");
18299
18344
  try {
18300
18345
  const yamlFiles = await findYamlFilesRecursively(presetsDir);
18301
18346
  if (yamlFiles.length === 0) {
18302
18347
  // eslint-disable-next-line no-console
18303
- console.log('No presets found in package.');
18348
+ console.log("No presets found in package.");
18304
18349
  return;
18305
18350
  }
18306
18351
  // eslint-disable-next-line no-console
18307
- console.log('Available presets:');
18308
- yamlFiles.forEach(file => {
18309
- const presetName = file.replace(/\.(yaml|yml)$/, '');
18352
+ console.log("Available presets:");
18353
+ yamlFiles.forEach((file) => {
18354
+ const presetName = file.replace(/\.(yaml|yml)$/, "");
18310
18355
  // eslint-disable-next-line no-console
18311
18356
  console.log(` :${presetName}`);
18312
18357
  });
18313
18358
  }
18314
18359
  catch (error) {
18315
18360
  // eslint-disable-next-line no-console
18316
- console.log('No presets directory found in package.');
18361
+ console.log("No presets directory found in package.");
18317
18362
  }
18318
18363
  }
18319
18364
  /**
@@ -18321,23 +18366,26 @@ async function listPresets() {
18321
18366
  */
18322
18367
  function collectRecipeOptions(value, previous) {
18323
18368
  // Split comma-separated values and add to the accumulated array
18324
- const newValues = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
18369
+ const newValues = value
18370
+ .split(",")
18371
+ .map((v) => v.trim())
18372
+ .filter((v) => v.length > 0);
18325
18373
  return previous.concat(newValues);
18326
18374
  }
18327
18375
  /**
18328
18376
  * Parses comma-separated values into array
18329
18377
  */
18330
18378
  function parseCommaSeparated(value) {
18331
- return value ? value.split(',') : undefined;
18379
+ return value ? value.split(",") : undefined;
18332
18380
  }
18333
18381
  /**
18334
18382
  * Validates and converts mode string to WriteMode enum
18335
18383
  */
18336
18384
  function parseWriteMode(mode) {
18337
18385
  const modeMap = {
18338
- 'append': WriteMode.APPEND,
18339
- 'prepend': WriteMode.PREPEND,
18340
- 'overwrite': WriteMode.OVERWRITE
18386
+ append: WriteMode.APPEND,
18387
+ prepend: WriteMode.PREPEND,
18388
+ overwrite: WriteMode.OVERWRITE,
18341
18389
  };
18342
18390
  return modeMap[mode] || WriteMode.OVERWRITE;
18343
18391
  }
@@ -18352,10 +18400,10 @@ function convertFiltersToAttrFilters(filters) {
18352
18400
  if (Array.isArray(value)) {
18353
18401
  // For array values, join with pipe (|) to indicate OR logic
18354
18402
  // This needs to be handled specially in the filter logic
18355
- const joinedValues = value.join('|');
18403
+ const joinedValues = value.join("|");
18356
18404
  attrFilters.push(`${key}=${joinedValues}`);
18357
18405
  }
18358
- else if (typeof value === 'string') {
18406
+ else if (typeof value === "string") {
18359
18407
  // For string values, create single filter entry
18360
18408
  attrFilters.push(`${key}=${value}`);
18361
18409
  }
@@ -18369,8 +18417,8 @@ function convertFiltersToAttrFilters(filters) {
18369
18417
  * Displays dry-run preview for recipe processing
18370
18418
  */
18371
18419
  async function displayRecipeDryRunPreview(recipePath, recipeData, expandedRecipe, baseOptions, debugLogger, cliBaseDir, cliOutFile, cliSrc) {
18372
- console.log('=== Recipe Dry-Run Preview ===\n');
18373
- const presetName = recipePath.startsWith(':') ? recipePath : undefined;
18420
+ console.log("=== Recipe Dry-Run Preview ===\n");
18421
+ const presetName = recipePath.startsWith(":") ? recipePath : undefined;
18374
18422
  if (presetName) {
18375
18423
  console.log(`📦 Recipe: ${presetName} (${expandedRecipe.length} steps)`);
18376
18424
  }
@@ -18387,32 +18435,36 @@ async function displayRecipeDryRunPreview(recipePath, recipeData, expandedRecipe
18387
18435
  // Resolve paths and options for this item
18388
18436
  const itemOut = cliOutFile || item.out || baseOptions.outFile;
18389
18437
  const outputFile = resolveOutputPath(itemOut, cliBaseDir, item._importBaseDir, recipeBaseDir, resolveRecipePath(recipePath));
18390
- const itemTypes = item.type ? parseCommaSeparated(item.type) : baseOptions.types;
18391
- const itemLanguages = item.language ? parseCommaSeparated(item.language) : baseOptions.languages;
18438
+ const itemTypes = item.type
18439
+ ? parseCommaSeparated(item.type)
18440
+ : baseOptions.types;
18441
+ const itemLanguages = item.language
18442
+ ? parseCommaSeparated(item.language)
18443
+ : baseOptions.languages;
18392
18444
  let combinedAttrFilters = [...baseOptions.attrFilters];
18393
18445
  if (item.filters) {
18394
18446
  const itemAttrFilters = convertFiltersToAttrFilters(item.filters);
18395
18447
  combinedAttrFilters = combinedAttrFilters.concat(itemAttrFilters);
18396
18448
  }
18397
- const itemSrcDir = resolveItemSrcDir(cliSrc, item.src, item._importSrc);
18449
+ const itemSrcDir = resolveItemSrcDir(cliSrc, item.src, item._importSrc, item._recipePath);
18398
18450
  console.log(` 📂 Source: ${itemSrcDir}`);
18399
- console.log(` 🔍 Filters: type=${itemTypes?.join(',') || '(none)'}, language=${itemLanguages?.join(',') || '(none)'}`);
18451
+ console.log(` 🔍 Filters: type=${itemTypes?.join(",") || "(none)"}, language=${itemLanguages?.join(",") || "(none)"}`);
18400
18452
  if (combinedAttrFilters.length > 0) {
18401
- console.log(` attributes=${combinedAttrFilters.join(', ')}`);
18453
+ console.log(` attributes=${combinedAttrFilters.join(", ")}`);
18402
18454
  }
18403
18455
  try {
18404
18456
  const { allFiles, filteredFiles } = await loadAndFilterFilesWithDetails(itemSrcDir, itemTypes, itemLanguages, combinedAttrFilters, debugLogger);
18405
18457
  totalInputFiles += allFiles.length;
18406
18458
  totalIncludedFiles += filteredFiles.length;
18407
- totalExcludedFiles += (allFiles.length - filteredFiles.length);
18459
+ totalExcludedFiles += allFiles.length - filteredFiles.length;
18408
18460
  console.log(`\n 📄 Input Files Analysis:`);
18409
18461
  if (filteredFiles.length > 0) {
18410
18462
  console.log(` ✓ INCLUDED (${filteredFiles.length} files):`);
18411
- filteredFiles.forEach(file => {
18463
+ filteredFiles.forEach((file) => {
18412
18464
  console.log(` - ${file.path}`);
18413
18465
  });
18414
18466
  }
18415
- const excludedFiles = allFiles.filter(f => !filteredFiles.includes(f));
18467
+ const excludedFiles = allFiles.filter((f) => !filteredFiles.includes(f));
18416
18468
  // Note: Individual excluded files are not displayed to keep output concise
18417
18469
  // Only the count is shown in the summary below
18418
18470
  console.log(`\n 📝 Output: ${outputFile}`);
@@ -18444,20 +18496,20 @@ function formatFileSize(sizeInBytes) {
18444
18496
  * Displays dry-run preview for single extraction
18445
18497
  */
18446
18498
  function displayDryRunPreview(srcDir, outFile, allFiles, filteredFiles, types, languages, attrFilters = [], verbose = false) {
18447
- console.log('=== Extract Command Dry-Run Preview ===\n');
18499
+ console.log("=== Extract Command Dry-Run Preview ===\n");
18448
18500
  console.log(`📂 Source Directory: ${srcDir}`);
18449
- console.log('🔍 Filters Applied:');
18450
- console.log(` - Type: ${types?.length ? types.join(', ') : '(none)'}`);
18451
- console.log(` - Language: ${languages?.length ? languages.join(', ') : '(none)'}`);
18452
- console.log(` - Attributes: ${attrFilters.length ? attrFilters.join(', ') : '(none)'}\n`);
18453
- console.log('📄 Input Files Analysis:');
18501
+ console.log("🔍 Filters Applied:");
18502
+ console.log(` - Type: ${types?.length ? types.join(", ") : "(none)"}`);
18503
+ console.log(` - Language: ${languages?.length ? languages.join(", ") : "(none)"}`);
18504
+ console.log(` - Attributes: ${attrFilters.length ? attrFilters.join(", ") : "(none)"}\n`);
18505
+ console.log("📄 Input Files Analysis:");
18454
18506
  if (filteredFiles.length > 0) {
18455
18507
  console.log(` ✓ INCLUDED (${filteredFiles.length} files):`);
18456
- filteredFiles.forEach(file => {
18508
+ filteredFiles.forEach((file) => {
18457
18509
  const attrs = Object.entries(file.attrs)
18458
- .filter(([key]) => key !== 'content')
18459
- .map(([key, value]) => `${key}: ${Array.isArray(value) ? value.join(',') : value}`)
18460
- .join(', ');
18510
+ .filter(([key]) => key !== "content")
18511
+ .map(([key, value]) => `${key}: ${Array.isArray(value) ? value.join(",") : value}`)
18512
+ .join(", ");
18461
18513
  console.log(` - ${file.path}`);
18462
18514
  if (verbose && attrs) {
18463
18515
  console.log(` → ${attrs}`);
@@ -18467,7 +18519,7 @@ function displayDryRunPreview(srcDir, outFile, allFiles, filteredFiles, types, l
18467
18519
  }
18468
18520
  });
18469
18521
  }
18470
- const excludedFiles = allFiles.filter(f => !filteredFiles.includes(f));
18522
+ const excludedFiles = allFiles.filter((f) => !filteredFiles.includes(f));
18471
18523
  // Note: Individual excluded files are not displayed to keep output concise
18472
18524
  // Only the count is shown in the summary below
18473
18525
  const totalSize = filteredFiles.reduce((sum, f) => sum + f.content.length, 0);
@@ -18482,26 +18534,26 @@ function displayDryRunPreview(srcDir, outFile, allFiles, filteredFiles, types, l
18482
18534
  }
18483
18535
  async function loadAndFilterFiles(srcDir, types, languages, attrFilters = [], debugLogger) {
18484
18536
  debugLogger?.log(`Starting file scan in directory: ${srcDir}`);
18485
- debugLogger?.time('MarkdownFileScanner.parseMarkdownFiles');
18537
+ debugLogger?.time("MarkdownFileScanner.parseMarkdownFiles");
18486
18538
  const files = await MarkdownFileScanner.parseMarkdownFiles(srcDir);
18487
- debugLogger?.timeEnd('MarkdownFileScanner.parseMarkdownFiles');
18539
+ debugLogger?.timeEnd("MarkdownFileScanner.parseMarkdownFiles");
18488
18540
  debugLogger?.log(`Total markdown files found: ${files.length}`);
18489
18541
  if (debugLogger?.isEnabled) {
18490
- debugLogger.log('Files discovered:');
18542
+ debugLogger.log("Files discovered:");
18491
18543
  files.forEach((file, index) => {
18492
18544
  debugLogger.log(` ${index + 1}. ${file.path}`);
18493
18545
  debugLogger.log(` Frontmatter attributes:`, Object.keys(file.attrs));
18494
18546
  });
18495
18547
  }
18496
- debugLogger?.time('File filtering');
18548
+ debugLogger?.time("File filtering");
18497
18549
  const filtered = filterFiles(files, { types, languages, attrFilters });
18498
- debugLogger?.timeEnd('File filtering');
18550
+ debugLogger?.timeEnd("File filtering");
18499
18551
  debugLogger?.log(`Files after filtering: ${filtered.length}`);
18500
18552
  if (debugLogger?.isEnabled && filtered.length !== files.length) {
18501
18553
  const excludedCount = files.length - filtered.length;
18502
18554
  debugLogger.log(`Excluded ${excludedCount} files due to filtering`);
18503
- const excludedFiles = files.filter(f => !filtered.includes(f));
18504
- excludedFiles.forEach(file => {
18555
+ const excludedFiles = files.filter((f) => !filtered.includes(f));
18556
+ excludedFiles.forEach((file) => {
18505
18557
  debugLogger.log(` Excluded: ${file.path} (attributes: ${JSON.stringify(file.attrs)})`);
18506
18558
  });
18507
18559
  }
@@ -18513,26 +18565,26 @@ async function loadAndFilterFiles(srcDir, types, languages, attrFilters = [], de
18513
18565
  */
18514
18566
  async function loadAndFilterFilesWithDetails(srcDir, types, languages, attrFilters = [], debugLogger) {
18515
18567
  debugLogger?.log(`Starting file scan in directory: ${srcDir}`);
18516
- debugLogger?.time('MarkdownFileScanner.parseMarkdownFiles');
18568
+ debugLogger?.time("MarkdownFileScanner.parseMarkdownFiles");
18517
18569
  const files = await MarkdownFileScanner.parseMarkdownFiles(srcDir);
18518
- debugLogger?.timeEnd('MarkdownFileScanner.parseMarkdownFiles');
18570
+ debugLogger?.timeEnd("MarkdownFileScanner.parseMarkdownFiles");
18519
18571
  debugLogger?.log(`Total markdown files found: ${files.length}`);
18520
18572
  if (debugLogger?.isEnabled) {
18521
- debugLogger.log('Files discovered:');
18573
+ debugLogger.log("Files discovered:");
18522
18574
  files.forEach((file, index) => {
18523
18575
  debugLogger.log(` ${index + 1}. ${file.path}`);
18524
18576
  debugLogger.log(` Frontmatter attributes:`, Object.keys(file.attrs));
18525
18577
  });
18526
18578
  }
18527
- debugLogger?.time('File filtering');
18579
+ debugLogger?.time("File filtering");
18528
18580
  const filtered = filterFiles(files, { types, languages, attrFilters });
18529
- debugLogger?.timeEnd('File filtering');
18581
+ debugLogger?.timeEnd("File filtering");
18530
18582
  debugLogger?.log(`Files after filtering: ${filtered.length}`);
18531
18583
  if (debugLogger?.isEnabled && filtered.length !== files.length) {
18532
18584
  const excludedCount = files.length - filtered.length;
18533
18585
  debugLogger.log(`Excluded ${excludedCount} files due to filtering`);
18534
- const excludedFiles = files.filter(f => !filtered.includes(f));
18535
- excludedFiles.forEach(file => {
18586
+ const excludedFiles = files.filter((f) => !filtered.includes(f));
18587
+ excludedFiles.forEach((file) => {
18536
18588
  debugLogger.log(` Excluded: ${file.path} (attributes: ${JSON.stringify(file.attrs)})`);
18537
18589
  });
18538
18590
  }
@@ -18544,21 +18596,21 @@ async function loadAndFilterFilesWithDetails(srcDir, types, languages, attrFilte
18544
18596
  async function processContentWithMode(outFile, newContent, mode, debugLogger) {
18545
18597
  debugLogger?.log(`Processing content with mode: ${mode} for file: ${outFile}`);
18546
18598
  if (mode === WriteMode.OVERWRITE) {
18547
- debugLogger?.log('Using overwrite mode - returning new content as-is');
18599
+ debugLogger?.log("Using overwrite mode - returning new content as-is");
18548
18600
  return newContent;
18549
18601
  }
18550
18602
  try {
18551
18603
  const existing = await fs.readFile(resolvePath(outFile), "utf-8");
18552
18604
  debugLogger?.log(`Existing file found with ${existing.length} characters`);
18553
18605
  if (mode === WriteMode.APPEND) {
18554
- debugLogger?.log('Appending new content to existing content');
18606
+ debugLogger?.log("Appending new content to existing content");
18555
18607
  const needsNewline = !/\n\s*$/.test(existing);
18556
18608
  return needsNewline
18557
18609
  ? `${existing}\n\n${newContent}`
18558
18610
  : `${existing}\n${newContent}`;
18559
18611
  }
18560
18612
  else if (mode === WriteMode.PREPEND) {
18561
- debugLogger?.log('Prepending new content to existing content');
18613
+ debugLogger?.log("Prepending new content to existing content");
18562
18614
  const needsNewline = !/\n\s*$/.test(newContent);
18563
18615
  return needsNewline
18564
18616
  ? `${newContent}\n\n${existing}`
@@ -18567,7 +18619,7 @@ async function processContentWithMode(outFile, newContent, mode, debugLogger) {
18567
18619
  }
18568
18620
  catch (error) {
18569
18621
  debugLogger?.log(`No existing file found or error reading file:`, error);
18570
- debugLogger?.log('Proceeding with new content only');
18622
+ debugLogger?.log("Proceeding with new content only");
18571
18623
  }
18572
18624
  return newContent;
18573
18625
  }
@@ -18577,7 +18629,7 @@ async function processContentWithMode(outFile, newContent, mode, debugLogger) {
18577
18629
  function applyTemplateToObject(obj, templateOptions, templateEngine) {
18578
18630
  const result = {};
18579
18631
  for (const [key, value] of Object.entries(obj)) {
18580
- if (typeof value === 'string') {
18632
+ if (typeof value === "string") {
18581
18633
  // Apply template processing to string values
18582
18634
  if (templateEngine.hasTemplateVariables(value)) {
18583
18635
  result[key] = templateEngine.renderTemplate(value, templateOptions);
@@ -18588,11 +18640,11 @@ function applyTemplateToObject(obj, templateOptions, templateEngine) {
18588
18640
  }
18589
18641
  else if (Array.isArray(value)) {
18590
18642
  // Process array elements
18591
- result[key] = value.map(item => typeof item === 'string' && templateEngine.hasTemplateVariables(item)
18643
+ result[key] = value.map((item) => typeof item === "string" && templateEngine.hasTemplateVariables(item)
18592
18644
  ? templateEngine.renderTemplate(item, templateOptions)
18593
18645
  : item);
18594
18646
  }
18595
- else if (value && typeof value === 'object') {
18647
+ else if (value && typeof value === "object") {
18596
18648
  // Recursively process nested objects
18597
18649
  result[key] = applyTemplateToObject(value, templateOptions, templateEngine);
18598
18650
  }
@@ -18605,35 +18657,37 @@ function applyTemplateToObject(obj, templateOptions, templateEngine) {
18605
18657
  }
18606
18658
  async function processSingle(options, debugLogger) {
18607
18659
  CliOptionValidator.validateExtractOptions(options);
18608
- const { srcDir, outFile, types, languages, attrFilters, title, mode, attr, vars, envFile, dryRun } = options;
18660
+ const { srcDir, outFile, types, languages, attrFilters, title, mode, attr, vars, envFile, dryRun, } = options;
18609
18661
  // Resolve template variables first
18610
18662
  const templateEngine = new TemplateEngine();
18611
18663
  const variableResolver = new VariableResolver();
18612
18664
  let resolvedVariables = {};
18613
18665
  // Always resolve variables - even if no CLI variables are provided,
18614
18666
  // we still need to check for required variables and use environment variables
18615
- debugLogger.time('Variable resolution');
18616
- debugLogger.log('Resolving template variables', { vars, envFile });
18667
+ debugLogger.time("Variable resolution");
18668
+ debugLogger.log("Resolving template variables", { vars, envFile });
18617
18669
  try {
18618
- const cliVariables = vars ? VariableResolver.parseCliVariables(vars) : undefined;
18670
+ const cliVariables = vars
18671
+ ? VariableResolver.parseCliVariables(vars)
18672
+ : undefined;
18619
18673
  resolvedVariables = await variableResolver.resolveVariables({
18620
18674
  cliVariables,
18621
18675
  envFile,
18622
- environmentVariables: true
18676
+ environmentVariables: true,
18623
18677
  });
18624
- debugLogger.log('Resolved variables:', Object.keys(resolvedVariables));
18678
+ debugLogger.log("Resolved variables:", Object.keys(resolvedVariables));
18625
18679
  }
18626
18680
  catch (error) {
18627
18681
  throw new Error(`Template variable resolution failed: ${error instanceof Error ? error.message : String(error)}`);
18628
18682
  }
18629
- debugLogger.timeEnd('Variable resolution');
18630
- debugLogger.time('File scanning and filtering');
18683
+ debugLogger.timeEnd("Variable resolution");
18684
+ debugLogger.time("File scanning and filtering");
18631
18685
  debugLogger.log(`Scanning directory: ${srcDir}`);
18632
- debugLogger.log('Applied filters:', { types, languages, attrFilters });
18686
+ debugLogger.log("Applied filters:", { types, languages, attrFilters });
18633
18687
  // In dry-run mode, we need both all files and filtered files to show exclusion details
18634
18688
  if (dryRun) {
18635
18689
  const { allFiles, filteredFiles } = await loadAndFilterFilesWithDetails(srcDir, types, languages, attrFilters, debugLogger);
18636
- debugLogger.timeEnd('File scanning and filtering');
18690
+ debugLogger.timeEnd("File scanning and filtering");
18637
18691
  debugLogger.log(`Found ${filteredFiles.length} files after filtering`);
18638
18692
  // Check if verbose mode is enabled (via debug flag)
18639
18693
  const verbose = debugLogger.isEnabled;
@@ -18642,42 +18696,42 @@ async function processSingle(options, debugLogger) {
18642
18696
  return;
18643
18697
  }
18644
18698
  const filtered = await loadAndFilterFiles(srcDir, types, languages, attrFilters, debugLogger);
18645
- debugLogger.timeEnd('File scanning and filtering');
18699
+ debugLogger.timeEnd("File scanning and filtering");
18646
18700
  debugLogger.log(`Found ${filtered.length} files after filtering`);
18647
- debugLogger.time('Content merging');
18701
+ debugLogger.time("Content merging");
18648
18702
  let merged = filtered.map((f) => f.content.trim()).join("\n\n");
18649
18703
  // Always create templateOptions with strict mode enabled
18650
18704
  const templateOptions = {
18651
18705
  variables: resolvedVariables,
18652
- strictMode: true
18706
+ strictMode: true,
18653
18707
  };
18654
18708
  // Pre-validate required variables before processing
18655
18709
  if (templateEngine.hasTemplateVariables(merged)) {
18656
- debugLogger.time('Required variable validation');
18710
+ debugLogger.time("Required variable validation");
18657
18711
  const requiredVars = templateEngine.extractRequiredVariables(merged);
18658
- debugLogger.log('Found required variables:', requiredVars);
18712
+ debugLogger.log("Found required variables:", requiredVars);
18659
18713
  if (requiredVars.length > 0) {
18660
18714
  try {
18661
18715
  templateEngine.validateRequiredVariables(merged, resolvedVariables);
18662
- debugLogger.log('All required variables are satisfied');
18716
+ debugLogger.log("All required variables are satisfied");
18663
18717
  }
18664
18718
  catch (error) {
18665
- debugLogger.log('Required variable validation failed');
18719
+ debugLogger.log("Required variable validation failed");
18666
18720
  throw error;
18667
18721
  }
18668
18722
  }
18669
- debugLogger.timeEnd('Required variable validation');
18670
- debugLogger.time('Template processing');
18671
- debugLogger.log('Applying template processing to merged content');
18723
+ debugLogger.timeEnd("Required variable validation");
18724
+ debugLogger.time("Template processing");
18725
+ debugLogger.log("Applying template processing to merged content");
18672
18726
  merged = templateEngine.renderTemplate(merged, templateOptions);
18673
- debugLogger.timeEnd('Template processing');
18727
+ debugLogger.timeEnd("Template processing");
18674
18728
  }
18675
18729
  const contentWithTitle = title ? `# ${title}\n\n${merged}` : merged;
18676
- debugLogger.timeEnd('Content merging');
18730
+ debugLogger.timeEnd("Content merging");
18677
18731
  debugLogger.log(`Generated content size: ${contentWithTitle.length} characters`);
18678
- debugLogger.time('Content mode processing');
18732
+ debugLogger.time("Content mode processing");
18679
18733
  const finalContent = await processContentWithMode(outFile, contentWithTitle, mode, debugLogger);
18680
- debugLogger.timeEnd('Content mode processing');
18734
+ debugLogger.timeEnd("Content mode processing");
18681
18735
  const resolved = resolvePath(outFile);
18682
18736
  const writer = new MarkdownWriter();
18683
18737
  debugLogger.log(`Writing to output file: ${resolved}`);
@@ -18685,21 +18739,21 @@ async function processSingle(options, debugLogger) {
18685
18739
  let frontMatter = attr ? { ...attr } : {};
18686
18740
  // Apply template processing to front matter if needed
18687
18741
  if (Object.keys(frontMatter).length > 0) {
18688
- debugLogger.log('Applying template processing to front matter');
18742
+ debugLogger.log("Applying template processing to front matter");
18689
18743
  frontMatter = applyTemplateToObject(frontMatter, templateOptions, templateEngine);
18690
18744
  }
18691
- debugLogger.log('Front matter to add:', frontMatter);
18692
- debugLogger.time('File writing');
18745
+ debugLogger.log("Front matter to add:", frontMatter);
18746
+ debugLogger.time("File writing");
18693
18747
  // Write file with or without front matter
18694
18748
  if (Object.keys(frontMatter).length > 0) {
18695
18749
  await writer.writeMarkdownFileWithFrontMatter(resolved, finalContent, frontMatter);
18696
- debugLogger.log('File written with front matter');
18750
+ debugLogger.log("File written with front matter");
18697
18751
  }
18698
18752
  else {
18699
18753
  await writer.writeMarkdownFile(resolved, finalContent);
18700
- debugLogger.log('File written without front matter');
18754
+ debugLogger.log("File written without front matter");
18701
18755
  }
18702
- debugLogger.timeEnd('File writing');
18756
+ debugLogger.timeEnd("File writing");
18703
18757
  // eslint-disable-next-line no-console
18704
18758
  console.log(`Extracted ${filtered.length} files to ${outFile}`);
18705
18759
  }
@@ -18757,7 +18811,7 @@ async function displayVariables(srcDir) {
18757
18811
  variableMap.set(variable.name, {
18758
18812
  required: variable.required,
18759
18813
  defaultValue: variable.defaultValue,
18760
- sources: new Set([file.path])
18814
+ sources: new Set([file.path]),
18761
18815
  });
18762
18816
  }
18763
18817
  }
@@ -18766,32 +18820,32 @@ async function displayVariables(srcDir) {
18766
18820
  for (const [name, info] of variableMap.entries()) {
18767
18821
  allVariables.push({
18768
18822
  variable: name,
18769
- required: info.required ? 'Yes' : 'No',
18770
- defaultValue: info.defaultValue ?? '(none)',
18771
- sources: Array.from(info.sources)
18823
+ required: info.required ? "Yes" : "No",
18824
+ defaultValue: info.defaultValue ?? "(none)",
18825
+ sources: Array.from(info.sources),
18772
18826
  });
18773
18827
  }
18774
18828
  if (allVariables.length === 0) {
18775
18829
  // eslint-disable-next-line no-console
18776
- console.log('No template variables found in source files.');
18830
+ console.log("No template variables found in source files.");
18777
18831
  // eslint-disable-next-line no-console
18778
- console.log('\nTemplate variables use the syntax ${VAR:default} for optional variables and !{VAR} for required variables.');
18832
+ console.log("\nTemplate variables use the syntax ${VAR:default} for optional variables and !{VAR} for required variables.");
18779
18833
  return;
18780
18834
  }
18781
18835
  // eslint-disable-next-line no-console
18782
- console.log('Template variables found in source files:\n');
18836
+ console.log("Template variables found in source files:\n");
18783
18837
  // Calculate column widths with reasonable limits
18784
- const maxVariableLength = Math.max(8, Math.min(30, ...allVariables.map(v => v.variable.length)));
18838
+ const maxVariableLength = Math.max(8, Math.min(30, ...allVariables.map((v) => v.variable.length)));
18785
18839
  const maxRequiredLength = 8; // "Required" header length
18786
- const maxDefaultLength = Math.max(12, Math.min(30, ...allVariables.map(v => v.defaultValue.length)));
18840
+ const maxDefaultLength = Math.max(12, Math.min(30, ...allVariables.map((v) => v.defaultValue.length)));
18787
18841
  // Header
18788
- const variableHeader = 'Variable'.padEnd(maxVariableLength);
18789
- const requiredHeader = 'Required'.padEnd(maxRequiredLength);
18790
- const defaultHeader = 'Default'.padEnd(maxDefaultLength);
18842
+ const variableHeader = "Variable".padEnd(maxVariableLength);
18843
+ const requiredHeader = "Required".padEnd(maxRequiredLength);
18844
+ const defaultHeader = "Default".padEnd(maxDefaultLength);
18791
18845
  // eslint-disable-next-line no-console
18792
18846
  console.log(`${variableHeader} | ${requiredHeader} | ${defaultHeader}`);
18793
18847
  // eslint-disable-next-line no-console
18794
- console.log('-'.repeat(maxVariableLength + maxRequiredLength + maxDefaultLength + 6));
18848
+ console.log("-".repeat(maxVariableLength + maxRequiredLength + maxDefaultLength + 6));
18795
18849
  // Sort by variable name for consistent output
18796
18850
  allVariables.sort((a, b) => a.variable.localeCompare(b.variable));
18797
18851
  // Data rows
@@ -18803,7 +18857,7 @@ async function displayVariables(srcDir) {
18803
18857
  console.log(`${variableCol} | ${requiredCol} | ${defaultCol}`);
18804
18858
  });
18805
18859
  // eslint-disable-next-line no-console
18806
- console.log('\nNote: Template variables use ${VAR:default} syntax for optional variables and !{VAR} for required variables.');
18860
+ console.log("\nNote: Template variables use ${VAR:default} syntax for optional variables and !{VAR} for required variables.");
18807
18861
  }
18808
18862
  /**
18809
18863
  * Handles the extract command with options parsing
@@ -18811,7 +18865,7 @@ async function displayVariables(srcDir) {
18811
18865
  async function handleExtractCommand(options) {
18812
18866
  const isDebugMode = Boolean(options.debug || options.verbose);
18813
18867
  const debugLogger = new DebugLogger(isDebugMode);
18814
- debugLogger.log('Starting extract command with options:', {
18868
+ debugLogger.log("Starting extract command with options:", {
18815
18869
  src: options.src,
18816
18870
  out: options.out,
18817
18871
  type: options.type,
@@ -18820,22 +18874,22 @@ async function handleExtractCommand(options) {
18820
18874
  title: options.title,
18821
18875
  mode: options.mode,
18822
18876
  recipeCount: options.recipe?.length || 0,
18823
- debug: isDebugMode
18877
+ debug: isDebugMode,
18824
18878
  });
18825
18879
  const extractOptions = {
18826
18880
  srcDir: resolveDefaultSrcDir(options.src),
18827
- outFile: options.out || './out/instruction.md',
18881
+ outFile: options.out || "./out/instruction.md",
18828
18882
  types: parseCommaSeparated(options.type),
18829
18883
  languages: parseCommaSeparated(options.language),
18830
18884
  attrFilters: parseCommaSeparated(options.attr) || [],
18831
18885
  title: options.title,
18832
- mode: parseWriteMode(options.mode || 'overwrite'),
18886
+ mode: parseWriteMode(options.mode || "overwrite"),
18833
18887
  debug: isDebugMode,
18834
18888
  vars: options.vars,
18835
18889
  envFile: options.envFile,
18836
18890
  dryRun: options.dryRun,
18837
18891
  };
18838
- debugLogger.log('Resolved extract options:', extractOptions);
18892
+ debugLogger.log("Resolved extract options:", extractOptions);
18839
18893
  if (options.recipe && options.recipe.length > 0) {
18840
18894
  debugLogger.log(`Processing ${options.recipe.length} recipe(s):`, options.recipe);
18841
18895
  // Validate all recipe files exist before processing starts
@@ -18850,7 +18904,7 @@ async function handleExtractCommand(options) {
18850
18904
  }
18851
18905
  }
18852
18906
  else {
18853
- debugLogger.log('Processing single extraction without recipe');
18907
+ debugLogger.log("Processing single extraction without recipe");
18854
18908
  // For single extraction, apply baseDir to outFile if provided
18855
18909
  if (options.baseDir) {
18856
18910
  extractOptions.outFile = resolveOutputPath(extractOptions.outFile, options.baseDir, undefined, undefined, undefined);
@@ -18895,10 +18949,10 @@ async function processMultipleRecipes(recipePaths, baseOptions, debugLogger, cli
18895
18949
  * Reads template files and extracts frontmatter for inheritance
18896
18950
  */
18897
18951
  async function loadTemplateFrontmatter(srcDir, types, languages, attrFilters, debugLogger) {
18898
- debugLogger?.log('Loading template frontmatter for inheritance');
18952
+ debugLogger?.log("Loading template frontmatter for inheritance");
18899
18953
  const templateFiles = await loadAndFilterFiles(srcDir, types, languages, attrFilters, debugLogger);
18900
18954
  debugLogger?.log(`Found ${templateFiles.length} template files for frontmatter inheritance`);
18901
- return templateFiles.map(file => file.attrs);
18955
+ return templateFiles.map((file) => file.attrs);
18902
18956
  }
18903
18957
  /**
18904
18958
  * Merges frontmatter values from multiple templates according to merge rules:
@@ -18908,8 +18962,8 @@ async function loadTemplateFrontmatter(srcDir, types, languages, attrFilters, de
18908
18962
  */
18909
18963
  function mergeTemplateFrontmatterValues(templateFrontmatters, fieldName) {
18910
18964
  const values = templateFrontmatters
18911
- .map(fm => fm[fieldName])
18912
- .filter(val => val !== undefined && val !== null);
18965
+ .map((fm) => fm[fieldName])
18966
+ .filter((val) => val !== undefined && val !== null);
18913
18967
  if (values.length === 0) {
18914
18968
  return undefined;
18915
18969
  }
@@ -18917,12 +18971,15 @@ function mergeTemplateFrontmatterValues(templateFrontmatters, fieldName) {
18917
18971
  return values[0];
18918
18972
  }
18919
18973
  // Check if all values are strings
18920
- if (values.every(val => typeof val === 'string')) {
18974
+ if (values.every((val) => typeof val === "string")) {
18921
18975
  // Split each string by newlines, flatten, remove duplicates while preserving order
18922
18976
  const allLines = [];
18923
18977
  const seen = new Set();
18924
18978
  for (const value of values) {
18925
- const lines = value.split('\n').map(line => line.trim()).filter(line => line.length > 0);
18979
+ const lines = value
18980
+ .split("\n")
18981
+ .map((line) => line.trim())
18982
+ .filter((line) => line.length > 0);
18926
18983
  for (const line of lines) {
18927
18984
  if (!seen.has(line)) {
18928
18985
  seen.add(line);
@@ -18930,10 +18987,10 @@ function mergeTemplateFrontmatterValues(templateFrontmatters, fieldName) {
18930
18987
  }
18931
18988
  }
18932
18989
  }
18933
- return allLines.join('\n');
18990
+ return allLines.join("\n");
18934
18991
  }
18935
18992
  // Check if all values are arrays
18936
- if (values.every(val => Array.isArray(val))) {
18993
+ if (values.every((val) => Array.isArray(val))) {
18937
18994
  return values.flat();
18938
18995
  }
18939
18996
  // For objects and mixed types, use the last value
@@ -18943,14 +19000,14 @@ function mergeTemplateFrontmatterValues(templateFrontmatters, fieldName) {
18943
19000
  * Processes @ syntax in frontmatter to inherit from template files
18944
19001
  */
18945
19002
  async function processFrontmatterInheritance(frontmatter, srcDir, types, languages, attrFilters, debugLogger) {
18946
- if (!frontmatter || typeof frontmatter !== 'object') {
19003
+ if (!frontmatter || typeof frontmatter !== "object") {
18947
19004
  return {};
18948
19005
  }
18949
19006
  const result = {};
18950
19007
  const inheritanceFields = [];
18951
19008
  // Separate inheritance fields (@ syntax) from regular fields
18952
19009
  for (const [key, value] of Object.entries(frontmatter)) {
18953
- if (key.startsWith('@') && value === true) {
19010
+ if (key.startsWith("@") && value === true) {
18954
19011
  inheritanceFields.push(key.slice(1)); // Remove @ prefix
18955
19012
  debugLogger?.log(`Found inheritance field: ${key} -> ${key.slice(1)}`);
18956
19013
  }
@@ -18960,14 +19017,14 @@ async function processFrontmatterInheritance(frontmatter, srcDir, types, languag
18960
19017
  }
18961
19018
  // If no inheritance fields, return as-is
18962
19019
  if (inheritanceFields.length === 0) {
18963
- debugLogger?.log('No frontmatter inheritance fields found');
19020
+ debugLogger?.log("No frontmatter inheritance fields found");
18964
19021
  return result;
18965
19022
  }
18966
19023
  debugLogger?.log(`Processing ${inheritanceFields.length} inheritance fields:`, inheritanceFields);
18967
19024
  // Load template frontmatters
18968
19025
  const templateFrontmatters = await loadTemplateFrontmatter(srcDir, types, languages, attrFilters, debugLogger);
18969
19026
  if (templateFrontmatters.length === 0) {
18970
- debugLogger?.log('No template files found for inheritance');
19027
+ debugLogger?.log("No template files found for inheritance");
18971
19028
  return result;
18972
19029
  }
18973
19030
  // Process each inheritance field
@@ -18981,7 +19038,7 @@ async function processFrontmatterInheritance(frontmatter, srcDir, types, languag
18981
19038
  debugLogger?.log(`No value found for inherited field ${fieldName} in templates`);
18982
19039
  }
18983
19040
  }
18984
- debugLogger?.log('Frontmatter after inheritance processing:', Object.keys(result));
19041
+ debugLogger?.log("Frontmatter after inheritance processing:", Object.keys(result));
18985
19042
  return result;
18986
19043
  }
18987
19044
  /**
@@ -18991,44 +19048,44 @@ async function processRecipe(recipePath, baseOptions, contentTracker, debugLogge
18991
19048
  const resolvedPath = resolveRecipePath(recipePath);
18992
19049
  debugLogger?.log(`Processing recipe at path: ${resolvedPath}`);
18993
19050
  try {
18994
- debugLogger?.time('Recipe file reading and parsing');
19051
+ debugLogger?.time("Recipe file reading and parsing");
18995
19052
  const content = await fs.readFile(resolvedPath, "utf-8");
18996
19053
  const data = YAML.parse(content);
18997
- debugLogger?.timeEnd('Recipe file reading and parsing');
19054
+ debugLogger?.timeEnd("Recipe file reading and parsing");
18998
19055
  if (!Array.isArray(data?.recipe)) {
18999
19056
  throw new Error("Invalid recipe file: 'recipe' array not found");
19000
19057
  }
19001
19058
  // Validate recipe structure and show warnings
19002
- debugLogger?.time('Recipe validation');
19059
+ debugLogger?.time("Recipe validation");
19003
19060
  try {
19004
19061
  const validator = await getRecipeValidator();
19005
19062
  const validationResult = validator.validateRecipe(data);
19006
19063
  // Always log validation warnings to help users improve their recipes
19007
19064
  if (validationResult.warnings.length > 0) {
19008
19065
  console.warn(`\nRecipe validation warnings for '${recipePath}':`);
19009
- validationResult.warnings.forEach(warning => {
19066
+ validationResult.warnings.forEach((warning) => {
19010
19067
  console.warn(` ${warning}`);
19011
19068
  });
19012
- console.warn(' These warnings do not prevent recipe execution but may indicate configuration issues.\n');
19069
+ console.warn(" These warnings do not prevent recipe execution but may indicate configuration issues.\n");
19013
19070
  }
19014
19071
  // Log errors but don't fail (maintain backward compatibility)
19015
19072
  if (validationResult.errors.length > 0) {
19016
- debugLogger?.log('Recipe validation errors (non-blocking):', validationResult.errors);
19073
+ debugLogger?.log("Recipe validation errors (non-blocking):", validationResult.errors);
19017
19074
  }
19018
19075
  }
19019
19076
  catch (validationError) {
19020
19077
  // Don't fail recipe processing if validation fails
19021
- debugLogger?.log('Recipe validation failed:', validationError);
19078
+ debugLogger?.log("Recipe validation failed:", validationError);
19022
19079
  }
19023
- debugLogger?.timeEnd('Recipe validation');
19080
+ debugLogger?.timeEnd("Recipe validation");
19024
19081
  debugLogger?.log(`Recipe contains ${data.recipe.length} items`);
19025
19082
  // Read recipe config for baseDir
19026
19083
  const recipeBaseDir = data.config?.baseDir;
19027
- debugLogger?.log('Recipe config:', { baseDir: recipeBaseDir });
19084
+ debugLogger?.log("Recipe config:", { baseDir: recipeBaseDir });
19028
19085
  // Expand any imports in the recipe
19029
- debugLogger?.time('Recipe import expansion');
19086
+ debugLogger?.time("Recipe import expansion");
19030
19087
  const expandedRecipe = await expandRecipeImports(data.recipe, resolvedPath, debugLogger);
19031
- debugLogger?.timeEnd('Recipe import expansion');
19088
+ debugLogger?.timeEnd("Recipe import expansion");
19032
19089
  debugLogger?.log(`After import expansion: ${expandedRecipe.length} items`);
19033
19090
  // If dry-run mode, display preview and exit
19034
19091
  if (baseOptions.dryRun) {
@@ -19043,7 +19100,7 @@ async function processRecipe(recipePath, baseOptions, contentTracker, debugLogge
19043
19100
  out: item.out,
19044
19101
  type: item.type,
19045
19102
  language: item.language,
19046
- mode: item.mode
19103
+ mode: item.mode,
19047
19104
  });
19048
19105
  // Priority: CLI --out option > recipe item.out > baseOptions.outFile default
19049
19106
  const itemOut = cliOutFile || item.out || baseOptions.outFile;
@@ -19054,12 +19111,14 @@ async function processRecipe(recipePath, baseOptions, contentTracker, debugLogge
19054
19111
  fallback: baseOptions.outFile,
19055
19112
  cliBaseDir,
19056
19113
  importBaseDir: item._importBaseDir,
19057
- recipeBaseDir
19114
+ recipeBaseDir,
19058
19115
  });
19059
19116
  // Generate the content that would be written to check for duplicates
19060
- const { srcDir, types, languages, attrFilters, title, attr, vars, envFile } = baseOptions;
19117
+ const { srcDir, types, languages, attrFilters, title, attr, vars, envFile, } = baseOptions;
19061
19118
  const itemTypes = item.type ? parseCommaSeparated(item.type) : types;
19062
- const itemLanguages = item.language ? parseCommaSeparated(item.language) : languages;
19119
+ const itemLanguages = item.language
19120
+ ? parseCommaSeparated(item.language)
19121
+ : languages;
19063
19122
  const itemTitle = item.title || title;
19064
19123
  // Combine base attrFilters with item-specific filters
19065
19124
  let combinedAttrFilters = [...attrFilters];
@@ -19073,43 +19132,52 @@ async function processRecipe(recipePath, baseOptions, contentTracker, debugLogge
19073
19132
  itemLanguages,
19074
19133
  combinedAttrFilters,
19075
19134
  baseAttrFilters: attrFilters,
19076
- itemFilters: item.filters || 'none'
19135
+ itemFilters: item.filters || "none",
19077
19136
  });
19078
19137
  // Resolve template variables for this item (merge import variables, recipe item variables, and CLI variables)
19079
19138
  let itemVars = vars;
19080
19139
  if (item.variables || item._importVariables) {
19081
- const cliVariables = vars ? VariableResolver.parseCliVariables(vars) : {};
19140
+ const cliVariables = vars
19141
+ ? VariableResolver.parseCliVariables(vars)
19142
+ : {};
19082
19143
  const importVariables = item._importVariables || {};
19083
19144
  const itemVariables = item.variables || {};
19084
19145
  // Merge with priority: CLI > item > import
19085
- const mergedVariables = { ...importVariables, ...itemVariables, ...cliVariables };
19146
+ const mergedVariables = {
19147
+ ...importVariables,
19148
+ ...itemVariables,
19149
+ ...cliVariables,
19150
+ };
19086
19151
  itemVars = Object.entries(mergedVariables)
19087
19152
  .map(([key, value]) => `${key}=${value}`)
19088
- .join(',');
19153
+ .join(",");
19089
19154
  debugLogger?.log(`Merged variables for item ${index + 1}:`, {
19090
19155
  importVariables,
19091
19156
  itemVariables,
19092
19157
  cliVariables,
19093
- merged: mergedVariables
19158
+ merged: mergedVariables,
19094
19159
  });
19095
19160
  }
19096
19161
  // Resolve source directory for this item with priority: CLI --src > item.src > import.src > default
19097
- const itemSrcDir = resolveItemSrcDir(cliSrc, item.src, item._importSrc);
19162
+ const itemSrcDir = resolveItemSrcDir(cliSrc, item.src, item._importSrc, item._recipePath);
19098
19163
  debugLogger?.log(`Resolved source directory for item ${index + 1}:`, {
19099
19164
  cliSrc,
19100
19165
  itemSrc: item.src,
19101
19166
  importSrc: item._importSrc,
19102
- resolved: itemSrcDir
19167
+ recipePath: item._recipePath,
19168
+ resolved: itemSrcDir,
19103
19169
  });
19104
19170
  debugLogger?.time(`Content generation for item ${index + 1}`);
19105
19171
  const filtered = await loadAndFilterFiles(itemSrcDir, itemTypes, itemLanguages, combinedAttrFilters, debugLogger);
19106
19172
  const merged = filtered.map((f) => f.content.trim()).join("\n\n");
19107
- const contentWithTitle = itemTitle ? `# ${itemTitle}\n\n${merged}` : merged;
19173
+ const contentWithTitle = itemTitle
19174
+ ? `# ${itemTitle}\n\n${merged}`
19175
+ : merged;
19108
19176
  debugLogger?.timeEnd(`Content generation for item ${index + 1}`);
19109
19177
  // Check if this exact content has already been written to this file
19110
- debugLogger?.time('Duplicate content check');
19178
+ debugLogger?.time("Duplicate content check");
19111
19179
  const isDuplicate = localTracker.hasContent(outputFile, contentWithTitle);
19112
- debugLogger?.timeEnd('Duplicate content check');
19180
+ debugLogger?.timeEnd("Duplicate content check");
19113
19181
  if (isDuplicate) {
19114
19182
  debugLogger?.log(`Skipping duplicate content for ${outputFile}`);
19115
19183
  // eslint-disable-next-line no-console
@@ -19158,11 +19226,11 @@ async function processRecipe(recipePath, baseOptions, contentTracker, debugLogge
19158
19226
  }
19159
19227
  }
19160
19228
  catch (error) {
19161
- if (recipePath.startsWith(':')) {
19229
+ if (recipePath.startsWith(":")) {
19162
19230
  const presetName = recipePath.slice(1);
19163
19231
  debugLogger?.log(`Error processing preset ${presetName}:`, error);
19164
19232
  // Only treat ENOENT errors as "preset not found"
19165
- if (error instanceof Error && error.message.includes('ENOENT')) {
19233
+ if (error instanceof Error && error.message.includes("ENOENT")) {
19166
19234
  throw new Error(`Preset ':${presetName}' not found. Use 'ai-rules presets' to list available presets.`);
19167
19235
  }
19168
19236
  // For other errors (like variable validation), add context but preserve the original error