Files
stack/packages/skills/tasks/scripts/tasks.js
2026-01-29 21:16:54 -06:00

430 lines
12 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* Mosaic Tasks CLI
*
* Command-line interface for Mosaic Stack's Tasks API
* Supports create, list, get, update, and delete operations
*/
const https = require('https');
const http = require('http');
const { URL } = require('url');
// Configuration from environment
const MOSAIC_API_URL = process.env.MOSAIC_API_URL || 'http://localhost:3001';
const MOSAIC_WORKSPACE_ID = process.env.MOSAIC_WORKSPACE_ID;
const MOSAIC_API_TOKEN = process.env.MOSAIC_API_TOKEN;
// Valid enum values
const TASK_STATUSES = ['NOT_STARTED', 'IN_PROGRESS', 'PAUSED', 'COMPLETED', 'ARCHIVED'];
const TASK_PRIORITIES = ['LOW', 'MEDIUM', 'HIGH'];
/**
* Make HTTP request to Mosaic API
*/
function apiRequest(method, path, body = null) {
return new Promise((resolve, reject) => {
if (!MOSAIC_API_TOKEN) {
reject(new Error('MOSAIC_API_TOKEN environment variable is required'));
return;
}
if (!MOSAIC_WORKSPACE_ID) {
reject(new Error('MOSAIC_WORKSPACE_ID environment variable is required'));
return;
}
const url = new URL(path, MOSAIC_API_URL);
const isHttps = url.protocol === 'https:';
const client = isHttps ? https : http;
const options = {
method,
hostname: url.hostname,
port: url.port || (isHttps ? 443 : 80),
path: url.pathname + url.search,
headers: {
'Authorization': `Bearer ${MOSAIC_API_TOKEN}`,
'X-Workspace-ID': MOSAIC_WORKSPACE_ID,
'Content-Type': 'application/json',
},
};
const req = client.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const parsed = data ? JSON.parse(data) : null;
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(parsed);
} else {
const error = new Error(
parsed?.message || `HTTP ${res.statusCode}: ${res.statusMessage}`
);
error.statusCode = res.statusCode;
error.response = parsed;
reject(error);
}
} catch (err) {
reject(new Error(`Failed to parse response: ${err.message}`));
}
});
});
req.on('error', (err) => {
reject(new Error(`Request failed: ${err.message}`));
});
if (body) {
req.write(JSON.stringify(body));
}
req.end();
});
}
/**
* Parse command-line arguments
*/
function parseArgs(argv) {
const args = {};
const positional = [];
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg.startsWith('--')) {
const key = arg.slice(2);
const value = argv[i + 1];
if (value && !value.startsWith('--')) {
args[key] = value;
i++; // Skip next arg since we consumed it
} else {
args[key] = true; // Flag without value
}
} else {
positional.push(arg);
}
}
return { args, positional };
}
/**
* Build query string from filters
*/
function buildQueryString(filters) {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(filters)) {
if (value !== undefined && value !== null) {
params.append(key, value);
}
}
const qs = params.toString();
return qs ? `?${qs}` : '';
}
/**
* Create a new task
*/
async function createTask(args) {
const body = {
title: args.title,
};
if (args.description) body.description = args.description;
if (args.status) {
if (!TASK_STATUSES.includes(args.status)) {
throw new Error(`Invalid status. Must be one of: ${TASK_STATUSES.join(', ')}`);
}
body.status = args.status;
}
if (args.priority) {
if (!TASK_PRIORITIES.includes(args.priority)) {
throw new Error(`Invalid priority. Must be one of: ${TASK_PRIORITIES.join(', ')}`);
}
body.priority = args.priority;
}
if (args.due) body.dueDate = args.due;
if (args['assignee-id']) body.assigneeId = args['assignee-id'];
if (args['project-id']) body.projectId = args['project-id'];
if (args['parent-id']) body.parentId = args['parent-id'];
if (args['sort-order']) body.sortOrder = parseInt(args['sort-order'], 10);
if (!body.title) {
throw new Error('--title is required');
}
const result = await apiRequest('POST', '/api/tasks', body);
console.log(JSON.stringify(result, null, 2));
return result;
}
/**
* List tasks with optional filters
*/
async function listTasks(args) {
const filters = {};
if (args.status) {
if (!TASK_STATUSES.includes(args.status)) {
throw new Error(`Invalid status. Must be one of: ${TASK_STATUSES.join(', ')}`);
}
filters.status = args.status;
}
if (args.priority) {
if (!TASK_PRIORITIES.includes(args.priority)) {
throw new Error(`Invalid priority. Must be one of: ${TASK_PRIORITIES.join(', ')}`);
}
filters.priority = args.priority;
}
if (args['assignee-id']) filters.assigneeId = args['assignee-id'];
if (args['project-id']) filters.projectId = args['project-id'];
if (args['parent-id']) filters.parentId = args['parent-id'];
if (args.page) filters.page = args.page;
if (args.limit) filters.limit = args.limit;
// Handle overdue filter
if (args.overdue) {
filters.dueDateTo = new Date().toISOString();
// Exclude completed and archived
if (!filters.status) {
console.error('Note: Overdue filter shows tasks with due date < now, excluding COMPLETED and ARCHIVED');
}
}
if (args['due-from']) filters.dueDateFrom = args['due-from'];
if (args['due-to']) filters.dueDateTo = args['due-to'];
const query = buildQueryString(filters);
const result = await apiRequest('GET', `/api/tasks${query}`);
// Filter out completed/archived for overdue query
if (args.overdue && result.data) {
result.data = result.data.filter(
task => task.status !== 'COMPLETED' && task.status !== 'ARCHIVED'
);
if (result.pagination) {
result.pagination.total = result.data.length;
}
}
console.log(JSON.stringify(result, null, 2));
return result;
}
/**
* Get a single task by ID
*/
async function getTask(taskId) {
if (!taskId) {
throw new Error('Task ID is required');
}
const result = await apiRequest('GET', `/api/tasks/${taskId}`);
console.log(JSON.stringify(result, null, 2));
return result;
}
/**
* Update a task
*/
async function updateTask(taskId, args) {
if (!taskId) {
throw new Error('Task ID is required');
}
const body = {};
if (args.title) body.title = args.title;
if (args.description !== undefined) body.description = args.description;
if (args.status) {
if (!TASK_STATUSES.includes(args.status)) {
throw new Error(`Invalid status. Must be one of: ${TASK_STATUSES.join(', ')}`);
}
body.status = args.status;
}
if (args.priority) {
if (!TASK_PRIORITIES.includes(args.priority)) {
throw new Error(`Invalid priority. Must be one of: ${TASK_PRIORITIES.join(', ')}`);
}
body.priority = args.priority;
}
if (args.due !== undefined) body.dueDate = args.due || null;
if (args['assignee-id'] !== undefined) body.assigneeId = args['assignee-id'] || null;
if (args['project-id'] !== undefined) body.projectId = args['project-id'] || null;
if (args['parent-id'] !== undefined) body.parentId = args['parent-id'] || null;
if (args['sort-order']) body.sortOrder = parseInt(args['sort-order'], 10);
if (Object.keys(body).length === 0) {
throw new Error('At least one field to update is required');
}
const result = await apiRequest('PATCH', `/api/tasks/${taskId}`, body);
console.log(JSON.stringify(result, null, 2));
return result;
}
/**
* Delete a task
*/
async function deleteTask(taskId) {
if (!taskId) {
throw new Error('Task ID is required');
}
const result = await apiRequest('DELETE', `/api/tasks/${taskId}`);
console.log(JSON.stringify(result || { success: true }, null, 2));
return result;
}
/**
* Show usage help
*/
function showHelp() {
console.log(`
Mosaic Tasks CLI
Usage:
node tasks.js <command> [options]
Commands:
create Create a new task
list List tasks with optional filters
get <id> Get a specific task by ID
update <id> Update a task
delete <id> Delete a task
help Show this help message
Create Options:
--title <text> Task title (required)
--description <text> Task description
--status <status> Status: ${TASK_STATUSES.join(', ')}
--priority <priority> Priority: ${TASK_PRIORITIES.join(', ')}
--due <iso-date> Due date (ISO 8601 format)
--assignee-id <uuid> Assign to user ID
--project-id <uuid> Link to project ID
--parent-id <uuid> Parent task ID (for subtasks)
--sort-order <number> Sort order (integer)
List Options:
--status <status> Filter by status
--priority <priority> Filter by priority
--assignee-id <uuid> Filter by assignee
--project-id <uuid> Filter by project
--parent-id <uuid> Filter by parent task
--due-from <iso-date> Filter tasks due after this date
--due-to <iso-date> Filter tasks due before this date
--overdue Show only overdue tasks
--page <number> Page number (default: 1)
--limit <number> Results per page (default: 50, max: 100)
Update Options:
--title <text> Update title
--description <text> Update description
--status <status> Update status
--priority <priority> Update priority
--due <iso-date> Update due date (empty string to clear)
--assignee-id <uuid> Update assignee (empty string to clear)
--project-id <uuid> Update project (empty string to clear)
--parent-id <uuid> Update parent (empty string to clear)
--sort-order <number> Update sort order
Environment Variables:
MOSAIC_API_URL API base URL (default: http://localhost:3001)
MOSAIC_WORKSPACE_ID Workspace ID (required)
MOSAIC_API_TOKEN Authentication token (required)
Examples:
# Create a task
node tasks.js create --title "Review PR #123" --priority HIGH
# List high priority tasks
node tasks.js list --priority HIGH
# List overdue tasks
node tasks.js list --overdue
# Get task details
node tasks.js get abc-123-def-456
# Mark task as completed
node tasks.js update abc-123-def-456 --status COMPLETED
# Delete a task
node tasks.js delete abc-123-def-456
`);
}
/**
* Main entry point
*/
async function main() {
const { args, positional } = parseArgs(process.argv.slice(2));
const command = positional[0];
try {
switch (command) {
case 'create':
await createTask(args);
break;
case 'list':
await listTasks(args);
break;
case 'get':
await getTask(positional[1]);
break;
case 'update':
await updateTask(positional[1], args);
break;
case 'delete':
await deleteTask(positional[1]);
break;
case 'help':
case undefined:
showHelp();
break;
default:
console.error(`Unknown command: ${command}`);
console.error('Run "node tasks.js help" for usage information');
process.exit(1);
}
} catch (error) {
console.error('Error:', error.message);
if (error.statusCode === 401) {
console.error('\nAuthentication failed. Check your MOSAIC_API_TOKEN.');
} else if (error.statusCode === 403) {
console.error('\nPermission denied. You may need higher workspace permissions.');
} else if (error.statusCode === 404) {
console.error('\nTask not found. Check the task ID.');
} else if (error.response) {
console.error('\nAPI Response:', JSON.stringify(error.response, null, 2));
}
process.exit(1);
}
}
// Run if executed directly
if (require.main === module) {
main();
}
module.exports = { createTask, listTasks, getTask, updateTask, deleteTask };