Skip to content
Open
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
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,9 @@ TM_PROVIDER=vikunja
VIKUNJA_API_URL=https://tasks.excelsi.dev
VIKUNJA_TOKEN=your_api_token_here
VIKUNJA_DEFAULT_PROJECT_ID=1

# Atlas Configuration
ATLAS_API_URL=http://localhost:3000
ATLAS_EMAIL=user@example.com
ATLAS_PASSWORD=your_password_here
ATLAS_DEFAULT_PROJECT_ID=
16 changes: 9 additions & 7 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
name: Publish

on:
release:
types: [published]
push:
branches: [main, master]
tags: ['v*']
tags: ["v*"]

env:
REGISTRY: ghcr.io
Expand All @@ -29,16 +26,21 @@ jobs:
- name: Setup Node (for npm publish)
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://npm.pkg.github.com'
scope: '@excelsi-innovations'
node-version: "22"
registry-url: "https://npm.pkg.github.com"
scope: "@excelsi-innovations"

- name: Install dependencies
run: bun install

- name: Build
run: bun run build

- name: Set version from tag
run: npm version "${GITHUB_REF_NAME#v}" --no-git-tag-version
env:
GITHUB_REF_NAME: ${{ github.ref_name }}

- name: Publish to GitHub Packages
run: npm publish
env:
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,30 @@ tm list
tm add "My new task"
```

## Atlas Provider

```bash
# Configure Atlas
tm config --provider atlas --url http://localhost:3000 --email user@example.com --password yourpassword
tm config --project <atlas-project-uuid> # Set default project

# Use it
tm projects # List Atlas projects
tm list # List tickets (todo status)
tm list --all # All tickets
tm add "New ticket" # Create ticket
tm done <ticket-uuid> # Mark as done
```

Or set environment variables instead of saved config:

```env
ATLAS_API_URL=http://localhost:3000
ATLAS_EMAIL=user@example.com
ATLAS_PASSWORD=yourpassword
ATLAS_DEFAULT_PROJECT_ID=<uuid>
```

## Usage

```bash
Expand Down Expand Up @@ -72,5 +96,6 @@ tm config --show --json
## Supported Providers

- [x] Vikunja
- [x] Atlas
- [ ] GitHub Projects (coming soon)
- [ ] Linear (coming soon)
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

211 changes: 211 additions & 0 deletions docs/providers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
# Providers

Providers are adapters between the `tm` CLI and a task management backend. Each provider implements the `TaskProvider` interface, translating the generic task model to and from the backend's own API and data types.

---

## `TaskProvider` Interface

Defined in `src/interfaces.ts`. All providers must implement this interface.

```typescript
interface TaskProvider {
readonly name: string;

listTasks(filter?: TaskFilter): Promise<Task[]>;
getTask(id: string): Promise<Task | null>;
createTask(input: CreateTaskInput): Promise<Task>;
updateTask(id: string, input: UpdateTaskInput): Promise<Task>;
deleteTask(id: string): Promise<void>;

// Optional
listProjects?(): Promise<Project[]>;
getProject?(id: string): Promise<Project | null>;
}
```

### Methods

| Method | Returns | Description |
| ----------------------- | ----------------- | ---------------------------------------------- |
| `listTasks(filter?)` | `Task[]` | List tasks, optionally filtered |
| `getTask(id)` | `Task \| null` | Fetch a single task by ID; `null` if not found |
| `createTask(input)` | `Task` | Create a new task |
| `updateTask(id, input)` | `Task` | Partially update a task |
| `deleteTask(id)` | `void` | Delete a task |
| `listProjects?()` | `Project[]` | List all projects (optional) |
| `getProject?(id)` | `Project \| null` | Fetch a single project (optional) |

---

## Core Types

### `Task`

```typescript
interface Task {
id: string;
title: string;
description?: string;
status: TaskStatus;
priority?: TaskPriority;
dueDate?: Date;
labels?: string[];
projectId?: string;
createdAt?: Date;
updatedAt?: Date;
}
```

### `TaskStatus`

```typescript
type TaskStatus = "todo" | "in_progress" | "done" | "cancelled";
```

### `TaskPriority`

```typescript
type TaskPriority = "low" | "medium" | "high" | "urgent";
```

### `CreateTaskInput`

```typescript
interface CreateTaskInput {
title: string;
description?: string;
priority?: TaskPriority;
dueDate?: Date;
labels?: string[];
projectId?: string;
}
```

### `UpdateTaskInput`

All fields optional — only provided fields are updated.

```typescript
interface UpdateTaskInput {
title?: string;
description?: string;
status?: TaskStatus;
priority?: TaskPriority;
dueDate?: Date;
labels?: string[];
}
```

### `TaskFilter`

```typescript
interface TaskFilter {
status?: TaskStatus;
priority?: TaskPriority;
projectId?: string;
labels?: string[];
}
```

### `Project`

```typescript
interface Project {
id: string;
name: string;
description?: string;
}
```

---

## `AtlasProvider`

File: `src/providers/atlas.ts`

### Config

```typescript
interface AtlasConfig {
apiUrl: string; // Base URL, e.g. "http://localhost:3000"
email: string; // Atlas user email
password: string; // Atlas user password
defaultProjectId?: string; // UUID — used when no projectId is passed to createTask/listTasks
}
```

