Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ wl list --json
wl list -n 5 --json
# List items filtered by status (open, in-progress, closed, etc.)
wl list --status open --json
wl list --status open,in-progress --json # comma-separated: matches any listed status
# List items filtered by priority (critical, high, medium, low)
wl list --priority high --json
# List items filtered by comma-separated tags
Expand Down
3 changes: 3 additions & 0 deletions CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,9 @@ Examples:
```sh
wl list
wl list -s open -p high
wl list -s open,in-progress # status is open OR in-progress
wl list --status open,completed,blocked
wl list -s open,in-progress --stage in_review # status AND stage filters
wl search "signup"
wl -F concise list -s in-progress
wl --json list -s open --tags backlog
Expand Down
7 changes: 7 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ worklog list --parent null
# Filter by status
worklog list -s in-progress

# Filter by multiple statuses (comma-separated, OR semantics)
worklog list -s open,in-progress
worklog list --status open,completed,blocked

# Filter by priority
worklog list -p high

Expand All @@ -37,6 +41,9 @@ worklog list --tags "backend,api"

# Combine filters
worklog list -s open -p high

# Combine multi-status with stage filter (AND semantics)
worklog list -s open,in-progress --stage in_review
```

### Viewing Work Items
Expand Down
6 changes: 4 additions & 2 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ export function createAPI(db: WorklogDatabase) {
const query: WorkItemQuery = {};

if (req.query.status) {
query.status = req.query.status as WorkItemStatus;
const raw = Array.isArray(req.query.status) ? (req.query.status as string[]).join(',') : req.query.status as string;
query.status = raw.split(',').map(s => s.trim()) as WorkItemStatus[];
}
if (req.query.priority) {
query.priority = req.query.priority as WorkItemPriority;
Expand Down Expand Up @@ -362,7 +363,8 @@ export function createAPI(db: WorklogDatabase) {
const query: WorkItemQuery = {};

if (req.query.status) {
query.status = req.query.status as WorkItemStatus;
const raw = Array.isArray(req.query.status) ? (req.query.status as string[]).join(',') : req.query.status as string;
query.status = raw.split(',').map(s => s.trim()) as WorkItemStatus[];
}
if (req.query.priority) {
query.priority = req.query.priority as WorkItemPriority;
Expand Down
2 changes: 1 addition & 1 deletion src/cli-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export interface CreateOptions {
}

export interface ListOptions {
status?: WorkItemStatus;
status?: string;
priority?: WorkItemPriority;
parent?: string;
tags?: string;
Expand Down
2 changes: 1 addition & 1 deletion src/commands/in-progress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default function register(ctx: PluginContext): void {
utils.requireInitialized();
const db = utils.getDatabase(options.prefix);

const query: WorkItemQuery = { status: 'in-progress' as WorkItemStatus };
const query: WorkItemQuery = { status: ['in-progress' as WorkItemStatus] };
if (options.assignee) {
query.assignee = options.assignee;
}
Expand Down
13 changes: 12 additions & 1 deletion src/commands/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,18 @@ export default function register(ctx: PluginContext): void {
const db = utils.getDatabase(options?.prefix);

const query: WorkItemQuery = {};
if (options.status) query.status = options.status as WorkItemStatus;
if (options.status) {
const validStatuses = ['open', 'in-progress', 'completed', 'blocked', 'deleted', 'input-needed'];
const statuses = options.status.split(',').map(s => s.trim());
for (const s of statuses) {
const normalized = s.replace(/_/g, '-');
if (!validStatuses.includes(normalized)) {
output.error(`Invalid status value: ${s}. Valid values: ${validStatuses.join(', ')}`, { success: false, error: 'invalid-arg' });
process.exit(1);
}
}
query.status = statuses.map(s => s.replace(/_/g, '-') as WorkItemStatus);
}
if (options.priority) query.priority = options.priority as WorkItemPriority;
if (options.parent) {
const normalizedParentId = utils.normalizeCliId(options.parent, options.prefix) || options.parent;
Expand Down
8 changes: 4 additions & 4 deletions src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -813,11 +813,11 @@ export class WorklogDatabase {
let items = this.store.getAllWorkItems();

if (query) {
if (query.status) {
if (query.status && query.status.length > 0) {
// Status values are normalized to hyphenated form on write/import,
// so we only need to normalize the query parameter for user input.
const normalizedQueryStatus = normalizeStatusValue(query.status) ?? query.status;
items = items.filter(item => item.status === normalizedQueryStatus);
// so we normalize each query value for comparison.
const normalizedStatuses = query.status.map(s => normalizeStatusValue(s) ?? s);
items = items.filter(item => normalizedStatuses.includes(item.status));
}
if (query.priority) {
items = items.filter(item => item.priority === query.priority);
Expand Down
4 changes: 2 additions & 2 deletions src/tui/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ export class TuiController {
};

const query: Partial<Record<string, unknown>> = {};
if (options.inProgress) query.status = 'in-progress';
if (options.inProgress) query.status = ['in-progress'];

const allItems: Item[] = listWorkItemsSafely(query, [], 'initial-load').items;
const showClosed = Boolean(options.all);
Expand Down Expand Up @@ -3048,7 +3048,7 @@ function updateDetailForIndex(idx: number, visible?: VisibleNode[]) {
const selected = getSelectedItem();
const selectedId = selected?.id;
const query: any = {};
if (status) query.status = status;
if (status) query.status = [status];
if (needsReviewFilter !== null) query.needsProducerReview = needsReviewFilter;
const listed = listWorkItemsSafely(query, state.items.slice(), 'refresh-list');
if (listed.busy) {
Expand Down
8 changes: 2 additions & 6 deletions src/tui/wl-db-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,8 @@ function buildListArgs(query: Record<string, unknown>): string[] {
const args: string[] = [];
// Map common query fields to wl list flags
if (query.status) {
// Support array of statuses
if (Array.isArray(query.status)) {
query.status.forEach((s: string) => args.push('--status', s));
} else {
args.push('--status', String(query.status));
}
const raw = Array.isArray(query.status) ? (query.status as string[]).join(',') : String(query.status);
args.push('--status', raw);
}
if (query.inProgress === true) {
args.push('--in-progress');
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export interface UpdateWorkItemInput {
audit?: WorkItemAudit;
}
export interface WorkItemQuery {
status?: WorkItemStatus;
status?: WorkItemStatus[];
priority?: WorkItemPriority;
parentId?: string | null;
tags?: string[];
Expand Down
1 change: 1 addition & 0 deletions templates/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ wl list --json
wl list -n 5 --json
# List items filtered by status (open, in-progress, closed, etc.)
wl list --status open --json
wl list --status open,in-progress --json # comma-separated: matches any listed status
# List items filtered by priority (critical, high, medium, low)
wl list --priority high --json
# List items filtered by comma-separated tags
Expand Down
57 changes: 57 additions & 0 deletions tests/cli/issue-status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,63 @@ describe('CLI Issue Status Tests', () => {
expect(humanStdout).toContain('Found 1 work item');
});

it('should filter by multiple comma-separated statuses', async () => {
const { stdout } = await execAsync(`tsx ${cliPath} --json list -s open,in-progress`);

const result = JSON.parse(stdout);
expect(result.success).toBe(true);
expect(result.workItems).toHaveLength(2);
const statuses = result.workItems.map((item: any) => item.status);
expect(statuses).toContain('open');
expect(statuses).toContain('in-progress');
});

it('should filter by multiple statuses with --status open,completed', async () => {
const { stdout } = await execAsync(`tsx ${cliPath} --json list --status open,completed`);

const result = JSON.parse(stdout);
expect(result.success).toBe(true);
expect(result.workItems).toHaveLength(2);
const statuses = result.workItems.map((item: any) => item.status);
expect(statuses).toContain('open');
expect(statuses).toContain('completed');
});

it('should combine comma-separated --status with --stage', async () => {
seedWorkItems(tempState.tempDir, [
{ title: 'Open In Progress', status: 'open', stage: 'in_progress' },
{ title: 'In Progress Review', status: 'in-progress', stage: 'in_review' },
{ title: 'Completed Done', status: 'completed', stage: 'done' },
]);

// --status open,in-progress AND --stage in_review should return only items matching both
const { stdout } = await execAsync(`tsx ${cliPath} --json list --status open,in-progress --stage in_review`);
const result = JSON.parse(stdout);
expect(result.success).toBe(true);
expect(result.workItems).toHaveLength(1);
expect(result.workItems[0].title).toBe('In Progress Review');
});

it('should return error for invalid status value', async () => {
try {
await execAsync(`tsx ${cliPath} --json list -s invalid_status`);
expect.fail('Should have thrown an error');
} catch (error: any) {
const result = JSON.parse(error.stderr || '{}');
expect(result.success).toBe(false);
}
});

it('should return error when any status in comma-separated list is invalid', async () => {
try {
await execAsync(`tsx ${cliPath} --json list -s open,invalid_status`);
expect.fail('Should have thrown an error');
} catch (error: any) {
const result = JSON.parse(error.stderr || '{}');
expect(result.success).toBe(false);
}
});

it('should still hide completed items in human mode when no stage filter is set', async () => {
// The default behavior (no --stage, no --status) should still hide completed items in human mode
const { stdout: humanStdout } = await execAsync(`tsx ${cliPath} list`);
Expand Down
24 changes: 21 additions & 3 deletions tests/database.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ describe('WorklogDatabase', () => {
it('should normalize status when querying with underscore form', () => {
db.create({ title: 'Test', status: 'in-progress' });
// Query using underscore form — should still find the item
const results = db.list({ status: 'in_progress' as any });
const results = db.list({ status: ['in_progress'] as any });
expect(results.length).toBe(1);
expect(results[0].status).toBe('in-progress');
});
Expand Down Expand Up @@ -189,26 +189,44 @@ describe('WorklogDatabase', () => {
});

it('should filter by status', () => {
const openItems = db.list({ status: 'open' });
const openItems = db.list({ status: ['open'] });
expect(openItems).toHaveLength(2);
openItems.forEach(item => expect(item.status).toBe('open'));
});

it('should filter by multiple statuses', () => {
const items = db.list({ status: ['open', 'completed'] });
expect(items).toHaveLength(3);
const statuses = items.map(item => item.status);
expect(statuses.filter(s => s === 'open')).toHaveLength(2);
expect(statuses.filter(s => s === 'completed')).toHaveLength(1);
});

it('should filter by priority', () => {
const highPriorityItems = db.list({ priority: 'high' });
expect(highPriorityItems).toHaveLength(2);
highPriorityItems.forEach(item => expect(item.priority).toBe('high'));
});

it('should filter by status and priority', () => {
const items = db.list({ status: 'open', priority: 'high' });
const items = db.list({ status: ['open'], priority: 'high' });
expect(items).toHaveLength(2);
items.forEach(item => {
expect(item.status).toBe('open');
expect(item.priority).toBe('high');
});
});

it('should combine multiple statuses with priority', () => {
const items = db.list({ status: ['open', 'blocked'], priority: 'high' });
expect(items).toHaveLength(2);
const statuses = items.map(item => item.status);
expect(statuses.filter(s => s === 'open')).toHaveLength(2);
items.forEach(item => {
expect(item.priority).toBe('high');
});
});

it('should filter by tags', () => {
const items = db.list({ tags: ['backend'] });
expect(items).toHaveLength(1);
Expand Down
2 changes: 1 addition & 1 deletion tests/sort-operations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ describe('Sort Operations', () => {
const item2 = db.create({ title: 'Task 2', status: 'in-progress', sortIndex: 100 });
const item3 = db.create({ title: 'Task 3', status: 'open', sortIndex: 200 });

const openItems = db.list({ status: 'open' });
const openItems = db.list({ status: ['open'] });

expect(openItems).toHaveLength(2);
// Check sortIndex values are preserved
Expand Down
2 changes: 1 addition & 1 deletion tests/tui/wl-db-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ describe('createWlDbAdapter', () => {
it('uses wl subcommands and unwraps list/show/create/update envelopes', () => {
const db = createWlDbAdapter();

expect(db.list({ status: 'open', assignee: 'Map' })).toEqual([baseWorkItem]);
expect(db.list({ status: ['open'], assignee: 'Map' })).toEqual([baseWorkItem]);
expect(db.get('WL-TEST-1')).toEqual(baseWorkItem);
expect(db.create({ title: 'Created item', description: 'Created description' })).toEqual({
...baseWorkItem,
Expand Down
Loading