Signed-off-by: OpenClaw Bot <ai-agent@topdoglabs.com>
This commit is contained in:
parent
742bd61e7b
commit
b1ee0da1b8
29
app/api/activity/route.ts
Normal file
29
app/api/activity/route.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { fetchGanttApi } from "@/lib/data/gantt-api";
|
||||
|
||||
interface ActivityPayload {
|
||||
tasks?: unknown[];
|
||||
projects?: unknown[];
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const payload = await fetchGanttApi<ActivityPayload>("/tasks?scope=all");
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
tasks: Array.isArray(payload.tasks) ? payload.tasks : [],
|
||||
projects: Array.isArray(payload.projects) ? payload.projects : [],
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to fetch activity data";
|
||||
console.error("[api/activity] Request failed", error);
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { supabaseClient } from "@/lib/supabase/client";
|
||||
import type { Database, ActivityItem } from "@/lib/supabase/database.types";
|
||||
|
||||
type Task = Database['public']['Tables']['tasks']['Row'];
|
||||
type Project = Database['public']['Tables']['projects']['Row'];
|
||||
type Project = Pick<Database["public"]["Tables"]["projects"]["Row"], "id" | "name" | "color">;
|
||||
type TaskStatus = Database["public"]["Tables"]["tasks"]["Row"]["status"];
|
||||
|
||||
export type ActivityFilterType = 'all' | 'task_created' | 'task_completed' | 'task_updated' | 'comment_added' | 'task_assigned';
|
||||
export type ActivityFilterType =
|
||||
| "all"
|
||||
| "task_created"
|
||||
| "task_completed"
|
||||
| "task_updated"
|
||||
| "comment_added"
|
||||
| "task_assigned";
|
||||
|
||||
interface UseActivityFeedOptions {
|
||||
limit?: number;
|
||||
@ -15,10 +20,8 @@ interface UseActivityFeedOptions {
|
||||
filterType?: ActivityFilterType;
|
||||
}
|
||||
|
||||
let canUseLeanTasksSelect = true;
|
||||
|
||||
interface UserDirectoryEntry {
|
||||
name: string;
|
||||
name?: string;
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
@ -32,10 +35,35 @@ interface NormalizedTaskComment {
|
||||
replies: NormalizedTaskComment[];
|
||||
}
|
||||
|
||||
interface UserDirectoryRow {
|
||||
interface ActivityApiTask {
|
||||
id: string;
|
||||
name: string | null;
|
||||
avatar_url: string | null;
|
||||
title: string;
|
||||
status: TaskStatus;
|
||||
projectId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
createdById?: string;
|
||||
createdByName?: string;
|
||||
createdByAvatarUrl?: string;
|
||||
updatedById?: string;
|
||||
updatedByName?: string;
|
||||
updatedByAvatarUrl?: string;
|
||||
assigneeId?: string;
|
||||
assigneeName?: string;
|
||||
assigneeAvatarUrl?: string;
|
||||
comments?: unknown;
|
||||
}
|
||||
|
||||
interface ActivityApiProject {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface ActivityApiResponse {
|
||||
tasks?: ActivityApiTask[];
|
||||
projects?: ActivityApiProject[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
function normalizeUserId(value: string | null | undefined): string | null {
|
||||
@ -50,9 +78,9 @@ function resolveUserName(
|
||||
fallbackName?: string | null
|
||||
): string {
|
||||
const normalizedUserId = normalizeUserId(userId);
|
||||
if (normalizedUserId && users[normalizedUserId]?.name) return users[normalizedUserId].name;
|
||||
if (normalizedUserId && users[normalizedUserId]?.name) return users[normalizedUserId].name as string;
|
||||
if (fallbackName && fallbackName.trim().length > 0) return fallbackName;
|
||||
if (!userId) return 'Unknown';
|
||||
if (!userId) return "Unknown";
|
||||
|
||||
const normalizedId = userId.trim().toLowerCase();
|
||||
if (normalizedId === "assistant") return "Assistant";
|
||||
@ -126,26 +154,36 @@ function flattenTaskComments(
|
||||
return flattened;
|
||||
}
|
||||
|
||||
function buildUserDirectory(rows: Array<{ id: string; name: string | null; avatar_url: string | null }>): UserDirectory {
|
||||
function buildUserDirectoryFromTasks(tasks: ActivityApiTask[]): UserDirectory {
|
||||
const directory: UserDirectory = {};
|
||||
|
||||
rows.forEach((row) => {
|
||||
const rawId = toNonEmptyString(row.id);
|
||||
const id = normalizeUserId(rawId);
|
||||
const name = toNonEmptyString(row.name);
|
||||
if (!id || !name) return;
|
||||
const upsert = (userId?: string, name?: string, avatarUrl?: string) => {
|
||||
const normalizedUserId = normalizeUserId(userId);
|
||||
if (!normalizedUserId) return;
|
||||
|
||||
directory[id] = {
|
||||
name,
|
||||
avatarUrl: toNonEmptyString(row.avatar_url) ?? undefined,
|
||||
const existing = directory[normalizedUserId] ?? {};
|
||||
const nextName = toNonEmptyString(name) ?? existing.name;
|
||||
const nextAvatarUrl = toNonEmptyString(avatarUrl) ?? existing.avatarUrl;
|
||||
|
||||
if (!nextName && !nextAvatarUrl) return;
|
||||
|
||||
directory[normalizedUserId] = {
|
||||
...(nextName ? { name: nextName } : {}),
|
||||
...(nextAvatarUrl ? { avatarUrl: nextAvatarUrl } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
tasks.forEach((task) => {
|
||||
upsert(task.createdById, task.createdByName, task.createdByAvatarUrl);
|
||||
upsert(task.updatedById, task.updatedByName, task.updatedByAvatarUrl);
|
||||
upsert(task.assigneeId, task.assigneeName, task.assigneeAvatarUrl);
|
||||
});
|
||||
|
||||
return directory;
|
||||
}
|
||||
|
||||
export function useActivityFeed(options: UseActivityFeedOptions = {}) {
|
||||
const { limit = 50, projectId, filterType = 'all' } = options;
|
||||
const { limit = 50, projectId, filterType = "all" } = options;
|
||||
const isDebug = process.env.NODE_ENV !== "production";
|
||||
|
||||
const [activities, setActivities] = useState<ActivityItem[]>([]);
|
||||
@ -153,119 +191,80 @@ export function useActivityFeed(options: UseActivityFeedOptions = {}) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchProjects = useCallback(async () => {
|
||||
try {
|
||||
const { data, error } = await supabaseClient
|
||||
.from('projects')
|
||||
.select('id, name, color')
|
||||
.order('name');
|
||||
|
||||
if (error) throw error;
|
||||
setProjects(data || []);
|
||||
} catch (err) {
|
||||
console.error('Error fetching projects:', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchActivities = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
let userRows: UserDirectoryRow[] = [];
|
||||
try {
|
||||
const response = await fetch("/api/users/directory", { cache: "no-store" });
|
||||
if (response.ok) {
|
||||
const payload = (await response.json()) as { users?: UserDirectoryRow[] };
|
||||
userRows = Array.isArray(payload.users) ? payload.users : [];
|
||||
} else {
|
||||
if (isDebug) {
|
||||
console.warn("[activity-feed] users directory API returned non-OK status", { status: response.status });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (isDebug) {
|
||||
console.warn("[activity-feed] users directory API request failed, falling back to client query", error);
|
||||
}
|
||||
const response = await fetch("/api/activity", { cache: "no-store" });
|
||||
const payload = (await response.json().catch(() => null)) as ActivityApiResponse | null;
|
||||
|
||||
if (!response.ok) {
|
||||
const message =
|
||||
payload?.error ||
|
||||
`Activity API request failed with status ${response.status}`;
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
if (userRows.length === 0) {
|
||||
const { data: fallbackUserRows, error: usersError } = await supabaseClient
|
||||
.from("users")
|
||||
.select("id, name, avatar_url");
|
||||
if (usersError) throw usersError;
|
||||
userRows = (fallbackUserRows || []) as UserDirectoryRow[];
|
||||
}
|
||||
const apiTasks = Array.isArray(payload?.tasks) ? payload.tasks : [];
|
||||
const apiProjects = (Array.isArray(payload?.projects) ? payload.projects : []).filter(
|
||||
(project) =>
|
||||
typeof project?.id === "string" &&
|
||||
typeof project?.name === "string" &&
|
||||
typeof project?.color === "string"
|
||||
);
|
||||
|
||||
const userDirectory = buildUserDirectory(userRows);
|
||||
const visibleProjects = apiProjects.map((project) => ({
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
color: project.color,
|
||||
}));
|
||||
setProjects(visibleProjects);
|
||||
|
||||
const userDirectory = buildUserDirectoryFromTasks(apiTasks);
|
||||
if (isDebug) {
|
||||
console.log("[activity-feed] user directory loaded", {
|
||||
console.log("[activity-feed] user directory derived from tasks", {
|
||||
userCount: Object.keys(userDirectory).length,
|
||||
sampleUserIds: Object.keys(userDirectory).slice(0, 5),
|
||||
});
|
||||
}
|
||||
|
||||
const runTasksQuery = async (selectClause: string) => {
|
||||
let query = supabaseClient
|
||||
.from('tasks')
|
||||
.select(selectClause)
|
||||
.order('updated_at', { ascending: false })
|
||||
.limit(limit);
|
||||
const projectById = new Map(visibleProjects.map((project) => [project.id, project]));
|
||||
|
||||
if (projectId) {
|
||||
query = query.eq('project_id', projectId);
|
||||
const tasks = apiTasks
|
||||
.filter((task) => (projectId ? task.projectId === projectId : true))
|
||||
.map((task) => ({
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
status: task.status,
|
||||
project_id: task.projectId,
|
||||
created_at: task.createdAt,
|
||||
updated_at: task.updatedAt,
|
||||
created_by_id: task.createdById ?? null,
|
||||
created_by_name: task.createdByName ?? null,
|
||||
updated_by_id: task.updatedById ?? null,
|
||||
updated_by_name: task.updatedByName ?? null,
|
||||
assignee_id: task.assigneeId ?? null,
|
||||
assignee_name: task.assigneeName ?? null,
|
||||
comments: task.comments,
|
||||
projects: projectById.get(task.projectId)
|
||||
? {
|
||||
name: projectById.get(task.projectId)?.name,
|
||||
color: projectById.get(task.projectId)?.color,
|
||||
}
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
let tasks: unknown[] | null = null;
|
||||
let tasksError: unknown = null;
|
||||
|
||||
if (canUseLeanTasksSelect) {
|
||||
const leanResult = await runTasksQuery(`
|
||||
id,
|
||||
title,
|
||||
status,
|
||||
project_id,
|
||||
created_at,
|
||||
updated_at,
|
||||
created_by_id,
|
||||
created_by_name,
|
||||
updated_by_id,
|
||||
updated_by_name,
|
||||
assignee_id,
|
||||
assignee_name,
|
||||
comments,
|
||||
projects:project_id (name, color)
|
||||
`);
|
||||
tasks = leanResult.data;
|
||||
tasksError = leanResult.error;
|
||||
: null,
|
||||
assignee: task.assigneeId
|
||||
? {
|
||||
id: task.assigneeId,
|
||||
name: task.assigneeName ?? null,
|
||||
}
|
||||
: null,
|
||||
}));
|
||||
|
||||
if (tasksError || !canUseLeanTasksSelect) {
|
||||
if (tasksError) canUseLeanTasksSelect = false;
|
||||
const fallback = await runTasksQuery(`
|
||||
*,
|
||||
projects:project_id (*),
|
||||
assignee:assignee_id (id, name)
|
||||
`);
|
||||
tasks = fallback.data;
|
||||
tasksError = fallback.error;
|
||||
}
|
||||
|
||||
if (tasksError) throw tasksError;
|
||||
|
||||
// Convert tasks to activity items
|
||||
const activityItems: ActivityItem[] = [];
|
||||
|
||||
const typedTasks = (tasks || []) as Array<Task & {
|
||||
projects: Pick<Project, "name" | "color"> | null;
|
||||
assignee?: { id?: string; name?: string | null } | null;
|
||||
}>;
|
||||
|
||||
const globalTaskUserNameById: Record<string, string> = {};
|
||||
typedTasks.forEach((task) => {
|
||||
tasks.forEach((task) => {
|
||||
const createdById = normalizeUserId(task.created_by_id);
|
||||
const updatedById = normalizeUserId(task.updated_by_id);
|
||||
const assigneeId = normalizeUserId(task.assignee_id);
|
||||
@ -277,7 +276,7 @@ export function useActivityFeed(options: UseActivityFeedOptions = {}) {
|
||||
}
|
||||
});
|
||||
|
||||
typedTasks.forEach((task) => {
|
||||
tasks.forEach((task) => {
|
||||
const project = task.projects;
|
||||
const taskUserNameById: Record<string, string> = {};
|
||||
|
||||
@ -312,6 +311,7 @@ export function useActivityFeed(options: UseActivityFeedOptions = {}) {
|
||||
);
|
||||
const createdByAvatarUrl = resolveUserAvatarUrl(task.created_by_id, userDirectory);
|
||||
const updatedByAvatarUrl = resolveUserAvatarUrl(task.updated_by_id, userDirectory);
|
||||
|
||||
if (isDebug && (!task.created_by_id || createdByName === "User")) {
|
||||
console.log("[activity-feed] created_by fallback", {
|
||||
taskId: task.id,
|
||||
@ -321,51 +321,48 @@ export function useActivityFeed(options: UseActivityFeedOptions = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
// Task creation activity
|
||||
if (filterType === 'all' || filterType === 'task_created') {
|
||||
if (filterType === "all" || filterType === "task_created") {
|
||||
activityItems.push({
|
||||
id: `${task.id}-created`,
|
||||
type: 'task_created',
|
||||
type: "task_created",
|
||||
task_id: task.id,
|
||||
task_title: task.title,
|
||||
project_id: task.project_id,
|
||||
project_name: project?.name || 'Unknown Project',
|
||||
project_color: project?.color || '#6B7280',
|
||||
user_id: task.created_by_id || '',
|
||||
project_name: project?.name || "Unknown Project",
|
||||
project_color: project?.color || "#6B7280",
|
||||
user_id: task.created_by_id || "",
|
||||
user_name: createdByName,
|
||||
user_avatar_url: createdByAvatarUrl,
|
||||
timestamp: pickTimestamp(task.created_at, task.updated_at),
|
||||
});
|
||||
}
|
||||
|
||||
// Task completion activity
|
||||
if (task.status === 'done' && (filterType === 'all' || filterType === 'task_completed')) {
|
||||
if (task.status === "done" && (filterType === "all" || filterType === "task_completed")) {
|
||||
activityItems.push({
|
||||
id: `${task.id}-completed`,
|
||||
type: 'task_completed',
|
||||
type: "task_completed",
|
||||
task_id: task.id,
|
||||
task_title: task.title,
|
||||
project_id: task.project_id,
|
||||
project_name: project?.name || 'Unknown Project',
|
||||
project_color: project?.color || '#6B7280',
|
||||
user_id: task.updated_by_id || task.created_by_id || '',
|
||||
project_name: project?.name || "Unknown Project",
|
||||
project_color: project?.color || "#6B7280",
|
||||
user_id: task.updated_by_id || task.created_by_id || "",
|
||||
user_name: updatedByName || createdByName,
|
||||
user_avatar_url: updatedByAvatarUrl || createdByAvatarUrl,
|
||||
timestamp: pickTimestamp(task.updated_at, task.created_at),
|
||||
});
|
||||
}
|
||||
|
||||
// Assignment activity
|
||||
if (task.assignee_id && (filterType === 'all' || filterType === 'task_assigned')) {
|
||||
if (task.assignee_id && (filterType === "all" || filterType === "task_assigned")) {
|
||||
activityItems.push({
|
||||
id: `${task.id}-assigned`,
|
||||
type: 'task_assigned',
|
||||
type: "task_assigned",
|
||||
task_id: task.id,
|
||||
task_title: task.title,
|
||||
project_id: task.project_id,
|
||||
project_name: project?.name || 'Unknown Project',
|
||||
project_color: project?.color || '#6B7280',
|
||||
user_id: task.updated_by_id || task.created_by_id || '',
|
||||
project_name: project?.name || "Unknown Project",
|
||||
project_color: project?.color || "#6B7280",
|
||||
user_id: task.updated_by_id || task.created_by_id || "",
|
||||
user_name: updatedByName || createdByName,
|
||||
user_avatar_url: updatedByAvatarUrl || createdByAvatarUrl,
|
||||
timestamp: pickTimestamp(task.updated_at, task.created_at),
|
||||
@ -373,19 +370,20 @@ export function useActivityFeed(options: UseActivityFeedOptions = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
// Task update activity (if updated after creation and not completed)
|
||||
if (task.updated_at !== task.created_at &&
|
||||
task.status !== 'done' &&
|
||||
(filterType === 'all' || filterType === 'task_updated')) {
|
||||
if (
|
||||
task.updated_at !== task.created_at &&
|
||||
task.status !== "done" &&
|
||||
(filterType === "all" || filterType === "task_updated")
|
||||
) {
|
||||
activityItems.push({
|
||||
id: `${task.id}-updated`,
|
||||
type: 'task_updated',
|
||||
type: "task_updated",
|
||||
task_id: task.id,
|
||||
task_title: task.title,
|
||||
project_id: task.project_id,
|
||||
project_name: project?.name || 'Unknown Project',
|
||||
project_color: project?.color || '#6B7280',
|
||||
user_id: task.updated_by_id || '',
|
||||
project_name: project?.name || "Unknown Project",
|
||||
project_color: project?.color || "#6B7280",
|
||||
user_id: task.updated_by_id || "",
|
||||
user_name: updatedByName,
|
||||
user_avatar_url: updatedByAvatarUrl,
|
||||
timestamp: pickTimestamp(task.updated_at, task.created_at),
|
||||
@ -393,8 +391,7 @@ export function useActivityFeed(options: UseActivityFeedOptions = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
// Comment activities
|
||||
if (task.comments && Array.isArray(task.comments) && (filterType === 'all' || filterType === 'comment_added')) {
|
||||
if (task.comments && Array.isArray(task.comments) && (filterType === "all" || filterType === "comment_added")) {
|
||||
const comments = normalizeTaskComments(task.comments);
|
||||
const flattenedComments = flattenTaskComments(comments);
|
||||
|
||||
@ -409,6 +406,7 @@ export function useActivityFeed(options: UseActivityFeedOptions = {}) {
|
||||
: null) ?? null
|
||||
);
|
||||
const commentAuthorAvatarUrl = resolveUserAvatarUrl(commentAuthorId, userDirectory);
|
||||
|
||||
if (isDebug && (commentAuthorName === "User" || commentAuthorName === "Unknown")) {
|
||||
console.log("[activity-feed] comment author fallback", {
|
||||
taskId: task.id,
|
||||
@ -422,12 +420,12 @@ export function useActivityFeed(options: UseActivityFeedOptions = {}) {
|
||||
|
||||
activityItems.push({
|
||||
id: `${task.id}-comment-${comment.id}-${path}`,
|
||||
type: 'comment_added',
|
||||
type: "comment_added",
|
||||
task_id: task.id,
|
||||
task_title: task.title,
|
||||
project_id: task.project_id,
|
||||
project_name: project?.name || 'Unknown Project',
|
||||
project_color: project?.color || '#6B7280',
|
||||
project_name: project?.name || "Unknown Project",
|
||||
project_color: project?.color || "#6B7280",
|
||||
user_id: commentAuthorId,
|
||||
user_name: commentAuthorName,
|
||||
user_avatar_url: commentAuthorAvatarUrl,
|
||||
@ -438,12 +436,8 @@ export function useActivityFeed(options: UseActivityFeedOptions = {}) {
|
||||
}
|
||||
});
|
||||
|
||||
// Sort by timestamp descending
|
||||
activityItems.sort((a, b) =>
|
||||
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
||||
);
|
||||
activityItems.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
||||
|
||||
// Apply limit after all activities are collected
|
||||
setActivities(activityItems.slice(0, limit));
|
||||
if (isDebug) {
|
||||
console.log("[activity-feed] activities prepared", {
|
||||
@ -454,31 +448,25 @@ export function useActivityFeed(options: UseActivityFeedOptions = {}) {
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching activities:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch activities');
|
||||
console.error("Error fetching activities:", err);
|
||||
setError(err instanceof Error ? err.message : "Failed to fetch activities");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [limit, projectId, filterType]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProjects();
|
||||
fetchActivities();
|
||||
}, [fetchProjects, fetchActivities]);
|
||||
}, [fetchActivities]);
|
||||
|
||||
// Poll for updates every 30 seconds (since realtime WebSocket is disabled)
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
fetchActivities();
|
||||
}, 30000); // 30 seconds
|
||||
}, 30000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [fetchActivities]);
|
||||
|
||||
// Note: Real-time subscription disabled due to WebSocket connection issues
|
||||
// The activity feed uses regular HTTP polling instead (30s interval)
|
||||
// To re-enable realtime, configure Supabase Realtime in your project settings
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
fetchActivities();
|
||||
}, [fetchActivities]);
|
||||
|
||||
@ -64,15 +64,25 @@ export async function fetchGanttApi<T>(endpoint: string): Promise<T> {
|
||||
},
|
||||
});
|
||||
|
||||
const payload = (await response.json().catch(() => null)) as
|
||||
const contentType = response.headers.get("content-type") || "";
|
||||
const isJson = contentType.includes("application/json");
|
||||
const payload = (isJson ? await response.json().catch(() => null) : null) as
|
||||
| { error?: string; message?: string }
|
||||
| null;
|
||||
const nonJsonBody = !isJson ? await response.text().catch(() => "") : "";
|
||||
|
||||
if (!response.ok) {
|
||||
const details = payload?.error || payload?.message || response.statusText;
|
||||
const details =
|
||||
payload?.error ||
|
||||
payload?.message ||
|
||||
(nonJsonBody ? nonJsonBody.replace(/\s+/g, " ").slice(0, 200) : response.statusText);
|
||||
throw new Error(`gantt-board API request failed (${response.status} ${endpoint}): ${details}`);
|
||||
}
|
||||
|
||||
if (!isJson) {
|
||||
throw new Error(`gantt-board API request failed (${response.status} ${endpoint}): expected JSON response`);
|
||||
}
|
||||
|
||||
return payload as T;
|
||||
})();
|
||||
|
||||
|
||||
240
scripts/README.md
Normal file
240
scripts/README.md
Normal file
@ -0,0 +1,240 @@
|
||||
# Mission Control CLI
|
||||
|
||||
A Next.js API-based CLI for managing Mission Control. Follows the same architecture principles as the Gantt Board CLI.
|
||||
|
||||
## Architecture
|
||||
|
||||
The Mission Control CLI follows a **clean API passthrough architecture**:
|
||||
|
||||
1. **API is the source of truth** - All business logic lives in the Mission Control API endpoints
|
||||
2. **CLI is thin** - CLI scripts parse arguments and call API endpoints, no direct database access
|
||||
3. **Shared code via delegation** - Task/project/sprint operations delegate to Gantt Board CLI
|
||||
4. **Mission Control specific features** call Mission Control API directly
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Authenticate
|
||||
./scripts/mc.sh auth login user@example.com password
|
||||
|
||||
# Search across tasks, projects, documents
|
||||
./scripts/mc.sh search "api design"
|
||||
|
||||
# List tasks with due dates
|
||||
./scripts/mc.sh due-dates
|
||||
|
||||
# Task operations (delegates to gantt-board)
|
||||
./scripts/mc.sh task list --status open
|
||||
./scripts/mc.sh task create --title "New task" --project "Mission Control"
|
||||
|
||||
# Project operations (delegates to gantt-board)
|
||||
./scripts/mc.sh project list
|
||||
|
||||
# Sprint operations (delegates to gantt-board)
|
||||
./scripts/mc.sh sprint list --active
|
||||
```
|
||||
|
||||
## Scripts
|
||||
|
||||
### Main CLI
|
||||
|
||||
- **`mc.sh`** - Main entry point for all Mission Control CLI operations
|
||||
|
||||
### Wrapper Scripts (Delegate to Gantt Board)
|
||||
|
||||
- **`task.sh`** - Task operations (delegates to gantt-board/scripts/task.sh)
|
||||
- **`project.sh`** - Project operations (delegates to gantt-board/scripts/project.sh)
|
||||
- **`sprint.sh`** - Sprint operations (delegates to gantt-board/scripts/sprint.sh)
|
||||
|
||||
### Library
|
||||
|
||||
- **`lib/api_client.sh`** - Shared HTTP client for Mission Control API calls
|
||||
|
||||
### Utilities
|
||||
|
||||
- **`update-task-status.js`** - Update task status (delegates to gantt-board)
|
||||
|
||||
## Configuration
|
||||
|
||||
Environment variables:
|
||||
|
||||
```bash
|
||||
# Mission Control API URL
|
||||
export MC_API_URL="http://localhost:3001/api"
|
||||
|
||||
# Path to gantt-board (auto-detected if not set)
|
||||
export GANTT_BOARD_DIR="/path/to/gantt-board"
|
||||
|
||||
# Cookie file for authentication
|
||||
export MC_COOKIE_FILE="$HOME/.config/mission-control/cookies.txt"
|
||||
```
|
||||
|
||||
## Command Reference
|
||||
|
||||
### Authentication
|
||||
|
||||
```bash
|
||||
./mc.sh auth login <email> <password>
|
||||
./mc.sh auth logout
|
||||
./mc.sh auth session
|
||||
```
|
||||
|
||||
### Search
|
||||
|
||||
```bash
|
||||
./mc.sh search "query string"
|
||||
```
|
||||
|
||||
Searches across:
|
||||
- Tasks (title, description)
|
||||
- Projects (name, description)
|
||||
- Sprints (name, goal)
|
||||
- Documents (title, content)
|
||||
|
||||
### Tasks with Due Dates
|
||||
|
||||
```bash
|
||||
./mc.sh due-dates
|
||||
```
|
||||
|
||||
Returns tasks with due dates, ordered by due date.
|
||||
|
||||
### Documents
|
||||
|
||||
```bash
|
||||
./mc.sh documents list
|
||||
./mc.sh documents get <id>
|
||||
```
|
||||
|
||||
### Task Operations (via Gantt Board)
|
||||
|
||||
```bash
|
||||
./mc.sh task list [--status <status>] [--priority <priority>]
|
||||
./mc.sh task get <task-id>
|
||||
./mc.sh task create --title "..." [--description "..."] [--project <name>]
|
||||
./mc.sh task update <task-id> [--status <status>] [--priority <priority>]
|
||||
./mc.sh task delete <task-id>
|
||||
```
|
||||
|
||||
See Gantt Board CLI documentation for full task command reference.
|
||||
|
||||
### Project Operations (via Gantt Board)
|
||||
|
||||
```bash
|
||||
./mc.sh project list
|
||||
./mc.sh project get <project-id-or-name>
|
||||
./mc.sh project create --name "..." [--description "..."]
|
||||
./mc.sh project update <project-id> [--name "..."] [--description "..."]
|
||||
```
|
||||
|
||||
### Sprint Operations (via Gantt Board)
|
||||
|
||||
```bash
|
||||
./mc.sh sprint list [--active]
|
||||
./mc.sh sprint get <sprint-id-or-name>
|
||||
./mc.sh sprint create --name "..." [--goal "..."]
|
||||
./mc.sh sprint close <sprint-id-or-name>
|
||||
```
|
||||
|
||||
## Architecture Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Mission Control CLI │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
|
||||
│ │ mc.sh │ │ task.sh │ │ project.sh │ │
|
||||
│ │ (main) │ │ (wrapper) │ │ (wrapper) │ │
|
||||
│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
|
||||
│ │ │ │ │
|
||||
│ │ └──────────┬──────────┘ │
|
||||
│ │ │ │
|
||||
│ │ ┌──────────────▼──────────────┐ │
|
||||
│ │ │ Gantt Board CLI │ │
|
||||
│ │ │ (task.sh, project.sh) │ │
|
||||
│ │ └──────────────┬──────────────┘ │
|
||||
│ │ │ │
|
||||
│ │ ▼ │
|
||||
│ │ ┌─────────────────────┐ │
|
||||
│ │ │ Gantt Board API │ │
|
||||
│ │ └─────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────────────────────────┐ │
|
||||
│ │ Mission Control API │ │
|
||||
│ │ /api/search, /api/tasks/with-due-dates, etc │ │
|
||||
│ └──────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Run the CLI contract test:
|
||||
|
||||
```bash
|
||||
npm run test:cli-contract
|
||||
```
|
||||
|
||||
This verifies:
|
||||
1. Mission Control CLI wrappers delegate to gantt-board CLI
|
||||
2. No direct database references in scripts/
|
||||
3. update-task-status.js delegates to gantt-board task.sh
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **No Direct Database Access** - CLI scripts never call the database directly
|
||||
2. **API Passthrough** - All operations go through API endpoints
|
||||
3. **Shared Functionality** - Common operations (tasks, projects, sprints) use Gantt Board
|
||||
4. **Clean Separation** - Mission Control specific features use Mission Control API
|
||||
|
||||
## Adding New Commands
|
||||
|
||||
To add a new Mission Control specific command:
|
||||
|
||||
1. Create the API endpoint in `app/api/<feature>/route.ts`
|
||||
2. Add the command handler in `scripts/mc.sh`
|
||||
3. Use `lib/api_client.sh` functions for HTTP calls
|
||||
4. Document the command in this README
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
# In mc.sh
|
||||
handle_feature() {
|
||||
mc_get "/feature" | jq .
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "GANTT_BOARD_DIR not set"
|
||||
|
||||
Set the environment variable:
|
||||
|
||||
```bash
|
||||
export GANTT_BOARD_DIR=/path/to/gantt-board
|
||||
```
|
||||
|
||||
Or use the auto-detection by placing gantt-board in a standard location:
|
||||
- `../../../gantt-board` (relative to mission-control)
|
||||
- `$HOME/Documents/Projects/OpenClaw/Web/gantt-board`
|
||||
|
||||
### "Not authenticated"
|
||||
|
||||
Login first:
|
||||
|
||||
```bash
|
||||
./scripts/mc.sh auth login user@example.com password
|
||||
```
|
||||
|
||||
### API Connection Errors
|
||||
|
||||
Verify Mission Control is running:
|
||||
|
||||
```bash
|
||||
curl http://localhost:3001/api/auth/session
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Gantt Board CLI](../../gantt-board/scripts/README.md)
|
||||
- [Mission Control API](../app/api/)
|
||||
111
scripts/lib/api_client.sh
Executable file
111
scripts/lib/api_client.sh
Executable file
@ -0,0 +1,111 @@
|
||||
#!/bin/bash
|
||||
# Mission Control API Client Library
|
||||
# Shared HTTP client for Mission Control CLI scripts
|
||||
# Follows the same pattern as gantt-board/scripts/lib/api_client.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration
|
||||
MC_API_URL="${MC_API_URL:-http://localhost:3001/api}"
|
||||
MC_COOKIE_FILE="${MC_COOKIE_FILE:-$HOME/.config/mission-control/cookies.txt}"
|
||||
|
||||
# Ensure cookie directory exists
|
||||
mkdir -p "$(dirname "$MC_COOKIE_FILE")"
|
||||
|
||||
# Make authenticated API call to Mission Control
|
||||
# Usage: mc_api_call <method> <endpoint> [data]
|
||||
mc_api_call() {
|
||||
local method="$1"
|
||||
local endpoint="$2"
|
||||
local data="${3:-}"
|
||||
|
||||
local url="${MC_API_URL}${endpoint}"
|
||||
local curl_opts=(
|
||||
-s
|
||||
-b "$MC_COOKIE_FILE"
|
||||
-c "$MC_COOKIE_FILE"
|
||||
-H "Content-Type: application/json"
|
||||
)
|
||||
|
||||
if [[ -n "$data" ]]; then
|
||||
curl_opts+=(-d "$data")
|
||||
fi
|
||||
|
||||
curl "${curl_opts[@]}" -X "$method" "$url"
|
||||
}
|
||||
|
||||
# GET request helper
|
||||
# Usage: mc_get <endpoint>
|
||||
mc_get() {
|
||||
mc_api_call "GET" "$1"
|
||||
}
|
||||
|
||||
# POST request helper
|
||||
# Usage: mc_post <endpoint> [data]
|
||||
mc_post() {
|
||||
local endpoint="$1"
|
||||
local data="${2:-}"
|
||||
mc_api_call "POST" "$endpoint" "$data"
|
||||
}
|
||||
|
||||
# DELETE request helper
|
||||
# Usage: mc_delete <endpoint>
|
||||
mc_delete() {
|
||||
mc_api_call "DELETE" "$1"
|
||||
}
|
||||
|
||||
# URL encode a string
|
||||
# Usage: url_encode <string>
|
||||
url_encode() {
|
||||
local str="$1"
|
||||
printf '%s' "$str" | jq -sRr @uri
|
||||
}
|
||||
|
||||
# Check if user is authenticated (cookie exists and is valid)
|
||||
mc_is_authenticated() {
|
||||
if [[ ! -f "$MC_COOKIE_FILE" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Try to get session - if it fails, not authenticated
|
||||
local response
|
||||
response=$(mc_get "/auth/session" 2>/dev/null || echo '{"user":null}')
|
||||
|
||||
# Check if we got a valid user back
|
||||
echo "$response" | jq -e '.user != null' >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Login to Mission Control
|
||||
# Usage: mc_login <email> <password>
|
||||
mc_login() {
|
||||
local email="$1"
|
||||
local password="$2"
|
||||
|
||||
local response
|
||||
response=$(mc_post "/auth/login" "$(jq -n --arg email "$email" --arg password "$password" '{email:$email,password:$password}')")
|
||||
|
||||
if echo "$response" | jq -e '.error' >/dev/null 2>&1; then
|
||||
echo "Login failed: $(echo "$response" | jq -r '.error')" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Login successful"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Logout from Mission Control
|
||||
mc_logout() {
|
||||
mc_post "/auth/logout"
|
||||
rm -f "$MC_COOKIE_FILE"
|
||||
echo "Logged out"
|
||||
}
|
||||
|
||||
# Export functions for use in other scripts
|
||||
export -f mc_api_call
|
||||
export -f mc_get
|
||||
export -f mc_post
|
||||
export -f mc_delete
|
||||
export -f url_encode
|
||||
export -f mc_is_authenticated
|
||||
export -f mc_login
|
||||
export -f mc_logout
|
||||
265
scripts/mc.sh
Executable file
265
scripts/mc.sh
Executable file
@ -0,0 +1,265 @@
|
||||
#!/bin/bash
|
||||
# Mission Control CLI - Main Entry Point
|
||||
# Usage: ./mc.sh <command> [args]
|
||||
#
|
||||
# Commands:
|
||||
# auth Authentication (login, logout, session)
|
||||
# task Task operations (delegates to gantt-board)
|
||||
# project Project operations (delegates to gantt-board)
|
||||
# sprint Sprint operations (delegates to gantt-board)
|
||||
# search Search across tasks, projects, documents
|
||||
# document Document management
|
||||
# due-dates Tasks with due dates
|
||||
# dashboard Dashboard data
|
||||
#
|
||||
# Environment:
|
||||
# MC_API_URL Mission Control API URL (default: http://localhost:3001/api)
|
||||
# GANTT_BOARD_DIR Path to gantt-board directory (auto-detected)
|
||||
# MC_COOKIE_FILE Path to cookie file (default: ~/.config/mission-control/cookies.txt)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
export SCRIPT_DIR
|
||||
|
||||
# Source API client library
|
||||
source "$SCRIPT_DIR/lib/api_client.sh"
|
||||
|
||||
# Auto-detect gantt-board directory
|
||||
if [[ -z "${GANTT_BOARD_DIR:-}" ]]; then
|
||||
# Try common locations
|
||||
if [[ -d "$SCRIPT_DIR/../../../gantt-board" ]]; then
|
||||
GANTT_BOARD_DIR="$SCRIPT_DIR/../../../gantt-board"
|
||||
elif [[ -d "$HOME/Documents/Projects/OpenClaw/Web/gantt-board" ]]; then
|
||||
GANTT_BOARD_DIR="$HOME/Documents/Projects/OpenClaw/Web/gantt-board"
|
||||
elif [[ -d "/Users/mattbruce/Documents/Projects/OpenClaw/Web/gantt-board" ]]; then
|
||||
GANTT_BOARD_DIR="/Users/mattbruce/Documents/Projects/OpenClaw/Web/gantt-board"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Show usage
|
||||
usage() {
|
||||
cat << 'EOF'
|
||||
Mission Control CLI
|
||||
|
||||
Usage: ./mc.sh <command> [args]
|
||||
|
||||
Commands:
|
||||
auth <subcommand> Authentication operations
|
||||
login <email> <password> Login to Mission Control
|
||||
logout Logout
|
||||
session Show current session
|
||||
|
||||
task <args> Task operations (delegates to gantt-board)
|
||||
See: ./task.sh --help
|
||||
|
||||
project <args> Project operations (delegates to gantt-board)
|
||||
See: ./project.sh --help
|
||||
|
||||
sprint <args> Sprint operations (delegates to gantt-board)
|
||||
See: ./sprint.sh --help
|
||||
|
||||
search <query> Search across tasks, projects, documents
|
||||
|
||||
due-dates List tasks with due dates
|
||||
|
||||
documents Document management
|
||||
list List all documents
|
||||
get <id> Get document by ID
|
||||
|
||||
dashboard Get dashboard data
|
||||
|
||||
Examples:
|
||||
./mc.sh auth login user@example.com password
|
||||
./mc.sh search "api design"
|
||||
./mc.sh due-dates
|
||||
./mc.sh task list --status open
|
||||
./mc.sh project list
|
||||
|
||||
Environment Variables:
|
||||
MC_API_URL Mission Control API URL (default: http://localhost:3001/api)
|
||||
GANTT_BOARD_DIR Path to gantt-board directory
|
||||
MC_COOKIE_FILE Path to cookie file
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
# Auth commands
|
||||
handle_auth() {
|
||||
local subcmd="${1:-}"
|
||||
|
||||
case "$subcmd" in
|
||||
login)
|
||||
local email="${2:-}"
|
||||
local password="${3:-}"
|
||||
if [[ -z "$email" || -z "$password" ]]; then
|
||||
echo "Usage: ./mc.sh auth login <email> <password>" >&2
|
||||
exit 1
|
||||
fi
|
||||
mc_login "$email" "$password"
|
||||
;;
|
||||
logout)
|
||||
mc_logout
|
||||
;;
|
||||
session)
|
||||
mc_get "/auth/session" | jq .
|
||||
;;
|
||||
*)
|
||||
echo "Unknown auth subcommand: $subcmd" >&2
|
||||
echo "Usage: ./mc.sh auth {login|logout|session}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Search command
|
||||
handle_search() {
|
||||
local query="${1:-}"
|
||||
|
||||
if [[ -z "$query" ]]; then
|
||||
echo "Usage: ./mc.sh search <query>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local encoded_query
|
||||
encoded_query=$(url_encode "$query")
|
||||
mc_get "/search?q=$encoded_query" | jq .
|
||||
}
|
||||
|
||||
# Due dates command
|
||||
handle_due_dates() {
|
||||
mc_get "/tasks/with-due-dates" | jq .
|
||||
}
|
||||
|
||||
# Documents command
|
||||
handle_documents() {
|
||||
local subcmd="${1:-list}"
|
||||
|
||||
case "$subcmd" in
|
||||
list)
|
||||
mc_get "/documents" | jq .
|
||||
;;
|
||||
get)
|
||||
local id="${2:-}"
|
||||
if [[ -z "$id" ]]; then
|
||||
echo "Usage: ./mc.sh documents get <id>" >&2
|
||||
exit 1
|
||||
fi
|
||||
mc_get "/documents?id=$id" | jq .
|
||||
;;
|
||||
*)
|
||||
echo "Unknown documents subcommand: $subcmd" >&2
|
||||
echo "Usage: ./mc.sh documents {list|get <id>}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Task command - delegate to gantt-board
|
||||
handle_task() {
|
||||
if [[ -z "${GANTT_BOARD_DIR:-}" ]]; then
|
||||
echo "Error: GANTT_BOARD_DIR not set and gantt-board not found" >&2
|
||||
echo "Please set GANTT_BOARD_DIR to the path of your gantt-board installation" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$GANTT_BOARD_DIR/scripts/task.sh" ]]; then
|
||||
echo "Error: gantt-board task.sh not found at $GANTT_BOARD_DIR/scripts/task.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Delegate to gantt-board task.sh
|
||||
exec "$GANTT_BOARD_DIR/scripts/task.sh" "$@"
|
||||
}
|
||||
|
||||
# Project command - delegate to gantt-board
|
||||
handle_project() {
|
||||
if [[ -z "${GANTT_BOARD_DIR:-}" ]]; then
|
||||
echo "Error: GANTT_BOARD_DIR not set and gantt-board not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$GANTT_BOARD_DIR/scripts/project.sh" ]]; then
|
||||
echo "Error: gantt-board project.sh not found at $GANTT_BOARD_DIR/scripts/project.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec "$GANTT_BOARD_DIR/scripts/project.sh" "$@"
|
||||
}
|
||||
|
||||
# Sprint command - delegate to gantt-board
|
||||
handle_sprint() {
|
||||
if [[ -z "${GANTT_BOARD_DIR:-}" ]]; then
|
||||
echo "Error: GANTT_BOARD_DIR not set and gantt-board not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$GANTT_BOARD_DIR/scripts/sprint.sh" ]]; then
|
||||
echo "Error: gantt-board sprint.sh not found at $GANTT_BOARD_DIR/scripts/sprint.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec "$GANTT_BOARD_DIR/scripts/sprint.sh" "$@"
|
||||
}
|
||||
|
||||
# Dashboard command
|
||||
handle_dashboard() {
|
||||
# For now, combine multiple API calls to build dashboard view
|
||||
echo "Fetching dashboard data..."
|
||||
echo ""
|
||||
|
||||
echo "=== Tasks with Due Dates ==="
|
||||
handle_due_dates | jq -r '.[] | "\(.due_date) | \(.priority) | \(.title)"' 2>/dev/null || echo "No tasks with due dates"
|
||||
|
||||
echo ""
|
||||
echo "=== Recent Activity ==="
|
||||
# This would need a dedicated API endpoint
|
||||
echo "(Recent activity endpoint not yet implemented)"
|
||||
}
|
||||
|
||||
# Main command dispatcher
|
||||
main() {
|
||||
local cmd="${1:-}"
|
||||
shift || true
|
||||
|
||||
case "$cmd" in
|
||||
auth)
|
||||
handle_auth "$@"
|
||||
;;
|
||||
search)
|
||||
handle_search "$@"
|
||||
;;
|
||||
due-dates)
|
||||
handle_due_dates
|
||||
;;
|
||||
documents|document)
|
||||
handle_documents "$@"
|
||||
;;
|
||||
task)
|
||||
handle_task "$@"
|
||||
;;
|
||||
project)
|
||||
handle_project "$@"
|
||||
;;
|
||||
sprint)
|
||||
handle_sprint "$@"
|
||||
;;
|
||||
dashboard)
|
||||
handle_dashboard
|
||||
;;
|
||||
help|--help|-h)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$cmd" ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
echo "Unknown command: $cmd" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@ -1,9 +1,39 @@
|
||||
#!/bin/bash
|
||||
# Mission Control Project CLI Wrapper
|
||||
# Delegates to gantt-board project.sh for project operations
|
||||
# This maintains the architecture principle: CLI is passthrough to API
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./lib/gantt_cli.sh
|
||||
source "$SCRIPT_DIR/lib/gantt_cli.sh"
|
||||
|
||||
run_gantt_cli "project.sh" "$@"
|
||||
# Auto-detect gantt-board directory if not set
|
||||
if [[ -z "${GANTT_BOARD_DIR:-}" ]]; then
|
||||
# Try common locations
|
||||
if [[ -d "$SCRIPT_DIR/../../../gantt-board" ]]; then
|
||||
GANTT_BOARD_DIR="$SCRIPT_DIR/../../../gantt-board"
|
||||
elif [[ -d "$HOME/Documents/Projects/OpenClaw/Web/gantt-board" ]]; then
|
||||
GANTT_BOARD_DIR="$HOME/Documents/Projects/OpenClaw/Web/gantt-board"
|
||||
elif [[ -d "/Users/mattbruce/Documents/Projects/OpenClaw/Web/gantt-board" ]]; then
|
||||
GANTT_BOARD_DIR="/Users/mattbruce/Documents/Projects/OpenClaw/Web/gantt-board"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Verify gantt-board is available
|
||||
if [[ -z "${GANTT_BOARD_DIR:-}" ]]; then
|
||||
echo "Error: GANTT_BOARD_DIR not set and gantt-board not found" >&2
|
||||
echo "" >&2
|
||||
echo "Please set GANTT_BOARD_DIR to the path of your gantt-board installation:" >&2
|
||||
echo " export GANTT_BOARD_DIR=/path/to/gantt-board" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$GANTT_BOARD_DIR/scripts/project.sh" ]]; then
|
||||
echo "Error: gantt-board project.sh not found at $GANTT_BOARD_DIR/scripts/project.sh" >&2
|
||||
echo "" >&2
|
||||
echo "Please ensure gantt-board is installed correctly." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Delegate all calls to gantt-board project.sh
|
||||
exec "$GANTT_BOARD_DIR/scripts/project.sh" "$@"
|
||||
|
||||
39
scripts/sprint.sh
Executable file
39
scripts/sprint.sh
Executable file
@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
# Mission Control Sprint CLI Wrapper
|
||||
# Delegates to gantt-board sprint.sh for sprint operations
|
||||
# This maintains the architecture principle: CLI is passthrough to API
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Auto-detect gantt-board directory if not set
|
||||
if [[ -z "${GANTT_BOARD_DIR:-}" ]]; then
|
||||
# Try common locations
|
||||
if [[ -d "$SCRIPT_DIR/../../../gantt-board" ]]; then
|
||||
GANTT_BOARD_DIR="$SCRIPT_DIR/../../../gantt-board"
|
||||
elif [[ -d "$HOME/Documents/Projects/OpenClaw/Web/gantt-board" ]]; then
|
||||
GANTT_BOARD_DIR="$HOME/Documents/Projects/OpenClaw/Web/gantt-board"
|
||||
elif [[ -d "/Users/mattbruce/Documents/Projects/OpenClaw/Web/gantt-board" ]]; then
|
||||
GANTT_BOARD_DIR="/Users/mattbruce/Documents/Projects/OpenClaw/Web/gantt-board"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Verify gantt-board is available
|
||||
if [[ -z "${GANTT_BOARD_DIR:-}" ]]; then
|
||||
echo "Error: GANTT_BOARD_DIR not set and gantt-board not found" >&2
|
||||
echo "" >&2
|
||||
echo "Please set GANTT_BOARD_DIR to the path of your gantt-board installation:" >&2
|
||||
echo " export GANTT_BOARD_DIR=/path/to/gantt-board" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$GANTT_BOARD_DIR/scripts/sprint.sh" ]]; then
|
||||
echo "Error: gantt-board sprint.sh not found at $GANTT_BOARD_DIR/scripts/sprint.sh" >&2
|
||||
echo "" >&2
|
||||
echo "Please ensure gantt-board is installed correctly." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Delegate all calls to gantt-board sprint.sh
|
||||
exec "$GANTT_BOARD_DIR/scripts/sprint.sh" "$@"
|
||||
@ -1,9 +1,39 @@
|
||||
#!/bin/bash
|
||||
# Mission Control Task CLI Wrapper
|
||||
# Delegates to gantt-board task.sh for task operations
|
||||
# This maintains the architecture principle: CLI is passthrough to API
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=./lib/gantt_cli.sh
|
||||
source "$SCRIPT_DIR/lib/gantt_cli.sh"
|
||||
|
||||
run_gantt_cli "task.sh" "$@"
|
||||
# Auto-detect gantt-board directory if not set
|
||||
if [[ -z "${GANTT_BOARD_DIR:-}" ]]; then
|
||||
# Try common locations
|
||||
if [[ -d "$SCRIPT_DIR/../../../gantt-board" ]]; then
|
||||
GANTT_BOARD_DIR="$SCRIPT_DIR/../../../gantt-board"
|
||||
elif [[ -d "$HOME/Documents/Projects/OpenClaw/Web/gantt-board" ]]; then
|
||||
GANTT_BOARD_DIR="$HOME/Documents/Projects/OpenClaw/Web/gantt-board"
|
||||
elif [[ -d "/Users/mattbruce/Documents/Projects/OpenClaw/Web/gantt-board" ]]; then
|
||||
GANTT_BOARD_DIR="/Users/mattbruce/Documents/Projects/OpenClaw/Web/gantt-board"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Verify gantt-board is available
|
||||
if [[ -z "${GANTT_BOARD_DIR:-}" ]]; then
|
||||
echo "Error: GANTT_BOARD_DIR not set and gantt-board not found" >&2
|
||||
echo "" >&2
|
||||
echo "Please set GANTT_BOARD_DIR to the path of your gantt-board installation:" >&2
|
||||
echo " export GANTT_BOARD_DIR=/path/to/gantt-board" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$GANTT_BOARD_DIR/scripts/task.sh" ]]; then
|
||||
echo "Error: gantt-board task.sh not found at $GANTT_BOARD_DIR/scripts/task.sh" >&2
|
||||
echo "" >&2
|
||||
echo "Please ensure gantt-board is installed correctly." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Delegate all calls to gantt-board task.sh
|
||||
exec "$GANTT_BOARD_DIR/scripts/task.sh" "$@"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user