@aramassa/ai-rules 0.1.1-npmjs.20250910072942 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -10,6 +10,7 @@ import require$$3 from 'node:fs';
10
10
  import require$$4 from 'node:process';
11
11
  import { fileURLToPath } from 'url';
12
12
  import { createHash } from 'crypto';
13
+ import { homedir } from 'os';
13
14
 
14
15
  /******************************************************************************
15
16
  Copyright (c) Microsoft Corporation.
@@ -10042,6 +10043,53 @@ class VariableResolver {
10042
10043
  }
10043
10044
  }
10044
10045
 
10046
+ /**
10047
+ * Path expansion utility that supports tilde (~) and environment variable expansion
10048
+ *
10049
+ * Features:
10050
+ * - Tilde expansion: ~/path -> /Users/username/path (Unix-like systems only)
10051
+ * - Environment variable expansion: $HOME/path -> /Users/username/path
10052
+ * - Environment variable expansion: ${VAR}/path -> /value/path
10053
+ * - Complex combinations: ~/projects/${PROJECT_NAME}/file.md
10054
+ *
10055
+ * @param inputPath - Path to expand and resolve
10056
+ * @returns Expanded and resolved absolute path
10057
+ * @throws Error if required environment variables are missing
10058
+ */
10059
+ function resolvePath(inputPath) {
10060
+ if (!inputPath || typeof inputPath !== 'string') {
10061
+ throw new Error('Path must be a non-empty string');
10062
+ }
10063
+ let expandedPath = inputPath;
10064
+ // 1. Handle tilde expansion first (Unix-like systems only)
10065
+ if (expandedPath.startsWith('~/')) {
10066
+ // Check if we're on Windows
10067
+ if (process.platform === 'win32') {
10068
+ throw new Error('Tilde (~) expansion is not supported on Windows. Use %USERPROFILE% or environment variables instead.');
10069
+ }
10070
+ const homeDir = homedir();
10071
+ expandedPath = path.join(homeDir, expandedPath.slice(2));
10072
+ }
10073
+ else if (expandedPath === '~') {
10074
+ if (process.platform === 'win32') {
10075
+ throw new Error('Tilde (~) expansion is not supported on Windows. Use %USERPROFILE% or environment variables instead.');
10076
+ }
10077
+ expandedPath = homedir();
10078
+ }
10079
+ // 2. Handle environment variable expansion
10080
+ // Support both $VAR and ${VAR} syntax
10081
+ expandedPath = expandedPath.replace(/\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (match, bracedVar, simpleVar) => {
10082
+ const varName = bracedVar || simpleVar;
10083
+ const envValue = process.env[varName];
10084
+ if (envValue === undefined) {
10085
+ throw new Error(`Environment variable '${varName}' is not defined`);
10086
+ }
10087
+ return envValue;
10088
+ });
10089
+ // 3. Resolve to absolute path
10090
+ return path.resolve(expandedPath);
10091
+ }
10092
+
10045
10093
  const EXCLUDED_STATS_ATTRS = new Set(["human-instruction", "title"]);
10046
10094
  const MAX_IMPORT_DEPTH = 10;
10047
10095
  /**
@@ -10094,7 +10142,7 @@ class ContentTracker {
10094
10142
  * Checks if the same content has already been written to the file
10095
10143
  */
