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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,6 @@ artifacts/
# Asana CLI skill — eval scratch & evals may contain private workspace data
skills/*-workspace/
skills/asana-cli/evals/

# Impeccable plugin session cache
.impeccable/
48 changes: 45 additions & 3 deletions docs/content/en/2.features/1.task-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,54 @@ View all tasks in a specific project:
bun run dev task list -p PROJECT_ID
```

### Include Completed Tasks
### Filter by Completion State

By default, only incomplete tasks are shown. Include completed tasks:
List only incomplete or only completed tasks:

```bash
bun run dev task list -a me -c -w WORKSPACE_ID
bun run dev task list -p PROJECT_ID --incomplete-only
bun run dev task list -p PROJECT_ID --completed-only
```

> `-c, --completed` is deprecated: despite its name it excludes completed tasks. Use `--incomplete-only` instead.

### Filter by Assignee

`-a` accepts `me`, a user GID, or `none` for unassigned tasks:

```bash
bun run dev task list -p PROJECT_ID -a none
```

### Expand Fields

Pull extra fields into the listing with `--fields` instead of one `task get` per task:

```bash
bun run dev task list -p PROJECT_ID --fields completed,assignee,due_on
```

### Count and Group

Return aggregates instead of rows:

```bash
bun run dev task list -p PROJECT_ID --count
bun run dev task list -p PROJECT_ID --incomplete-only --group-by assignee --format json
```

Example output:

```json
{
"summary": {
"total": 12,
"groups": [
{ "assignee": "Alice", "count": 7 },
{ "assignee": "unassigned", "count": 5 }
]
}
}
```

### List Tasks for Another User
Expand Down
48 changes: 45 additions & 3 deletions docs/content/ko/2.features/1.task-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,54 @@ View all tasks in a specific project:
bun run dev task list -p PROJECT_ID
```

### Include Completed Tasks
### 완료 상태로 필터링

By default, only incomplete tasks are shown. Include completed tasks:
미완료 또는 완료 작업만 조회합니다:

```bash
bun run dev task list -a me -c -w WORKSPACE_ID
bun run dev task list -p PROJECT_ID --incomplete-only
bun run dev task list -p PROJECT_ID --completed-only
```

> `-c, --completed` 플래그는 폐기 예정입니다. 이름과 달리 완료된 작업을 제외하므로 `--incomplete-only`를 사용하세요.

### 담당자로 필터링

`-a` 옵션은 `me`, 사용자 GID 외에 미할당 작업을 뜻하는 `none`도 지원합니다:

```bash
bun run dev task list -p PROJECT_ID -a none
```

### 필드 확장

`--fields`로 목록에 추가 필드를 포함하면 작업마다 `task get`을 호출할 필요가 없습니다:

```bash
bun run dev task list -p PROJECT_ID --fields completed,assignee,due_on
```

### 개수 집계

행 목록 대신 합계를 반환합니다:

```bash
bun run dev task list -p PROJECT_ID --count
bun run dev task list -p PROJECT_ID --incomplete-only --group-by assignee --format json
```

출력 예시:

```json
{
"summary": {
"total": 12,
"groups": [
{ "assignee": "Alice", "count": 7 },
{ "assignee": "unassigned", "count": 5 }
]
}
}
```

### List Tasks for Another User
Expand Down
107 changes: 77 additions & 30 deletions src/commands/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ import { toTaskView } from '../lib/asana-views'
import { emitError, emitResult } from '../lib/axi-output'
import { loadConfig } from '../lib/config'
import { handleAsanaError, isNotFoundError } from '../lib/error-handler'
import {
applyAssigneeFilter,
applyCompletionFilter,
buildOptFields,
effectiveColumns,
GROUP_BY_FIELDS,
needsClientAssigneeFilter,
parseTaskListQuery,
summarizeTasks,
toTaskRows,
} from '../lib/task-list-query'
import { validateDateFormat, validateGid, validateUpdateFields, ValidationError } from '../lib/validators'
import { formatOutput, getOutputFormat } from '../utils/formatter'
import { createTaskCustomFieldCommand } from './custom-field'
Expand Down Expand Up @@ -34,6 +45,33 @@ function failValidation(error: ValidationError, command: Command): never {
process.exit(1)
}

/**
* Fetch one listing page for `task list`. Assignee filtering happens
* server-side only in workspace mode with a concrete assignee; project
* listings and `--assignee none` filter client-side (clientAssignee=true).
*/
async function fetchTaskPage(
client: ReturnType<typeof getAsanaClient>,
options: TaskListOptions,
workspace: string | undefined,
params: Record<string, any>,
clientAssignee: boolean,
): Promise<any[]> {
if (options.project) {
const result = await client.tasks.findByProject(options.project, params)
return result.data || []
}
if (options.assignee && !clientAssignee) {
const result = await client.tasks.findAll({ ...params, assignee: options.assignee, workspace })
return result.data || []
}
if (workspace) {
const result = await client.tasks.findAll({ ...params, workspace })
return result.data || []
}
throw new Error('Specify workspace, project, or assignee to list tasks')
}

