Skip to content

Commit 2c898e6

Browse files
TaskManager refactor to reduce module length
1 parent c436149 commit 2c898e6

14 files changed

+1330
-239
lines changed

jest.config.cjs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
1+
const { createDefaultEsmPreset } = require('ts-jest');
2+
3+
const presetConfig = createDefaultEsmPreset({
4+
useESM: true,
5+
});
6+
17
module.exports = {
2-
preset: 'ts-jest/presets/default-esm',
8+
...presetConfig,
39
testEnvironment: 'node',
4-
extensionsToTreatAsEsm: ['.ts'],
510
moduleNameMapper: {
611
'^(\\.{1,2}/.*)\\.js$': '$1',
712
},
8-
transform: {
9-
'^.+\\.ts$': [
10-
'ts-jest',
11-
{
12-
useESM: true,
13-
},
14-
],
15-
},
1613
modulePathIgnorePatterns: ['<rootDir>/dist/'],
1714
// Force Jest to exit after all tests have completed
1815
forceExit: true,

jest.resolver.mts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { SyncResolver } from 'jest-resolve';
2+
3+
const mjsResolver: SyncResolver = (path, options) => {
4+
const mjsExtRegex = /\.m?[jt]s$/i;
5+
const resolver = options.defaultResolver;
6+
if (mjsExtRegex.test(path)) {
7+
try {
8+
return resolver(path.replace(/\.mjs$/, '.mts').replace(/\.js$/, '.ts'), options);
9+
} catch {
10+
// use default resolver
11+
}
12+
}
13+
14+
return resolver(path, options);
15+
};
16+
17+
export default mjsResolver;

src/client/cli.ts

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
import { TaskManager } from "../server/TaskManager.js";
1212
import { createError, normalizeError } from "../utils/errors.js";
1313
import { formatCliError } from "./errors.js";
14+
import fs from "fs/promises";
15+
import type { StandardResponse } from "../types/index.js";
1416

1517
const program = new Command();
1618

@@ -396,7 +398,12 @@ program
396398
} else {
397399
console.log(chalk.yellow(`\nNo tasks found${filterState ? ` matching state '${filterState}'` : ''} in project ${projectId}.`));
398400
}
399-
} catch (error) {
401+
} catch (error: unknown) {
402+
if (error instanceof Error) {
403+
console.error(chalk.red(`Error fetching details for project ${projectId}: ${error.message}`));
404+
} else {
405+
console.error(chalk.red(`Error fetching details for project ${projectId}: Unknown error`));
406+
}
400407
// Handle ProjectNotFound specifically if desired, otherwise let generic handler catch
401408
const normalized = normalizeError(error);
402409
if (normalized.code === ErrorCode.ProjectNotFound) {
@@ -457,8 +464,12 @@ program
457464
} else {
458465
console.log(chalk.yellow(' No tasks in this project.'));
459466
}
460-
} catch (error) {
461-
console.error(chalk.red(`Error fetching details for project ${pSummary.projectId}: ${formatCliError(normalizeError(error))}`));
467+
} catch (error: unknown) {
468+
if (error instanceof Error) {
469+
console.error(chalk.red(`Error fetching details for project ${pSummary.projectId}: ${error.message}`));
470+
} else {
471+
console.error(chalk.red(`Error fetching details for project ${pSummary.projectId}: Unknown error`));
472+
}
462473
}
463474
}
464475
}
@@ -469,4 +480,92 @@ program
469480
}
470481
});
471482

483+
program
484+
.command("generate-plan")
485+
.description("Generate a project plan using an LLM")
486+
.requiredOption("--prompt <text>", "Prompt text to feed to the LLM")
487+
.option("--model <model>", "LLM model to use", "gpt-4-turbo")
488+
.option("--provider <provider>", "LLM provider to use (openai, google, or deepseek)", "openai")
489+
.option("--attachment <file>", "File to attach as context (can be specified multiple times)", collect, [])
490+
.action(async (options) => {
491+
try {
492+
console.log(chalk.blue(`Generating project plan from prompt...`));
493+
494+
// Read attachment files if provided
495+
const attachments: string[] = [];
496+
for (const file of options.attachment) {
497+
try {
498+
const content = await fs.readFile(file, 'utf-8');
499+
attachments.push(content);
500+
} catch (error: unknown) {
501+
if (error instanceof Error) {
502+
console.error(chalk.yellow(`Warning: Could not read attachment file ${chalk.bold(file)}: ${error.message}`));
503+
} else {
504+
console.error(chalk.yellow(`Warning: Could not read attachment file ${chalk.bold(file)}: Unknown error`));
505+
}
506+
}
507+
}
508+
509+
// Call the generateProjectPlan method
510+
const response = await taskManager.generateProjectPlan({
511+
prompt: options.prompt,
512+
provider: options.provider,
513+
model: options.model,
514+
attachments,
515+
});
516+
517+
if ('error' in response) {
518+
throw response.error;
519+
}
520+
521+
if (response.status !== "success") {
522+
throw createError(
523+
ErrorCode.InvalidResponseFormat,
524+
"Unexpected response format from TaskManager"
525+
);
526+
}
527+
528+
const data = response.data as {
529+
projectId: string;
530+
totalTasks: number;
531+
tasks: Array<{
532+
id: string;
533+
title: string;
534+
description: string;
535+
}>;
536+
message?: string;
537+
};
538+
539+
// Display the results
540+
console.log(chalk.green(`✅ Project plan generated successfully!`));
541+
console.log(chalk.cyan('\n📋 Project details:'));
542+
console.log(` - ${chalk.bold('Project ID:')} ${data.projectId}`);
543+
console.log(` - ${chalk.bold('Total Tasks:')} ${data.totalTasks}`);
544+
545+
console.log(chalk.cyan('\n📝 Tasks:'));
546+
data.tasks.forEach((task) => {
547+
console.log(`\n ${chalk.bold(task.id)}:`);
548+
console.log(` Title: ${task.title}`);
549+
console.log(` Description: ${task.description}`);
550+
});
551+
552+
if (data.message) {
553+
console.log(`\n${data.message}`);
554+
}
555+
} catch (err: unknown) {
556+
if (err instanceof Error) {
557+
console.error(chalk.yellow(`Warning: ${err.message}`));
558+
} else {
559+
const normalized = normalizeError(err);
560+
console.error(chalk.red(formatCliError(normalized)));
561+
}
562+
process.exit(1);
563+
}
564+
});
565+
566+
// Helper function for collecting multiple values for the same option
567+
function collect(value: string, previous: string[]) {
568+
return previous.concat([value]);
569+
}
570+
472571
program.parse(process.argv);

