glob extensions

This commit is contained in:
lovebird 2025-06-04 07:45:37 +02:00
parent ccad5c5407
commit d5ad21d4fc
5 changed files with 63 additions and 89 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -377,22 +377,12 @@ export const run = async (opts: IKBotTask): Promise<ProcessRunResult[]> => {
ITEM: item,
variables: { ITEM: item }
}
//override model if item is a model id
const model = _models.find(m => m.id === item)
if (model) {
itemOpts.model = item
}
let currentItemSpecificIncludes = [forward_slash(item)];
const itemPathInfo = path.parse(item);
// Only add corresponding .cpp if --glob-extension=match-cpp is set
if (opts.globExtension === 'match-cpp' && itemPathInfo.ext === '.h') {
const cppFilePath = path.join(itemPathInfo.dir, `${itemPathInfo.name}.cpp`);
if (exists(cppFilePath) && isFile(cppFilePath)) {
currentItemSpecificIncludes.push(forward_slash(cppFilePath));
}
}
itemOpts.include = [...opts.include, ...currentItemSpecificIncludes];

View File

@ -6,18 +6,11 @@ import { sync as dir } from '@polymech/fs/dir'
import { createItem as toNode } from '@polymech/fs/inspect'
import { sync as exists } from '@polymech/fs/exists'
import { isFile, forward_slash } from '@polymech/commons'
import { logger } from './index.js'
import { lookup } from 'mime-types'
import { globSync } from 'glob'
import { globSync, hasMagic } from 'glob'
import { EXCLUDE_GLOB, MAX_FILE_SIZE } from './constants.js'
import { defaultMimeRegistry, IHandlerResult } from './mime-handlers.js'
import { ChatCompletionContentPartImage } from 'openai/resources/index.mjs'
import { IKBotTask, ICollector } from '@polymech/ai-tools'
import { supported } from './commands/run-assistant.js'
import { handleWebUrl } from './http.js'
import { pathInfoEx } from '@polymech/commons'
import { DEFAULT_ROOTS, DEFAULT_VARS } from '@polymech/commons'
import { variables, generateSingleFileVariables } from './variables.js'
import { IKBotTask} from '@polymech/ai-tools'
import { resolveVariables } from '@polymech/commons'
import { generateSingleFileVariables } from './variables.js'
import { E_GlobExtensionType } from './zod_schema.js'
export const default_filters = {
@ -44,26 +37,25 @@ const globExtensionPresets: Map<E_GlobExtensionType, string> = new Map([
]);
const resolveAndGlobExtensionPattern = (
initialFilePath: string,
patternString: string,
projectPath: string
resolvedPatternString: string,
): string[] => {
const fileVars = generateSingleFileVariables(initialFilePath, projectPath);
let substitutedPattern = patternString;
for (const key in fileVars) {
const placeholder = new RegExp(`\\\${\\s*${key}\\s*}`, 'g');
substitutedPattern = substitutedPattern.replace(placeholder, fileVars[key]);
}
try {
const foundFiles = globSync(substitutedPattern, {
cwd: projectPath,
absolute: false,
nodir: true
});
return foundFiles.map(f => path.resolve(projectPath, f));
if (!hasMagic(resolvedPatternString)) {
// No magic characters, treat as a literal path
if (default_filters.exists(resolvedPatternString) && default_filters.isFile(resolvedPatternString)) {
return [forward_slash(resolvedPatternString)];
}
return []; // Literal path does not exist or is not a file
} else {
// Has magic characters, use globSync to expand
const foundFiles = globSync(resolvedPatternString, {
absolute: true, // Expecting resolvedPatternString to be absolute or glob to handle it
nodir: true
});
return foundFiles.map(f => forward_slash(f));
}
} catch (e) {
// console.warn(`Error processing globExtension pattern "${resolvedPatternString}": ${e.message}`);
return [];
}
};
@ -118,13 +110,16 @@ export const glob = (
const allFilesToConsider = new Set<string>(initialAbsoluteFiles);
if (options && typeof options.globExtension === 'string' && options.globExtension.trim() !== '') {
let actualPatternToUse = options.globExtension;
let rawPatternString = options.globExtension;
if (globExtensionPresets.has(options.globExtension as E_GlobExtensionType)) {
actualPatternToUse = globExtensionPresets.get(options.globExtension as E_GlobExtensionType)!;
rawPatternString = globExtensionPresets.get(options.globExtension as E_GlobExtensionType)!;
}
for (const initialFile of [...initialAbsoluteFiles]) {
const additionalFiles = resolveAndGlobExtensionPattern(initialFile, actualPatternToUse, projectPath);
const fileVars = generateSingleFileVariables(initialFile, projectPath);
const fullyResolvedPattern = resolveVariables(rawPatternString, false, fileVars, false);
const additionalFiles = resolveAndGlobExtensionPattern(fullyResolvedPattern);
additionalFiles.forEach(f => allFilesToConsider.add(f));
}
}
@ -133,20 +128,16 @@ export const glob = (
return false;
}
const relativeFilePath = path.relative(projectPath, absoluteFilePath);
const checkResult = globSync([forward_slash(relativeFilePath)], {
cwd: projectPath,
ignore: [...ignorePatterns],
nodir: true,
absolute: false
});
if (checkResult.length === 0) {
return false;
}
return true;
});
return { files: finalFiles.map(f => forward_slash(f)), webUrls }
}

