excludes, include --dst for glob extensions

This commit is contained in:
lovebird 2025-06-04 13:43:41 +02:00
parent 976dea2757
commit d4712949be
13 changed files with 659 additions and 200 deletions

File diff suppressed because one or more lines are too long

View File

@ -6,7 +6,7 @@ export declare const default_filters: {
size: (filePath: string) => boolean;
};
export declare const isWebUrl: (str: string) => boolean;
export declare const glob: (projectPath: string, include?: string[], exclude?: string[], options?: IKBotTask) => {
export declare const glob: (projectPath: string, include?: string[], rawExcludeOptions?: string[], options?: IKBotTask) => {
files: string[];
webUrls: Set<string>;
};

File diff suppressed because one or more lines are too long

View File

@ -8,5 +8,6 @@ import { IKBotTask } from '@polymech/ai-tools';
export declare const isPathOutsideSafe: (pathA: string, pathB: string) => boolean;
export declare const base64: (filePath: string) => string | null;
export declare const images: (files: string[]) => ChatCompletionContentPartImage[];
export declare function get(projectPath: string, include: string[], options: IKBotTask): Promise<Array<IHandlerResult>>;
export declare function get(projectPath: string, // This is already an absolute path from processRun/complete_messages
include: string[], options: IKBotTask): Promise<Array<IHandlerResult>>;
export declare function vectorize(file: string, options: IKBotTask): Promise<string>;

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,3 @@
import { IKBotTask } from '@polymech/ai-tools';
export declare const generateSingleFileVariables: (filePath: string, projectPath: string) => Record<string, string>;
export declare const sourceVariables: (filePath: string, projectPath: string) => Record<string, string>;
export declare const variables: (options: IKBotTask) => any;

File diff suppressed because one or more lines are too long

View File

@ -111,17 +111,19 @@ export const complete_messages = async (
messages: Array<ChatCompletionMessageParam>,
files: any[]
}> => {
let messages: Array<ChatCompletionMessageParam> = []
let chatMessages: Array<ChatCompletionMessageParam> = []
const promptMessage = await prompt(opts)
if (!promptMessage?.content) {
return { messages: [], files: [] }
}
messages.push(promptMessage as ChatCompletionMessageParam)
messages.push((await preferences(opts)) as ChatCompletionMessageParam)
// Get content from files and web URLs
let files = await get(path.resolve(options.path || '.'), options.include, options) || []
files = files.map(f => {
chatMessages.push(promptMessage as ChatCompletionMessageParam)
chatMessages.push((await preferences(opts)) as ChatCompletionMessageParam)
const projectPath = path.resolve(options.path || '.')
let sourceFiles = await get(projectPath, options.include, options) || []
const processedFileMessages: ChatCompletionMessageParam[] = sourceFiles.map(f => {
if (f.path && f.content && typeof f.content === 'string') {
const mimeType = lookup(f.path)
// Check if the mime type is not binary (heuristic: starts with 'text/' or is a common non-binary type)
@ -147,8 +149,9 @@ Original Content:
}
return { ...f, role: 'user' }
})
messages = [...messages as Array<ChatCompletionMessageParam>, ...files]
return { messages, files }
chatMessages = [...chatMessages, ...processedFileMessages]
return { messages: chatMessages, files: sourceFiles }
}
/**
@ -223,6 +226,7 @@ export const execute_request = async (
* @returns - The result of the task execution
*/
export const processRun = async (opts: IKBotTask): Promise<ProcessRunResult> => {
const intialIncludes = [...opts.include]
let options = await complete_options(opts)
if (!options) {
return null
@ -231,10 +235,12 @@ export const processRun = async (opts: IKBotTask): Promise<ProcessRunResult> =>
const client = options.client
const { messages, files } = await complete_messages(opts, options)
if(intialIncludes.length > 0 && files.length === 0) {
return ""
}
if (messages.length === 0) {
return ""
}
const params = await complete_params(options, messages)
const logDir = path.resolve(resolve(opts.logs || './logs'))

View File

@ -1,15 +1,13 @@
import * as path from 'node:path'
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 { createItem as toNode } from '@polymech/fs/inspect'
import { isFile, forward_slash, resolveVariables } from '@polymech/commons'
import { globSync, hasMagic } from 'glob'
import { EXCLUDE_GLOB, MAX_FILE_SIZE } from './constants.js'
import { IKBotTask} from '@polymech/ai-tools'
import { resolveVariables } from '@polymech/commons'
import { generateSingleFileVariables } from './variables.js'
import { E_GlobExtensionType } from './zod_schema.js'
import { IKBotTask } from '@polymech/ai-tools'
import { sourceVariables } from './variables.js'
import { E_GlobExtensionType, E_Mode } from './zod_schema.js'
export const default_filters = {
isFile,
@ -19,123 +17,121 @@ export const default_filters = {
const isPathInside = (childPath: string, parentPath: string): boolean => {
const relation = path.relative(parentPath, childPath);
return Boolean(
relation &&
!relation.startsWith('..') &&
!relation.startsWith('..' + path.sep)
);
return Boolean(relation && !relation.startsWith('..') && !relation.startsWith('..' + path.sep));
};
export const isWebUrl = (str: string): boolean => {
return /^https?:\/\//.test(str);
}
export const isWebUrl = (str: string): boolean => /^https?:\/\//.test(str);
const globExtensionPresets: Map<E_GlobExtensionType, string> = new Map([
['match-cpp', '${SRC_DIR}/${SRC_NAME}*.cpp']
]);
const resolveAndGlobExtensionPattern = (
resolvedPatternString: string,
): string[] => {
const resolveAndGlobExtensionPattern = (resolvedPatternString: string): string[] => {
try {
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
return [];
} 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
});
const foundFiles = globSync(resolvedPatternString, { absolute: true, nodir: true });
return foundFiles.map(f => forward_slash(f));
}
} catch (e) {
// console.warn(`Error processing globExtension pattern "${resolvedPatternString}": ${e.message}`);
return [];
}
} catch (e) { return []; }
};
export const glob = (
projectPath: string,
include: string[] = [],
exclude: string[] = [],
rawExcludeOptions: string[] = [],
options?: IKBotTask
): { files: string[], webUrls: Set<string> } => {
if (!exists(projectPath)) {
dir(projectPath)
return { files: [], webUrls: new Set<string>() }
dir(projectPath);
return { files: [], webUrls: new Set<string>() };
}
const filters = new Set<string>()
const absolutePathsFromInclude = new Set<string>()
const webUrls = new Set<string>()
const ignorePatterns = new Set<string>(EXCLUDE_GLOB)
const includeFilters = new Set<string>(); // Renamed from 'filters' to be specific
const absolutePathsFromInclude = new Set<string>();
const webUrls = new Set<string>();
const staticIgnorePatterns = new Set<string>(EXCLUDE_GLOB);
const dynamicExcludePatterns: string[] = [];
include.forEach(pattern => {
if (isWebUrl(pattern)) {
webUrls.add(pattern)
return
}
if (path.isAbsolute(pattern)) {
absolutePathsFromInclude.add(path.resolve(pattern));
} else {
filters.add(pattern)
}
})
exclude.forEach(pattern => {
if (isWebUrl(pattern)) {
return;
}
ignorePatterns.add(pattern);
(rawExcludeOptions || []).forEach(pattern => {
if (pattern.includes('${')) { dynamicExcludePatterns.push(pattern); }
else { staticIgnorePatterns.add(pattern); }
});
const initialRelativeGlobResults = globSync([...filters], {
cwd: projectPath,
absolute: false,
ignore: [...ignorePatterns],
nodir: true
})
include.forEach(pattern => {
if (isWebUrl(pattern)) { webUrls.add(pattern); return; }
if (path.isAbsolute(pattern)) { absolutePathsFromInclude.add(path.resolve(pattern)); }
else { includeFilters.add(pattern); }
});
const initialAbsoluteFiles = new Set<string>([
const initialRelativeGlobResults = globSync([...includeFilters], {
cwd: projectPath, absolute: false, ignore: [...staticIgnorePatterns], nodir: true
});
const allInitialInputFiles = new Set<string>([
...initialRelativeGlobResults.map(file => path.resolve(projectPath, file)),
...Array.from(absolutePathsFromInclude)
]);
const allFilesToConsider = new Set<string>(initialAbsoluteFiles);
let filesSurvivingPrimaryDynamicExclude = new Set<string>();
if (dynamicExcludePatterns.length > 0 && allInitialInputFiles.size > 0) {
for (const inputFile of allInitialInputFiles) {
let isDynamicallyExcluded = false;
const fileVars = sourceVariables(inputFile, projectPath);
for (const dynamicPattern of dynamicExcludePatterns) {
const resolvedDynamicExcludeGlob = resolveVariables(dynamicPattern, false, fileVars, false);
let foundExcludedMatches: string[] = [];
try {
if (path.isAbsolute(resolvedDynamicExcludeGlob)) {
foundExcludedMatches = globSync(resolvedDynamicExcludeGlob, { absolute: true, nodir: true, cwd: process.cwd() });
} else {
foundExcludedMatches = globSync(resolvedDynamicExcludeGlob, { cwd: projectPath, absolute: true, nodir: true });
}
} catch (e) { options?.logger?.warn(`[Dynamic Exclude] Error globbing exclude pattern ${resolvedDynamicExcludeGlob}: ${e.message}`); }
if (foundExcludedMatches.length > 0) {
//options?.logger?.info(`[Dynamic Exclude - Pass 1] Input file ${path.relative(projectPath, inputFile)} excluded by pattern ${resolvedDynamicExcludeGlob}`);
isDynamicallyExcluded = true;
break;
}
}
if (!isDynamicallyExcluded) {
filesSurvivingPrimaryDynamicExclude.add(inputFile);
}
}
} else {
filesSurvivingPrimaryDynamicExclude = new Set<string>(allInitialInputFiles);
}
const finalCandidates = new Set<string>(filesSurvivingPrimaryDynamicExclude);
if (options && typeof options.globExtension === 'string' && options.globExtension.trim() !== '') {
let rawPatternString = options.globExtension;
let patternFromPresetOrCustom = options.globExtension;
if (globExtensionPresets.has(options.globExtension as E_GlobExtensionType)) {
rawPatternString = globExtensionPresets.get(options.globExtension as E_GlobExtensionType)!;
patternFromPresetOrCustom = globExtensionPresets.get(options.globExtension as E_GlobExtensionType)!;
}
for (const initialFile of [...initialAbsoluteFiles]) {
const fileVars = generateSingleFileVariables(initialFile, projectPath);
const fullyResolvedPattern = resolveVariables(rawPatternString, false, fileVars, false);
for (const sourceFileForExtension of filesSurvivingPrimaryDynamicExclude) {
const fileVars = sourceVariables(sourceFileForExtension, projectPath);
const fullyResolvedPattern = resolveVariables(patternFromPresetOrCustom, false, fileVars, false);
const additionalFiles = resolveAndGlobExtensionPattern(fullyResolvedPattern);
additionalFiles.forEach(f => allFilesToConsider.add(f));
additionalFiles.forEach(f => finalCandidates.add(f));
}
}
const finalFiles = Array.from(allFilesToConsider).filter(absoluteFilePath => {
const trulyFinalFiles = Array.from(finalCandidates).filter(absoluteFilePath => {
if (!Object.keys(default_filters).every((key) => default_filters[key](absoluteFilePath))) {
return false;
}
const relativeFilePath = path.relative(projectPath, absoluteFilePath);
const checkResult = globSync([forward_slash(relativeFilePath)], {
cwd: projectPath,
ignore: [...ignorePatterns],
nodir: true,
absolute: false
const staticCheckResult = globSync([forward_slash(relativeFilePath)], {
cwd: projectPath, ignore: [...staticIgnorePatterns], nodir: true, absolute: false
});
if (checkResult.length === 0) {
return false;
}
if (staticCheckResult.length === 0) { return false; }
return true;
});
return { files: finalFiles.map(f => forward_slash(f)), webUrls }
}
return { files: trulyFinalFiles.map(f => forward_slash(f)), webUrls };
};

View File

@ -5,7 +5,7 @@ import { sync as read } from '@polymech/fs/read'
// import { createItem as toNode } from '@polymech/fs/inspect' // Moved to glob.ts
import { sync as exists } from '@polymech/fs/exists' // Still needed for vectorize
import { isFile, forward_slash } from '@polymech/commons' // isFile potentially still needed for vectorize
import { isFile, forward_slash, resolve as resolvePath } from '@polymech/commons' // Renamed resolve to resolvePath to avoid conflict
import { logger } from './index.js'
import { lookup } from 'mime-types'
// import { globSync } from 'glob' // Moved to glob.ts
@ -16,6 +16,8 @@ import { IKBotTask, ICollector } from '@polymech/ai-tools'
import { supported } from './commands/run-assistant.js'
import { handleWebUrl } from './http.js'
import { glob } from './glob.js' // Import glob from glob.ts
import { sourceVariables } from './variables.js' // Import for dynamic exclusion
import { E_Mode } from './zod_schema.js' // Import E_Mode for the check
/**
* @todos
@ -65,21 +67,48 @@ export const images = (files: string[]): ChatCompletionContentPartImage[] => {
// glob function definition removed from here
export async function get(
projectPath: string,
projectPath: string, // This is already an absolute path from processRun/complete_messages
include: string[] = [],
options: IKBotTask
): Promise<Array<IHandlerResult>> {
const { files, webUrls } = glob(projectPath, include, options.exclude, options)
const { files: initialAbsoluteFilePaths, webUrls } = glob(projectPath, include, options.exclude, options)
const fileResults = files.map((fullPath) => {
let filesToProcess = initialAbsoluteFilePaths;
// --- Dynamic Exclusion based on --dst existence (for completion mode) ---
if (options.dst && options.mode === E_Mode.COMPLETION && filesToProcess.length > 0) {
const filesToKeepAfterDstCheck = [];
for (const absoluteSrcFilePath of filesToProcess) {
// No need to check fileObj.path, as these are already absolute string paths
const fileSpecificVars = sourceVariables(absoluteSrcFilePath, projectPath)
const fullVarsForDst = {
...options.variables, // Global variables from complete_options
...fileSpecificVars, // File-specific variables
MODEL: options.model ? path.parse(options.model).name : 'unknown_model',
ROUTER: options.router || 'unknown_router'
}
const potentialDstPath = path.resolve(resolvePath(options.dst, false, fullVarsForDst));
if (exists(potentialDstPath)) {
options.logger?.info(`Skipping source file ${path.relative(projectPath, absoluteSrcFilePath)} as output ${potentialDstPath} already exists.`);
} else {
filesToKeepAfterDstCheck.push(absoluteSrcFilePath);
}
}
filesToProcess = filesToKeepAfterDstCheck;
}
// --- End Dynamic Exclusion ---
// Process file contents from the final list of files
const fileResults = filesToProcess.map((fullPath) => { // fullPath is an absolute path
try {
const relativePath = forward_slash(path.relative(projectPath, fullPath))
const relativePath = forward_slash(path.relative(projectPath, fullPath)) // This is correct for mime handlers and message construction
if (isFile(fullPath) && exists(fullPath)) {
const mimeType = lookup(fullPath) || 'text/plain'
const mimeType = lookup(fullPath) || 'text/plain' // Use fullPath for lookup
const handler = defaultMimeRegistry.getHandler(mimeType)
if (handler) {
return handler.handle(fullPath, relativePath)
return handler.handle(fullPath, relativePath) // Pass absolute and relative paths to handler
}
// Fallback for text/* if specific handler not found
return defaultMimeRegistry.getHandler('text/*')?.handle(fullPath, relativePath) || null
}
return null

View File

@ -3,7 +3,7 @@ import { pathInfoEx } from '@polymech/commons'
import { DEFAULT_ROOTS, DEFAULT_VARS } from '@polymech/commons'
import { IKBotTask } from '@polymech/ai-tools'
export const generateSingleFileVariables = (filePath: string, projectPath: string): Record<string, string> => {
export const sourceVariables = (filePath: string, projectPath: string): Record<string, string> => {
const fileSpecificVariables: Record<string, string> = {};
const srcParts = path.parse(filePath);

View File

@ -0,0 +1,279 @@
#ifndef PHAPP_H
#define PHAPP_H
#include "config.h"
#include "config-modbus.h"
#include "features.h"
#include <enums.h>
#include <vector>
#include <xmath.h>
#include <macros.h>
#include <App.h>
#include <Component.h>
#include <Bridge.h>
#include <SerialMessage.h>
#include <ArduinoLog.h>
#include <Logger.h>
#include <ArduinoJson.h>
#include <LittleFS.h>
#include <xstatistics.h>
#include <modbus/ModbusTCP.h>
#include <modbus/ModbusTypes.h>
#include <profiles/SignalPlot.h>
#include <profiles/WiFiNetworkSettings.h>
#include <components/OmronE5.h>
class POT;
class Relay;
class RS485;
class Pos3Analog;
class StatusLight;
class RESTServer;
class PIDController;
class TemperatureProfile;
class SignalPlot;
class SAKO_VFD;
class MB_GPIO;
class AnalogLevelSwitch;
class LEDFeedback;
class Extruder;
class Plunger;
class Joystick;
class PHApp;
class AmperageBudgetManager;
class AsyncWebServerRequest;
class PHApp : public App
{
public:
//////////////////////////////////////////////////////////////
// Enums
//////////////////////////////////////////////////////////////
enum CONTROLLER_STATE
{
E_CS_OK = 0,
E_CS_ERROR = 10
};
enum APP_STATE
{
RESET = 0,
EXTRUDING = 1,
STANDBY = 2,
ERROR = 5,
PID_TIMEOUT = 11,
FEED_TIMEOUT = 12,
CONTROL_PANEL_INVALID = 13,
PID_ERROR = 20,
FEED_ERROR = 40,
};
//////////////////////////////////////////////////////////////
// Constructor / Destructor
//////////////////////////////////////////////////////////////
PHApp();
~PHApp() override;
//////////////////////////////////////////////////////////////
// Core Application Logic
//////////////////////////////////////////////////////////////
virtual short setup();
virtual short onRun();
short loop() override;
short load(short val0 = 0, short val1 = 0);
virtual short serial_register(Bridge *bridge);
virtual Component *byId(ushort id);
// App States & Error Handling
short _state;
short _cstate;
short _error;
short setAppState(short newState);
short getAppState(short val);
short getLastError() { return _error; }
short setLastError(short val = 0) { _error = val; return _error; }
short onError(short id, short code);
short clearError();
short reset(short arg1, short arg2); // Related to resetting state?
//////////////////////////////////////////////////////////////
// Components
//////////////////////////////////////////////////////////////
SerialMessage *com_serial;
POT *pot_0;
POT *pot_1;
POT *pot_2;
StatusLight *statusLight_0;
StatusLight *statusLight_1;
Relay *relay_0;
Relay *relay_1;
Relay *relay_2;
Relay *relay_3;
Relay *relay_4;
Relay *relay_5;
Relay *relay_6;
Relay *relay_7;
Pos3Analog *pos3Analog_0;
Pos3Analog *pos3Analog_1;
PIDController *pidController_0;
SAKO_VFD *vfd_0;
Extruder *extruder_0;
Plunger *plunger_0;
MB_GPIO *gpio_0;
AnalogLevelSwitch *analogLevelSwitch_0;
LEDFeedback *ledFeedback_0;
Joystick *joystick_0;
AmperageBudgetManager *pidManagerAmperage;
// Component Callbacks/Control
short onStop(short code = 0);
short onWarning(short code);
//////////////////////////////////////////////////////////////
// Logging
//////////////////////////////////////////////////////////////
std::vector<String> logBuffer;
size_t currentLogIndex = 0;
CircularLogPrinter *logPrinter = nullptr;
std::vector<String> getLogSnapshot()
{
std::vector<String> snapshot;
snapshot.reserve(logBuffer.size());
if (logBuffer.size() < LOG_BUFFER_LINES)
{
for (size_t i = 0; i < logBuffer.size(); ++i)
{
snapshot.push_back(logBuffer[i]);
}
}
else
{
// Buffer is full and circular
size_t startIndex = (currentLogIndex + 1) % LOG_BUFFER_LINES; // <-- Note: LOG_BUFFER_LINES is now defined in Logger.h
for (size_t i = 0; i < LOG_BUFFER_LINES; ++i)
{
snapshot.push_back(logBuffer[(startIndex + i) % LOG_BUFFER_LINES]);
}
}
return snapshot;
}
//////////////////////////////////////////////////////////////
// Network Management
//////////////////////////////////////////////////////////////
short setupNetwork();
short loadNetworkSettings();
short saveNetworkSettings(JsonObject& doc);
WiFiNetworkSettings wifiSettings;
//////////////////////////////////////////////////////////////
// Modbus TCP
//////////////////////////////////////////////////////////////
ModbusTCP *modbusManager;
short loopModbus();
#ifdef ENABLE_MODBUS_TCP
short setupModbus();
short mb_tcp_write(short address, short value) override;
short mb_tcp_write(MB_Registers *reg, short networkValue) override;
short mb_tcp_read(short address) override;
void mb_tcp_register(ModbusTCP *manager) const override;
ModbusBlockView *mb_tcp_blocks() const override;
int client_count;
int client_max;
int client_total;
millis_t client_track_ts;
short updateClientCount(short val0, short val1);
short resetClientStats(short val0, short val1);
short getClientStats(short val0, short val1);
// Modbus PID Specific (Conditional)
short getConnectedClients() const; // Returns number of currently connected Modbus TCP clients
#ifdef ENABLE_PID
short getPid2Register(short offset, short unused);
short setPid2Register(short offset, short value);
#endif // ENABLE_PID
#endif // ENABLE_MODBUS_TCP
RESTServer *webServer;
short loopWeb();
#ifdef ENABLE_RS485
friend class RS485Devices;
#endif // ENABLE_RS485
//////////////////////////////////////////////////////////////
// Component Overrides / Message Handling
/////////////////////////////////////////////////////////////
/**
* @brief Handles incoming messages, including RTU updates via void*.
*/
short onMessage(int id, E_CALLS verb, E_MessageFlags flags, void* user, Component *src) override;
//////////////////////////////////////////////////////////////
// Debugging & Utility Methods
//////////////////////////////////////////////////////////////
void printRegisters();
short list(short val0, short val1);
short print(short arg1, short arg2);
//////////////////////////////////////////////////////////////
// Profiling & Feature Specific (Conditional)
//////////////////////////////////////////////////////////////
#ifdef ENABLE_PROFILER
static uint32_t initialFreeHeap;
static uint64_t initialCpuTicks;
#endif // ENABLE_PROFILER
#ifdef ENABLE_PROFILE_TEMPERATURE
TemperatureProfile* tempProfiles[PROFILE_TEMPERATURE_COUNT]; // Array to hold multiple temperature profiles
void getProfilesHandler(AsyncWebServerRequest *request);
void setProfilesHandler(AsyncWebServerRequest *request, JsonVariant &json, int slot); // Adjusted for body handling
bool saveProfilesToJson();
#endif // ENABLE_PROCESS_PROFILE
#ifdef ENABLE_PROFILE_SIGNAL_PLOT
SignalPlot* signalPlots[PROFILE_SIGNAL_PLOT_COUNT]; // Array to hold multiple signal plot profiles
void getSignalPlotsHandler(AsyncWebServerRequest *request);
void setSignalPlotsHandler(AsyncWebServerRequest *request, JsonVariant &json, int slot);
bool saveSignalPlotsToJson();
// Methods to control SignalPlot from TemperatureProfile
void startSignalPlot(short slotId);
void stopSignalPlot(short slotId);
void enableSignalPlot(short slotId, bool enable);
void pauseSignalPlot(short slotId);
void resumeSignalPlot(short slotId);
#endif // ENABLE_PROFILE_SIGNAL_PLOT
//////////////////////////////////////////////////////////////
// Web Server
//////////////////////////////////////////////////////////////
/**
* @brief Register routes with the RESTServer. This will be called upon built-in RESTServer initialization.
*
* @param server The RESTServer instance to register routes with.
* @return short The result of the operation.
*/
short registerRoutes(RESTServer *instance);
// Network settings handlers
#ifdef ENABLE_WEBSERVER_WIFI_SETTINGS
void handleGetNetworkSettings(AsyncWebServerRequest *request);
void handleSetNetworkSettings(AsyncWebServerRequest *request, JsonVariant &json);
#endif
void getSystemLogsHandler(AsyncWebServerRequest *request);
void getBridgeMethodsHandler(AsyncWebServerRequest *request);
private:
//////////////////////////////////////////////////////////////
// Private Methods
//////////////////////////////////////////////////////////////
void handleSerialCommand(const String &command); // Moved here as it's private impl detail
void cleanupComponents(); // Moved here as it's private impl detail
};
#endif

View File

@ -1,11 +1,12 @@
import * as path from 'node:path'
import * as fsSync from 'node:fs' // For reading test file content directly
// import * as fs from 'node:fs/promises' // No longer needed for this test
import { describe, it, expect } from 'vitest' // Removed afterAll, beforeAll
import { E_Mode, run, IKBotTask, complete_options, complete_messages, complete_params } from '../../src/index'
import { E_Mode, run, IKBotTask, complete_options, complete_messages, complete_params, E_WrapMode } from '../../src/index'
// import { LOGGING_DIRECTORY } from '../../src/constants.js' // No longer needed for this test
// import { sync as rimrafSync } from 'rimraf' // No longer needed for this test
describe('globExtension with complete_params output', () => {
describe('globExtension with collected files verification', () => {
const testDataBaseDir = path.resolve(__dirname, '../test-data');
const testDataRoot = path.resolve(testDataBaseDir, 'glob');
// const defaultLogsDir = path.resolve(LOGGING_DIRECTORY); // No longer needed
@ -29,54 +30,149 @@ describe('globExtension with complete_params output', () => {
debug: () => {}, info: () => {}, warn: () => {}, error: () => {}, fatal: () => {},
} as any;
it('should include .h, related .cpp files, and .md file using brace expansion in globExtension', async () => {
it('should collect .h, related .cpp, and .md files when using brace expansion in globExtension', async () => {
const initialOpts: IKBotTask = {
path: testDataRoot,
include: ['*.h'],
globExtension: '${SRC_DIR}/${SRC_NAME}*.{cpp,md}',
mode: E_Mode.COMPLETION,
prompt: 'test-prompt-brace-expansion',
prompt: 'test-prompt-direct-file-check',
logger: mockLogger,
// wrap: 'none' // Default is 'none', explicitly test this or remove for default
};
// 1. Complete Options
const completedOptions = await complete_options(initialOpts);
expect(completedOptions).not.toBeNull();
if (!completedOptions) return;
if (!completedOptions) return;
// 2. Complete Messages
const { messages: gatheredMessages } = await complete_messages(initialOpts, completedOptions);
expect(gatheredMessages).toBeInstanceOf(Array);
// 2. Complete Messages - and get the raw `files` array
const { files: collectedFileObjects } = await complete_messages(initialOpts, completedOptions);
expect(collectedFileObjects).toBeInstanceOf(Array);
// 3. Complete Params
const finalParams = await complete_params(completedOptions, gatheredMessages);
expect(finalParams.messages).toBeInstanceOf(Array);
// 4. Assert on finalParams.messages
const collectedPathsFromFinalParams: string[] = finalParams.messages
.filter((msg: any) => msg.role === 'user' && (typeof msg.content === 'string' || msg.path))
.map((msg: any) => {
const contentStr = typeof msg.content === 'string' ? msg.content : '';
const metaPathMatch = contentStr.match(/^File: (.*)$/m);
if (metaPathMatch && metaPathMatch[1]) {
const metaRelPath = metaPathMatch[1];
return path.normalize(path.resolve(testDataRoot, metaRelPath));
}
if (msg.path && typeof msg.path === 'string') {
return path.normalize(path.resolve(testDataRoot, msg.path));
// 3. Assert directly on the paths from collectedFileObjects
// These paths should be relative to `completedOptions.path` (which is testDataRoot)
const actualCollectedPaths: string[] = collectedFileObjects
.map((fileObj: any) => {
if (fileObj && typeof fileObj.path === 'string') {
return path.normalize(path.resolve(testDataRoot, fileObj.path));
}
return null;
})
.filter((p: string | null) => p !== null) as string[];
const collectedPathsSet = new Set(collectedPathsFromFinalParams);
const collectedPathsSet = new Set(actualCollectedPaths);
// console.log("Expected absolute paths:", expectedAbsoluteFilePaths);
// console.log("Actual collected absolute paths:", actualCollectedPaths);
expectedAbsoluteFilePaths.forEach(expectedFile => {
expect(collectedPathsSet.has(expectedFile), `Expected file ${path.basename(expectedFile)} (${expectedFile}) to be in finalParams.messages.`).toBe(true);
expect(collectedPathsSet.has(expectedFile), `Expected file ${path.basename(expectedFile)} (${expectedFile}) to be collected.`).toBe(true);
});
expect(collectedPathsSet.size).toBe(expectedAbsoluteFilePaths.length);
expect(collectedPathsSet.size, "Number of unique collected files should match expected").toBe(expectedAbsoluteFilePaths.length);
}, 10000);
});
describe('globExtension and wrap modes with complete_params output', () => {
const testDataBaseDir = path.resolve(__dirname, '../test-data');
const testDataRoot = path.resolve(testDataBaseDir, 'glob');
const expectedFileNamesDefaultTest = [
'PHApp.h',
'PHApp.cpp',
'PHApp-Modbus.cpp',
'PHApp-Profiles.cpp',
'PHAppNetwork.cpp',
'PHAppSettings.cpp',
'PHAppWeb.cpp',
'PHApp.md'
];
const expectedAbsoluteFilePathsDefaultTest = expectedFileNamesDefaultTest.map(f => path.normalize(path.resolve(testDataRoot, f)));
const mockLogger = {
debug: () => {}, info: () => {}, warn: () => {}, error: () => {}, fatal: () => {},
} as any;
it('should collect .h, related .cpp, and .md files when using brace expansion in globExtension (default wrap:none)', async () => {
const initialOpts: IKBotTask = {
path: testDataRoot,
include: ['*.h'],
globExtension: '${SRC_DIR}/${SRC_NAME}*.{cpp,md}',
mode: E_Mode.COMPLETION,
prompt: 'test-prompt-direct-file-check',
logger: mockLogger,
};
// ... (existing test logic for wrap:none - this test remains the same)
// 1. Complete Options
const completedOptions = await complete_options(initialOpts);
expect(completedOptions).not.toBeNull();
if (!completedOptions) return;
// 2. Complete Messages - and get the raw `files` array
const { files: collectedFileObjects } = await complete_messages(initialOpts, completedOptions);
expect(collectedFileObjects).toBeInstanceOf(Array);
const actualCollectedPaths: string[] = collectedFileObjects
.map((fileObj: any) => {
if (fileObj && typeof fileObj.path === 'string') {
return path.normalize(path.resolve(testDataRoot, fileObj.path));
}
return null;
})
.filter((p: string | null) => p !== null) as string[];
const collectedPathsSet = new Set(actualCollectedPaths);
expectedAbsoluteFilePathsDefaultTest.forEach(expectedFile => {
expect(collectedPathsSet.has(expectedFile), `Expected file ${path.basename(expectedFile)} (${expectedFile}) to be collected.`).toBe(true);
});
expect(collectedPathsSet.size, "Number of unique collected files should match expected").toBe(expectedAbsoluteFilePathsDefaultTest.length);
}, 10000);
// New test case for wrap: 'meta'
it('should correctly wrap content with metadata when options.wrap is \'meta\'', async () => {
const targetFileName = 'PHApp.h';
const targetFileAbsolutePath = path.normalize(path.resolve(testDataRoot, targetFileName));
const originalFileContent = fsSync.readFileSync(targetFileAbsolutePath, 'utf-8');
const initialOptsMeta: IKBotTask = {
path: testDataRoot,
include: [targetFileName], // Focus on a single known text file
wrap: E_WrapMode.enum.meta, // Explicitly set wrap mode to meta
mode: E_Mode.COMPLETION,
prompt: 'test-prompt-wrap-meta',
logger: mockLogger,
};
const completedOptionsMeta = await complete_options(initialOptsMeta);
expect(completedOptionsMeta).not.toBeNull();
if (!completedOptionsMeta) return;
const { messages: gatheredMessagesMeta } = await complete_messages(initialOptsMeta, completedOptionsMeta);
const finalParamsMeta = await complete_params(completedOptionsMeta, gatheredMessagesMeta);
const targetFileMessage = finalParamsMeta.messages.find((msg: any) => {
if (msg.role === 'user' && typeof msg.content === 'string') {
// Check if the content contains the file path, good indicator for meta-wrapped file
// More robust: check if msg.content includes `File: ${targetFileName}`
return msg.content.includes(`File: ${targetFileName}`);
}
return false;
});
expect(targetFileMessage, `Message for ${targetFileName} should be found`).toBeDefined();
if (!targetFileMessage) return;
const messageContent = targetFileMessage.content as string;
expect(messageContent).toContain(`File: ${targetFileName}`);
expect(messageContent).toContain(`Absolute Path: ${targetFileAbsolutePath}`);
// expect(messageContent).toContain(`CWD: ${process.cwd()}`); // CWD can vary based on test runner, more fragile
expect(messageContent).toContain('\nOriginal Content:\n' + originalFileContent);
expect(messageContent.startsWith('IMPORTANT: The following information is file metadata.')).toBe(true);
expect(messageContent.includes('METADATA_START')).toBe(true);
expect(messageContent.includes('METADATA_END')).toBe(true);
}, 10000);
});
// To run this test, you would typically use your npm script, e.g., `npm run vi-test` or `npx vitest`