src/server/FileSystemService.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
2+
import { dirname, join } from "node:path";
3+
import { homedir } from "node:os";
4+
import { TaskManagerFile, ErrorCode } from "../types/index.js";
5+
import { createError } from "../utils/errors.js";
6+
7+
export interface InitializedTaskData {
8+
data: TaskManagerFile;
9+
maxProjectId: number;
10+
maxTaskId: number;
11+
}
12+
13+
export class FileSystemService {
14+
private filePath: string;
15+
16+
constructor(filePath: string) {
17+
this.filePath = filePath;
18+
}
19+
20+
/**
21+
* Gets the platform-appropriate app data directory
22+
*/
23+
public static getAppDataDir(): string {
24+
const platform = process.platform;
25+
26+
if (platform === 'darwin') {
27+
// macOS: ~/Library/Application Support/taskqueue-mcp
28+
return join(homedir(), 'Library', 'Application Support', 'taskqueue-mcp');
29+
} else if (platform === 'win32') {
30+
// Windows: %APPDATA%\taskqueue-mcp (usually C:\Users\<user>\AppData\Roaming\taskqueue-mcp)
31+
return join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'taskqueue-mcp');
32+
} else {
33+
// Linux/Unix/Others: Use XDG Base Directory if available, otherwise ~/.local/share/taskqueue-mcp
34+
const xdgDataHome = process.env.XDG_DATA_HOME;
35+
const linuxDefaultDir = join(homedir(), '.local', 'share', 'taskqueue-mcp');
36+
return xdgDataHome ? join(xdgDataHome, 'taskqueue-mcp') : linuxDefaultDir;
37+
}
38+
}
39+
40+
/**
41+
* Loads and initializes task data from the JSON file
42+
*/
43+
public async loadAndInitializeTasks(): Promise<InitializedTaskData> {
44+
const data = await this.loadTasks();
45+
const { maxProjectId, maxTaskId } = this.calculateMaxIds(data);
46+
47+
return {
48+
data,
49+
maxProjectId,
50+
maxTaskId
51+
};
52+
}
53+
54+
private calculateMaxIds(data: TaskManagerFile): { maxProjectId: number; maxTaskId: number } {
55+
const allTaskIds: number[] = [];
56+
const allProjectIds: number[] = [];
57+
58+
for (const proj of data.projects) {
59+
const projNum = Number.parseInt(proj.projectId.replace("proj-", ""), 10);
60+
if (!Number.isNaN(projNum)) {
61+
allProjectIds.push(projNum);
62+
}
63+
for (const t of proj.tasks) {
64+
const tNum = Number.parseInt(t.id.replace("task-", ""), 10);
65+
if (!Number.isNaN(tNum)) {
66+
allTaskIds.push(tNum);
67+
}
68+
}
69+
}
70+
71+
return {
72+
maxProjectId: allProjectIds.length > 0 ? Math.max(...allProjectIds) : 0,
73+
maxTaskId: allTaskIds.length > 0 ? Math.max(...allTaskIds) : 0
74+
};
75+
}
76+
77+
/**
78+
* Loads raw task data from the JSON file
79+
*/
80+
private async loadTasks(): Promise<TaskManagerFile> {
81+
try {
82+
const data = await readFile(this.filePath, "utf-8");
83+
return JSON.parse(data);
84+
} catch (error) {
85+
// Initialize with empty data for any initialization error
86+
// This includes file not found, permission issues, invalid JSON, etc.
87+
return { projects: [] };
88+
}
89+
}
90+
91+
/**
92+
* Saves task data to the JSON file
93+
*/
94+
public async saveTasks(data: TaskManagerFile): Promise<void> {
95+
try {
96+
// Ensure directory exists before writing
97+
const dir = dirname(this.filePath);
98+
await mkdir(dir, { recursive: true });
99+
100+
await writeFile(
101+
this.filePath,
102+
JSON.stringify(data, null, 2),
103+
"utf-8"
104+
);
105+
} catch (error) {
106+
if (error instanceof Error && error.message.includes("EROFS")) {
107+
throw createError(
108+
ErrorCode.ReadOnlyFileSystem,
109+
"Cannot save tasks: read-only file system",
110+
{ originalError: error }
111+
);
112+
}
113+
throw createError(
114+
ErrorCode.FileWriteError,
115+
"Failed to save tasks file",
116+
{ originalError: error }
117+
);
118+
}
119+
}
120+
}

0 commit comments

Comments
 (0)