mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-29 22:02:04 +00:00
feat(cli): add --watch flag to list command for real-time updates (#1526)
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> Fixes #1526 PR review comments from CodeRabbit and changeset-bot
This commit is contained in:
12
.changeset/list-watch-compact.md
Normal file
12
.changeset/list-watch-compact.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Add watch mode and compact output to list command
|
||||
|
||||
- Add `-w/--watch` flag to continuously monitor task changes with real-time updates
|
||||
- Add `-c/--compact` flag for minimal task output format
|
||||
- Add `--no-header` flag to hide the command header
|
||||
- Support file-based watching via fs.watch for local tasks.json
|
||||
- Support API-based watching via Supabase Realtime for authenticated users
|
||||
- Display last sync timestamp and source in watch mode
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
type Task,
|
||||
type TaskStatus,
|
||||
type TmCore,
|
||||
type WatchSubscription,
|
||||
createTmCore
|
||||
} from '@tm/core';
|
||||
import type { StorageType } from '@tm/core';
|
||||
@@ -24,6 +25,8 @@ import {
|
||||
displayDashboards,
|
||||
displayRecommendedNextTask,
|
||||
displaySuggestedNextSteps,
|
||||
displaySyncMessage,
|
||||
displayWatchFooter,
|
||||
getPriorityBreakdown,
|
||||
getTaskDescription
|
||||
} from '../ui/index.js';
|
||||
@@ -42,8 +45,11 @@ export interface ListCommandOptions {
|
||||
withSubtasks?: boolean;
|
||||
format?: OutputFormat;
|
||||
json?: boolean;
|
||||
compact?: boolean;
|
||||
noHeader?: boolean;
|
||||
silent?: boolean;
|
||||
project?: string;
|
||||
watch?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,11 +93,17 @@ export class ListTasksCommand extends Command {
|
||||
'text'
|
||||
)
|
||||
.option('--json', 'Output in JSON format (shorthand for --format json)')
|
||||
.option(
|
||||
'-c, --compact',
|
||||
'Output in compact format (shorthand for --format compact)'
|
||||
)
|
||||
.option('--no-header', 'Hide the command header')
|
||||
.option('--silent', 'Suppress output (useful for programmatic usage)')
|
||||
.option(
|
||||
'-p, --project <path>',
|
||||
'Project root directory (auto-detected if not provided)'
|
||||
)
|
||||
.option('-w, --watch', 'Watch for changes and update list automatically')
|
||||
.action(async (statusArg?: string, options?: ListCommandOptions) => {
|
||||
// Handle special "all" keyword to show with subtasks
|
||||
let status = statusArg || options?.status;
|
||||
@@ -127,6 +139,9 @@ export class ListTasksCommand extends Command {
|
||||
await this.initializeCore(getProjectRoot(options.project));
|
||||
|
||||
// Get tasks from core
|
||||
if (options.watch) {
|
||||
await this.watchTasks(options);
|
||||
} else {
|
||||
const result = await this.getTasks(options);
|
||||
|
||||
// Store result for programmatic access
|
||||
@@ -136,11 +151,76 @@ export class ListTasksCommand extends Command {
|
||||
if (!options.silent) {
|
||||
this.displayResults(result, options);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
displayError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch for changes and update list
|
||||
* Uses tm-core's unified watch API which handles both file and API storage
|
||||
*/
|
||||
private async watchTasks(options: ListCommandOptions): Promise<void> {
|
||||
if (!this.tmCore) {
|
||||
throw new Error('TmCore not initialized');
|
||||
}
|
||||
|
||||
// Initial render
|
||||
let result = await this.getTasks(options);
|
||||
let lastSync = new Date();
|
||||
|
||||
console.clear();
|
||||
this.displayResults(result, options);
|
||||
|
||||
const storageType = result.storageType;
|
||||
displayWatchFooter(storageType, lastSync);
|
||||
|
||||
let subscription: WatchSubscription | undefined;
|
||||
|
||||
try {
|
||||
// Subscribe to task changes via tm-core
|
||||
subscription = await this.tmCore.tasks.watch(
|
||||
async (event) => {
|
||||
if (event.type === 'change') {
|
||||
try {
|
||||
// Re-fetch tasks
|
||||
result = await this.getTasks(options);
|
||||
lastSync = new Date();
|
||||
|
||||
// Clear and display
|
||||
console.clear();
|
||||
this.displayResults(result, options);
|
||||
|
||||
// Show sync message with timestamp
|
||||
displaySyncMessage(storageType, lastSync);
|
||||
displayWatchFooter(storageType, lastSync);
|
||||
} catch {
|
||||
// Ignore errors during watch (e.g. partial writes)
|
||||
}
|
||||
} else if (event.type === 'error' && event.error) {
|
||||
console.error(chalk.red(`\n⚠ Watch error: ${event.error.message}`));
|
||||
}
|
||||
},
|
||||
{ tag: options.tag }
|
||||
);
|
||||
|
||||
// Cleanup on process termination
|
||||
const cleanup = () => {
|
||||
subscription?.unsubscribe();
|
||||
process.exit(0);
|
||||
};
|
||||
process.on('SIGINT', cleanup);
|
||||
process.on('SIGTERM', cleanup);
|
||||
|
||||
// Keep process alive
|
||||
await new Promise(() => {});
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red(`Watch mode error: ${error.message}`));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate command options
|
||||
*/
|
||||
@@ -219,9 +299,13 @@ export class ListTasksCommand extends Command {
|
||||
result: ListTasksResult,
|
||||
options: ListCommandOptions
|
||||
): void {
|
||||
// If --json flag is set, override format to 'json'
|
||||
// Resolve format: --json and --compact flags override --format option
|
||||
const format = (
|
||||
options.json ? 'json' : options.format || 'text'
|
||||
options.json
|
||||
? 'json'
|
||||
: options.compact
|
||||
? 'compact'
|
||||
: options.format || 'text'
|
||||
) as OutputFormat;
|
||||
|
||||
switch (format) {
|
||||
@@ -230,12 +314,12 @@ export class ListTasksCommand extends Command {
|
||||
break;
|
||||
|
||||
case 'compact':
|
||||
this.displayCompact(result.tasks, options.withSubtasks);
|
||||
this.displayCompact(result, options);
|
||||
break;
|
||||
|
||||
case 'text':
|
||||
default:
|
||||
this.displayText(result, options.withSubtasks, options.status);
|
||||
this.displayText(result, options);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -264,12 +348,25 @@ export class ListTasksCommand extends Command {
|
||||
/**
|
||||
* Display in compact format
|
||||
*/
|
||||
private displayCompact(tasks: Task[], withSubtasks?: boolean): void {
|
||||
private displayCompact(
|
||||
data: ListTasksResult,
|
||||
options: ListCommandOptions
|
||||
): void {
|
||||
const { tasks, tag, storageType } = data;
|
||||
|
||||
// Display header unless --no-header is set
|
||||
if (options.noHeader !== true) {
|
||||
displayCommandHeader(this.tmCore, {
|
||||
tag: tag || 'master',
|
||||
storageType
|
||||
});
|
||||
}
|
||||
|
||||
tasks.forEach((task) => {
|
||||
const icon = STATUS_ICONS[task.status];
|
||||
console.log(`${chalk.cyan(task.id)} ${icon} ${task.title}`);
|
||||
|
||||
if (withSubtasks && task.subtasks?.length) {
|
||||
if (options.withSubtasks && task.subtasks?.length) {
|
||||
task.subtasks.forEach((subtask) => {
|
||||
const subIcon = STATUS_ICONS[subtask.status];
|
||||
console.log(
|
||||
@@ -285,16 +382,17 @@ export class ListTasksCommand extends Command {
|
||||
*/
|
||||
private displayText(
|
||||
data: ListTasksResult,
|
||||
withSubtasks?: boolean,
|
||||
_statusFilter?: string
|
||||
options: ListCommandOptions
|
||||
): void {
|
||||
const { tasks, tag, storageType } = data;
|
||||
|
||||
// Display header using utility function
|
||||
// Display header unless --no-header is set
|
||||
if (options.noHeader !== true) {
|
||||
displayCommandHeader(this.tmCore, {
|
||||
tag: tag || 'master',
|
||||
storageType
|
||||
});
|
||||
}
|
||||
|
||||
// No tasks message
|
||||
if (tasks.length === 0) {
|
||||
@@ -328,7 +426,7 @@ export class ListTasksCommand extends Command {
|
||||
// Task table
|
||||
console.log(
|
||||
ui.createTaskTable(tasks, {
|
||||
showSubtasks: withSubtasks,
|
||||
showSubtasks: options.withSubtasks,
|
||||
showDependencies: true,
|
||||
showComplexity: true // Enable complexity column
|
||||
})
|
||||
|
||||
@@ -9,3 +9,4 @@ export * from './header.component.js';
|
||||
export * from './next-task.component.js';
|
||||
export * from './suggested-steps.component.js';
|
||||
export * from './task-detail.component.js';
|
||||
export * from './watch-footer.component.js';
|
||||
|
||||
41
apps/cli/src/ui/components/watch-footer.component.ts
Normal file
41
apps/cli/src/ui/components/watch-footer.component.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @fileoverview Watch mode footer component for real-time task updates
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { formatTime } from '@tm/core';
|
||||
|
||||
/**
|
||||
* Get display label for storage source
|
||||
*/
|
||||
function getSourceLabel(storageType: 'api' | 'file'): string {
|
||||
return storageType === 'api' ? 'Hamster Studio' : 'tasks.json';
|
||||
}
|
||||
|
||||
/**
|
||||
* Display watch status footer
|
||||
*/
|
||||
export function displayWatchFooter(
|
||||
storageType: 'api' | 'file',
|
||||
lastSync: Date
|
||||
): void {
|
||||
const syncTime = formatTime(lastSync);
|
||||
const source = getSourceLabel(storageType);
|
||||
|
||||
console.log(chalk.dim(`\nWatching ${source} for changes...`));
|
||||
console.log(chalk.gray(`Last synced: ${syncTime}`));
|
||||
console.log(chalk.dim('Press Ctrl+C to exit'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display sync notification message
|
||||
*/
|
||||
export function displaySyncMessage(
|
||||
storageType: 'api' | 'file',
|
||||
syncTime: Date
|
||||
): void {
|
||||
const formattedTime = formatTime(syncTime);
|
||||
const source = getSourceLabel(storageType);
|
||||
|
||||
console.log(chalk.blue(`\nℹ ${source} updated at ${formattedTime}`));
|
||||
}
|
||||
@@ -28,6 +28,15 @@ sidebarTitle: "CLI Commands"
|
||||
|
||||
# List tasks with a specific status and include subtasks
|
||||
task-master list --status=<status> --with-subtasks
|
||||
|
||||
# Watch for changes and auto-refresh the list
|
||||
task-master list --watch
|
||||
task-master list -w
|
||||
|
||||
# Combine watch with other options
|
||||
task-master list all -w # Watch all tasks with subtasks
|
||||
task-master list -w --compact # Watch in compact format
|
||||
task-master list --status=pending -w # Watch pending tasks only
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
|
||||
@@ -27,6 +27,20 @@ description: "A comprehensive reference of all available Task Master commands"
|
||||
|
||||
# List tasks with a specific status and include subtasks
|
||||
task-master list --status=<status> --with-subtasks
|
||||
|
||||
# Watch for changes and auto-refresh
|
||||
task-master list --watch
|
||||
task-master list -w
|
||||
|
||||
# Compact output format
|
||||
task-master list --compact
|
||||
task-master list -c
|
||||
|
||||
# Watch mode with compact output
|
||||
task-master list -w -c
|
||||
|
||||
# Hide the header
|
||||
task-master list --no-header
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
|
||||
@@ -101,7 +101,8 @@ This document provides a detailed reference for interacting with Taskmaster, cov
|
||||
* `withSubtasks`: `Include subtasks indented under their parent tasks in the list.` (CLI: `--with-subtasks`)
|
||||
* `tag`: `Specify which tag context to list tasks from. Defaults to the current active tag.` (CLI: `--tag <name>`)
|
||||
* `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
|
||||
* **Usage:** Get an overview of the project status, often used at the start of a work session.
|
||||
* `watch`: `Watch for changes and auto-refresh the list in real-time. Works with file storage (fs.watch) and API storage (Supabase Realtime).` (CLI: `-w, --watch`)
|
||||
* **Usage:** Get an overview of the project status, often used at the start of a work session. Use `--watch` to keep the list live-updating as tasks change.
|
||||
|
||||
### 4. Get Next Task (`next_task`)
|
||||
|
||||
|
||||
@@ -235,6 +235,17 @@ export interface IStorage {
|
||||
* @returns Promise that resolves to tags with statistics
|
||||
*/
|
||||
getTagsWithStats(): Promise<TagsWithStatsResult>;
|
||||
|
||||
/**
|
||||
* Watch for changes to tasks
|
||||
* @param callback - Function called when tasks change
|
||||
* @param options - Watch options (debounce, tag)
|
||||
* @returns Subscription handle with unsubscribe method
|
||||
*/
|
||||
watch(
|
||||
callback: (event: WatchEvent) => void,
|
||||
options?: WatchOptions
|
||||
): Promise<WatchSubscription>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -282,6 +293,41 @@ export interface TagsWithStatsResult {
|
||||
totalTags: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch event types
|
||||
*/
|
||||
export type WatchEventType = 'change' | 'error';
|
||||
|
||||
/**
|
||||
* Watch event payload
|
||||
*/
|
||||
export interface WatchEvent {
|
||||
/** Type of event */
|
||||
type: WatchEventType;
|
||||
/** Timestamp of the event */
|
||||
timestamp: Date;
|
||||
/** Error if type is 'error' */
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch options
|
||||
*/
|
||||
export interface WatchOptions {
|
||||
/** Debounce time in milliseconds (default: 100) */
|
||||
debounceMs?: number;
|
||||
/** Tag context for file storage */
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch subscription handle
|
||||
*/
|
||||
export interface WatchSubscription {
|
||||
/** Unsubscribe from watch events */
|
||||
unsubscribe: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage statistics interface
|
||||
*/
|
||||
@@ -380,6 +426,10 @@ export abstract class BaseStorage implements IStorage {
|
||||
abstract getStorageType(): 'file' | 'api';
|
||||
abstract getCurrentBriefName(): string | null;
|
||||
abstract getTagsWithStats(): Promise<TagsWithStatsResult>;
|
||||
abstract watch(
|
||||
callback: (event: WatchEvent) => void,
|
||||
options?: WatchOptions
|
||||
): Promise<WatchSubscription>;
|
||||
/**
|
||||
* Utility method to generate backup filename
|
||||
* @param originalPath - Original file path
|
||||
|
||||
@@ -36,10 +36,14 @@ export type * from './common/types/index.js';
|
||||
// Common interfaces
|
||||
export type * from './common/interfaces/index.js';
|
||||
|
||||
// Storage interfaces - TagInfo and TagsWithStatsResult
|
||||
// Storage interfaces - TagInfo, TagsWithStatsResult, and Watch types
|
||||
export type {
|
||||
TagInfo,
|
||||
TagsWithStatsResult
|
||||
TagsWithStatsResult,
|
||||
WatchEvent,
|
||||
WatchEventType,
|
||||
WatchOptions,
|
||||
WatchSubscription
|
||||
} from './common/interfaces/storage.interface.js';
|
||||
|
||||
// Storage adapters - FileStorage for direct local file access
|
||||
|
||||
@@ -12,7 +12,10 @@ import type {
|
||||
IStorage,
|
||||
LoadTasksOptions,
|
||||
StorageStats,
|
||||
UpdateStatusResult
|
||||
UpdateStatusResult,
|
||||
WatchEvent,
|
||||
WatchOptions,
|
||||
WatchSubscription
|
||||
} from '../../../common/interfaces/storage.interface.js';
|
||||
import { getLogger } from '../../../common/logger/factory.js';
|
||||
import type {
|
||||
@@ -941,6 +944,92 @@ export class ApiStorage implements IStorage {
|
||||
this.tagsCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch for changes to tasks via Supabase Realtime
|
||||
* Subscribes to postgres_changes for the tasks table filtered by brief_id
|
||||
*
|
||||
* Optional debouncing is supported for collaborative scenarios where multiple
|
||||
* rapid changes (e.g., bulk operations, multiple users) could cause UI flicker.
|
||||
* Default: 300ms (higher than FileStorage's 100ms since Realtime events are cleaner)
|
||||
*/
|
||||
async watch(
|
||||
callback: (event: WatchEvent) => void,
|
||||
options?: WatchOptions
|
||||
): Promise<WatchSubscription> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
const authManager = AuthManager.getInstance();
|
||||
const context = authManager.getContext();
|
||||
|
||||
if (!context?.briefId) {
|
||||
throw new TaskMasterError(
|
||||
'No brief context found for watching. Please connect to a brief first.',
|
||||
ERROR_CODES.NO_BRIEF_SELECTED,
|
||||
{ operation: 'watch' }
|
||||
);
|
||||
}
|
||||
|
||||
const supabase = authManager.supabaseClient.getClient();
|
||||
const channelName = `tasks-watch-${context.briefId}-${Date.now()}`;
|
||||
const debounceMs = options?.debounceMs ?? 300;
|
||||
|
||||
let debounceTimer: NodeJS.Timeout | undefined;
|
||||
let closed = false;
|
||||
|
||||
const debouncedCallback = () => {
|
||||
if (closed) return;
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer);
|
||||
}
|
||||
debounceTimer = setTimeout(() => {
|
||||
if (!closed) {
|
||||
callback({
|
||||
type: 'change',
|
||||
timestamp: new Date()
|
||||
});
|
||||
}
|
||||
}, debounceMs);
|
||||
};
|
||||
|
||||
const channel = supabase
|
||||
.channel(channelName)
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: '*',
|
||||
schema: 'public',
|
||||
table: 'tasks',
|
||||
filter: `brief_id=eq.${context.briefId}`
|
||||
},
|
||||
debouncedCallback
|
||||
)
|
||||
.subscribe((status, error) => {
|
||||
if (
|
||||
status === 'CHANNEL_ERROR' ||
|
||||
status === 'TIMED_OUT' ||
|
||||
status === 'CLOSED' ||
|
||||
error
|
||||
) {
|
||||
callback({
|
||||
type: 'error',
|
||||
timestamp: new Date(),
|
||||
error:
|
||||
error || new Error(`Channel subscription ${status.toLowerCase()}`)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
unsubscribe: () => {
|
||||
closed = true;
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer);
|
||||
}
|
||||
channel.unsubscribe();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure storage is initialized
|
||||
*/
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* @fileoverview Refactored file-based storage implementation for Task Master
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
ERROR_CODES,
|
||||
@@ -11,7 +12,10 @@ import type {
|
||||
IStorage,
|
||||
LoadTasksOptions,
|
||||
StorageStats,
|
||||
UpdateStatusResult
|
||||
UpdateStatusResult,
|
||||
WatchEvent,
|
||||
WatchOptions,
|
||||
WatchSubscription
|
||||
} from '../../../../common/interfaces/storage.interface.js';
|
||||
import type {
|
||||
Task,
|
||||
@@ -882,6 +886,68 @@ export class FileStorage implements IStorage {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch for changes to tasks file
|
||||
* Uses fs.watch with debouncing to detect file changes
|
||||
*/
|
||||
async watch(
|
||||
callback: (event: WatchEvent) => void,
|
||||
options?: WatchOptions
|
||||
): Promise<WatchSubscription> {
|
||||
const tasksPath = this.pathResolver.getTasksPath();
|
||||
const debounceMs = options?.debounceMs ?? 100;
|
||||
|
||||
// Ensure file exists before watching
|
||||
const fileExists = await this.fileOps.exists(tasksPath);
|
||||
if (!fileExists) {
|
||||
throw new TaskMasterError(
|
||||
'Tasks file not found. Initialize the project first.',
|
||||
ERROR_CODES.NOT_FOUND,
|
||||
{ path: tasksPath }
|
||||
);
|
||||
}
|
||||
|
||||
let debounceTimer: NodeJS.Timeout | undefined;
|
||||
let closed = false;
|
||||
|
||||
const watcher = fs.watch(tasksPath, (eventType, filename) => {
|
||||
if (closed) return;
|
||||
if (filename && eventType === 'change') {
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer);
|
||||
}
|
||||
debounceTimer = setTimeout(() => {
|
||||
if (!closed) {
|
||||
callback({
|
||||
type: 'change',
|
||||
timestamp: new Date()
|
||||
});
|
||||
}
|
||||
}, debounceMs);
|
||||
}
|
||||
});
|
||||
|
||||
watcher.on('error', (error) => {
|
||||
if (!closed) {
|
||||
callback({
|
||||
type: 'error',
|
||||
timestamp: new Date(),
|
||||
error
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
unsubscribe: () => {
|
||||
closed = true;
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer);
|
||||
}
|
||||
watcher.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrich tasks with complexity data from the complexity report
|
||||
* Private helper method called by loadTasks()
|
||||
|
||||
@@ -37,7 +37,8 @@ function createMockStorage(tasks: Task[] = []): IStorage {
|
||||
getStats: vi.fn(),
|
||||
getStorageType: vi.fn().mockReturnValue('file'),
|
||||
getCurrentBriefName: vi.fn().mockReturnValue(null),
|
||||
getTagsWithStats: vi.fn()
|
||||
getTagsWithStats: vi.fn(),
|
||||
watch: vi.fn().mockResolvedValue({ unsubscribe: vi.fn() })
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,11 @@ import type {
|
||||
} from './services/preflight-checker.service.js';
|
||||
import type { TaskValidationResult } from './services/task-loader.service.js';
|
||||
import type { ExpandTaskResult } from '../integration/services/task-expansion.service.js';
|
||||
import type {
|
||||
WatchEvent,
|
||||
WatchOptions,
|
||||
WatchSubscription
|
||||
} from '../../common/interfaces/storage.interface.js';
|
||||
|
||||
/**
|
||||
* Tasks Domain - Unified API for all task operations
|
||||
@@ -389,6 +394,25 @@ export class TasksDomain {
|
||||
return this.taskService.getStorageType();
|
||||
}
|
||||
|
||||
// ========== Watch ==========
|
||||
|
||||
/**
|
||||
* Watch for changes to tasks
|
||||
* For file storage: uses fs.watch on tasks.json with debouncing
|
||||
* For API storage: uses Supabase Realtime subscriptions
|
||||
*
|
||||
* @param callback - Function called when tasks change
|
||||
* @param options - Watch options (debounce, tag)
|
||||
* @returns Subscription handle with unsubscribe method
|
||||
*/
|
||||
async watch(
|
||||
callback: (event: WatchEvent) => void,
|
||||
options?: WatchOptions
|
||||
): Promise<WatchSubscription> {
|
||||
const storage = this.taskService.getStorage();
|
||||
return storage.watch(callback, options);
|
||||
}
|
||||
|
||||
// ========== Task File Generation ==========
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* @fileoverview Time utilities for formatting relative timestamps
|
||||
* @fileoverview Time utilities for formatting timestamps
|
||||
* Shared across CLI, MCP, extension, and other interfaces
|
||||
*/
|
||||
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { format, formatDistanceToNow } from 'date-fns';
|
||||
|
||||
/**
|
||||
* Format a date as relative time from now (e.g., "2 hours ago", "3 days ago")
|
||||
@@ -16,3 +16,12 @@ export function formatRelativeTime(date: string | Date): string {
|
||||
// Use date-fns for robust formatting with proper edge case handling
|
||||
return formatDistanceToNow(dateObj, { addSuffix: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date as a time string (e.g., "02:30:45 PM")
|
||||
* @param date - Date object to format
|
||||
* @returns Formatted time string in 12-hour format with seconds
|
||||
*/
|
||||
export function formatTime(date: Date): string {
|
||||
return format(date, 'hh:mm:ss a');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user