@task-mcp/shared 1.0.14 → 1.0.16
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/algorithms/critical-path.test.js +1 -1
- package/dist/algorithms/critical-path.test.js.map +1 -1
- package/dist/algorithms/dependency-integrity.test.js +1 -1
- package/dist/algorithms/dependency-integrity.test.js.map +1 -1
- package/dist/algorithms/tech-analysis.test.js +1 -1
- package/dist/algorithms/tech-analysis.test.js.map +1 -1
- package/dist/algorithms/topological-sort.test.js +1 -1
- package/dist/algorithms/topological-sort.test.js.map +1 -1
- package/dist/schemas/index.d.ts +1 -2
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +0 -2
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/response-format.d.ts +8 -13
- package/dist/schemas/response-format.d.ts.map +1 -1
- package/dist/schemas/response-format.js.map +1 -1
- package/dist/schemas/task.d.ts +3 -9
- package/dist/schemas/task.d.ts.map +1 -1
- package/dist/schemas/task.js +3 -3
- package/dist/schemas/task.js.map +1 -1
- package/dist/schemas/view.d.ts +16 -8
- package/dist/schemas/view.d.ts.map +1 -1
- package/dist/schemas/view.js +2 -1
- package/dist/schemas/view.js.map +1 -1
- package/dist/utils/dashboard-renderer.d.ts +20 -13
- package/dist/utils/dashboard-renderer.d.ts.map +1 -1
- package/dist/utils/dashboard-renderer.js +46 -37
- package/dist/utils/dashboard-renderer.js.map +1 -1
- package/dist/utils/dashboard-renderer.test.js +84 -87
- package/dist/utils/dashboard-renderer.test.js.map +1 -1
- package/dist/utils/hierarchy.test.js +1 -1
- package/dist/utils/hierarchy.test.js.map +1 -1
- package/dist/utils/id.d.ts +2 -19
- package/dist/utils/id.d.ts.map +1 -1
- package/dist/utils/id.js +0 -43
- package/dist/utils/id.js.map +1 -1
- package/dist/utils/id.test.js +3 -38
- package/dist/utils/id.test.js.map +1 -1
- package/dist/utils/index.d.ts +6 -3
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +8 -4
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/projection.d.ts +1 -10
- package/dist/utils/projection.d.ts.map +1 -1
- package/dist/utils/projection.js +0 -48
- package/dist/utils/projection.js.map +1 -1
- package/dist/utils/projection.test.js +2 -66
- package/dist/utils/projection.test.js.map +1 -1
- package/dist/utils/workspace.d.ts +100 -0
- package/dist/utils/workspace.d.ts.map +1 -0
- package/dist/utils/workspace.js +173 -0
- package/dist/utils/workspace.js.map +1 -0
- package/package.json +1 -1
- package/src/algorithms/critical-path.test.ts +1 -1
- package/src/algorithms/dependency-integrity.test.ts +1 -1
- package/src/algorithms/tech-analysis.test.ts +1 -1
- package/src/algorithms/topological-sort.test.ts +1 -1
- package/src/schemas/index.ts +2 -11
- package/src/schemas/response-format.ts +10 -15
- package/src/schemas/task.ts +3 -3
- package/src/schemas/view.ts +2 -1
- package/src/utils/dashboard-renderer.test.ts +92 -90
- package/src/utils/dashboard-renderer.ts +63 -48
- package/src/utils/hierarchy.test.ts +1 -1
- package/src/utils/id.test.ts +2 -48
- package/src/utils/id.ts +1 -48
- package/src/utils/index.ts +17 -8
- package/src/utils/projection.test.ts +1 -81
- package/src/utils/projection.ts +0 -62
- package/src/utils/workspace.ts +182 -0
- package/src/schemas/project.ts +0 -68
package/src/utils/id.test.ts
CHANGED
|
@@ -2,16 +2,12 @@ import { describe, test, expect } from "bun:test";
|
|
|
2
2
|
import {
|
|
3
3
|
generateId,
|
|
4
4
|
generateTaskId,
|
|
5
|
-
generateProjectId,
|
|
6
5
|
generateInboxId,
|
|
7
6
|
isValidTaskId,
|
|
8
|
-
isValidProjectId,
|
|
9
7
|
isValidInboxId,
|
|
10
8
|
validateTaskId,
|
|
11
|
-
validateProjectId,
|
|
12
9
|
validateInboxId,
|
|
13
10
|
assertValidTaskId,
|
|
14
|
-
assertValidProjectId,
|
|
15
11
|
assertValidInboxId,
|
|
16
12
|
InvalidIdError,
|
|
17
13
|
} from "./id.js";
|
|
@@ -42,13 +38,6 @@ describe("ID Generation", () => {
|
|
|
42
38
|
});
|
|
43
39
|
});
|
|
44
40
|
|
|
45
|
-
describe("generateProjectId", () => {
|
|
46
|
-
test("generates project ID with correct prefix", () => {
|
|
47
|
-
const id = generateProjectId();
|
|
48
|
-
expect(id).toMatch(/^proj_[a-z0-9]+$/);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
41
|
describe("generateInboxId", () => {
|
|
53
42
|
test("generates inbox ID with correct prefix", () => {
|
|
54
43
|
const id = generateInboxId();
|
|
@@ -79,19 +68,6 @@ describe("Simple ID Validation (boolean)", () => {
|
|
|
79
68
|
});
|
|
80
69
|
});
|
|
81
70
|
|
|
82
|
-
describe("isValidProjectId", () => {
|
|
83
|
-
test("returns true for valid project IDs", () => {
|
|
84
|
-
expect(isValidProjectId("proj_abc123")).toBe(true);
|
|
85
|
-
expect(isValidProjectId("proj_a")).toBe(true);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("returns false for invalid project IDs", () => {
|
|
89
|
-
expect(isValidProjectId("")).toBe(false);
|
|
90
|
-
expect(isValidProjectId("project_abc123")).toBe(false);
|
|
91
|
-
expect(isValidProjectId("task_abc123")).toBe(false);
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
71
|
describe("isValidInboxId", () => {
|
|
96
72
|
test("returns true for valid inbox IDs", () => {
|
|
97
73
|
expect(isValidInboxId("inbox_abc123")).toBe(true);
|
|
@@ -154,18 +130,6 @@ describe("Detailed ID Validation", () => {
|
|
|
154
130
|
});
|
|
155
131
|
});
|
|
156
132
|
|
|
157
|
-
describe("validateProjectId", () => {
|
|
158
|
-
test("returns valid: true for valid project IDs", () => {
|
|
159
|
-
expect(validateProjectId("proj_abc123")).toEqual({ valid: true });
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test("returns error for missing prefix", () => {
|
|
163
|
-
const result = validateProjectId("project_abc");
|
|
164
|
-
expect(result.valid).toBe(false);
|
|
165
|
-
expect(result.reason).toContain("proj_");
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
133
|
describe("validateInboxId", () => {
|
|
170
134
|
test("returns valid: true for valid inbox IDs", () => {
|
|
171
135
|
expect(validateInboxId("inbox_abc123")).toEqual({ valid: true });
|
|
@@ -206,16 +170,6 @@ describe("Assert Functions", () => {
|
|
|
206
170
|
});
|
|
207
171
|
});
|
|
208
172
|
|
|
209
|
-
describe("assertValidProjectId", () => {
|
|
210
|
-
test("does not throw for valid ID", () => {
|
|
211
|
-
expect(() => assertValidProjectId("proj_abc123")).not.toThrow();
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
test("throws InvalidIdError for invalid ID", () => {
|
|
215
|
-
expect(() => assertValidProjectId("project_abc")).toThrow(InvalidIdError);
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
|
|
219
173
|
describe("assertValidInboxId", () => {
|
|
220
174
|
test("does not throw for valid ID", () => {
|
|
221
175
|
expect(() => assertValidInboxId("inbox_abc123")).not.toThrow();
|
|
@@ -245,8 +199,8 @@ describe("InvalidIdError", () => {
|
|
|
245
199
|
});
|
|
246
200
|
|
|
247
201
|
test("exposes idType, invalidValue, and reason", () => {
|
|
248
|
-
const error = new InvalidIdError("
|
|
249
|
-
expect(error.idType).toBe("
|
|
202
|
+
const error = new InvalidIdError("task", "wrong", "missing prefix");
|
|
203
|
+
expect(error.idType).toBe("task");
|
|
250
204
|
expect(error.invalidValue).toBe("wrong");
|
|
251
205
|
expect(error.reason).toBe("missing prefix");
|
|
252
206
|
});
|
package/src/utils/id.ts
CHANGED
|
@@ -16,13 +16,6 @@ export function generateTaskId(): string {
|
|
|
16
16
|
return generateId("task");
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
/**
|
|
20
|
-
* Generate a project ID
|
|
21
|
-
*/
|
|
22
|
-
export function generateProjectId(): string {
|
|
23
|
-
return generateId("proj");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
19
|
/**
|
|
27
20
|
* Generate a view ID
|
|
28
21
|
*/
|
|
@@ -30,14 +23,6 @@ export function generateViewId(): string {
|
|
|
30
23
|
return generateId("view");
|
|
31
24
|
}
|
|
32
25
|
|
|
33
|
-
/**
|
|
34
|
-
* Validate a project ID format
|
|
35
|
-
* @returns true if valid format (proj_[alphanumeric])
|
|
36
|
-
*/
|
|
37
|
-
export function isValidProjectId(id: string): boolean {
|
|
38
|
-
return /^proj_[a-z0-9]+$/.test(id);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
26
|
/**
|
|
42
27
|
* Validate a task ID format
|
|
43
28
|
* @returns true if valid format (task_[alphanumeric])
|
|
@@ -66,7 +51,7 @@ export function isValidInboxId(id: string): boolean {
|
|
|
66
51
|
*/
|
|
67
52
|
export class InvalidIdError extends Error {
|
|
68
53
|
constructor(
|
|
69
|
-
public readonly idType: "task" | "
|
|
54
|
+
public readonly idType: "task" | "inbox" | "view",
|
|
70
55
|
public readonly invalidValue: unknown,
|
|
71
56
|
public readonly reason: string
|
|
72
57
|
) {
|
|
@@ -105,28 +90,6 @@ export function validateTaskId(id: unknown): IdValidationResult {
|
|
|
105
90
|
return { valid: true };
|
|
106
91
|
}
|
|
107
92
|
|
|
108
|
-
/**
|
|
109
|
-
* Validate project ID with detailed error information
|
|
110
|
-
*/
|
|
111
|
-
export function validateProjectId(id: unknown): IdValidationResult {
|
|
112
|
-
if (id === null || id === undefined) {
|
|
113
|
-
return { valid: false, reason: "Project ID is required" };
|
|
114
|
-
}
|
|
115
|
-
if (typeof id !== "string") {
|
|
116
|
-
return { valid: false, reason: `Project ID must be a string (received ${typeof id})` };
|
|
117
|
-
}
|
|
118
|
-
if (id.length === 0) {
|
|
119
|
-
return { valid: false, reason: "Project ID cannot be empty" };
|
|
120
|
-
}
|
|
121
|
-
if (!id.startsWith("proj_")) {
|
|
122
|
-
return { valid: false, reason: "Project ID must start with 'proj_' prefix" };
|
|
123
|
-
}
|
|
124
|
-
if (!/^proj_[a-z0-9]+$/.test(id)) {
|
|
125
|
-
return { valid: false, reason: "Project ID contains invalid characters" };
|
|
126
|
-
}
|
|
127
|
-
return { valid: true };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
93
|
/**
|
|
131
94
|
* Validate inbox ID with detailed error information
|
|
132
95
|
*/
|
|
@@ -159,16 +122,6 @@ export function assertValidTaskId(id: unknown): asserts id is string {
|
|
|
159
122
|
}
|
|
160
123
|
}
|
|
161
124
|
|
|
162
|
-
/**
|
|
163
|
-
* Assert valid project ID, throw if invalid
|
|
164
|
-
*/
|
|
165
|
-
export function assertValidProjectId(id: unknown): asserts id is string {
|
|
166
|
-
const result = validateProjectId(id);
|
|
167
|
-
if (!result.valid) {
|
|
168
|
-
throw new InvalidIdError("project", id, result.reason!);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
125
|
/**
|
|
173
126
|
* Assert valid inbox ID, throw if invalid
|
|
174
127
|
*/
|
package/src/utils/index.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
export {
|
|
2
2
|
generateId,
|
|
3
3
|
generateTaskId,
|
|
4
|
-
generateProjectId,
|
|
5
4
|
generateViewId,
|
|
6
|
-
isValidProjectId,
|
|
7
5
|
isValidTaskId,
|
|
8
6
|
generateInboxId,
|
|
9
7
|
isValidInboxId,
|
|
@@ -11,10 +9,8 @@ export {
|
|
|
11
9
|
InvalidIdError,
|
|
12
10
|
type IdValidationResult,
|
|
13
11
|
validateTaskId,
|
|
14
|
-
validateProjectId,
|
|
15
12
|
validateInboxId,
|
|
16
13
|
assertValidTaskId,
|
|
17
|
-
assertValidProjectId,
|
|
18
14
|
assertValidInboxId,
|
|
19
15
|
} from "./id.js";
|
|
20
16
|
export { PriorityQueue } from "./priority-queue.js";
|
|
@@ -51,8 +47,6 @@ export {
|
|
|
51
47
|
projectTask,
|
|
52
48
|
projectTasks,
|
|
53
49
|
projectTasksPaginated,
|
|
54
|
-
projectProject,
|
|
55
|
-
projectProjects,
|
|
56
50
|
projectInboxItem,
|
|
57
51
|
projectInboxItems,
|
|
58
52
|
formatResponse,
|
|
@@ -69,13 +63,16 @@ export {
|
|
|
69
63
|
renderStatusWidget,
|
|
70
64
|
renderActionsWidget,
|
|
71
65
|
renderInboxWidget,
|
|
72
|
-
|
|
66
|
+
renderWorkspacesTable,
|
|
67
|
+
renderProjectsTable, // Legacy alias
|
|
73
68
|
renderTasksTable,
|
|
74
69
|
renderDashboard,
|
|
75
|
-
|
|
70
|
+
renderWorkspaceDashboard,
|
|
71
|
+
renderProjectDashboard, // Legacy alias
|
|
76
72
|
renderGlobalDashboard,
|
|
77
73
|
type DashboardStats,
|
|
78
74
|
type DependencyMetrics,
|
|
75
|
+
type WorkspaceInfo,
|
|
79
76
|
type DashboardData,
|
|
80
77
|
type RenderDashboardOptions,
|
|
81
78
|
} from "./dashboard-renderer.js";
|
|
@@ -123,3 +120,15 @@ export {
|
|
|
123
120
|
// Banner
|
|
124
121
|
banner,
|
|
125
122
|
} from "./terminal-ui.js";
|
|
123
|
+
|
|
124
|
+
// Workspace detection
|
|
125
|
+
export {
|
|
126
|
+
normalizeWorkspace,
|
|
127
|
+
getGitRepoRoot,
|
|
128
|
+
getGitRepoRootSync,
|
|
129
|
+
getWorkspaceFromGit,
|
|
130
|
+
getWorkspaceFromGitSync,
|
|
131
|
+
getWorkspaceFromPath,
|
|
132
|
+
detectWorkspace,
|
|
133
|
+
detectWorkspaceSync,
|
|
134
|
+
} from "./workspace.js";
|
|
@@ -3,8 +3,6 @@ import {
|
|
|
3
3
|
projectTask,
|
|
4
4
|
projectTasks,
|
|
5
5
|
projectTasksPaginated,
|
|
6
|
-
projectProject,
|
|
7
|
-
projectProjects,
|
|
8
6
|
projectInboxItem,
|
|
9
7
|
projectInboxItems,
|
|
10
8
|
formatResponse,
|
|
@@ -13,14 +11,13 @@ import {
|
|
|
13
11
|
summarizeList,
|
|
14
12
|
} from "./projection.js";
|
|
15
13
|
import type { Task } from "../schemas/task.js";
|
|
16
|
-
import type { Project } from "../schemas/project.js";
|
|
17
14
|
import type { InboxItem } from "../schemas/inbox.js";
|
|
18
15
|
|
|
19
16
|
// Test fixtures
|
|
20
17
|
const createTask = (overrides: Partial<Task> = {}): Task => {
|
|
21
18
|
const base: Task = {
|
|
22
19
|
id: "task-1",
|
|
23
|
-
|
|
20
|
+
workspace: "test-workspace",
|
|
24
21
|
title: "Test Task",
|
|
25
22
|
status: "pending",
|
|
26
23
|
priority: "medium",
|
|
@@ -37,19 +34,6 @@ const createTask = (overrides: Partial<Task> = {}): Task => {
|
|
|
37
34
|
return { ...base, ...overrides };
|
|
38
35
|
};
|
|
39
36
|
|
|
40
|
-
const createProject = (overrides: Partial<Project> = {}): Project => ({
|
|
41
|
-
id: "proj-1",
|
|
42
|
-
name: "Test Project",
|
|
43
|
-
status: "active",
|
|
44
|
-
createdAt: "2025-01-01T00:00:00.000Z",
|
|
45
|
-
updatedAt: "2025-01-01T00:00:00.000Z",
|
|
46
|
-
completionPercentage: 50,
|
|
47
|
-
description: "A test project",
|
|
48
|
-
totalTasks: 10,
|
|
49
|
-
completedTasks: 5,
|
|
50
|
-
...overrides,
|
|
51
|
-
});
|
|
52
|
-
|
|
53
37
|
const createInboxItem = (overrides: Partial<InboxItem> = {}): InboxItem => ({
|
|
54
38
|
id: "inbox-1",
|
|
55
39
|
content: "Quick idea for later",
|
|
@@ -196,70 +180,6 @@ describe("projectTasksPaginated", () => {
|
|
|
196
180
|
});
|
|
197
181
|
});
|
|
198
182
|
|
|
199
|
-
describe("projectProject", () => {
|
|
200
|
-
test("concise format returns essential fields", () => {
|
|
201
|
-
const project = createProject();
|
|
202
|
-
const result = projectProject(project, "concise");
|
|
203
|
-
|
|
204
|
-
expect(result).toEqual({
|
|
205
|
-
id: "proj-1",
|
|
206
|
-
name: "Test Project",
|
|
207
|
-
status: "active",
|
|
208
|
-
completionPercentage: 50,
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
test("concise format omits undefined completionPercentage", () => {
|
|
213
|
-
// Create project without completionPercentage by deleting it
|
|
214
|
-
const project = createProject();
|
|
215
|
-
delete (project as Record<string, unknown>)["completionPercentage"];
|
|
216
|
-
const result = projectProject(project, "concise");
|
|
217
|
-
|
|
218
|
-
expect(result).not.toHaveProperty("completionPercentage");
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
test("standard format includes more fields", () => {
|
|
222
|
-
const project = createProject();
|
|
223
|
-
const result = projectProject(project, "standard");
|
|
224
|
-
|
|
225
|
-
expect(result).toHaveProperty("id");
|
|
226
|
-
expect(result).toHaveProperty("name");
|
|
227
|
-
expect(result).toHaveProperty("status");
|
|
228
|
-
expect(result).toHaveProperty("completionPercentage");
|
|
229
|
-
expect(result).toHaveProperty("description");
|
|
230
|
-
expect(result).toHaveProperty("totalTasks");
|
|
231
|
-
expect(result).toHaveProperty("completedTasks");
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
test("detailed format returns full project", () => {
|
|
235
|
-
const project = createProject();
|
|
236
|
-
const result = projectProject(project, "detailed");
|
|
237
|
-
|
|
238
|
-
expect(result).toBe(project);
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
describe("projectProjects", () => {
|
|
243
|
-
test("projects all projects without limit", () => {
|
|
244
|
-
const projects = [createProject({ id: "p1" }), createProject({ id: "p2" })];
|
|
245
|
-
const result = projectProjects(projects, "concise");
|
|
246
|
-
|
|
247
|
-
expect(result).toHaveLength(2);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
test("limits projects when limit provided", () => {
|
|
251
|
-
const projects = [
|
|
252
|
-
createProject({ id: "p1" }),
|
|
253
|
-
createProject({ id: "p2" }),
|
|
254
|
-
createProject({ id: "p3" }),
|
|
255
|
-
];
|
|
256
|
-
const result = projectProjects(projects, "concise", 1);
|
|
257
|
-
|
|
258
|
-
expect(result).toHaveLength(1);
|
|
259
|
-
expect(result[0]?.id).toBe("p1");
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
|
|
263
183
|
describe("projectInboxItem", () => {
|
|
264
184
|
test("concise format returns 3 essential fields", () => {
|
|
265
185
|
const item = createInboxItem();
|
package/src/utils/projection.ts
CHANGED
|
@@ -6,14 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { Task } from "../schemas/task.js";
|
|
9
|
-
import type { Project } from "../schemas/project.js";
|
|
10
9
|
import type { InboxItem } from "../schemas/inbox.js";
|
|
11
10
|
import type {
|
|
12
11
|
ResponseFormat,
|
|
13
12
|
TaskSummary,
|
|
14
13
|
TaskPreview,
|
|
15
|
-
ProjectSummary,
|
|
16
|
-
ProjectPreview,
|
|
17
14
|
InboxSummary,
|
|
18
15
|
InboxPreview,
|
|
19
16
|
PaginatedResponse,
|
|
@@ -84,65 +81,6 @@ export function projectTasksPaginated(
|
|
|
84
81
|
};
|
|
85
82
|
}
|
|
86
83
|
|
|
87
|
-
/**
|
|
88
|
-
* Project a single project to the specified format
|
|
89
|
-
*/
|
|
90
|
-
export function projectProject(
|
|
91
|
-
project: Project,
|
|
92
|
-
format: ResponseFormat
|
|
93
|
-
): ProjectSummary | ProjectPreview | Project {
|
|
94
|
-
switch (format) {
|
|
95
|
-
case "concise": {
|
|
96
|
-
const summary: ProjectSummary = {
|
|
97
|
-
id: project.id,
|
|
98
|
-
name: project.name,
|
|
99
|
-
status: project.status,
|
|
100
|
-
};
|
|
101
|
-
if (project.completionPercentage !== undefined) {
|
|
102
|
-
summary.completionPercentage = project.completionPercentage;
|
|
103
|
-
}
|
|
104
|
-
return summary;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
case "standard": {
|
|
108
|
-
const preview: ProjectPreview = {
|
|
109
|
-
id: project.id,
|
|
110
|
-
name: project.name,
|
|
111
|
-
status: project.status,
|
|
112
|
-
};
|
|
113
|
-
if (project.completionPercentage !== undefined) {
|
|
114
|
-
preview.completionPercentage = project.completionPercentage;
|
|
115
|
-
}
|
|
116
|
-
if (project.description !== undefined) {
|
|
117
|
-
preview.description = project.description;
|
|
118
|
-
}
|
|
119
|
-
if (project.totalTasks !== undefined) {
|
|
120
|
-
preview.totalTasks = project.totalTasks;
|
|
121
|
-
}
|
|
122
|
-
if (project.completedTasks !== undefined) {
|
|
123
|
-
preview.completedTasks = project.completedTasks;
|
|
124
|
-
}
|
|
125
|
-
return preview;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
case "detailed":
|
|
129
|
-
default:
|
|
130
|
-
return project;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Project multiple projects with optional limit
|
|
136
|
-
*/
|
|
137
|
-
export function projectProjects(
|
|
138
|
-
projects: Project[],
|
|
139
|
-
format: ResponseFormat,
|
|
140
|
-
limit?: number
|
|
141
|
-
): (ProjectSummary | ProjectPreview | Project)[] {
|
|
142
|
-
const sliced = limit ? projects.slice(0, limit) : projects;
|
|
143
|
-
return sliced.map((project) => projectProject(project, format));
|
|
144
|
-
}
|
|
145
|
-
|
|
146
84
|
/**
|
|
147
85
|
* Project a single inbox item to the specified format
|
|
148
86
|
*/
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace detection utilities
|
|
3
|
+
*
|
|
4
|
+
* Automatically detects workspace name from:
|
|
5
|
+
* 1. Git repository root directory name
|
|
6
|
+
* 2. TASKS_DIR path basename (fallback)
|
|
7
|
+
* 3. Current working directory basename (final fallback)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { exec } from "node:child_process";
|
|
11
|
+
import { promisify } from "node:util";
|
|
12
|
+
import { basename } from "node:path";
|
|
13
|
+
|
|
14
|
+
const execAsync = promisify(exec);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Normalize a string to a valid workspace name.
|
|
18
|
+
*
|
|
19
|
+
* Rules:
|
|
20
|
+
* - Lowercase
|
|
21
|
+
* - Replace spaces with hyphens
|
|
22
|
+
* - Keep alphanumeric, hyphens, and underscores
|
|
23
|
+
* - Trim leading/trailing hyphens
|
|
24
|
+
*
|
|
25
|
+
* @param name - Raw name to normalize
|
|
26
|
+
* @returns Normalized workspace name
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* normalizeWorkspace('My Project'); // 'my-project'
|
|
31
|
+
* normalizeWorkspace('Task-MCP'); // 'task-mcp'
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function normalizeWorkspace(name: string): string {
|
|
35
|
+
return name
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
.replace(/\s+/g, "-")
|
|
38
|
+
.replace(/[^a-z0-9_-]/g, "")
|
|
39
|
+
.replace(/^-+|-+$/g, "")
|
|
40
|
+
|| "default";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get the git repository root directory path.
|
|
45
|
+
*
|
|
46
|
+
* @param cwd - Working directory (defaults to process.cwd())
|
|
47
|
+
* @returns Absolute path to git root, or null if not a git repo
|
|
48
|
+
*/
|
|
49
|
+
export async function getGitRepoRoot(cwd?: string): Promise<string | null> {
|
|
50
|
+
try {
|
|
51
|
+
const { stdout } = await execAsync("git rev-parse --show-toplevel", {
|
|
52
|
+
cwd: cwd ?? process.cwd(),
|
|
53
|
+
});
|
|
54
|
+
return stdout.trim() || null;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Synchronously get the git repository root directory path.
|
|
62
|
+
* Uses execSync - prefer async version when possible.
|
|
63
|
+
*
|
|
64
|
+
* @param cwd - Working directory (defaults to process.cwd())
|
|
65
|
+
* @returns Absolute path to git root, or null if not a git repo
|
|
66
|
+
*/
|
|
67
|
+
export function getGitRepoRootSync(cwd?: string): string | null {
|
|
68
|
+
try {
|
|
69
|
+
const { execSync } = require("node:child_process");
|
|
70
|
+
const result = execSync("git rev-parse --show-toplevel", {
|
|
71
|
+
cwd: cwd ?? process.cwd(),
|
|
72
|
+
encoding: "utf-8",
|
|
73
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
74
|
+
});
|
|
75
|
+
return (result as string).trim() || null;
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Detect workspace name from git repository.
|
|
83
|
+
*
|
|
84
|
+
* @param cwd - Working directory (defaults to process.cwd())
|
|
85
|
+
* @returns Workspace name derived from git repo, or null
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```typescript
|
|
89
|
+
* // In /home/user/projects/task-mcp (git repo)
|
|
90
|
+
* await getWorkspaceFromGit(); // 'task-mcp'
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export async function getWorkspaceFromGit(cwd?: string): Promise<string | null> {
|
|
94
|
+
const repoRoot = await getGitRepoRoot(cwd);
|
|
95
|
+
if (!repoRoot) return null;
|
|
96
|
+
return normalizeWorkspace(basename(repoRoot));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Synchronous version of getWorkspaceFromGit.
|
|
101
|
+
*/
|
|
102
|
+
export function getWorkspaceFromGitSync(cwd?: string): string | null {
|
|
103
|
+
const repoRoot = getGitRepoRootSync(cwd);
|
|
104
|
+
if (!repoRoot) return null;
|
|
105
|
+
return normalizeWorkspace(basename(repoRoot));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get workspace name from a file path.
|
|
110
|
+
* Uses the last directory name in the path.
|
|
111
|
+
*
|
|
112
|
+
* @param path - File system path
|
|
113
|
+
* @returns Normalized workspace name
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```typescript
|
|
117
|
+
* getWorkspaceFromPath('/home/user/.tasks'); // 'user'
|
|
118
|
+
* getWorkspaceFromPath('/projects/my-app/.tasks'); // 'my-app'
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export function getWorkspaceFromPath(path: string): string {
|
|
122
|
+
// Remove trailing .tasks if present to get the parent directory
|
|
123
|
+
const cleanPath = path.replace(/[\/\\]?\.tasks[\/\\]?$/, "");
|
|
124
|
+
const name = basename(cleanPath);
|
|
125
|
+
return normalizeWorkspace(name);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Detect workspace name using multiple strategies.
|
|
130
|
+
*
|
|
131
|
+
* Strategy order:
|
|
132
|
+
* 1. Git repository root basename (most reliable)
|
|
133
|
+
* 2. TASKS_DIR parent directory basename
|
|
134
|
+
* 3. Current working directory basename
|
|
135
|
+
*
|
|
136
|
+
* @param tasksDir - Optional tasks directory path
|
|
137
|
+
* @returns Normalized workspace name
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```typescript
|
|
141
|
+
* // In a git repo called 'my-project'
|
|
142
|
+
* await detectWorkspace(); // 'my-project'
|
|
143
|
+
*
|
|
144
|
+
* // With explicit TASKS_DIR
|
|
145
|
+
* await detectWorkspace('/projects/app/.tasks'); // 'app'
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
export async function detectWorkspace(tasksDir?: string): Promise<string> {
|
|
149
|
+
// Strategy 1: Git repo detection
|
|
150
|
+
const gitWorkspace = await getWorkspaceFromGit();
|
|
151
|
+
if (gitWorkspace) {
|
|
152
|
+
return gitWorkspace;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Strategy 2: TASKS_DIR path
|
|
156
|
+
if (tasksDir) {
|
|
157
|
+
return getWorkspaceFromPath(tasksDir);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Strategy 3: CWD basename
|
|
161
|
+
return normalizeWorkspace(basename(process.cwd()));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Synchronous version of detectWorkspace.
|
|
166
|
+
* Prefer async version when possible.
|
|
167
|
+
*/
|
|
168
|
+
export function detectWorkspaceSync(tasksDir?: string): string {
|
|
169
|
+
// Strategy 1: Git repo detection
|
|
170
|
+
const gitWorkspace = getWorkspaceFromGitSync();
|
|
171
|
+
if (gitWorkspace) {
|
|
172
|
+
return gitWorkspace;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Strategy 2: TASKS_DIR path
|
|
176
|
+
if (tasksDir) {
|
|
177
|
+
return getWorkspaceFromPath(tasksDir);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Strategy 3: CWD basename
|
|
181
|
+
return normalizeWorkspace(basename(process.cwd()));
|
|
182
|
+
}
|
package/src/schemas/project.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { Priority } from "./task.js";
|
|
3
|
-
|
|
4
|
-
// Project status
|
|
5
|
-
export const ProjectStatus = z.enum([
|
|
6
|
-
"active",
|
|
7
|
-
"on_hold",
|
|
8
|
-
"completed",
|
|
9
|
-
"archived",
|
|
10
|
-
]);
|
|
11
|
-
export type ProjectStatus = z.infer<typeof ProjectStatus>;
|
|
12
|
-
|
|
13
|
-
// Context definition
|
|
14
|
-
export const Context = z.object({
|
|
15
|
-
name: z.string(),
|
|
16
|
-
color: z.string().optional(), // hex color
|
|
17
|
-
description: z.string().optional(),
|
|
18
|
-
});
|
|
19
|
-
export type Context = z.infer<typeof Context>;
|
|
20
|
-
|
|
21
|
-
// Project schema
|
|
22
|
-
export const Project = z.object({
|
|
23
|
-
id: z.string(),
|
|
24
|
-
name: z.string(),
|
|
25
|
-
description: z.string().optional(),
|
|
26
|
-
status: ProjectStatus,
|
|
27
|
-
|
|
28
|
-
// Project-level settings
|
|
29
|
-
defaultPriority: Priority.optional(),
|
|
30
|
-
contexts: z.array(Context).optional(),
|
|
31
|
-
|
|
32
|
-
// Metadata
|
|
33
|
-
createdAt: z.string(),
|
|
34
|
-
updatedAt: z.string(),
|
|
35
|
-
targetDate: z.string().optional(),
|
|
36
|
-
sortOrder: z.number().optional(), // User-defined display order (auto-assigned if not specified)
|
|
37
|
-
|
|
38
|
-
// Computed stats
|
|
39
|
-
completionPercentage: z.number().optional(),
|
|
40
|
-
criticalPathLength: z.number().optional(), // Total minutes on critical path
|
|
41
|
-
blockedTaskCount: z.number().optional(),
|
|
42
|
-
totalTasks: z.number().optional(),
|
|
43
|
-
completedTasks: z.number().optional(),
|
|
44
|
-
});
|
|
45
|
-
export type Project = z.infer<typeof Project>;
|
|
46
|
-
|
|
47
|
-
// Project creation input
|
|
48
|
-
export const ProjectCreateInput = z.object({
|
|
49
|
-
name: z.string(),
|
|
50
|
-
description: z.string().optional(),
|
|
51
|
-
defaultPriority: Priority.optional(),
|
|
52
|
-
contexts: z.array(Context).optional(),
|
|
53
|
-
targetDate: z.string().optional(),
|
|
54
|
-
sortOrder: z.number().optional(), // Auto-assigned if not specified
|
|
55
|
-
});
|
|
56
|
-
export type ProjectCreateInput = z.infer<typeof ProjectCreateInput>;
|
|
57
|
-
|
|
58
|
-
// Project update input
|
|
59
|
-
export const ProjectUpdateInput = z.object({
|
|
60
|
-
name: z.string().optional(),
|
|
61
|
-
description: z.string().optional(),
|
|
62
|
-
status: ProjectStatus.optional(),
|
|
63
|
-
defaultPriority: Priority.optional(),
|
|
64
|
-
contexts: z.array(Context).optional(),
|
|
65
|
-
targetDate: z.string().optional(),
|
|
66
|
-
sortOrder: z.number().optional(),
|
|
67
|
-
});
|
|
68
|
-
export type ProjectUpdateInput = z.infer<typeof ProjectUpdateInput>;
|