@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.
Files changed (70) hide show
  1. package/dist/algorithms/critical-path.test.js +1 -1
  2. package/dist/algorithms/critical-path.test.js.map +1 -1
  3. package/dist/algorithms/dependency-integrity.test.js +1 -1
  4. package/dist/algorithms/dependency-integrity.test.js.map +1 -1
  5. package/dist/algorithms/tech-analysis.test.js +1 -1
  6. package/dist/algorithms/tech-analysis.test.js.map +1 -1
  7. package/dist/algorithms/topological-sort.test.js +1 -1
  8. package/dist/algorithms/topological-sort.test.js.map +1 -1
  9. package/dist/schemas/index.d.ts +1 -2
  10. package/dist/schemas/index.d.ts.map +1 -1
  11. package/dist/schemas/index.js +0 -2
  12. package/dist/schemas/index.js.map +1 -1
  13. package/dist/schemas/response-format.d.ts +8 -13
  14. package/dist/schemas/response-format.d.ts.map +1 -1
  15. package/dist/schemas/response-format.js.map +1 -1
  16. package/dist/schemas/task.d.ts +3 -9
  17. package/dist/schemas/task.d.ts.map +1 -1
  18. package/dist/schemas/task.js +3 -3
  19. package/dist/schemas/task.js.map +1 -1
  20. package/dist/schemas/view.d.ts +16 -8
  21. package/dist/schemas/view.d.ts.map +1 -1
  22. package/dist/schemas/view.js +2 -1
  23. package/dist/schemas/view.js.map +1 -1
  24. package/dist/utils/dashboard-renderer.d.ts +20 -13
  25. package/dist/utils/dashboard-renderer.d.ts.map +1 -1
  26. package/dist/utils/dashboard-renderer.js +46 -37
  27. package/dist/utils/dashboard-renderer.js.map +1 -1
  28. package/dist/utils/dashboard-renderer.test.js +84 -87
  29. package/dist/utils/dashboard-renderer.test.js.map +1 -1
  30. package/dist/utils/hierarchy.test.js +1 -1
  31. package/dist/utils/hierarchy.test.js.map +1 -1
  32. package/dist/utils/id.d.ts +2 -19
  33. package/dist/utils/id.d.ts.map +1 -1
  34. package/dist/utils/id.js +0 -43
  35. package/dist/utils/id.js.map +1 -1
  36. package/dist/utils/id.test.js +3 -38
  37. package/dist/utils/id.test.js.map +1 -1
  38. package/dist/utils/index.d.ts +6 -3
  39. package/dist/utils/index.d.ts.map +1 -1
  40. package/dist/utils/index.js +8 -4
  41. package/dist/utils/index.js.map +1 -1
  42. package/dist/utils/projection.d.ts +1 -10
  43. package/dist/utils/projection.d.ts.map +1 -1
  44. package/dist/utils/projection.js +0 -48
  45. package/dist/utils/projection.js.map +1 -1
  46. package/dist/utils/projection.test.js +2 -66
  47. package/dist/utils/projection.test.js.map +1 -1
  48. package/dist/utils/workspace.d.ts +100 -0
  49. package/dist/utils/workspace.d.ts.map +1 -0
  50. package/dist/utils/workspace.js +173 -0
  51. package/dist/utils/workspace.js.map +1 -0
  52. package/package.json +1 -1
  53. package/src/algorithms/critical-path.test.ts +1 -1
  54. package/src/algorithms/dependency-integrity.test.ts +1 -1
  55. package/src/algorithms/tech-analysis.test.ts +1 -1
  56. package/src/algorithms/topological-sort.test.ts +1 -1
  57. package/src/schemas/index.ts +2 -11
  58. package/src/schemas/response-format.ts +10 -15
  59. package/src/schemas/task.ts +3 -3
  60. package/src/schemas/view.ts +2 -1
  61. package/src/utils/dashboard-renderer.test.ts +92 -90
  62. package/src/utils/dashboard-renderer.ts +63 -48
  63. package/src/utils/hierarchy.test.ts +1 -1
  64. package/src/utils/id.test.ts +2 -48
  65. package/src/utils/id.ts +1 -48
  66. package/src/utils/index.ts +17 -8
  67. package/src/utils/projection.test.ts +1 -81
  68. package/src/utils/projection.ts +0 -62
  69. package/src/utils/workspace.ts +182 -0
  70. package/src/schemas/project.ts +0 -68
@@ -22,7 +22,7 @@ function createTask(
22
22
  title: `Task ${id}`,
23
23
  status: "pending",
24
24
  priority: "medium",
25
- projectId: "test-project",
25
+ workspace: "test-workspace",
26
26
  createdAt: new Date().toISOString(),
27
27
  updatedAt: new Date().toISOString(),
28
28
  parentId,
@@ -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("project", "wrong", "missing prefix");
249
- expect(error.idType).toBe("project");
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" | "project" | "inbox" | "view",
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
  */
@@ -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
- renderProjectsTable,
66
+ renderWorkspacesTable,
67
+ renderProjectsTable, // Legacy alias
73
68
  renderTasksTable,
74
69
  renderDashboard,
75
- renderProjectDashboard,
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
- projectId: "project-1",
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();
@@ -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
+ }
@@ -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>;