### Status Mapping

| Atlas API value | `TaskStatus` |
| --------------- | ------------- |
| `BACKLOG` | `todo` |
| `TODO` | `todo` |
| `IN_REVIEW` | `todo` |
| `IN_PROGRESS` | `in_progress` |
| `DONE` | `done` |
| `CANCELLED` | `cancelled` |

### Priority Mapping

| Atlas API value | `TaskPriority` |
| --------------- | -------------- |
| `NONE` | `low` |
| `LOW` | `low` |
| `MEDIUM` | `medium` |
| `HIGH` | `high` |
| `URGENT` | `urgent` |

### Notes

- **Authentication** is lazy: the first API call triggers `POST /api/v1/auth/login` automatically. The token is stored in memory for the lifetime of the provider instance.
- **401 handling**: a single re-authentication retry is performed transparently. If it fails again, the error propagates.
- **Create ticket** uses a project-nested route (`POST /api/v1/projects/:projectId/tickets`). A `projectId` must be available — either from `CreateTaskInput.projectId` or `AtlasConfig.defaultProjectId`.
- **All other ticket operations** use the top-level route (`/api/v1/tickets/:id`).
- **Label filtering** is performed client-side — the Atlas API does not support filtering by label name.

---

## `VikunjaProvider`

File: `src/providers/vikunja.ts`

### Config

```typescript
interface VikunjaConfig {
apiUrl: string; // Base URL, e.g. "https://tasks.example.com"
token: string; // Vikunja API token
defaultProjectId?: number; // Numeric project ID (not a UUID)
}
```

### Status Mapping

Vikunja uses a boolean `done` field rather than a status enum.

| Vikunja field | `TaskStatus` |
| ------------- | ------------ |
| `done: false` | `todo` |
| `done: true` | `done` |

> `in_progress` and `cancelled` are not supported by Vikunja and will not round-trip correctly.

### Priority Mapping

Vikunja uses a numeric `priority` field (0–4).

| Vikunja `priority` | `TaskPriority` |
| ------------------ | -------------- |
| 0–1 | `low` |
| 2 | `medium` |
| 3 | `high` |
| 4 | `urgent` |

### Notes

- **`defaultProjectId` is a `number`**, not a string UUID. Passing a non-numeric string will result in a `NaN` project ID.
- **`listTasks`** fetches all tasks (`GET /api/v1/tasks/all`) and applies `projectId`, status, and label filters client-side, except `done` which is passed as a query param.
- **`createTask`** uses `PUT /api/v1/projects/:id/tasks` (Vikunja's creation endpoint).
- **`updateTask`** uses `POST /api/v1/tasks/:id` (not `PATCH`).
- Labels are not written on create — Vikunja requires a separate labels API call not currently implemented.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@excelsi-innovations/task-manager",
"version": "0.1.0",
"version": "0.1.1",
"description": "Agnostic task management CLI with support for Vikunja, GitHub Projects, and more.",
"author": "Excelsi Innovations",
"license": "private",
Expand Down
39 changes: 32 additions & 7 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { homedir } from "os";
import { join } from "path";

export interface TaskManagerConfig {
provider: string;
Expand All @@ -14,10 +14,16 @@ export interface TaskManagerConfig {
owner: string;
repo: string;
};
atlas?: {
apiUrl: string;
email: string;
password: string;
defaultProjectId?: string;
};
}

const CONFIG_DIR = join(homedir(), '.config', 'task-manager');
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
const CONFIG_DIR = join(homedir(), ".config", "task-manager");
const CONFIG_FILE = join(CONFIG_DIR, "config.json");

export function getConfigPath(): string {
return CONFIG_FILE;
Expand All @@ -29,7 +35,7 @@ export function loadConfig(): TaskManagerConfig | null {
}

try {
const content = readFileSync(CONFIG_FILE, 'utf-8');
const content = readFileSync(CONFIG_FILE, "utf-8");
return JSON.parse(content) as TaskManagerConfig;
} catch {
return null;
Expand All @@ -52,7 +58,7 @@ export function getEffectiveConfig(): TaskManagerConfig | null {
// Check if we have env vars for vikunja
if (process.env.VIKUNJA_API_URL && process.env.VIKUNJA_TOKEN) {
return {
provider: process.env.TM_PROVIDER || 'vikunja',
provider: process.env.TM_PROVIDER || "vikunja",
vikunja: {
apiUrl: process.env.VIKUNJA_API_URL,
token: process.env.VIKUNJA_TOKEN,
Expand All @@ -63,5 +69,24 @@ export function getEffectiveConfig(): TaskManagerConfig | null {
};
}

// Check if we have env vars for Atlas
if (
process.env.ATLAS_API_URL &&
process.env.ATLAS_EMAIL &&
process.env.ATLAS_PASSWORD
) {
return {
provider: process.env.TM_PROVIDER || "atlas",
atlas: {
apiUrl: process.env.ATLAS_API_URL,
email: process.env.ATLAS_EMAIL,
password: process.env.ATLAS_PASSWORD,
defaultProjectId:
process.env.ATLAS_DEFAULT_PROJECT_ID ??
fileConfig?.atlas?.defaultProjectId,
},
};
}

return fileConfig;
}
Loading