#!/usr/bin/env node /** * Mosaic Calendar CLI * Interacts with Mosaic Stack's Events API for calendar management */ import https from 'https'; import http from 'http'; import { URL } from 'url'; // Configuration const API_URL = process.env.MOSAIC_API_URL || 'http://localhost:3001'; const WORKSPACE_ID = process.env.MOSAIC_WORKSPACE_ID; const API_TOKEN = process.env.MOSAIC_API_TOKEN; /** * Make HTTP request to Mosaic API */ function apiRequest(method, path, body = null) { return new Promise((resolve, reject) => { const url = new URL(path, 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: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${API_TOKEN}`, 'X-Workspace-ID': WORKSPACE_ID } }; const req = client.request(options, (res) => { let data = ''; res.on('data', (chunk) => data += chunk); res.on('end', () => { try { const parsed = data ? JSON.parse(data) : {}; if (res.statusCode >= 200 && res.statusCode < 300) { resolve(parsed); } else { // Provide helpful error messages based on status code let errorMsg = `HTTP ${res.statusCode}: ${parsed.message || data}`; if (res.statusCode === 401) { errorMsg += '\n → Check MOSAIC_API_TOKEN is valid'; } else if (res.statusCode === 403) { errorMsg += '\n → Verify workspace permissions and MOSAIC_WORKSPACE_ID'; } else if (res.statusCode === 404) { errorMsg += '\n → Resource not found. Check the event ID'; } else if (res.statusCode === 400) { errorMsg += '\n → Invalid request. Check date formats and required fields'; } reject(new Error(errorMsg)); } } catch (err) { reject(new Error(`Failed to parse response: ${data}`)); } }); }); req.on('error', reject); if (body) { req.write(JSON.stringify(body)); } req.end(); }); } /** * Parse command-line arguments */ function parseArgs(args) { const parsed = { _: [] }; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg.startsWith('--')) { const key = arg.slice(2); const value = args[i + 1]; if (value && !value.startsWith('--')) { parsed[key] = value; i++; } else { parsed[key] = true; } } else { parsed._.push(arg); } } return parsed; } /** * Create a new event */ async function createEvent(args) { if (!args.title || !args.start) { throw new Error('Required: --title and --start (ISO 8601 date)'); } const body = { title: args.title, startTime: args.start, }; if (args.end) body.endTime = args.end; if (args.description) body.description = args.description; if (args.location) body.location = args.location; if (args['all-day'] !== undefined) body.allDay = args['all-day'] === 'true'; if (args['project-id']) body.projectId = args['project-id']; if (args.metadata) body.metadata = JSON.parse(args.metadata); const result = await apiRequest('POST', '/events', body); return result; } /** * List events with optional filters */ async function listEvents(args) { const params = new URLSearchParams(); if (args.from) params.append('startFrom', args.from); if (args.to) params.append('startTo', args.to); if (args['project-id']) params.append('projectId', args['project-id']); if (args['all-day'] !== undefined) params.append('allDay', args['all-day']); if (args.page) params.append('page', args.page); if (args.limit) params.append('limit', args.limit); const query = params.toString(); const path = `/events${query ? '?' + query : ''}`; const result = await apiRequest('GET', path); return result; } /** * Get a specific event by ID */ async function getEvent(eventId) { if (!eventId) { throw new Error('Event ID required'); } const result = await apiRequest('GET', `/events/${eventId}`); return result; } /** * Update an event */ async function updateEvent(eventId, args) { if (!eventId) { throw new Error('Event ID required'); } const body = {}; if (args.title) body.title = args.title; if (args.start) body.startTime = args.start; if (args.end) body.endTime = args.end; if (args.description) body.description = args.description; if (args.location) body.location = args.location; if (args['all-day'] !== undefined) body.allDay = args['all-day'] === 'true'; if (args['project-id']) body.projectId = args['project-id']; if (args.metadata) body.metadata = JSON.parse(args.metadata); const result = await apiRequest('PATCH', `/events/${eventId}`, body); return result; } /** * Delete an event */ async function deleteEvent(eventId) { if (!eventId) { throw new Error('Event ID required'); } const result = await apiRequest('DELETE', `/events/${eventId}`); return result; } /** * Main CLI handler */ async function main() { const args = parseArgs(process.argv.slice(2)); const command = args._[0]; // Validate environment if (!WORKSPACE_ID) { console.error('Error: MOSAIC_WORKSPACE_ID environment variable required'); process.exit(1); } if (!API_TOKEN) { console.error('Error: MOSAIC_API_TOKEN environment variable required'); process.exit(1); } try { let result; switch (command) { case 'create': result = await createEvent(args); console.log(JSON.stringify(result, null, 2)); break; case 'list': result = await listEvents(args); console.log(JSON.stringify(result, null, 2)); break; case 'get': result = await getEvent(args._[1]); console.log(JSON.stringify(result, null, 2)); break; case 'update': result = await updateEvent(args._[1], args); console.log(JSON.stringify(result, null, 2)); break; case 'delete': result = await deleteEvent(args._[1]); console.log(JSON.stringify(result, null, 2)); break; case 'help': default: console.log(` Mosaic Calendar CLI Usage: node calendar.js [options] Commands: create Create a new event list List events with optional filters get Get a specific event by ID update Update an event delete Delete an event Create Options: --title Event title (required) --start Start time in ISO 8601 format (required) --end End time in ISO 8601 format --description Event description --location Event location --all-day All-day event (true/false) --project-id Link to project UUID --metadata JSON metadata object List Options: --from Start date filter (ISO 8601) --to End date filter (ISO 8601) --project-id Filter by project UUID --all-day Filter all-day events (true/false) --page Page number (default: 1) --limit Results per page (default: 50, max: 100) Update Options: Same as create options (all optional) Examples: node calendar.js create --title "Meeting" --start "2024-01-30T15:00:00Z" node calendar.js list --from "2024-01-30" --to "2024-02-05" node calendar.js get abc-123-def node calendar.js update abc-123-def --title "Updated Meeting" node calendar.js delete abc-123-def Environment: MOSAIC_API_URL API base URL (default: http://localhost:3001) MOSAIC_WORKSPACE_ID Workspace UUID (required) MOSAIC_API_TOKEN Authentication token (required) `); break; } } catch (error) { console.error('Error:', error.message); process.exit(1); } } // Run if called directly main(); export { createEvent, listEvents, getEvent, updateEvent, deleteEvent, apiRequest };