View File

@ -20,7 +20,8 @@ describe('globExtension with complete_params output', () => {
'PHApp-Profiles.cpp',
'PHAppNetwork.cpp',
'PHAppSettings.cpp',
'PHAppWeb.cpp'
'PHAppWeb.cpp',
'PHApp.md'
];
const expectedAbsoluteFilePaths = expectedFileNames.map(f => path.normalize(path.resolve(testDataRoot, f)));
@ -28,27 +29,25 @@ describe('globExtension with complete_params output', () => {
debug: () => {}, info: () => {}, warn: () => {}, error: () => {}, fatal: () => {},
} as any;
it('should correctly include .h and related .cpp files using the "match-cpp" preset', async () => {
it('should include .h, related .cpp files, and .md file using brace expansion in globExtension', async () => {
const initialOpts: IKBotTask = {
path: testDataRoot,
include: ['*.h'],
globExtension: 'match-cpp',
globExtension: '${SRC_DIR}/${SRC_NAME}*.{cpp,md}',
mode: E_Mode.COMPLETION,
prompt: 'test-prompt-preset-match-cpp',
prompt: 'test-prompt-brace-expansion',
logger: mockLogger,
};
// 1. Complete Options
const completedOptions = await complete_options(initialOpts);
delete initialOpts.client;
expect(completedOptions).not.toBeNull();
if (!completedOptions) return; // Guard for type safety
// 2. Complete Messages (this is where glob and globExtension are applied)
const { messages: gatheredMessages, files: gatheredFiles } = await complete_messages(initialOpts, completedOptions);
expect(gatheredMessages).toBeInstanceOf(Array);
expect(gatheredMessages.length).toBeGreaterThan(0); // Prefs, Prompt + Files
if (!completedOptions) return;
// 2. Complete Messages
const { messages: gatheredMessages } = await complete_messages(initialOpts, completedOptions);
expect(gatheredMessages).toBeInstanceOf(Array);
// 3. Complete Params
const finalParams = await complete_params(completedOptions, gatheredMessages);
expect(finalParams.messages).toBeInstanceOf(Array);
@ -64,7 +63,6 @@ describe('globExtension with complete_params output', () => {
return path.normalize(path.resolve(testDataRoot, metaRelPath));
}
if (msg.path && typeof msg.path === 'string') {
// Assuming msg.path from complete_messages is relative to initialOpts.path (testDataRoot)
return path.normalize(path.resolve(testDataRoot, msg.path));
}
return null;
@ -73,14 +71,11 @@ describe('globExtension with complete_params output', () => {
const collectedPathsSet = new Set(collectedPathsFromFinalParams);
// console.log("Expected paths:", expectedAbsoluteFilePaths);
// console.log("Collected paths from finalParams.messages:", collectedPathsFromFinalParams);
expectedAbsoluteFilePaths.forEach(expectedFile => {
expect(collectedPathsSet.has(expectedFile), `Expected file ${path.basename(expectedFile)} (${expectedFile}) to be in finalParams.messages.`).toBe(true);
});
expect(collectedPathsSet.size, "Number of unique collected files in finalParams.messages should match expected").toBe(expectedAbsoluteFilePaths.length);
expect(collectedPathsSet.size).toBe(expectedAbsoluteFilePaths.length);
}, 10000);
});