10096
10144
  hasContent(outputFile, content) {
10097
- const resolvedFile = path.resolve(outputFile);
10145
+ const resolvedFile = resolvePath(outputFile);
10098
10146
  const contentHash = this.generateContentHash(content);
10099
10147
  const fileHashes = this.fileContentHashes.get(resolvedFile);
10100
10148
  return fileHashes ? fileHashes.has(contentHash) : false;
@@ -10103,7 +10151,7 @@ class ContentTracker {
10103
10151
  * Records that content has been written to a file
10104
10152
  */
10105
10153
  addContent(outputFile, content) {
10106
- const resolvedFile = path.resolve(outputFile);
10154
+ const resolvedFile = resolvePath(outputFile);
10107
10155
  const contentHash = this.generateContentHash(content);
10108
10156
  if (!this.fileContentHashes.has(resolvedFile)) {
10109
10157
  this.fileContentHashes.set(resolvedFile, new Set());
@@ -10114,7 +10162,7 @@ class ContentTracker {
10114
10162
  * Checks if a file has been written to (for auto-append logic)
10115
10163
  */
10116
10164
  hasFile(outputFile) {
10117
- const resolvedFile = path.resolve(outputFile);
10165
+ const resolvedFile = resolvePath(outputFile);
10118
10166
  return this.fileContentHashes.has(resolvedFile);
10119
10167
  }
10120
10168
  /**
@@ -10160,8 +10208,56 @@ function resolveDefaultSrcDir(providedSrc) {
10160
10208
  return path.join(packageRoot, 'artifact');
10161
10209
  }
10162
10210
  /**
10163
- * Resolves recipe path, supporting package presets with colon syntax
10211
+ * Resolves output path with baseDir support following priority order:
10212
+ * 1. CLI baseDir option (highest priority)
10213
+ * 2. Import-level baseDir (new priority level)
10214
+ * 3. Recipe config.baseDir
10215
+ * 4. Existing behavior (relative to recipe file or current directory)
10164
10216
  */
10217
+ function resolveOutputPath(itemOut, cliBaseDir, importBaseDir, recipeBaseDir, recipePath) {
10218
+ // Check if the itemOut contains environment variables or tilde - if so, try to expand
10219
+ let processedItemOut = itemOut;
10220
+ if (itemOut.includes('$') || itemOut.startsWith('~')) {
10221
+ try {
10222
+ processedItemOut = resolvePath(itemOut);
10223
+ // If expansion resulted in an absolute path, use it as-is (ignore baseDir)
10224
+ if (path.isAbsolute(processedItemOut)) {
10225
+ return processedItemOut;
10226
+ }
10227
+ }
10228
+ catch (error) {
10229
+ // If expansion fails, continue with original itemOut
10230
+ processedItemOut = itemOut;
10231
+ }
10232
+ }
10233
+ // If output path is already absolute, use it as-is (ignore baseDir)
10234
+ if (path.isAbsolute(processedItemOut)) {
10235
+ return processedItemOut;
10236
+ }
10237
+ // Determine effective baseDir with priority: CLI > Import > Recipe > undefined
10238
+ const effectiveBaseDir = cliBaseDir || importBaseDir || recipeBaseDir;
10239
+ if (effectiveBaseDir) {
10240
+ // Use baseDir + relative path
10241
+ const expandedBaseDir = resolvePath(effectiveBaseDir);
10242
+ return path.resolve(expandedBaseDir, processedItemOut);
10243
+ }
10244
+ // Existing behavior: resolve relative to recipe file directory or current directory
10245
+ // For preset files (located in presets/ directory), resolve relative to project root instead
10246
+ let baseDirectory = '.';
10247
+ if (recipePath) {
10248
+ const packageRoot = findPackageRoot();
10249
+ const presetsDir = path.join(packageRoot, 'presets');
10250
+ // If the recipe is in the presets directory, use project root as base
10251
+ if (path.dirname(recipePath) === presetsDir) {
10252
+ baseDirectory = process.cwd();
10253
+ }
10254
+ else {
10255
+ // For regular recipe files, use recipe file directory as base
10256
+ baseDirectory = path.dirname(recipePath);
10257
+ }
10258
+ }
10259
+ return path.resolve(baseDirectory, processedItemOut);
10260
+ }
10165
10261
  function resolveRecipePath(recipePath) {
10166
10262
  // If recipe starts with ':', resolve to package preset
10167
10263
  if (recipePath.startsWith(':')) {
@@ -10260,11 +10356,26 @@ async function expandRecipeImports(items, currentPath, debugLogger, visited = ne
10260
10356
  throw new Error(`Invalid imported recipe file '${importPath}': 'recipe' array not found`);
10261
10357
  }
10262
10358
  debugLogger?.log(`Import contains ${importData.recipe.length} items`);
10359
+ // Check if this import item has an import-level baseDir
10360
+ const importLevelBaseDir = item.baseDir;
10361
+ if (importLevelBaseDir) {
10362
+ debugLogger?.log(`Import has baseDir: ${importLevelBaseDir}`);
10363
+ }
10263
10364
  // Recursively expand imports in the imported recipe
10264
10365
  debugLogger?.time(`Expanding nested imports in: ${resolvedImportPath}`);
10265
10366
  const expandedImported = await expandRecipeImports(importData.recipe, resolvedImportPath, debugLogger, newVisited, imported, depth + 1);
10266
10367
  debugLogger?.timeEnd(`Expanding nested imports in: ${resolvedImportPath}`);
10267
10368
  debugLogger?.log(`Nested expansion yielded ${expandedImported.length} items`);
10369
+ // If import has baseDir, tag all expanded items with it
10370
+ if (importLevelBaseDir) {
10371
+ expandedImported.forEach(expandedItem => {
10372
+ // Only set _importBaseDir if not already set (to preserve nested import baseDir priority)
10373
+ if (!expandedItem._importBaseDir) {
10374
+ expandedItem._importBaseDir = importLevelBaseDir;
10375
+ debugLogger?.log(`Tagged item '${expandedItem.title || expandedItem.out || 'untitled'}' with import baseDir: ${importLevelBaseDir}`);
10376
+ }
10377
+ });
10378
+ }
10268
10379
  // Add all expanded items from the import
10269
10380
  expanded.push(...expandedImported);
10270
10381
  }
@@ -10313,13 +10424,14 @@ function setupProgram() {
10313
10424
  .command('extract')
10314
10425
  .description('Extract and merge markdown files')
10315
10426
  .option('--src <path>', 'Source directory')
10316
- .option('--out <path>', 'Output file', './out/instruction.md')
10427
+ .option('--out <path>', 'Output file')
10317
10428
  .option('--type <types>', 'Filter by type (comma-separated)')
10318
10429
  .option('--language <languages>', 'Filter by language (comma-separated)')
10319
10430
  .option('--attr <attributes>', 'Filter by attributes (comma-separated)')
10320
10431
  .option('--title <title>', 'Title for the output')
10321
10432
  .option('--mode <mode>', 'Write mode: append, prepend, overwrite', 'overwrite')
10322
10433
  .option('--recipe <path>', 'Recipe file path or package preset (e.g., :typescript). Can be specified multiple times or comma-separated.', collectRecipeOptions, [])
10434
+ .option('--base-dir <path>', 'Base directory for output files (supports ~ and environment variable expansion)')
10323
10435
  .option('--vars <variables>', 'Template variables in key=value format (comma-separated)')
10324
10436
  .option('--env-file <path>', 'Path to .env file for template variables')
10325
10437
  .option('--debug', 'Enable debug logging')
@@ -10345,6 +10457,33 @@ function setupProgram() {
10345
10457
  });
10346
10458
  return program;
10347
10459
  }
10460
+ /**
10461
+ * Recursively scans a directory for YAML files and returns their relative paths
10462
+ */
10463
+ async function findYamlFilesRecursively(dir, baseDir = dir) {
10464
+ const yamlFiles = [];
10465
+ try {
10466
+ const items = await fs.readdir(dir, { withFileTypes: true });
10467
+ for (const item of items) {
10468
+ const fullPath = path.join(dir, item.name);
10469
+ if (item.isDirectory()) {
10470
+ // Recursively scan subdirectories
10471
+ const subFiles = await findYamlFilesRecursively(fullPath, baseDir);
10472
+ yamlFiles.push(...subFiles);
10473
+ }
10474
+ else if (item.isFile() && (item.name.endsWith('.yaml') || item.name.endsWith('.yml'))) {
10475
+ // Get relative path from base directory
10476
+ const relativePath = path.relative(baseDir, fullPath);
10477
+ yamlFiles.push(relativePath);
10478
+ }
10479
+ }
10480
+ }
10481
+ catch (error) {
10482
+ // Log errors for individual directories/files that can't be read
10483
+ console.warn(`Warning: Could not read directory "${dir}":`, error);
10484
+ }
10485
+ return yamlFiles;
10486
+ }
10348
10487
  /**
10349
10488
  * Lists available package presets
10350
10489
  */
@@ -10352,8 +10491,7 @@ async function listPresets() {
10352
10491
  const packageRoot = findPackageRoot();
10353
10492
  const presetsDir = path.join(packageRoot, 'presets');
10354
10493
  try {
10355
- const files = await fs.readdir(presetsDir);
10356
- const yamlFiles = files.filter(file => file.endsWith('.yaml') || file.endsWith('.yml'));
10494
+ const yamlFiles = await findYamlFilesRecursively(presetsDir);
10357
10495
  if (yamlFiles.length === 0) {
10358
10496
  // eslint-disable-next-line no-console
10359
10497
  console.log('No presets found in package.');
@@ -10458,7 +10596,7 @@ async function processContentWithMode(outFile, newContent, mode, debugLogger) {
10458
10596
  return newContent;
10459
10597
  }
10460
10598
  try {
10461
- const existing = await fs.readFile(path.resolve(outFile), "utf-8");
10599
+ const existing = await fs.readFile(resolvePath(outFile), "utf-8");
10462
10600
  debugLogger?.log(`Existing file found with ${existing.length} characters`);
10463
10601
  if (mode === WriteMode.APPEND) {
10464
10602
  debugLogger?.log('Appending new content to existing content');
@@ -10577,7 +10715,7 @@ async function processSingle(options, debugLogger) {
10577
10715
  debugLogger.time('Content mode processing');
10578
10716
  const finalContent = await processContentWithMode(outFile, contentWithTitle, mode, debugLogger);
10579
10717
  debugLogger.timeEnd('Content mode processing');
10580
- const resolved = path.resolve(outFile);
10718
+ const resolved = resolvePath(outFile);
10581
10719
  const writer = new MarkdownWriter();
10582
10720
  debugLogger.log(`Writing to output file: ${resolved}`);
10583
10721
  // Build front matter
@@ -10723,7 +10861,7 @@ async function handleExtractCommand(options) {
10723
10861
  });
10724
10862
  const extractOptions = {
10725
10863
  srcDir: resolveDefaultSrcDir(options.src),
10726
- outFile: options.out,
10864
+ outFile: options.out || './out/instruction.md',
10727
10865
  types: parseCommaSeparated(options.type),
10728
10866
  languages: parseCommaSeparated(options.language),
10729
10867
  attrFilters: parseCommaSeparated(options.attr) || [],
@@ -10740,15 +10878,20 @@ async function handleExtractCommand(options) {
10740
10878
  await validateRecipeFilesExist(options.recipe);
10741
10879
  if (options.recipe.length === 1) {
10742
10880
  // Single recipe - maintain backward compatibility
10743
- await processRecipe(options.recipe[0], extractOptions, new ContentTracker(), debugLogger);
10881
+ await processRecipe(options.recipe[0], extractOptions, new ContentTracker(), debugLogger, options.baseDir, options.out);
10744
10882
  }
10745
10883
  else {
10746
10884
  // Multiple recipes - process in order with auto-append
10747
- await processMultipleRecipes(options.recipe, extractOptions, debugLogger);
10885
+ await processMultipleRecipes(options.recipe, extractOptions, debugLogger, options.baseDir, options.out);
10748
10886
  }
10749
10887
  }
10750
10888
  else {
10751
10889
  debugLogger.log('Processing single extraction without recipe');
10890
+ // For single extraction, apply baseDir to outFile if provided
10891
+ if (options.baseDir) {
10892
+ extractOptions.outFile = resolveOutputPath(extractOptions.outFile, options.baseDir, undefined, undefined, undefined);
10893
+ debugLogger.log(`Applied CLI baseDir to output file: ${extractOptions.outFile}`);
10894
+ }
10752
10895
  await processSingle(extractOptions, debugLogger);
10753
10896
  }
10754
10897
  }
@@ -10765,13 +10908,13 @@ async function handleStatsCommand(options) {
10765
10908
  /**
10766
10909
  * Processes multiple recipe files in the specified order
10767
10910
  */
10768
- async function processMultipleRecipes(recipePaths, baseOptions, debugLogger) {
10911
+ async function processMultipleRecipes(recipePaths, baseOptions, debugLogger, cliBaseDir, cliOutFile) {
10769
10912
  const contentTracker = new ContentTracker();
10770
10913
  debugLogger.log(`Processing ${recipePaths.length} recipes in order`);
10771
10914
  for (const recipePath of recipePaths) {
10772
10915
  try {
10773
10916
  debugLogger.log(`Starting processing of recipe: ${recipePath}`);
10774
- await processRecipe(recipePath, baseOptions, contentTracker, debugLogger);
10917
+ await processRecipe(recipePath, baseOptions, contentTracker, debugLogger, cliBaseDir, cliOutFile);
10775
10918
  debugLogger.log(`Completed processing of recipe: ${recipePath}`);
10776
10919
  }
10777
10920
  catch (error) {
@@ -10784,10 +10927,103 @@ async function processMultipleRecipes(recipePaths, baseOptions, debugLogger) {
10784
10927
  }
10785
10928
  }
10786
10929
  }
10930
+ /**
10931
+ * Reads template files and extracts frontmatter for inheritance
10932
+ */
10933
+ async function loadTemplateFrontmatter(srcDir, types, languages, attrFilters, debugLogger) {
10934
+ debugLogger?.log('Loading template frontmatter for inheritance');
10935
+ const templateFiles = await loadAndFilterFiles(srcDir, types, languages, attrFilters, debugLogger);
10936
+ debugLogger?.log(`Found ${templateFiles.length} template files for frontmatter inheritance`);
10937
+ return templateFiles.map(file => file.attrs);
10938
+ }
10939
+ /**
10940
+ * Merges frontmatter values from multiple templates according to merge rules:
10941
+ * - Strings: concatenate with newlines, removing duplicates while preserving order
10942
+ * - Arrays: combine all elements
10943
+ * - Objects: later values overwrite
10944
+ */
10945
+ function mergeTemplateFrontmatterValues(templateFrontmatters, fieldName) {
10946
+ const values = templateFrontmatters
10947
+ .map(fm => fm[fieldName])
10948
+ .filter(val => val !== undefined && val !== null);
10949
+ if (values.length === 0) {
10950
+ return undefined;
10951
+ }
10952
+ if (values.length === 1) {
10953
+ return values[0];
10954
+ }
10955
+ // Check if all values are strings
10956
+ if (values.every(val => typeof val === 'string')) {
10957
+ // Split each string by newlines, flatten, remove duplicates while preserving order
10958
+ const allLines = [];
10959
+ const seen = new Set();
10960
+ for (const value of values) {
10961
+ const lines = value.split('\n').map(line => line.trim()).filter(line => line.length > 0);
10962
+ for (const line of lines) {
10963
+ if (!seen.has(line)) {
10964
+ seen.add(line);
10965
+ allLines.push(line);
10966
+ }
10967
+ }
10968
+ }
10969
+ return allLines.join('\n');
10970
+ }
10971
+ // Check if all values are arrays
10972
+ if (values.every(val => Array.isArray(val))) {
10973
+ return values.flat();
10974
+ }
10975
+ // For objects and mixed types, use the last value
10976
+ return values[values.length - 1];
10977
+ }
10978
+ /**
10979
+ * Processes @ syntax in frontmatter to inherit from template files
10980
+ */
10981
+ async function processFrontmatterInheritance(frontmatter, srcDir, types, languages, attrFilters, debugLogger) {
10982
+ if (!frontmatter || typeof frontmatter !== 'object') {
10983
+ return {};
10984
+ }
10985
+ const result = {};
10986
+ const inheritanceFields = [];
10987
+ // Separate inheritance fields (@ syntax) from regular fields
10988
+ for (const [key, value] of Object.entries(frontmatter)) {
10989
+ if (key.startsWith('@') && value === true) {
10990
+ inheritanceFields.push(key.slice(1)); // Remove @ prefix
10991
+ debugLogger?.log(`Found inheritance field: ${key} -> ${key.slice(1)}`);
10992
+ }
10993
+ else {
10994
+ result[key] = value;
10995
+ }
10996
+ }
10997
+ // If no inheritance fields, return as-is
10998
+ if (inheritanceFields.length === 0) {
10999
+ debugLogger?.log('No frontmatter inheritance fields found');
11000
+ return result;
11001
+ }
11002
+ debugLogger?.log(`Processing ${inheritanceFields.length} inheritance fields:`, inheritanceFields);
11003
+ // Load template frontmatters
11004
+ const templateFrontmatters = await loadTemplateFrontmatter(srcDir, types, languages, attrFilters, debugLogger);
11005
+ if (templateFrontmatters.length === 0) {
11006
+ debugLogger?.log('No template files found for inheritance');
11007
+ return result;
11008
+ }
11009
+ // Process each inheritance field
11010
+ for (const fieldName of inheritanceFields) {
11011
+ const mergedValue = mergeTemplateFrontmatterValues(templateFrontmatters, fieldName);
11012
+ if (mergedValue !== undefined) {
11013
+ result[fieldName] = mergedValue;
11014
+ debugLogger?.log(`Inherited field ${fieldName}:`, mergedValue);
11015
+ }
11016
+ else {
11017
+ debugLogger?.log(`No value found for inherited field ${fieldName} in templates`);
11018
+ }
11019
+ }
11020
+ debugLogger?.log('Frontmatter after inheritance processing:', Object.keys(result));
11021
+ return result;
11022
+ }
10787
11023
  /**
10788
11024
  * Processes a recipe file with multiple extract operations
10789
11025
  */
10790
- async function processRecipe(recipePath, baseOptions, contentTracker, debugLogger) {
11026
+ async function processRecipe(recipePath, baseOptions, contentTracker, debugLogger, cliBaseDir, cliOutFile) {
10791
11027
  const resolvedPath = resolveRecipePath(recipePath);
10792
11028
  debugLogger?.log(`Processing recipe at path: ${resolvedPath}`);
10793
11029
  try {
@@ -10799,6 +11035,9 @@ async function processRecipe(recipePath, baseOptions, contentTracker, debugLogge
10799
11035
  throw new Error("Invalid recipe file: 'recipe' array not found");
10800
11036
  }
10801
11037
  debugLogger?.log(`Recipe contains ${data.recipe.length} items`);
11038
+ // Read recipe config for baseDir
11039
+ const recipeBaseDir = data.config?.baseDir;
11040
+ debugLogger?.log('Recipe config:', { baseDir: recipeBaseDir });
10802
11041
  // Expand any imports in the recipe
10803
11042
  debugLogger?.time('Recipe import expansion');
10804
11043
  const expandedRecipe = await expandRecipeImports(data.recipe, resolvedPath, debugLogger);
@@ -10814,8 +11053,17 @@ async function processRecipe(recipePath, baseOptions, contentTracker, debugLogge
10814
11053
  language: item.language,
10815
11054
  mode: item.mode
10816
11055
  });
10817
- const outputFile = item.out || baseOptions.outFile;
10818
- const resolvedOutputFile = path.resolve(outputFile);
11056
+ // Priority: CLI --out option > recipe item.out > baseOptions.outFile default
11057
+ const itemOut = cliOutFile || item.out || baseOptions.outFile;
11058
+ const outputFile = resolveOutputPath(itemOut, cliBaseDir, item._importBaseDir, recipeBaseDir, resolvedPath);
11059
+ debugLogger?.log(`Resolved output path: ${itemOut} -> ${outputFile}`, {
11060
+ cliOutFile,
11061
+ itemOut: item.out,
11062
+ fallback: baseOptions.outFile,
11063
+ cliBaseDir,
11064
+ importBaseDir: item._importBaseDir,
11065
+ recipeBaseDir
11066
+ });
10819
11067
  // Generate the content that would be written to check for duplicates
10820
11068
  const { srcDir, types, languages, attrFilters, title, attr, vars, envFile } = baseOptions;
10821
11069
  const itemTypes = item.type ? parseCommaSeparated(item.type) : types;
@@ -10867,7 +11115,7 @@ async function processRecipe(recipePath, baseOptions, contentTracker, debugLogge
10867
11115
  effectiveMode = parseWriteMode(item.mode);
10868
11116
  debugLogger?.log(`Using explicit mode from recipe item: ${effectiveMode}`);
10869
11117
  }
10870
- else if (localTracker.hasFile(resolvedOutputFile)) {
11118
+ else if (localTracker.hasFile(outputFile)) {
10871
11119
  // Auto-append for duplicate output files when no explicit mode
10872
11120
  effectiveMode = WriteMode.APPEND;
10873
11121
  debugLogger?.log(`Auto-appending due to existing output file: ${effectiveMode}`);
@@ -10875,6 +11123,10 @@ async function processRecipe(recipePath, baseOptions, contentTracker, debugLogge
10875
11123
  else {
10876
11124
  debugLogger?.log(`Using base mode: ${effectiveMode}`);
10877
11125
  }
11126
+ // Process frontmatter inheritance (@ syntax)
11127
+ debugLogger?.time(`Frontmatter inheritance processing for item ${index + 1}`);
11128
+ const processedFrontmatter = await processFrontmatterInheritance(item.frontmatter, baseOptions.srcDir, itemTypes, itemLanguages, combinedAttrFilters, debugLogger);
11129
+ debugLogger?.timeEnd(`Frontmatter inheritance processing for item ${index + 1}`);
10878
11130
  const options = {
10879
11131
  srcDir: baseOptions.srcDir,
10880
11132
  outFile: outputFile,
@@ -10883,7 +11135,7 @@ async function processRecipe(recipePath, baseOptions, contentTracker, debugLogge
10883
11135
  attrFilters: combinedAttrFilters,
10884
11136
  title: itemTitle,
10885
11137
  mode: effectiveMode,
10886
- attr: item.frontmatter,
11138
+ attr: processedFrontmatter,
10887
11139
  debug: baseOptions.debug,
10888
11140
  vars: itemVars,
10889
11141
  envFile: envFile,
@@ -10932,3 +11184,5 @@ run().catch((error) => {
10932
11184
  console.error("Unhandled error:", error);
10933
11185
  process.exit(1);
10934
11186
  });
11187
+
11188
+ export { findYamlFilesRecursively };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Path expansion utility that supports tilde (~) and environment variable expansion
3
+ *
4
+ * Features:
5
+ * - Tilde expansion: ~/path -> /Users/username/path (Unix-like systems only)
6
+ * - Environment variable expansion: $HOME/path -> /Users/username/path
7
+ * - Environment variable expansion: ${VAR}/path -> /value/path
8
+ * - Complex combinations: ~/projects/${PROJECT_NAME}/file.md
9
+ *
10
+ * @param inputPath - Path to expand and resolve
11
+ * @returns Expanded and resolved absolute path
12
+ * @throws Error if required environment variables are missing
13
+ */
14
+ export declare function resolvePath(inputPath: string): string;
15
+ /**
16
+ * Safely resolve a path with environment variable expansion, returning the original path
17
+ * if expansion fails (useful for optional environment variables)
18
+ *
19
+ * @param inputPath - Path to expand and resolve
20
+ * @param fallbackToOriginal - If true, return path.resolve(inputPath) when expansion fails
21
+ * @returns Expanded and resolved absolute path, or resolved original path on failure
22
+ */
23
+ export declare function resolvePathSafe(inputPath: string, fallbackToOriginal?: boolean): string;
24
+ //# sourceMappingURL=pathExpansion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pathExpansion.d.ts","sourceRoot":"","sources":["../../src/utils/pathExpansion.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAuCrD;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,kBAAkB,GAAE,OAAc,GAAG,MAAM,CAS7F"}
@@ -0,0 +1,67 @@
1
+ import { homedir } from "os";
2
+ import path from "path";
3
+ /**
4
+ * Path expansion utility that supports tilde (~) and environment variable expansion
5
+ *
6
+ * Features:
7
+ * - Tilde expansion: ~/path -> /Users/username/path (Unix-like systems only)
8
+ * - Environment variable expansion: $HOME/path -> /Users/username/path
9
+ * - Environment variable expansion: ${VAR}/path -> /value/path
10
+ * - Complex combinations: ~/projects/${PROJECT_NAME}/file.md
11
+ *
12
+ * @param inputPath - Path to expand and resolve
13
+ * @returns Expanded and resolved absolute path
14
+ * @throws Error if required environment variables are missing
15
+ */
16
+ export function resolvePath(inputPath) {
17
+ if (!inputPath || typeof inputPath !== 'string') {
18
+ throw new Error('Path must be a non-empty string');
19
+ }
20
+ let expandedPath = inputPath;
21
+ // 1. Handle tilde expansion first (Unix-like systems only)
22
+ if (expandedPath.startsWith('~/')) {
23
+ // Check if we're on Windows
24
+ if (process.platform === 'win32') {
25
+ throw new Error('Tilde (~) expansion is not supported on Windows. Use %USERPROFILE% or environment variables instead.');
26
+ }
27
+ const homeDir = homedir();
28
+ expandedPath = path.join(homeDir, expandedPath.slice(2));
29
+ }
30
+ else if (expandedPath === '~') {
31
+ if (process.platform === 'win32') {
32
+ throw new Error('Tilde (~) expansion is not supported on Windows. Use %USERPROFILE% or environment variables instead.');
33
+ }
34
+ expandedPath = homedir();
35
+ }
36
+ // 2. Handle environment variable expansion
37
+ // Support both $VAR and ${VAR} syntax
38
+ expandedPath = expandedPath.replace(/\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (match, bracedVar, simpleVar) => {
39
+ const varName = bracedVar || simpleVar;
40
+ const envValue = process.env[varName];
41
+ if (envValue === undefined) {
42
+ throw new Error(`Environment variable '${varName}' is not defined`);
43
+ }
44
+ return envValue;
45
+ });
46
+ // 3. Resolve to absolute path
47
+ return path.resolve(expandedPath);
48
+ }
49
+ /**
50
+ * Safely resolve a path with environment variable expansion, returning the original path
51
+ * if expansion fails (useful for optional environment variables)
52
+ *
53
+ * @param inputPath - Path to expand and resolve
54
+ * @param fallbackToOriginal - If true, return path.resolve(inputPath) when expansion fails
55
+ * @returns Expanded and resolved absolute path, or resolved original path on failure
56
+ */
57
+ export function resolvePathSafe(inputPath, fallbackToOriginal = true) {
58
+ try {
59
+ return resolvePath(inputPath);
60
+ }
61
+ catch (error) {
62
+ if (fallbackToOriginal) {
63
+ return path.resolve(inputPath);
64
+ }
65
+ throw error;
66
+ }
67
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aramassa/ai-rules",
3
- "version": "0.1.1-npmjs.20250910072942",
3
+ "version": "0.1.5",
4
4
  "description": "This repository collects guidelines and instructions for developing AI agents. It contains documents covering communication rules, coding standards, testing strategies, and general operational practices.",
5
5
  "workspaces": [
6
6
  "packages/extract",
@@ -24,8 +24,10 @@
24
24
  "start:cli": "node dist/cli.js",
25
25
  "start:dev:cli": "tsx src/cli.ts",
26
26
  "test": "vitest run --environment node test",
27
+ "test:ci": "vitest run --environment node",
27
28
  "test:all": "npm run test --workspaces",
28
- "build": "npm run --ws build && tsc -p tsconfig.build.json",
29
+ "build": "npm run --ws build && tsc -p tsconfig.build.json && chmod +x dist/cli.js",
30
+ "build:ci": "npm run --ws build && tsc -p tsconfig.build.json && chmod +x dist/cli.js",
29
31
  "build:pkg:clean": "npm run --ws clean",
30
32
  "clean": "npm run build:pkg:clean && rm -rf dist",
31
33
  "root:build": "npm run build && if [ \"$NODE_ENV\" != \"test\" ]; then rollup -c && chmod +x dist/cli.js; fi"
@@ -51,7 +53,7 @@
51
53
  "@rollup/plugin-node-resolve": "^16.0.1",
52
54
  "@rollup/plugin-typescript": "^12.1.4",
53
55
  "@types/commander": "^2.12.0",
54
- "@types/node": "^24.3.0",
56
+ "@types/node": "^24.5.2",
55
57
  "fast-glob": "^3.3.3",
56
58
  "gray-matter": "^4.0.3",
57
59
  "rollup": "^4.45.0",
package/presets/README.md CHANGED
@@ -62,6 +62,31 @@ npx @aramassa/ai-rules extract --recipe presets/infrastructure-ansible.yaml --sr
62
62
  - DevOps・SRE関連プロジェクト
63
63
  - 設定管理・自動化プロジェクト
64
64
 
65
+ ### `chrome-extension.yaml`
66
+
67
+ Chrome Extension(Manifest V3)開発プロジェクト向けの包括的なinstruction構成です。
68
+
69
+ **含まれる内容:**
70
+
71
+ - Chrome Extension Manifest V3 Development Rules - 現代的なChrome Extension開発のベストプラクティス
72
+ - Code Quality Tools - JavaScript/Chrome Extension向けのコード品質管理
73
+ - Package Management Rules - パッケージ管理とビルドプロセス
74
+ - Git Rules - バージョン管理とワークフロー
75
+ - Basic Communication Rules - 基本的なコミュニケーションルール
76
+
77
+ **使用方法:**
78
+
79
+ ```bash
80
+ npx @aramassa/ai-rules extract --recipe presets/chrome-extension.yaml --src artifact/instructions
81
+ ```
82
+
83
+ **対象プロジェクト:**
84
+
85
+ - Chrome Extension開発プロジェクト(Manifest V3)
86
+ - JavaScript/TypeScriptベースのブラウザ拡張機能
87
+ - Webブラウザの機能拡張・自動化プロジェクト
88
+ - Service Worker、Content Scripts、Popup UIを含む複合アプリケーション
89
+
65
90
  ### `chatmodes.yaml`
66
91
 
67
92
  AI対話支援機能(chatmodes)をプロジェクトに統合するための設定です。
@@ -70,6 +95,7 @@ AI対話支援機能(chatmodes)をプロジェクトに統合するための
70
95
 
71
96
  - Instruction Improve Chatmode - プロジェクト指示の改善支援
72
97
  - Planning Chatmode - todo_plans作成支援
98
+ - Bug Reproduce Chatmode - GitHub Issue バグ再現・調査・修正提案支援
73
99
 
74
100
  **使用方法:**
75
101