Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
e501d87
wip
tomsonpl Jun 26, 2025
092131e
working solution
tomsonpl Jun 26, 2025
03af88f
fix
tomsonpl Jun 27, 2025
e83b877
fix
tomsonpl Jun 27, 2025
a00b911
fix
tomsonpl Jun 27, 2025
8c125f3
add tests
tomsonpl Jun 27, 2025
3aad7df
fix
tomsonpl Jun 27, 2025
996ea8e
fixfix
tomsonpl Jun 30, 2025
94fe63c
fixfixfixfix
tomsonpl Jul 1, 2025
6fb2798
fixfixfixfix
tomsonpl Jul 1, 2025
1b7ce50
Merge branch 'main' into fix-script-picker
tomsonpl Jul 1, 2025
2386ef4
fix
tomsonpl Jul 1, 2025
640c204
fix
tomsonpl Jul 1, 2025
13ad9c2
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Jul 1, 2025
ac26217
fix
tomsonpl Jul 2, 2025
80d1359
Merge remote-tracking branch 'origin/fix-script-picker' into fix-scri…
tomsonpl Jul 2, 2025
1c54c63
fix tests
tomsonpl Jul 2, 2025
6006022
fix tests
tomsonpl Jul 2, 2025
29f0720
fix
tomsonpl Jul 2, 2025
bc069b1
fix
tomsonpl Jul 2, 2025
9a82853
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Jul 2, 2025
dd96b9e
fix
tomsonpl Jul 2, 2025
c801684
Merge remote-tracking branch 'origin/fix-script-picker' into fix-scri…
tomsonpl Jul 2, 2025
f959310
fix
tomsonpl Jul 2, 2025
9062cc5
fix types
tomsonpl Jul 3, 2025
e41b63e
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Jul 3, 2025
95c21e7
fix tests
tomsonpl Jul 3, 2025
754f02c
Merge remote-tracking branch 'origin/fix-script-picker' into fix-scri…
tomsonpl Jul 3, 2025
6c25ece
Merge branch 'main' into fix-script-picker
tomsonpl Jul 3, 2025
4bbd14b
Merge branch 'main' into fix-script-picker
tomsonpl Jul 3, 2025
999a30c
Merge branch 'main' into fix-script-picker
tomsonpl Jul 4, 2025
7f7302a
apply feedback
tomsonpl Jul 4, 2025
7e003e7
Merge remote-tracking branch 'origin/fix-script-picker' into fix-scri…
tomsonpl Jul 4, 2025
5fc2069
fix eslint
tomsonpl Jul 4, 2025
972b823
Merge branch 'main' into fix-script-picker
tomsonpl Jul 7, 2025
6fd6f81
remove initializeStateArgs and use default
tomsonpl Jul 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export const SUGGESTIONS_INTERNAL_ROUTE = `${BASE_INTERNAL_ENDPOINT_ROUTE}/sugge

/** Base Actions route. Used to get a list of all actions and is root to other action related routes */
export const BASE_ENDPOINT_ACTION_ROUTE = `${BASE_ENDPOINT_ROUTE}/action`;
export const BASE_INTERNAL_ENDPOINT_ACTION_ROUTE = `${BASE_INTERNAL_ENDPOINT_ROUTE}/action`;

export const ISOLATE_HOST_ROUTE_V2 = `${BASE_ENDPOINT_ACTION_ROUTE}/isolate`;
export const UNISOLATE_HOST_ROUTE_V2 = `${BASE_ENDPOINT_ACTION_ROUTE}/unisolate`;
Expand All @@ -97,7 +98,9 @@ export const EXECUTE_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/execute`;
export const UPLOAD_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/upload`;
export const SCAN_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/scan`;
export const RUN_SCRIPT_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/run_script`;
export const CUSTOM_SCRIPTS_ROUTE = `${BASE_ENDPOINT_ACTION_ROUTE}/custom_scripts`;

/** Internal Endpoint Actions Routes */
export const CUSTOM_SCRIPTS_ROUTE = `${BASE_INTERNAL_ENDPOINT_ACTION_ROUTE}/custom_scripts`;

/** Endpoint Actions Routes */
export const ACTION_STATUS_ROUTE = `${BASE_ENDPOINT_ROUTE}/action_status`;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { ParsedCommandInterface } from '../service/types';
import type { ArgSelectorState, EnteredCommand } from '../components/console_state/types';

/**
* Base interface for command-specific handlers that define how different commands
* should behave in terms of argument parsing, state management, and text reconstruction.
*/
export interface CommandHandler {
/** The name of the command this handler supports */
readonly name: string;

/**
* Returns the list of argument names that should use empty strings for bare flags
* instead of boolean true. These are typically selector arguments that can have values.
*/
getEmptyStringArguments(): string[];

/**
* Handle command text reconstruction when selector values change.
* Returns the complete command text including all arguments with proper formatting.
*/
reconstructCommandText?(parsedInput: ParsedCommandInterface): {
leftOfCursorText?: string;
rightOfCursorText?: string;
parsedInput: ParsedCommandInterface;
};

/**
* Handle any additional state synchronization between parsed input and entered command.
* This is called after parsing to ensure consistency between different state representations.
*/
syncState?(parsedInput: ParsedCommandInterface, enteredCommand: EnteredCommand): void;

/**
* Calculate replacement length for text deduplication when selector values change.
* Used to determine how much text to replace when updating command arguments.
*/
calculateReplacementLength?(args: {
argChrLength: number;
argState?: ArgSelectorState;
selectorValue: string;
input: string;
startSearchIndexForNextArg: number;
charAfterArgName: string;
}): number;
}

/**
* Base class providing common functionality that most command handlers can extend.
*/
export abstract class BaseCommandHandler implements CommandHandler {
abstract readonly name: string;

// Upload uses boolean flags (bare flags = true), not empty strings
// File selectors work with standard selector patterns only
getEmptyStringArguments(): string[] {
return [];
}

reconstructCommandText(parsedInput: ParsedCommandInterface): {
leftOfCursorText?: string;
rightOfCursorText?: string;
parsedInput: ParsedCommandInterface;
} {
return {
parsedInput,
};
}

syncState(parsedInput: ParsedCommandInterface, enteredCommand: EnteredCommand): void {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { CommandRegistry } from './command_registry';
import { RunscriptCommandHandler } from './runscript_handler';
import { BaseCommandHandler } from './base_command_handler';
import type { CommandHandler } from './base_command_handler';
import type { EnteredCommand } from '../components/console_state/types';
import type { ParsedCommandInterface } from '../service/types';

describe('CommandRegistry', () => {
let registry: CommandRegistry;

beforeEach(() => {
registry = new CommandRegistry();
});

describe('register', () => {
it('registers a command handler and delegates methods', () => {
const handler = new RunscriptCommandHandler();
registry.register(handler);
expect(registry.getEmptyStringArguments('runscript')).toEqual(['ScriptName', 'CloudFile']);
});

it('registers multiple command handlers and delegates to each', () => {
const runscriptHandler = new RunscriptCommandHandler();
const mockHandler = new (class extends BaseCommandHandler {
readonly name = 'other';
})();
registry.register(runscriptHandler);
registry.register(mockHandler);
expect(registry.getEmptyStringArguments('runscript')).toEqual(['ScriptName', 'CloudFile']);
expect(registry.getEmptyStringArguments('other')).toEqual([]);
});
});

describe('getEmptyStringArguments', () => {
it('returns [] for unregistered commands', () => {
expect(registry.getEmptyStringArguments('unknown')).toEqual([]);
expect(registry.getEmptyStringArguments('upload')).toEqual([]);
});

it('returns correct empty string arguments for registered commands', () => {
registry.register(new RunscriptCommandHandler());
expect(registry.getEmptyStringArguments('runscript')).toEqual(['ScriptName', 'CloudFile']);
expect(registry.getEmptyStringArguments('upload')).toEqual([]);
});

it('handles upload command correctly without handler', () => {
// Upload command uses default boolean flag behavior, not empty string arguments
const emptyStringArgs = registry.getEmptyStringArguments('upload');
expect(emptyStringArgs).toEqual([]);
expect(emptyStringArgs.includes('file')).toBe(false);
});
});

describe('delegation to registered handlers', () => {
let mockHandler: CommandHandler;
beforeEach(() => {
mockHandler = {
name: 'mock',
getEmptyStringArguments: jest.fn().mockReturnValue([]),
reconstructCommandText: jest.fn().mockReturnValue('mock command'),
syncState: jest.fn(),
};
});

it('calls reconstructCommandText on registered handler', () => {
registry.register(mockHandler);
const parsedInput = {
name: 'mock',
args: {},
hasArg: jest.fn(),
} as unknown as ParsedCommandInterface;
const result = registry.reconstructCommandText(parsedInput);
expect(result).toBe('mock command');
expect(mockHandler.reconstructCommandText).toHaveBeenCalledWith(parsedInput);
});

it('calls syncState on registered handler', () => {
registry.register(mockHandler);
const parsedInput = {
name: 'mock',
args: {},
hasArg: jest.fn(),
} as unknown as ParsedCommandInterface;
const enteredCommand = {} as EnteredCommand;
registry.syncState(parsedInput, enteredCommand);
expect(mockHandler.syncState).toHaveBeenCalledWith(parsedInput, enteredCommand);
});
});

describe('fallback/default behavior for unregistered commands', () => {
it('returns default values and does not throw for unregistered commands', () => {
const parsedInput = {
name: 'unknown',
args: {},
hasArg: jest.fn(),
} as unknown as ParsedCommandInterface;
const enteredCommand = {} as EnteredCommand;
// Should not throw and should do nothing for void methods
expect(() => registry.syncState(parsedInput, enteredCommand)).not.toThrow();
// Should return default object for reconstructCommandText
expect(registry.reconstructCommandText(parsedInput)).toEqual({ parsedInput });
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { CommandHandler } from './base_command_handler';
import type { ParsedCommandInterface } from '../service/types';
import type { ArgSelectorState, EnteredCommand } from '../components/console_state/types';

/**
* Registry for managing command-specific handlers. This centralizes all command-specific
* logic and eliminates the need for scattered if statements throughout the codebase.
*/
export class CommandRegistry {
private handlers = new Map<string, CommandHandler>();

/**
* Register a command handler for a specific command name.
*/
register(handler: CommandHandler): void {
this.handlers.set(handler.name, handler);
}

/**
* Get the list of arguments that should use empty strings for bare flags
* instead of boolean true for the given command.
*/
getEmptyStringArguments(commandName: string): string[] {
const handler = this.handlers.get(commandName);
return handler?.getEmptyStringArguments() ?? [];
}

/**
* Reconstruct command text when selector values change.
*/
reconstructCommandText(parsedInput: ParsedCommandInterface): {
leftOfCursorText?: string;
rightOfCursorText?: string;
parsedInput: ParsedCommandInterface;
} {
const handler = this.handlers.get(parsedInput.name);
return (
handler?.reconstructCommandText?.(parsedInput) ?? {
parsedInput,
}
);
}

/**
* Sync state between parsed input and entered command.
*/
syncState(parsedInput: ParsedCommandInterface, enteredCommand: EnteredCommand): void {
const handler = this.handlers.get(parsedInput.name);
if (handler?.syncState) {
handler.syncState(parsedInput, enteredCommand);
}
}

/**
* Calculate replacement length for deduplication for the given command.
*/
calculateReplacementLength(
commandName: string,
args: {
argChrLength: number;
argState?: ArgSelectorState;
selectorValue: string;
input: string;
startSearchIndexForNextArg: number;
charAfterArgName: string;
}
): number {
const handler = this.handlers.get(commandName);
if (handler && typeof handler.calculateReplacementLength === 'function') {
return handler.calculateReplacementLength(args);
}
// Default: just replace argument name (no deduplication logic)
return args.argChrLength;
}
}

// Create and export a singleton instance
export const commandRegistry = new CommandRegistry();
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export { BaseCommandHandler } from './base_command_handler';
export type { CommandHandler } from './base_command_handler';
export { CommandRegistry, commandRegistry } from './command_registry';
export { RunscriptCommandHandler } from './runscript_handler';
export { UploadCommandHandler } from './upload_handler';

// Initialize the command registry with all available handlers
import { commandRegistry } from './command_registry';
import { RunscriptCommandHandler } from './runscript_handler';
import { UploadCommandHandler } from './upload_handler';

// Register all command handlers
commandRegistry.register(new RunscriptCommandHandler());
commandRegistry.register(new UploadCommandHandler());
Loading