Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
99 changes: 86 additions & 13 deletions src/commands/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,46 @@ import { resolveTenantId } from '../config.ts';

export type SearchResult = { items: Array<Record<string, unknown>>; total?: number };

/**
* Flags that are valid globally (not specific to any search resource).
*/
export const GLOBAL_FLAGS = new Set([
'profile', 'sortBy', 'asc', 'desc', 'help', 'version', 'limit',
]);
Comment on lines +15 to +17
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Including version in GLOBAL_FLAGS means --version will never be reported as unknown for any search <resource> invocation, even though it’s only meaningful for certain commands/resources. To make unknown-flag detection effective, consider removing version from GLOBAL_FLAGS and instead adding it only to the specific SEARCH_RESOURCE_FLAGS entries where it’s actually consumed (e.g. process-definition if supported there).

Copilot uses AI. Check for mistakes.

/**
* Valid search filter flags per resource (values keys that are consumed by the search handler).
* This map is used to detect when a user passes a flag that looks valid but is not recognized
* for the specific resource, causing silent filter drops.
*/
export const SEARCH_RESOURCE_FLAGS: Record<string, Set<string>> = {
'process-definition': new Set(['bpmnProcessId', 'id', 'processDefinitionId', 'name', 'key', 'iid', 'iname']),
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The process-definition flags are missing 'version' in the SEARCH_RESOURCE_FLAGS set. The version filter is a valid option for process-definition searches (see searchProcessDefinitions function signature at line 195 and usage at src/index.ts:680). However, note that src/index.ts:680 accesses 'version_num' instead of 'version', which appears to be inconsistent with the parseArgs configuration that defines 'version' (src/index.ts:77). This needs clarification - if 'version_num' is the correct property name, it should be added to the flags set; if 'version' is correct, then both the flags set and line 680 need updating.

Suggested change
'process-definition': new Set(['bpmnProcessId', 'id', 'processDefinitionId', 'name', 'key', 'iid', 'iname']),
'process-definition': new Set(['bpmnProcessId', 'id', 'processDefinitionId', 'name', 'key', 'iid', 'iname', 'version_num']),

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing flag in SEARCH_RESOURCE_FLAGS: The search process definitions handler in index.ts (line 680) accepts version_num flag, but 'version_num' is not included in SEARCH_RESOURCE_FLAGS['process-definition']. This will cause detectUnknownSearchFlags to incorrectly report it as an unknown flag if a user tries to use it. Add 'version_num' to the process-definition flag set.

Suggested change
'process-definition': new Set(['bpmnProcessId', 'id', 'processDefinitionId', 'name', 'key', 'iid', 'iname']),
'process-definition': new Set(['bpmnProcessId', 'id', 'processDefinitionId', 'name', 'key', 'iid', 'iname', 'version_num']),

Copilot uses AI. Check for mistakes.
'process-instance': new Set(['bpmnProcessId', 'id', 'processDefinitionId', 'processDefinitionKey', 'state', 'key', 'parentProcessInstanceKey', 'iid']),
'user-task': new Set(['state', 'assignee', 'processInstanceKey', 'processDefinitionKey', 'elementId', 'iassignee']),
'incident': new Set(['state', 'processInstanceKey', 'processDefinitionKey', 'bpmnProcessId', 'id', 'processDefinitionId', 'errorType', 'errorMessage', 'ierrorMessage', 'iid']),
'jobs': new Set(['state', 'type', 'processInstanceKey', 'processDefinitionKey', 'itype']),
Comment on lines +26 to +29
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SEARCH_RESOURCE_FLAGS is missing flags that are actually supported by the corresponding search handlers, so users will get false "unknown flag" warnings. For example, searchProcessInstances/searchUserTasks/searchJobs accept --between and --dateField, and searchIncidents accepts --between, but none of these appear in the per-resource sets. Add the missing flags to the appropriate resource entries so detectUnknownSearchFlags() doesn’t warn on documented options.

Suggested change
'process-instance': new Set(['bpmnProcessId', 'id', 'processDefinitionId', 'processDefinitionKey', 'state', 'key', 'parentProcessInstanceKey', 'iid']),
'user-task': new Set(['state', 'assignee', 'processInstanceKey', 'processDefinitionKey', 'elementId', 'iassignee']),
'incident': new Set(['state', 'processInstanceKey', 'processDefinitionKey', 'bpmnProcessId', 'id', 'processDefinitionId', 'errorType', 'errorMessage', 'ierrorMessage', 'iid']),
'jobs': new Set(['state', 'type', 'processInstanceKey', 'processDefinitionKey', 'itype']),
'process-instance': new Set(['bpmnProcessId', 'id', 'processDefinitionId', 'processDefinitionKey', 'state', 'key', 'parentProcessInstanceKey', 'iid', 'between', 'dateField']),
'user-task': new Set(['state', 'assignee', 'processInstanceKey', 'processDefinitionKey', 'elementId', 'iassignee', 'between', 'dateField']),
'incident': new Set(['state', 'processInstanceKey', 'processDefinitionKey', 'bpmnProcessId', 'id', 'processDefinitionId', 'errorType', 'errorMessage', 'ierrorMessage', 'iid', 'between']),
'jobs': new Set(['state', 'type', 'processInstanceKey', 'processDefinitionKey', 'itype', 'between', 'dateField']),

Copilot uses AI. Check for mistakes.
'variable': new Set(['name', 'value', 'processInstanceKey', 'scopeKey', 'fullValue', 'iname', 'ivalue', 'limit']),
};

