@hasna/todos 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +459 -22
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2599,6 +2599,10 @@ function getComment(id, db) {
|
|
|
2599
2599
|
const d = db || getDatabase();
|
|
2600
2600
|
return d.query("SELECT * FROM task_comments WHERE id = ?").get(id);
|
|
2601
2601
|
}
|
|
2602
|
+
function listComments(taskId, db) {
|
|
2603
|
+
const d = db || getDatabase();
|
|
2604
|
+
return d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(taskId);
|
|
2605
|
+
}
|
|
2602
2606
|
var init_comments = __esm(() => {
|
|
2603
2607
|
init_types();
|
|
2604
2608
|
init_database();
|
|
@@ -6965,6 +6969,389 @@ ${text}` }] };
|
|
|
6965
6969
|
});
|
|
6966
6970
|
});
|
|
6967
6971
|
|
|
6972
|
+
// src/server/serve.ts
|
|
6973
|
+
var exports_serve = {};
|
|
6974
|
+
__export(exports_serve, {
|
|
6975
|
+
startServer: () => startServer
|
|
6976
|
+
});
|
|
6977
|
+
import { execSync } from "child_process";
|
|
6978
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
6979
|
+
import { join as join2, dirname as dirname2, extname } from "path";
|
|
6980
|
+
import { fileURLToPath } from "url";
|
|
6981
|
+
function resolveDashboardDir() {
|
|
6982
|
+
const candidates = [];
|
|
6983
|
+
try {
|
|
6984
|
+
const scriptDir = dirname2(fileURLToPath(import.meta.url));
|
|
6985
|
+
candidates.push(join2(scriptDir, "..", "dashboard", "dist"));
|
|
6986
|
+
candidates.push(join2(scriptDir, "..", "..", "dashboard", "dist"));
|
|
6987
|
+
} catch {}
|
|
6988
|
+
if (process.argv[1]) {
|
|
6989
|
+
const mainDir = dirname2(process.argv[1]);
|
|
6990
|
+
candidates.push(join2(mainDir, "..", "dashboard", "dist"));
|
|
6991
|
+
candidates.push(join2(mainDir, "..", "..", "dashboard", "dist"));
|
|
6992
|
+
}
|
|
6993
|
+
candidates.push(join2(process.cwd(), "dashboard", "dist"));
|
|
6994
|
+
for (const candidate of candidates) {
|
|
6995
|
+
if (existsSync2(candidate))
|
|
6996
|
+
return candidate;
|
|
6997
|
+
}
|
|
6998
|
+
return join2(process.cwd(), "dashboard", "dist");
|
|
6999
|
+
}
|
|
7000
|
+
function json(data, status = 200, port) {
|
|
7001
|
+
return new Response(JSON.stringify(data), {
|
|
7002
|
+
status,
|
|
7003
|
+
headers: {
|
|
7004
|
+
"Content-Type": "application/json",
|
|
7005
|
+
"Access-Control-Allow-Origin": port ? `http://localhost:${port}` : "*",
|
|
7006
|
+
...SECURITY_HEADERS
|
|
7007
|
+
}
|
|
7008
|
+
});
|
|
7009
|
+
}
|
|
7010
|
+
function getPackageVersion() {
|
|
7011
|
+
try {
|
|
7012
|
+
const pkgPath = join2(dirname2(fileURLToPath(import.meta.url)), "..", "..", "package.json");
|
|
7013
|
+
return JSON.parse(readFileSync(pkgPath, "utf-8")).version || "0.0.0";
|
|
7014
|
+
} catch {
|
|
7015
|
+
return "0.0.0";
|
|
7016
|
+
}
|
|
7017
|
+
}
|
|
7018
|
+
function serveStaticFile(filePath) {
|
|
7019
|
+
if (!existsSync2(filePath))
|
|
7020
|
+
return null;
|
|
7021
|
+
const ext = extname(filePath);
|
|
7022
|
+
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
7023
|
+
return new Response(Bun.file(filePath), {
|
|
7024
|
+
headers: { "Content-Type": contentType }
|
|
7025
|
+
});
|
|
7026
|
+
}
|
|
7027
|
+
async function startServer(port, options) {
|
|
7028
|
+
const shouldOpen = options?.open ?? true;
|
|
7029
|
+
const dashboardDir = resolveDashboardDir();
|
|
7030
|
+
const dashboardExists = existsSync2(dashboardDir);
|
|
7031
|
+
if (!dashboardExists) {
|
|
7032
|
+
console.error(`
|
|
7033
|
+
Dashboard not found at: ${dashboardDir}`);
|
|
7034
|
+
console.error(`Run this to build it:
|
|
7035
|
+
`);
|
|
7036
|
+
console.error(` cd dashboard && bun install && bun run build
|
|
7037
|
+
`);
|
|
7038
|
+
console.error(`Or from the project root:
|
|
7039
|
+
`);
|
|
7040
|
+
console.error(` bun run build:dashboard
|
|
7041
|
+
`);
|
|
7042
|
+
}
|
|
7043
|
+
const server2 = Bun.serve({
|
|
7044
|
+
port,
|
|
7045
|
+
async fetch(req) {
|
|
7046
|
+
const url = new URL(req.url);
|
|
7047
|
+
const path = url.pathname;
|
|
7048
|
+
const method = req.method;
|
|
7049
|
+
if (path === "/api/tasks" && method === "GET") {
|
|
7050
|
+
try {
|
|
7051
|
+
const filter = {};
|
|
7052
|
+
const status = url.searchParams.get("status");
|
|
7053
|
+
const priority = url.searchParams.get("priority");
|
|
7054
|
+
const projectId = url.searchParams.get("project_id");
|
|
7055
|
+
if (status)
|
|
7056
|
+
filter.status = status;
|
|
7057
|
+
if (priority)
|
|
7058
|
+
filter.priority = priority;
|
|
7059
|
+
if (projectId)
|
|
7060
|
+
filter.project_id = projectId;
|
|
7061
|
+
const tasks = listTasks(filter);
|
|
7062
|
+
const projectCache = new Map;
|
|
7063
|
+
const enriched = tasks.map((t) => {
|
|
7064
|
+
let projectName;
|
|
7065
|
+
if (t.project_id) {
|
|
7066
|
+
if (projectCache.has(t.project_id)) {
|
|
7067
|
+
projectName = projectCache.get(t.project_id);
|
|
7068
|
+
} else {
|
|
7069
|
+
const p = getProject(t.project_id);
|
|
7070
|
+
projectName = p?.name;
|
|
7071
|
+
if (projectName)
|
|
7072
|
+
projectCache.set(t.project_id, projectName);
|
|
7073
|
+
}
|
|
7074
|
+
}
|
|
7075
|
+
return { ...t, project_name: projectName };
|
|
7076
|
+
});
|
|
7077
|
+
return json(enriched, 200, port);
|
|
7078
|
+
} catch (e) {
|
|
7079
|
+
return json({ error: e instanceof Error ? e.message : "Failed to list tasks" }, 500, port);
|
|
7080
|
+
}
|
|
7081
|
+
}
|
|
7082
|
+
const taskGetMatch = path.match(/^\/api\/tasks\/([^/]+)$/);
|
|
7083
|
+
if (taskGetMatch && method === "GET") {
|
|
7084
|
+
try {
|
|
7085
|
+
const id = taskGetMatch[1];
|
|
7086
|
+
const task = getTaskWithRelations(id);
|
|
7087
|
+
if (!task)
|
|
7088
|
+
return json({ error: "Task not found" }, 404, port);
|
|
7089
|
+
let projectName;
|
|
7090
|
+
if (task.project_id) {
|
|
7091
|
+
const p = getProject(task.project_id);
|
|
7092
|
+
projectName = p?.name;
|
|
7093
|
+
}
|
|
7094
|
+
return json({ ...task, project_name: projectName }, 200, port);
|
|
7095
|
+
} catch (e) {
|
|
7096
|
+
return json({ error: e instanceof Error ? e.message : "Failed to get task" }, 500, port);
|
|
7097
|
+
}
|
|
7098
|
+
}
|
|
7099
|
+
if (path === "/api/tasks" && method === "POST") {
|
|
7100
|
+
try {
|
|
7101
|
+
const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
|
|
7102
|
+
if (contentLength > MAX_BODY_SIZE)
|
|
7103
|
+
return json({ error: "Request body too large" }, 413, port);
|
|
7104
|
+
const body = await req.json();
|
|
7105
|
+
if (!body.title || typeof body.title !== "string") {
|
|
7106
|
+
return json({ error: "Missing required field: title" }, 400, port);
|
|
7107
|
+
}
|
|
7108
|
+
const task = createTask({
|
|
7109
|
+
title: body.title,
|
|
7110
|
+
description: body.description,
|
|
7111
|
+
priority: body.priority,
|
|
7112
|
+
project_id: body.project_id,
|
|
7113
|
+
parent_id: body.parent_id,
|
|
7114
|
+
tags: body.tags,
|
|
7115
|
+
assigned_to: body.assigned_to,
|
|
7116
|
+
agent_id: body.agent_id,
|
|
7117
|
+
status: body.status
|
|
7118
|
+
});
|
|
7119
|
+
return json(task, 201, port);
|
|
7120
|
+
} catch (e) {
|
|
7121
|
+
return json({ error: e instanceof Error ? e.message : "Failed to create task" }, 500, port);
|
|
7122
|
+
}
|
|
7123
|
+
}
|
|
7124
|
+
const taskPatchMatch = path.match(/^\/api\/tasks\/([^/]+)$/);
|
|
7125
|
+
if (taskPatchMatch && method === "PATCH") {
|
|
7126
|
+
try {
|
|
7127
|
+
const id = taskPatchMatch[1];
|
|
7128
|
+
const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
|
|
7129
|
+
if (contentLength > MAX_BODY_SIZE)
|
|
7130
|
+
return json({ error: "Request body too large" }, 413, port);
|
|
7131
|
+
const body = await req.json();
|
|
7132
|
+
if (typeof body.version !== "number") {
|
|
7133
|
+
return json({ error: "Missing required field: version" }, 400, port);
|
|
7134
|
+
}
|
|
7135
|
+
const task = updateTask(id, {
|
|
7136
|
+
version: body.version,
|
|
7137
|
+
title: body.title,
|
|
7138
|
+
description: body.description,
|
|
7139
|
+
status: body.status,
|
|
7140
|
+
priority: body.priority,
|
|
7141
|
+
assigned_to: body.assigned_to,
|
|
7142
|
+
tags: body.tags,
|
|
7143
|
+
metadata: body.metadata
|
|
7144
|
+
});
|
|
7145
|
+
return json(task, 200, port);
|
|
7146
|
+
} catch (e) {
|
|
7147
|
+
const status = e instanceof Error && e.name === "VersionConflictError" ? 409 : 500;
|
|
7148
|
+
return json({ error: e instanceof Error ? e.message : "Failed to update task" }, status, port);
|
|
7149
|
+
}
|
|
7150
|
+
}
|
|
7151
|
+
const taskDeleteMatch = path.match(/^\/api\/tasks\/([^/]+)$/);
|
|
7152
|
+
if (taskDeleteMatch && method === "DELETE") {
|
|
7153
|
+
try {
|
|
7154
|
+
const id = taskDeleteMatch[1];
|
|
7155
|
+
const deleted = deleteTask(id);
|
|
7156
|
+
if (!deleted)
|
|
7157
|
+
return json({ error: "Task not found" }, 404, port);
|
|
7158
|
+
return json({ deleted: true }, 200, port);
|
|
7159
|
+
} catch (e) {
|
|
7160
|
+
return json({ error: e instanceof Error ? e.message : "Failed to delete task" }, 500, port);
|
|
7161
|
+
}
|
|
7162
|
+
}
|
|
7163
|
+
const taskStartMatch = path.match(/^\/api\/tasks\/([^/]+)\/start$/);
|
|
7164
|
+
if (taskStartMatch && method === "POST") {
|
|
7165
|
+
try {
|
|
7166
|
+
const id = taskStartMatch[1];
|
|
7167
|
+
const body = await req.json();
|
|
7168
|
+
const agentId = body.agent_id || "dashboard";
|
|
7169
|
+
const task = startTask(id, agentId);
|
|
7170
|
+
return json(task, 200, port);
|
|
7171
|
+
} catch (e) {
|
|
7172
|
+
const status = e instanceof Error && e.name === "TaskNotFoundError" ? 404 : e instanceof Error && e.name === "LockError" ? 409 : 500;
|
|
7173
|
+
return json({ error: e instanceof Error ? e.message : "Failed to start task" }, status, port);
|
|
7174
|
+
}
|
|
7175
|
+
}
|
|
7176
|
+
const taskCompleteMatch = path.match(/^\/api\/tasks\/([^/]+)\/complete$/);
|
|
7177
|
+
if (taskCompleteMatch && method === "POST") {
|
|
7178
|
+
try {
|
|
7179
|
+
const id = taskCompleteMatch[1];
|
|
7180
|
+
const body = await req.json();
|
|
7181
|
+
const agentId = body.agent_id;
|
|
7182
|
+
const task = completeTask(id, agentId);
|
|
7183
|
+
return json(task, 200, port);
|
|
7184
|
+
} catch (e) {
|
|
7185
|
+
const status = e instanceof Error && e.name === "TaskNotFoundError" ? 404 : e instanceof Error && e.name === "LockError" ? 409 : 500;
|
|
7186
|
+
return json({ error: e instanceof Error ? e.message : "Failed to complete task" }, status, port);
|
|
7187
|
+
}
|
|
7188
|
+
}
|
|
7189
|
+
const commentsGetMatch = path.match(/^\/api\/tasks\/([^/]+)\/comments$/);
|
|
7190
|
+
if (commentsGetMatch && method === "GET") {
|
|
7191
|
+
try {
|
|
7192
|
+
const taskId = commentsGetMatch[1];
|
|
7193
|
+
const comments = listComments(taskId);
|
|
7194
|
+
return json(comments, 200, port);
|
|
7195
|
+
} catch (e) {
|
|
7196
|
+
return json({ error: e instanceof Error ? e.message : "Failed to list comments" }, 500, port);
|
|
7197
|
+
}
|
|
7198
|
+
}
|
|
7199
|
+
const commentsPostMatch = path.match(/^\/api\/tasks\/([^/]+)\/comments$/);
|
|
7200
|
+
if (commentsPostMatch && method === "POST") {
|
|
7201
|
+
try {
|
|
7202
|
+
const taskId = commentsPostMatch[1];
|
|
7203
|
+
const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
|
|
7204
|
+
if (contentLength > MAX_BODY_SIZE)
|
|
7205
|
+
return json({ error: "Request body too large" }, 413, port);
|
|
7206
|
+
const body = await req.json();
|
|
7207
|
+
if (!body.content || typeof body.content !== "string") {
|
|
7208
|
+
return json({ error: "Missing required field: content" }, 400, port);
|
|
7209
|
+
}
|
|
7210
|
+
const comment = addComment({
|
|
7211
|
+
task_id: taskId,
|
|
7212
|
+
content: body.content,
|
|
7213
|
+
agent_id: body.agent_id,
|
|
7214
|
+
session_id: body.session_id
|
|
7215
|
+
});
|
|
7216
|
+
return json(comment, 201, port);
|
|
7217
|
+
} catch (e) {
|
|
7218
|
+
return json({ error: e instanceof Error ? e.message : "Failed to add comment" }, 500, port);
|
|
7219
|
+
}
|
|
7220
|
+
}
|
|
7221
|
+
if (path === "/api/projects" && method === "GET") {
|
|
7222
|
+
try {
|
|
7223
|
+
const projects = listProjects();
|
|
7224
|
+
return json(projects, 200, port);
|
|
7225
|
+
} catch (e) {
|
|
7226
|
+
return json({ error: e instanceof Error ? e.message : "Failed to list projects" }, 500, port);
|
|
7227
|
+
}
|
|
7228
|
+
}
|
|
7229
|
+
if (path === "/api/projects" && method === "POST") {
|
|
7230
|
+
try {
|
|
7231
|
+
const contentLength = parseInt(req.headers.get("content-length") || "0", 10);
|
|
7232
|
+
if (contentLength > MAX_BODY_SIZE)
|
|
7233
|
+
return json({ error: "Request body too large" }, 413, port);
|
|
7234
|
+
const body = await req.json();
|
|
7235
|
+
if (!body.name || typeof body.name !== "string") {
|
|
7236
|
+
return json({ error: "Missing required field: name" }, 400, port);
|
|
7237
|
+
}
|
|
7238
|
+
const project = createProject({
|
|
7239
|
+
name: body.name,
|
|
7240
|
+
path: body.path || process.cwd(),
|
|
7241
|
+
description: body.description
|
|
7242
|
+
});
|
|
7243
|
+
return json(project, 201, port);
|
|
7244
|
+
} catch (e) {
|
|
7245
|
+
return json({ error: e instanceof Error ? e.message : "Failed to create project" }, 500, port);
|
|
7246
|
+
}
|
|
7247
|
+
}
|
|
7248
|
+
if (path === "/api/search" && method === "GET") {
|
|
7249
|
+
try {
|
|
7250
|
+
const q = url.searchParams.get("q");
|
|
7251
|
+
if (!q)
|
|
7252
|
+
return json({ error: "Missing query parameter: q" }, 400, port);
|
|
7253
|
+
const projectId = url.searchParams.get("project_id") || undefined;
|
|
7254
|
+
const results = searchTasks(q, projectId);
|
|
7255
|
+
return json(results, 200, port);
|
|
7256
|
+
} catch (e) {
|
|
7257
|
+
return json({ error: e instanceof Error ? e.message : "Search failed" }, 500, port);
|
|
7258
|
+
}
|
|
7259
|
+
}
|
|
7260
|
+
if (path === "/api/system/version" && method === "GET") {
|
|
7261
|
+
try {
|
|
7262
|
+
const current = getPackageVersion();
|
|
7263
|
+
const npmRes = await fetch("https://registry.npmjs.org/@hasna/todos/latest");
|
|
7264
|
+
if (!npmRes.ok) {
|
|
7265
|
+
return json({ current, latest: current, updateAvailable: false }, 200, port);
|
|
7266
|
+
}
|
|
7267
|
+
const data = await npmRes.json();
|
|
7268
|
+
const latest = data.version;
|
|
7269
|
+
return json({ current, latest, updateAvailable: current !== latest }, 200, port);
|
|
7270
|
+
} catch {
|
|
7271
|
+
const current = getPackageVersion();
|
|
7272
|
+
return json({ current, latest: current, updateAvailable: false }, 200, port);
|
|
7273
|
+
}
|
|
7274
|
+
}
|
|
7275
|
+
if (path === "/api/system/update" && method === "POST") {
|
|
7276
|
+
try {
|
|
7277
|
+
let useBun = false;
|
|
7278
|
+
try {
|
|
7279
|
+
execSync("which bun", { stdio: "ignore" });
|
|
7280
|
+
useBun = true;
|
|
7281
|
+
} catch {}
|
|
7282
|
+
const cmd = useBun ? "bun add -g @hasna/todos@latest" : "npm install -g @hasna/todos@latest";
|
|
7283
|
+
execSync(cmd, { stdio: "ignore", timeout: 60000 });
|
|
7284
|
+
return json({ success: true, message: "Updated! Restart the server to use the new version." }, 200, port);
|
|
7285
|
+
} catch (e) {
|
|
7286
|
+
return json({ success: false, message: e instanceof Error ? e.message : "Update failed" }, 500, port);
|
|
7287
|
+
}
|
|
7288
|
+
}
|
|
7289
|
+
if (method === "OPTIONS") {
|
|
7290
|
+
return new Response(null, {
|
|
7291
|
+
headers: {
|
|
7292
|
+
"Access-Control-Allow-Origin": `http://localhost:${port}`,
|
|
7293
|
+
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
|
|
7294
|
+
"Access-Control-Allow-Headers": "Content-Type"
|
|
7295
|
+
}
|
|
7296
|
+
});
|
|
7297
|
+
}
|
|
7298
|
+
if (dashboardExists && (method === "GET" || method === "HEAD")) {
|
|
7299
|
+
if (path !== "/") {
|
|
7300
|
+
const filePath = join2(dashboardDir, path);
|
|
7301
|
+
const res2 = serveStaticFile(filePath);
|
|
7302
|
+
if (res2)
|
|
7303
|
+
return res2;
|
|
7304
|
+
}
|
|
7305
|
+
const indexPath = join2(dashboardDir, "index.html");
|
|
7306
|
+
const res = serveStaticFile(indexPath);
|
|
7307
|
+
if (res)
|
|
7308
|
+
return res;
|
|
7309
|
+
}
|
|
7310
|
+
return json({ error: "Not found" }, 404, port);
|
|
7311
|
+
}
|
|
7312
|
+
});
|
|
7313
|
+
const shutdown = () => {
|
|
7314
|
+
server2.stop();
|
|
7315
|
+
process.exit(0);
|
|
7316
|
+
};
|
|
7317
|
+
process.on("SIGINT", shutdown);
|
|
7318
|
+
process.on("SIGTERM", shutdown);
|
|
7319
|
+
const serverUrl = `http://localhost:${port}`;
|
|
7320
|
+
console.log(`Todos Dashboard running at ${serverUrl}`);
|
|
7321
|
+
if (shouldOpen) {
|
|
7322
|
+
try {
|
|
7323
|
+
const { exec } = await import("child_process");
|
|
7324
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
7325
|
+
exec(`${openCmd} ${serverUrl}`);
|
|
7326
|
+
} catch {}
|
|
7327
|
+
}
|
|
7328
|
+
return server2;
|
|
7329
|
+
}
|
|
7330
|
+
var MIME_TYPES, SECURITY_HEADERS, MAX_BODY_SIZE;
|
|
7331
|
+
var init_serve = __esm(() => {
|
|
7332
|
+
init_tasks();
|
|
7333
|
+
init_projects();
|
|
7334
|
+
init_comments();
|
|
7335
|
+
init_search();
|
|
7336
|
+
MIME_TYPES = {
|
|
7337
|
+
".html": "text/html; charset=utf-8",
|
|
7338
|
+
".js": "application/javascript",
|
|
7339
|
+
".css": "text/css",
|
|
7340
|
+
".json": "application/json",
|
|
7341
|
+
".png": "image/png",
|
|
7342
|
+
".jpg": "image/jpeg",
|
|
7343
|
+
".svg": "image/svg+xml",
|
|
7344
|
+
".ico": "image/x-icon",
|
|
7345
|
+
".woff": "font/woff",
|
|
7346
|
+
".woff2": "font/woff2"
|
|
7347
|
+
};
|
|
7348
|
+
SECURITY_HEADERS = {
|
|
7349
|
+
"X-Content-Type-Options": "nosniff",
|
|
7350
|
+
"X-Frame-Options": "DENY"
|
|
7351
|
+
};
|
|
7352
|
+
MAX_BODY_SIZE = 1024 * 1024;
|
|
7353
|
+
});
|
|
7354
|
+
|
|
6968
7355
|
// src/cli/components/Header.tsx
|
|
6969
7356
|
import { Box, Text } from "ink";
|
|
6970
7357
|
import { jsxDEV, Fragment } from "react/jsx-dev-runtime";
|
|
@@ -8000,9 +8387,18 @@ init_projects();
|
|
|
8000
8387
|
init_comments();
|
|
8001
8388
|
init_search();
|
|
8002
8389
|
import chalk from "chalk";
|
|
8003
|
-
import { execSync } from "child_process";
|
|
8004
|
-
import { existsSync as
|
|
8005
|
-
import { basename, dirname as
|
|
8390
|
+
import { execSync as execSync2 } from "child_process";
|
|
8391
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
8392
|
+
import { basename, dirname as dirname3, join as join3, resolve as resolve2 } from "path";
|
|
8393
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8394
|
+
function getPackageVersion2() {
|
|
8395
|
+
try {
|
|
8396
|
+
const pkgPath = join3(dirname3(fileURLToPath2(import.meta.url)), "..", "..", "package.json");
|
|
8397
|
+
return JSON.parse(readFileSync2(pkgPath, "utf-8")).version || "0.0.0";
|
|
8398
|
+
} catch {
|
|
8399
|
+
return "0.0.0";
|
|
8400
|
+
}
|
|
8401
|
+
}
|
|
8006
8402
|
var program2 = new Command;
|
|
8007
8403
|
function handleError(e) {
|
|
8008
8404
|
console.error(chalk.red(e instanceof Error ? e.message : String(e)));
|
|
@@ -8019,7 +8415,7 @@ function resolveTaskId(partialId) {
|
|
|
8019
8415
|
}
|
|
8020
8416
|
function detectGitRoot() {
|
|
8021
8417
|
try {
|
|
8022
|
-
return
|
|
8418
|
+
return execSync2("git rev-parse --show-toplevel", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
8023
8419
|
} catch {
|
|
8024
8420
|
return null;
|
|
8025
8421
|
}
|
|
@@ -8064,7 +8460,7 @@ function formatTaskLine(t) {
|
|
|
8064
8460
|
const tags = t.tags.length > 0 ? chalk.dim(` [${t.tags.join(",")}]`) : "";
|
|
8065
8461
|
return `${chalk.dim(t.id.slice(0, 8))} ${statusFn(t.status.padEnd(11))} ${priorityFn(t.priority.padEnd(8))} ${t.title}${assigned}${lock}${tags}`;
|
|
8066
8462
|
}
|
|
8067
|
-
program2.name("todos").description("Universal task management for AI coding agents").version(
|
|
8463
|
+
program2.name("todos").description("Universal task management for AI coding agents").version(getPackageVersion2()).option("--project <path>", "Project path").option("--json", "Output as JSON").option("--agent <name>", "Agent name").option("--session <id>", "Session ID");
|
|
8068
8464
|
program2.command("add <title>").description("Create a new task").option("-d, --description <text>", "Task description").option("-p, --priority <level>", "Priority: low, medium, high, critical").option("--parent <id>", "Parent task ID").option("--tags <tags>", "Comma-separated tags").option("--assign <agent>", "Assign to agent").option("--status <status>", "Initial status").action((title, opts) => {
|
|
8069
8465
|
const globalOpts = program2.opts();
|
|
8070
8466
|
const projectId = autoProject(globalOpts);
|
|
@@ -8478,45 +8874,45 @@ program2.command("mcp").description("Start MCP server (stdio)").option("--regist
|
|
|
8478
8874
|
var HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
8479
8875
|
function getMcpBinaryPath() {
|
|
8480
8876
|
try {
|
|
8481
|
-
const p =
|
|
8877
|
+
const p = execSync2("which todos-mcp", { encoding: "utf-8" }).trim();
|
|
8482
8878
|
if (p)
|
|
8483
8879
|
return p;
|
|
8484
8880
|
} catch {}
|
|
8485
|
-
const bunBin =
|
|
8486
|
-
if (
|
|
8881
|
+
const bunBin = join3(HOME, ".bun", "bin", "todos-mcp");
|
|
8882
|
+
if (existsSync3(bunBin))
|
|
8487
8883
|
return bunBin;
|
|
8488
8884
|
return "todos-mcp";
|
|
8489
8885
|
}
|
|
8490
8886
|
function readJsonFile(path) {
|
|
8491
|
-
if (!
|
|
8887
|
+
if (!existsSync3(path))
|
|
8492
8888
|
return {};
|
|
8493
8889
|
try {
|
|
8494
|
-
return JSON.parse(
|
|
8890
|
+
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
8495
8891
|
} catch {
|
|
8496
8892
|
return {};
|
|
8497
8893
|
}
|
|
8498
8894
|
}
|
|
8499
8895
|
function writeJsonFile(path, data) {
|
|
8500
|
-
const dir =
|
|
8501
|
-
if (!
|
|
8896
|
+
const dir = dirname3(path);
|
|
8897
|
+
if (!existsSync3(dir))
|
|
8502
8898
|
mkdirSync2(dir, { recursive: true });
|
|
8503
8899
|
writeFileSync(path, JSON.stringify(data, null, 2) + `
|
|
8504
8900
|
`);
|
|
8505
8901
|
}
|
|
8506
8902
|
function readTomlFile(path) {
|
|
8507
|
-
if (!
|
|
8903
|
+
if (!existsSync3(path))
|
|
8508
8904
|
return "";
|
|
8509
|
-
return
|
|
8905
|
+
return readFileSync2(path, "utf-8");
|
|
8510
8906
|
}
|
|
8511
8907
|
function writeTomlFile(path, content) {
|
|
8512
|
-
const dir =
|
|
8513
|
-
if (!
|
|
8908
|
+
const dir = dirname3(path);
|
|
8909
|
+
if (!existsSync3(dir))
|
|
8514
8910
|
mkdirSync2(dir, { recursive: true });
|
|
8515
8911
|
writeFileSync(path, content);
|
|
8516
8912
|
}
|
|
8517
8913
|
function registerClaude(binPath) {
|
|
8518
8914
|
const cwd = process.cwd();
|
|
8519
|
-
const configPath =
|
|
8915
|
+
const configPath = join3(cwd, ".mcp.json");
|
|
8520
8916
|
const config = readJsonFile(configPath);
|
|
8521
8917
|
config["todos"] = {
|
|
8522
8918
|
command: binPath,
|
|
@@ -8527,7 +8923,7 @@ function registerClaude(binPath) {
|
|
|
8527
8923
|
}
|
|
8528
8924
|
function unregisterClaude() {
|
|
8529
8925
|
const cwd = process.cwd();
|
|
8530
|
-
const configPath =
|
|
8926
|
+
const configPath = join3(cwd, ".mcp.json");
|
|
8531
8927
|
const config = readJsonFile(configPath);
|
|
8532
8928
|
if (!("todos" in config)) {
|
|
8533
8929
|
console.log(chalk.dim(`Claude Code: todos not found in ${configPath}`));
|
|
@@ -8538,7 +8934,7 @@ function unregisterClaude() {
|
|
|
8538
8934
|
console.log(chalk.green(`Claude Code: unregistered from ${configPath}`));
|
|
8539
8935
|
}
|
|
8540
8936
|
function registerCodex(binPath) {
|
|
8541
|
-
const configPath =
|
|
8937
|
+
const configPath = join3(HOME, ".codex", "config.toml");
|
|
8542
8938
|
let content = readTomlFile(configPath);
|
|
8543
8939
|
content = removeTomlBlock(content, "mcp_servers.todos");
|
|
8544
8940
|
const block = `
|
|
@@ -8552,7 +8948,7 @@ args = []
|
|
|
8552
8948
|
console.log(chalk.green(`Codex CLI: registered in ${configPath}`));
|
|
8553
8949
|
}
|
|
8554
8950
|
function unregisterCodex() {
|
|
8555
|
-
const configPath =
|
|
8951
|
+
const configPath = join3(HOME, ".codex", "config.toml");
|
|
8556
8952
|
let content = readTomlFile(configPath);
|
|
8557
8953
|
if (!content.includes("[mcp_servers.todos]")) {
|
|
8558
8954
|
console.log(chalk.dim(`Codex CLI: todos not found in ${configPath}`));
|
|
@@ -8585,7 +8981,7 @@ function removeTomlBlock(content, blockName) {
|
|
|
8585
8981
|
`);
|
|
8586
8982
|
}
|
|
8587
8983
|
function registerGemini(binPath) {
|
|
8588
|
-
const configPath =
|
|
8984
|
+
const configPath = join3(HOME, ".gemini", "settings.json");
|
|
8589
8985
|
const config = readJsonFile(configPath);
|
|
8590
8986
|
if (!config["mcpServers"]) {
|
|
8591
8987
|
config["mcpServers"] = {};
|
|
@@ -8599,7 +8995,7 @@ function registerGemini(binPath) {
|
|
|
8599
8995
|
console.log(chalk.green(`Gemini CLI: registered in ${configPath}`));
|
|
8600
8996
|
}
|
|
8601
8997
|
function unregisterGemini() {
|
|
8602
|
-
const configPath =
|
|
8998
|
+
const configPath = join3(HOME, ".gemini", "settings.json");
|
|
8603
8999
|
const config = readJsonFile(configPath);
|
|
8604
9000
|
const servers = config["mcpServers"];
|
|
8605
9001
|
if (!servers || !("todos" in servers)) {
|
|
@@ -8647,6 +9043,47 @@ function unregisterMcp(agent) {
|
|
|
8647
9043
|
}
|
|
8648
9044
|
}
|
|
8649
9045
|
}
|
|
9046
|
+
program2.command("serve").aliases(["dashboard", "open"]).description("Start the web dashboard").option("-p, --port <port>", "Port number", "19420").option("--no-open", "Don't auto-open browser").action(async (opts) => {
|
|
9047
|
+
const { startServer: startServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
|
|
9048
|
+
await startServer2(parseInt(opts.port, 10), { open: opts.open });
|
|
9049
|
+
});
|
|
9050
|
+
program2.command("upgrade").alias("self-update").description("Update todos to the latest version").option("--check", "Only check for updates, don't install").action(async (opts) => {
|
|
9051
|
+
try {
|
|
9052
|
+
const currentVersion = getPackageVersion2();
|
|
9053
|
+
const res = await fetch("https://registry.npmjs.org/@hasna/todos/latest");
|
|
9054
|
+
if (!res.ok) {
|
|
9055
|
+
console.error(chalk.red("Failed to check for updates."));
|
|
9056
|
+
process.exit(1);
|
|
9057
|
+
}
|
|
9058
|
+
const data = await res.json();
|
|
9059
|
+
const latestVersion = data.version;
|
|
9060
|
+
console.log(` Current: ${chalk.dim(currentVersion)}`);
|
|
9061
|
+
console.log(` Latest: ${chalk.green(latestVersion)}`);
|
|
9062
|
+
if (currentVersion === latestVersion) {
|
|
9063
|
+
console.log(chalk.green(`
|
|
9064
|
+
Already up to date!`));
|
|
9065
|
+
return;
|
|
9066
|
+
}
|
|
9067
|
+
if (opts.check) {
|
|
9068
|
+
console.log(chalk.yellow(`
|
|
9069
|
+
Update available: ${currentVersion} \u2192 ${latestVersion}`));
|
|
9070
|
+
return;
|
|
9071
|
+
}
|
|
9072
|
+
let useBun = false;
|
|
9073
|
+
try {
|
|
9074
|
+
execSync2("which bun", { stdio: "ignore" });
|
|
9075
|
+
useBun = true;
|
|
9076
|
+
} catch {}
|
|
9077
|
+
const cmd = useBun ? "bun add -g @hasna/todos@latest" : "npm install -g @hasna/todos@latest";
|
|
9078
|
+
console.log(chalk.dim(`
|
|
9079
|
+
Running: ${cmd}`));
|
|
9080
|
+
execSync2(cmd, { stdio: "inherit" });
|
|
9081
|
+
console.log(chalk.green(`
|
|
9082
|
+
Updated to ${latestVersion}!`));
|
|
9083
|
+
} catch (e) {
|
|
9084
|
+
handleError(e);
|
|
9085
|
+
}
|
|
9086
|
+
});
|
|
8650
9087
|
program2.command("interactive").description("Launch interactive TUI").action(async () => {
|
|
8651
9088
|
const { renderApp: renderApp2 } = await Promise.resolve().then(() => (init_App(), exports_App));
|
|
8652
9089
|
const globalOpts = program2.opts();
|