export function createTaskCommand(): Command {
const task = new Command('task')
.description('Manage Asana tasks')
Expand Down Expand Up @@ -115,61 +153,70 @@ export function createTaskCommand(): Command {
task
.command('list')
.description('List tasks')
.option('-a, --assignee <assignee>', 'Filter by assignee (use "me" for current user)')
.option('-a, --assignee <assignee>', 'Filter by assignee ("me", "none" for unassigned, or user GID)')
.option('-w, --workspace <workspace>', 'Workspace GID')
.option('-p, --project <project>', 'Project GID')
.option('-c, --completed', 'Include completed tasks')
.option('-c, --completed', 'Deprecated: excludes completed tasks (use --incomplete-only)')
.option('--fields <fields>', 'Extra fields per task, comma-separated (e.g. completed,assignee,due_on)')
.option('--completed-only', 'Only list completed tasks')
.option('--incomplete-only', 'Only list incomplete tasks')
.option('--count', 'Output only the task count')
.option('--group-by <field>', `Output counts grouped by field (${GROUP_BY_FIELDS.join('|')})`)
.action(async (options: TaskListOptions, command: Command) => {
// Declared outside try so the error handler can report it
let workspace: string | undefined
try {
const query = parseTaskListQuery(options)
const client = getAsanaClient()
const config = loadConfig()
workspace = options.workspace || config?.workspace

let tasks: any

if (options.project) {
tasks = await client.tasks.findByProject(options.project, {
completed_since: options.completed ? 'now' : undefined,
})
}
else if (options.assignee) {
const assignee = options.assignee === 'me' ? 'me' : options.assignee
tasks = await client.tasks.findAll({
assignee,
workspace,
completed_since: options.completed ? 'now' : undefined,
})
const clientAssignee = needsClientAssigneeFilter(query, !!options.project)
const params: Record<string, any> = {}
const optFields = buildOptFields(query, clientAssignee)
if (optFields) {
params.opt_fields = optFields
}
else if (workspace) {
tasks = await client.tasks.findAll({
workspace,
completed_since: options.completed ? 'now' : undefined,
})
}
else {
throw new Error('Specify workspace, project, or assignee to list tasks')
// completed_since=now is the API's incomplete-only filter; the
// deprecated -c/--completed flag historically mapped to it too.
if (query.incompleteOnly || options.completed) {
params.completed_since = 'now'
}

const taskList = tasks.data || []
let taskList = await fetchTaskPage(client, options, workspace, params, clientAssignee)

if (taskList.length === 0) {
console.log(chalk.yellow('No tasks found'))
return
if (clientAssignee && query.assignee) {
const assignee = query.assignee === 'me' ? (await client.users.me()).gid : query.assignee
taskList = applyAssigneeFilter(taskList, assignee)
}
taskList = applyCompletionFilter(taskList, query)

// Resolve --format from the global options. Use getOutputFormat
// (optsWithGlobals) rather than walking the parent chain by hand — the
// global option lives on the root command, and the hand-walked lookup
// here previously stopped one level short and silently ignored --format.
const format = getOutputFormat(command)

// Format output based on selected format
const output = formatOutput({ tasks: taskList }, { format, colors: process.stdout.isTTY })
if (query.count || query.groupBy) {
const summary = summarizeTasks(taskList, query.groupBy)
console.log(formatOutput({ summary }, { format, colors: process.stdout.isTTY }))
return
}

if (taskList.length === 0) {
console.log(chalk.yellow('No tasks found'))
return
}

const columns = effectiveColumns(query, clientAssignee)
const tasks = columns.length > 0 ? toTaskRows(taskList, columns) : taskList
const output = formatOutput({ tasks }, { format, colors: process.stdout.isTTY })
console.log(output)
}
catch (error) {
if (error instanceof ValidationError) {
failValidation(error, command)
}
handleAsanaError(error, 'Task listing', {
Workspace: workspace,
Project: options.project,
Expand Down
3 changes: 3 additions & 0 deletions src/constants/errorIds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ export const ERROR_IDS = {
INVALID_USER_IDENTIFIER: 'INVALID_USER_IDENTIFIER',
INVALID_TAG_COLOR: 'INVALID_TAG_COLOR',
INVALID_ASANA_URL: 'INVALID_ASANA_URL',
INVALID_FIELD_NAME: 'INVALID_FIELD_NAME',
INVALID_GROUP_BY: 'INVALID_GROUP_BY',
CONFLICTING_OPTIONS: 'CONFLICTING_OPTIONS',
FILE_NOT_FOUND: 'FILE_NOT_FOUND',

// Custom field errors
Expand Down
Loading