/**
* Detect flags the user set that are not recognized for the given search resource.
* Returns the list of unknown flag names (without the --prefix).
*/
export function detectUnknownSearchFlags(values: Record<string, unknown>, normalizedResource: string): string[] {
const validFlags = SEARCH_RESOURCE_FLAGS[normalizedResource]
|| SEARCH_RESOURCE_FLAGS[normalizedResource.replace(/s$/, '')];
if (!validFlags) return [];

const unknown: string[] = [];
for (const [key, val] of Object.entries(values)) {
if (val === undefined || val === false) continue; // not set by the user
if (GLOBAL_FLAGS.has(key)) continue;
if (validFlags.has(key)) continue;
unknown.push(key);
}
return unknown;
}

/**
* Detect wildcard characters (* or ?) in a string value and return
* a $like filter object for the API. Returns the plain string for exact match.
Expand Down Expand Up @@ -70,6 +110,9 @@ const toBigIntSafe = (value: unknown): bigint => {
}
};

/** Default page size the Camunda REST API uses when no explicit limit is set */
const API_DEFAULT_PAGE_SIZE = 100;

/** Max page size for case-insensitive search (client-side filtering needs broader result set) */
const CI_PAGE_SIZE = 1000;

Expand Down Expand Up @@ -110,14 +153,38 @@ function formatCriterion(fieldLabel: string, value: string | number | boolean, i
*/
function logSearchCriteria(logger: Logger, resourceName: string, criteria: string[]): void {
if (criteria.length === 0) {
logger.info(`Searching ${resourceName}`);
logger.info(`Searching ${resourceName} (no filters)`);
} else if (criteria.length === 1) {
logger.info(`Searching ${resourceName} where ${criteria[0]}`);
} else {
logger.info(`Searching ${resourceName} where ${criteria.join(' AND ')}`);
}
}

/**
* Log a "no results" message with 🕳️ emoji and contextual hint.
*/
function logNoResults(logger: Logger, resourceName: string, hasFilters: boolean, unknownFlags?: string[]): void {
logger.info(`🕳️ No ${resourceName} found matching the criteria`);
if (unknownFlags && unknownFlags.length > 0) {
logger.warn(`Possibly unused flag(s): ${unknownFlags.map(f => `--${f}`).join(', ')}. Run "c8ctl help search" for valid options.`);
} else if (!hasFilters) {
logger.info('No filters were applied. Use "c8ctl help search" to see available filter flags.');
}
}

/**
* Log the result count with a truncation warning when the count matches the API default page size.
*/
function logResultCount(logger: Logger, count: number, resourceName: string, hasFilters: boolean): void {
logger.info(`Found ${count} ${resourceName}`);
if (count === API_DEFAULT_PAGE_SIZE && !hasFilters) {
logger.warn(`Showing first ${API_DEFAULT_PAGE_SIZE} results (API default page size). More results may exist — add filters to narrow down.`);
} else if (count === API_DEFAULT_PAGE_SIZE) {
logger.warn(`Result count equals the API default page size (${API_DEFAULT_PAGE_SIZE}). There may be more results.`);
}
}

/**
* Search process definitions
*/
Expand All @@ -131,6 +198,7 @@ export async function searchProcessDefinitions(options: {
iName?: string;
sortBy?: string;
sortOrder?: SortOrder;
_unknownFlags?: string[];
}): Promise<SearchResult | undefined> {
const logger = getLogger();
const client = createClient(options.profile);
Expand Down Expand Up @@ -217,9 +285,9 @@ export async function searchProcessDefinitions(options: {
}));
tableData = sortTableData(tableData, options.sortBy, logger, options.sortOrder);
logger.table(tableData);
logger.info(`Found ${result.items.length} process definition(s)`);
logResultCount(logger, result.items.length, 'process definition(s)', criteria.length > 0);
} else {
logger.info('No process definitions found matching the criteria');
logNoResults(logger, 'process definitions', criteria.length > 0, options._unknownFlags);
}

return result as SearchResult;
Expand All @@ -242,6 +310,7 @@ export async function searchProcessInstances(options: {
iProcessDefinitionId?: string;
sortBy?: string;
sortOrder?: SortOrder;
_unknownFlags?: string[];
}): Promise<SearchResult | undefined> {
const logger = getLogger();
const client = createClient(options.profile);
Expand Down Expand Up @@ -318,9 +387,9 @@ export async function searchProcessInstances(options: {
}));
tableData = sortTableData(tableData, options.sortBy, logger, options.sortOrder);
logger.table(tableData);
logger.info(`Found ${result.items.length} process instance(s)`);
logResultCount(logger, result.items.length, 'process instance(s)', criteria.length > 0);
} else {
logger.info('No process instances found matching the criteria');
logNoResults(logger, 'process instances', criteria.length > 0, options._unknownFlags);
}

return result as SearchResult;
Expand All @@ -343,6 +412,7 @@ export async function searchUserTasks(options: {
iAssignee?: string;
sortBy?: string;
sortOrder?: SortOrder;
_unknownFlags?: string[];
}): Promise<SearchResult | undefined> {
const logger = getLogger();
const client = createClient(options.profile);
Expand Down Expand Up @@ -420,9 +490,9 @@ export async function searchUserTasks(options: {
}));
tableData = sortTableData(tableData, options.sortBy, logger, options.sortOrder);
logger.table(tableData);
logger.info(`Found ${result.items.length} user task(s)`);
logResultCount(logger, result.items.length, 'user task(s)', criteria.length > 0);
} else {
logger.info('No user tasks found matching the criteria');
logNoResults(logger, 'user tasks', criteria.length > 0, options._unknownFlags);
}

return result as SearchResult;
Expand All @@ -447,6 +517,7 @@ export async function searchIncidents(options: {
iProcessDefinitionId?: string;
sortBy?: string;
sortOrder?: SortOrder;
_unknownFlags?: string[];
}): Promise<SearchResult | undefined> {
const logger = getLogger();
const client = createClient(options.profile);
Expand Down Expand Up @@ -538,9 +609,9 @@ export async function searchIncidents(options: {
}));
tableData = sortTableData(tableData, options.sortBy, logger, options.sortOrder);
logger.table(tableData);
logger.info(`Found ${result.items.length} incident(s)`);
logResultCount(logger, result.items.length, 'incident(s)', criteria.length > 0);
} else {
logger.info('No incidents found matching the criteria');
logNoResults(logger, 'incidents', criteria.length > 0, options._unknownFlags);
}

return result as SearchResult;
Expand All @@ -562,6 +633,7 @@ export async function searchJobs(options: {
iType?: string;
sortBy?: string;
sortOrder?: SortOrder;
_unknownFlags?: string[];
}): Promise<SearchResult | undefined> {
const logger = getLogger();
const client = createClient(options.profile);
Expand Down Expand Up @@ -632,9 +704,9 @@ export async function searchJobs(options: {
}));
tableData = sortTableData(tableData, options.sortBy, logger, options.sortOrder);
logger.table(tableData);
logger.info(`Found ${result.items.length} job(s)`);
logResultCount(logger, result.items.length, 'job(s)', criteria.length > 0);
} else {
logger.info('No jobs found matching the criteria');
logNoResults(logger, 'jobs', criteria.length > 0, options._unknownFlags);
}

return result as SearchResult;
Expand All @@ -659,6 +731,7 @@ export async function searchVariables(options: {
sortBy?: string;
sortOrder?: SortOrder;
limit?: number;
_unknownFlags?: string[];
}): Promise<SearchResult | undefined> {
const logger = getLogger();
const client = createClient(options.profile);
Expand Down Expand Up @@ -767,13 +840,13 @@ export async function searchVariables(options: {
});
tableData = sortTableData(tableData, options.sortBy, logger, options.sortOrder);
logger.table(tableData);
logger.info(`Found ${result.items.length} variable(s)`);
logResultCount(logger, result.items.length, 'variable(s)', criteria.length > 0);

if (!options.fullValue && result.items.some((v: any) => v.isTruncated)) {
logger.info('Some values are truncated. Use --fullValue to see full values.');
}
} else {
logger.info('No variables found matching the criteria');
logNoResults(logger, 'variables', criteria.length > 0, options._unknownFlags);
}

return result as SearchResult;
Expand Down
26 changes: 23 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
searchIncidents,
searchJobs,
searchVariables,
detectUnknownSearchFlags,
} from './commands/search.ts';
import { listUserTasks, completeUserTask } from './commands/user-tasks.ts';
import { listIncidents, getIncident, resolveIncident } from './commands/incidents.ts';
Expand Down Expand Up @@ -142,6 +143,17 @@ function resolveProcessDefinitionId(values: any): string | undefined {
return (values.id || values.processDefinitionId || values.bpmnProcessId) as string | undefined;
}

/**
* Warn about unrecognized flags for a search resource.
*/
function warnUnknownSearchFlags(logger: ReturnType<typeof getLogger>, unknownFlags: string[], resource: string): void {
if (unknownFlags.length === 0) return;
const flagList = unknownFlags.map(f => `--${f}`).join(', ');
logger.warn(
`Flag(s) ${flagList} not recognized for 'search ${resource}'. They will be ignored. Run "c8ctl help search" for valid options.`,
);
}

/**
* Main CLI handler
*/
Expand Down Expand Up @@ -657,33 +669,37 @@ async function main() {
// Handle search commands
if (verb === 'search') {
const normalizedSearchResource = normalizeResource(resource);
const unknownFlags = detectUnknownSearchFlags(values as Record<string, unknown>, normalizedSearchResource);
warnUnknownSearchFlags(logger, unknownFlags, resource);

if (normalizedSearchResource === 'process-definition' || normalizedSearchResource === 'process-definitions') {
await searchProcessDefinitions({
profile: values.profile as string | undefined,
processDefinitionId: values.bpmnProcessId as string | undefined,
processDefinitionId: resolveProcessDefinitionId(values),
name: values.name as string | undefined,
version: (values.version_num && typeof values.version_num === 'string') ? parseInt(values.version_num) : undefined,
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code accesses 'values.version_num' but this property is not defined in the parseArgs options configuration (src/index.ts:73-130). The parseArgs options include 'version' (line 77) but not 'version_num'. This will result in 'version_num' always being undefined, making the version filter for process-definition search non-functional. Either: 1) Change this to use 'values.version' to match the parseArgs configuration, or 2) Add 'version_num' to the parseArgs options if that's the intended flag name.

Suggested change
version: (values.version_num && typeof values.version_num === 'string') ? parseInt(values.version_num) : undefined,
version: (values.version && typeof values.version === 'string') ? parseInt(values.version) : undefined,

Copilot uses AI. Check for mistakes.
key: values.key as string | undefined,
Comment on lines 688 to 692
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

values.version is being parsed as the process-definition filter version. This conflicts with the documented global --version/-v flag (intended to show CLI version) and also makes detectUnknownSearchFlags() treat version as globally valid (so search <other-resource> --version ... won’t be warned about). Consider introducing a dedicated filter flag (e.g. --processDefinitionVersion) and keeping --version as a boolean global flag; also validate the parsed number (avoid passing NaN).

Copilot uses AI. Check for mistakes.
iProcessDefinitionId: values.iid as string | undefined,
iName: values.iname as string | undefined,
sortBy: values.sortBy as string | undefined,
sortOrder,
_unknownFlags: unknownFlags,
});
return;
}

if (normalizedSearchResource === 'process-instance' || normalizedSearchResource === 'process-instances') {
await searchProcessInstances({
profile: values.profile as string | undefined,
processDefinitionId: values.bpmnProcessId as string | undefined,
processDefinitionId: resolveProcessDefinitionId(values),
processDefinitionKey: values.processDefinitionKey as string | undefined,
state: values.state as string | undefined,
key: values.key as string | undefined,
parentProcessInstanceKey: values.parentProcessInstanceKey as string | undefined,
iProcessDefinitionId: values.iid as string | undefined,
sortBy: values.sortBy as string | undefined,
sortOrder,
_unknownFlags: unknownFlags,
});
return;
}
Expand All @@ -699,6 +715,7 @@ async function main() {
iAssignee: values.iassignee as string | undefined,
sortBy: values.sortBy as string | undefined,
sortOrder,
_unknownFlags: unknownFlags,
});
return;
}
Expand All @@ -709,13 +726,14 @@ async function main() {
state: values.state as string | undefined,
processInstanceKey: values.processInstanceKey as string | undefined,
processDefinitionKey: values.processDefinitionKey as string | undefined,
processDefinitionId: values.bpmnProcessId as string | undefined,
processDefinitionId: resolveProcessDefinitionId(values),
errorType: values.errorType as string | undefined,
errorMessage: values.errorMessage as string | undefined,
iErrorMessage: values.ierrorMessage as string | undefined,
iProcessDefinitionId: values.iid as string | undefined,
sortBy: values.sortBy as string | undefined,
sortOrder,
_unknownFlags: unknownFlags,
});
return;
}
Expand All @@ -730,6 +748,7 @@ async function main() {
iType: values.itype as string | undefined,
sortBy: values.sortBy as string | undefined,
sortOrder,
_unknownFlags: unknownFlags,
});
return;
}
Expand All @@ -747,6 +766,7 @@ async function main() {
sortBy: values.sortBy as string | undefined,
sortOrder,
limit,
_unknownFlags: unknownFlags,
});
return;
}
Expand Down
Loading