@gonzui/claude-task-manager
Version:
Task management extension for Claude Code with archiving and history
774 lines (725 loc) • 32 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TaskManager = void 0;
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const date_fns_1 = require("date-fns");
const child_process_1 = require("child_process");
const chalk_1 = __importDefault(require("chalk"));
const types_1 = require("../types");
const i18n_1 = require("./i18n");
class TaskManager {
constructor(workingDir) {
// If no directory specified, try to find project root with .claude-tasks
const baseDir = workingDir || this.findProjectRoot() || process.cwd();
this.config = {
workingDir: baseDir,
taskFile: path.join(baseDir, 'task.md'),
archiveDir: path.join(baseDir, 'archive'),
configDir: path.join(baseDir, '.claude-tasks'),
configFile: path.join(baseDir, '.claude-tasks', 'config.json')
};
this.i18n = i18n_1.I18n.getInstance();
// Initialize i18n with default language if not already initialized
// This ensures i18n is ready even if init() hasn't been called yet
if (!this.i18n.isInitialized()) {
this.i18n.init('en').catch(() => {
// Ignore initialization errors in constructor
});
}
}
/**
* Detect initial language from environment
*/
detectInitialLanguage() {
// Check environment variables
const lang = process.env.LANG || process.env.LANGUAGE || '';
// If Japanese locale is detected, use Japanese
if (lang.toLowerCase().includes('ja')) {
return 'ja';
}
// Default to English
return 'en';
}
/**
* Find the project root by looking for .claude-tasks directory
* Similar to how git finds .git directory
*/
findProjectRoot() {
let currentDir = process.cwd();
const root = path.parse(currentDir).root;
while (currentDir !== root) {
const configDir = path.join(currentDir, '.claude-tasks');
try {
if (fs.existsSync(configDir) && fs.statSync(configDir).isDirectory()) {
return currentDir;
}
}
catch {
// Ignore errors and continue searching
}
currentDir = path.dirname(currentDir);
}
// Check root directory as well
const rootConfigDir = path.join(root, '.claude-tasks');
try {
if (fs.existsSync(rootConfigDir) && fs.statSync(rootConfigDir).isDirectory()) {
return root;
}
}
catch {
// Ignore errors
}
return null;
}
async init() {
try {
// Create necessary directories
await fs.ensureDir(this.config.archiveDir);
await fs.ensureDir(this.config.configDir);
// Create config file if it doesn't exist
if (!await fs.pathExists(this.config.configFile)) {
// Determine initial language from environment or default to 'en'
const initialLang = this.detectInitialLanguage();
// Initialize i18n BEFORE creating config to get proper defaults
await this.i18n.init(initialLang);
// Now create config with language-aware defaults
const isJapanese = initialLang === 'ja';
const defaultConfig = {
created: new Date().toISOString(),
taskTemplate: this.getDefaultTaskTemplate(),
claudeCommand: 'claude',
defaultTaskTitle: isJapanese ? '新しいタスク' : 'New Task',
archiveDir: 'archive',
language: initialLang,
defaultPrerequisites: isJapanese ? ['<!-- 前提条件を記入してください -->'] : ['<!-- Add prerequisites here -->'],
defaultRules: isJapanese ? ['<!-- ルールを記入してください -->'] : ['<!-- Add rules here -->'],
defaultTasks: isJapanese ? ['タスク 1', 'タスク 2', 'タスク 3'] : ['Task 1', 'Task 2', 'Task 3']
};
await fs.writeJson(this.config.configFile, defaultConfig, { spaces: 2 });
}
else {
// If config exists, initialize i18n with configured language
const config = await this.getConfig();
await this.i18n.init(config.language || 'en');
}
// Create initial task file if it doesn't exist
if (!await fs.pathExists(this.config.taskFile)) {
await this.createTaskFile('Initial Task', 'Welcome to Claude Task Manager!');
}
// Create Claude Code custom command if .claude/commands directory exists
await this.createClaudeCustomCommand();
// Update .gitignore
await this.updateGitignore();
}
catch (error) {
throw new types_1.FileSystemError(this.i18n.t('errors.initFailed', { error: error instanceof Error ? error.message : 'Unknown error' }), this.config.workingDir, 'init');
}
}
async createNewTask(options = {}) {
try {
// Ensure i18n is initialized with project config
if (!this.i18n.isInitialized()) {
try {
const config = await this.getConfig();
await this.i18n.init(config.language || 'en');
}
catch {
await this.i18n.init('en');
}
}
// Archive current task if it exists
if (await fs.pathExists(this.config.taskFile)) {
await this.archiveCurrentTask();
}
// Create new task file
const title = options.title || `Task ${(0, date_fns_1.format)(new Date(), 'yyyy-MM-dd HH:mm')}`;
const description = options.description || this.i18n.t('commands.new.success', { title: '' }).replace('✅ ', '').replace(': ', '');
await this.createTaskFile(title, description, options);
return this.config.taskFile;
}
catch (error) {
throw new types_1.TaskManagerError(this.i18n.t('errors.createTaskFailed', { error: error instanceof Error ? error.message : 'Unknown error' }), 'CREATE_TASK_ERROR');
}
}
async createTaskFile(title, description, options = {}) {
const template = await this.getTaskTemplate();
const config = await this.getConfig();
const isJapanese = config.language === 'ja';
// Helper function to format array to markdown list
const formatArrayToMarkdown = (items, defaultValue) => {
if (Array.isArray(items)) {
if (items.length === 0) {
return defaultValue;
}
// First item without bullet if it's a comment
const firstItem = items[0];
const isComment = firstItem.startsWith('<!--') && firstItem.endsWith('-->');
let result = isComment ? firstItem : `- ${firstItem}`;
// Rest of items with bullets
for (let i = 1; i < items.length; i++) {
result += `\n- ${items[i]}`;
}
return result;
}
return items || defaultValue;
};
const defaultPrerequisites = isJapanese ? '<!-- 前提条件を記入してください -->' : '<!-- Add prerequisites here -->';
const defaultRules = isJapanese ? '<!-- ルールを記入してください -->' : '<!-- Add rules here -->';
const defaultTaskItems = isJapanese ? '- タスク 1\n- タスク 2\n- タスク 3' : '- Task 1\n- Task 2\n- Task 3';
const variables = {
TITLE: title,
DESCRIPTION: description,
DATE: new Date().toISOString(),
TIMESTAMP: (0, date_fns_1.format)(new Date(), 'yyyy-MM-dd HH:mm:ss'),
PRIORITY: options.priority || 'medium',
TAGS: options.tags?.join(', ') || '',
PREREQUISITES: options.prerequisites || formatArrayToMarkdown(config.defaultPrerequisites, defaultPrerequisites),
RULES: options.rules || formatArrayToMarkdown(config.defaultRules, defaultRules),
TASKS: options.tasks || formatArrayToMarkdown(config.defaultTasks, defaultTaskItems)
};
let content = template;
Object.entries(variables).forEach(([key, value]) => {
content = content.replace(new RegExp(`{{${key}}}`, 'g'), value);
});
await fs.writeFile(this.config.taskFile, content);
}
async archiveCurrentTask() {
try {
if (!await fs.pathExists(this.config.taskFile)) {
return null;
}
const now = new Date();
const timestamp = (0, date_fns_1.format)(now, 'yyyy-MM-dd_HH-mm-ss');
const milliseconds = now.getMilliseconds().toString().padStart(3, '0');
const archiveFileName = `${timestamp}-${milliseconds}_task.md`;
const archivePath = path.join(this.config.archiveDir, archiveFileName);
// Read current task and add archive metadata
const currentContent = await fs.readFile(this.config.taskFile, 'utf8');
const archivedContent = `<!-- Archived: ${new Date().toISOString()} -->\n\n${currentContent}`;
await fs.writeFile(archivePath, archivedContent);
await fs.remove(this.config.taskFile);
return archivePath;
}
catch (error) {
throw new types_1.FileSystemError(`Failed to archive current task: ${error instanceof Error ? error.message : 'Unknown error'}`, this.config.taskFile, 'archive');
}
}
async runTask(verbose = false, debug = false, editPermission = true) {
if (!await fs.pathExists(this.config.taskFile)) {
throw new types_1.TaskManagerError('No task.md file found. Run "claude-task new" first.', 'NO_TASK_FILE');
}
const startTime = Date.now();
try {
const config = await this.getConfig();
const taskContent = await fs.readFile(this.config.taskFile, 'utf8');
// Execute Claude with task content
const claudeCommand = config.claudeCommand || 'claude';
console.log(`\n🚀 Executing task with ${claudeCommand}...`);
// Get relative path for display and prompt
const taskPath = path.resolve(this.config.taskFile);
const relativePath = path.relative(process.cwd(), taskPath);
// Create prompt with @task.md reference and exit instruction
const prompt = `Please execute the tasks in @${relativePath} and then exit. Do not enter interactive mode.`;
// Show task file being executed
console.log(chalk_1.default.gray(`📄 Task file: ${relativePath}`));
// Debug output
if (debug) {
console.log(chalk_1.default.gray(`🔍 Command: ${claudeCommand}`));
console.log(chalk_1.default.gray(`🔍 Full path: ${taskPath}`));
console.log(chalk_1.default.gray(`🔍 Prompt: ${prompt}`));
}
let result;
try {
// Execute Claude with the prompt
result = await this.executeClaude(prompt, claudeCommand, editPermission);
console.log(chalk_1.default.green('\n✅ Claude Code execution completed'));
}
catch (error) {
console.error(chalk_1.default.red('\n❌ Claude Code execution failed'));
throw error;
}
const duration = Date.now() - startTime;
const executionResult = {
success: true,
output: result,
timestamp: new Date().toISOString(),
duration
};
// Update task file with execution log
await this.logExecution(executionResult);
return executionResult;
}
catch (error) {
const duration = Date.now() - startTime;
const executionResult = {
success: false,
output: '',
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString(),
duration
};
await this.logExecution(executionResult);
throw error;
}
}
async executeClaude(prompt, claudeCommand = 'claude', editPermission = true) {
return new Promise((resolve, reject) => {
// Execute claude with the prompt in non-interactive mode
// Use 'inherit' to allow Claude to show output in the terminal
// Use --print flag to exit after completing the task
// Use --dangerously-skip-permissions to allow file edits (if editPermission is true)
const args = editPermission
? ['--dangerously-skip-permissions', '--print', prompt]
: ['--print', prompt];
const claudeProcess = (0, child_process_1.spawn)(claudeCommand, args, {
stdio: 'inherit',
shell: false
});
claudeProcess.on('close', (code) => {
if (code === 0) {
resolve('Claude Code execution completed');
}
else {
reject(new types_1.ClaudeExecutionError(`Claude Code failed with exit code ${code}`, code || undefined));
}
});
claudeProcess.on('error', (error) => {
reject(new types_1.ClaudeExecutionError(`Failed to start Claude Code: ${error.message}`));
});
});
}
async recordExecution(result) {
await this.logExecution(result);
}
async logExecution(result) {
const status = result.success ? '✅ Success' : '❌ Failed';
const logEntry = `\n\n## Execution Log - ${(0, date_fns_1.format)(new Date(result.timestamp), 'yyyy-MM-dd HH:mm:ss')} (${result.duration}ms)\n\n**Status:** ${status}\n\n${result.success ? result.output : result.error}\n\n---\n`;
await fs.appendFile(this.config.taskFile, logEntry);
}
async getHistory(limit = 10) {
try {
if (!await fs.pathExists(this.config.archiveDir)) {
return [];
}
const files = await fs.readdir(this.config.archiveDir);
const taskFiles = files
.filter(file => file.endsWith('_task.md'))
.sort()
.reverse()
.slice(0, limit);
const history = [];
for (const file of taskFiles) {
const filePath = path.join(this.config.archiveDir, file);
const content = await fs.readFile(filePath, 'utf8');
const stats = await fs.stat(filePath);
const titleMatch = content.match(/# (.+)/);
const title = titleMatch ? titleMatch[1] : 'Untitled';
const date = file.split('_')[0];
history.push({
file,
title,
date,
path: filePath,
size: stats.size
});
}
return history;
}
catch (error) {
throw new types_1.FileSystemError(this.i18n.t('errors.historyFailed', { error: error instanceof Error ? error.message : 'Unknown error' }), this.config.archiveDir, 'read');
}
}
async getStatus() {
try {
const currentTaskExists = await fs.pathExists(this.config.taskFile);
let currentTask = null;
let currentTaskSize;
if (currentTaskExists) {
const content = await fs.readFile(this.config.taskFile, 'utf8');
const stats = await fs.stat(this.config.taskFile);
const titleMatch = content.match(/# (.+)/);
currentTask = titleMatch ? titleMatch[1] : 'Untitled Task';
currentTaskSize = stats.size;
}
const archiveFiles = await fs.pathExists(this.config.archiveDir)
? (await fs.readdir(this.config.archiveDir)).filter(f => f.endsWith('_task.md'))
: [];
// Get last run time and count executions from task file
let lastRun = null;
let totalExecutions = 0;
if (currentTaskExists) {
const content = await fs.readFile(this.config.taskFile, 'utf8');
const logMatches = content.match(/## Execution Log - (.+?) \(/g);
if (logMatches && logMatches.length > 0) {
totalExecutions = logMatches.length;
const lastLogMatch = logMatches[logMatches.length - 1];
const timeMatch = lastLogMatch.match(/## Execution Log - (.+?) \(/);
lastRun = timeMatch ? timeMatch[1] : null;
}
}
return {
currentTask,
archivedCount: archiveFiles.length,
lastRun,
totalExecutions,
currentTaskSize
};
}
catch (error) {
throw new types_1.TaskManagerError(this.i18n.t('errors.statusFailed', { error: error instanceof Error ? error.message : 'Unknown error' }), 'GET_STATUS_ERROR');
}
}
async getTaskTemplate() {
try {
const config = await this.getConfig();
return config.taskTemplate || this.getDefaultTaskTemplate();
}
catch {
return this.getDefaultTaskTemplate();
}
}
getDefaultTaskTemplate() {
try {
const t = this.i18n.getNamespace('taskTemplate');
// Check if namespace is properly loaded
if (!t || !t.title) {
throw new Error('i18n taskTemplate namespace not loaded');
}
return `${t.title}
${t.created}
${t.priority}
${t.tags}
${t.description}
${t.prerequisites}
${t.rules}
${t.tasks}
${t.context}
${t.notes}
${t.footer}
`;
}
catch {
// Fallback template if i18n is not initialized
return `# {{TITLE}}
**Created:** {{DATE}}
**Priority:** {{PRIORITY}}
**Tags:** {{TAGS}}
## Description
{{DESCRIPTION}}
## Prerequisites
{{PREREQUISITES}}
## Rules
{{RULES}}
## Tasks
{{TASKS}}
## Context
<!-- Add context for Claude Code execution -->
## Notes
<!-- Add your notes here -->
---
*Generated by Claude Task Manager*`;
}
}
async getConfig() {
try {
if (await fs.pathExists(this.config.configFile)) {
return await fs.readJson(this.config.configFile);
}
return {
created: new Date().toISOString(),
taskTemplate: this.getDefaultTaskTemplate(),
claudeCommand: 'claude'
};
}
catch (error) {
throw new types_1.FileSystemError(this.i18n.t('errors.configFailed', { error: error instanceof Error ? error.message : 'Unknown error' }), this.config.configFile, 'read');
}
}
async updateConfig(updates) {
try {
const currentConfig = await this.getConfig();
let newConfig = { ...currentConfig, ...updates };
// Update i18n language if changed
if (updates.language && updates.language !== this.i18n.getLanguage()) {
await this.i18n.init(updates.language);
// Update language-specific defaults if they haven't been customized
const isJapanese = updates.language === 'ja';
// Only update defaults if they match the old language defaults
const isDefaultPrerequisites = (val) => {
if (typeof val === 'string') {
return val === '<!-- Add prerequisites here -->' || val === '<!-- 前提条件を記入してください -->';
}
if (Array.isArray(val) && val.length === 1) {
return val[0] === '<!-- Add prerequisites here -->' || val[0] === '<!-- 前提条件を記入してください -->';
}
return false;
};
const isDefaultRules = (val) => {
if (typeof val === 'string') {
return val === '<!-- Add rules here -->' || val === '<!-- ルールを記入してください -->';
}
if (Array.isArray(val) && val.length === 1) {
return val[0] === '<!-- Add rules here -->' || val[0] === '<!-- ルールを記入してください -->';
}
return false;
};
const isDefaultTasks = (val) => {
if (typeof val === 'string') {
return val === '- [ ] Task 1\n- [ ] Task 2\n- [ ] Task 3' || val === '- [ ] タスク 1\n- [ ] タスク 2\n- [ ] タスク 3' ||
val === '- Task 1\n- Task 2\n- Task 3' || val === '- タスク 1\n- タスク 2\n- タスク 3';
}
if (Array.isArray(val) && val.length === 3) {
return (val[0] === 'Task 1' && val[1] === 'Task 2' && val[2] === 'Task 3') ||
(val[0] === 'タスク 1' && val[1] === 'タスク 2' && val[2] === 'タスク 3');
}
return false;
};
if (isDefaultPrerequisites(currentConfig.defaultPrerequisites)) {
newConfig.defaultPrerequisites = isJapanese ? ['<!-- 前提条件を記入してください -->'] : ['<!-- Add prerequisites here -->'];
}
if (isDefaultRules(currentConfig.defaultRules)) {
newConfig.defaultRules = isJapanese ? ['<!-- ルールを記入してください -->'] : ['<!-- Add rules here -->'];
}
if (isDefaultTasks(currentConfig.defaultTasks)) {
newConfig.defaultTasks = isJapanese ? ['タスク 1', 'タスク 2', 'タスク 3'] : ['Task 1', 'Task 2', 'Task 3'];
}
if (currentConfig.defaultTaskTitle === 'New Task' ||
currentConfig.defaultTaskTitle === '新しいタスク') {
newConfig.defaultTaskTitle = isJapanese ? '新しいタスク' : 'New Task';
}
// Update the task template with the new language
newConfig.taskTemplate = this.getDefaultTaskTemplate();
}
await fs.writeJson(this.config.configFile, newConfig, { spaces: 2 });
}
catch (error) {
throw new types_1.FileSystemError(`Failed to update config: ${error instanceof Error ? error.message : 'Unknown error'}`, this.config.configFile, 'write');
}
}
async createClaudeCustomCommand() {
const claudeDir = path.join(this.config.workingDir, '.claude');
const claudeCommandsDir = path.join(claudeDir, 'commands');
try {
// Check if .claude directory exists
if (await fs.pathExists(claudeDir)) {
// Create commands directory inside existing .claude directory
await fs.ensureDir(claudeCommandsDir);
const taskCommandPath = path.join(claudeCommandsDir, 'task.md');
// Generate custom command content based on current language
const taskCommandContent = this.generateCustomCommandContent();
await fs.writeFile(taskCommandPath, taskCommandContent);
console.log(this.i18n.t('commands.init.customCommand'));
}
}
catch (error) {
// If we can't create the custom command, just log a warning and continue
console.warn('Could not create Claude custom command:', error);
}
}
generateCustomCommandContent() {
// Always generate English version for better Claude Code compatibility
return `# /task - Claude Task Manager
Task management custom command for Claude Code.
## Usage
\`/task <action> [options]\`
## Actions
### Create New Task
\`/task new "<task name>" [--priority high|medium|low] [--tags tag1,tag2]\`
Creates a task and saves it to task.md file.
### Check Current Task
\`/task status\`
Shows current task and its progress.
### Execute Task
\`/task run\`
Executes current task with Claude Code using task.md content as context.
### Task History
\`/task history [--limit n]\`
Shows past and archived tasks.
### Archive Task
\`/task archive\`
Moves completed task to archive folder.
## Implementation
This command uses the \`claude-task\` CLI tool for task management.
### How to Process Commands
**IMPORTANT**: Follow these instructions to execute actual CLI commands using the Bash tool.
1. When \`/task new "task name"\` is executed:
\`\`\`bash
claude-task new "task name"
\`\`\`
- Execute this command using the Bash tool
- Pass optional arguments appropriately (e.g., \`--priority high --tags auth,backend\`)
2. When \`/task status\` is executed:
\`\`\`bash
claude-task status
\`\`\`
- Execute this command using the Bash tool
3. When \`/task run\` is executed:
- Reference the \`.md\` file content
- Execute work according to the task content
4. When \`/task history\` is executed:
\`\`\`bash
claude-task history
\`\`\`
- Execute this command using the Bash tool
- Add \`--limit\` option if provided
5. When \`/task archive\` is executed:
\`\`\`bash
claude-task archive
\`\`\`
- Execute this command using the Bash tool
### Process Flow
\`\`\`
User: /task new "Implement new feature"
↓
Claude: Execute \`claude-task new "Implement new feature"\` using Bash tool
↓
Display result
\`\`\`
## Examples
1. Create new task:
\`\`\`
/task new "Implement user authentication" --priority high --tags auth,backend
\`\`\`
→ Execute with Bash: \`claude-task new "Implement user authentication" --priority high --tags auth,backend\`
2. Check current task:
\`\`\`
/task status
\`\`\`
→ Execute with Bash: \`claude-task status\`
3. Execute task:
\`\`\`
/task run
\`\`\`
→ Reference .md and execute work according to content
4. View task history:
\`\`\`
/task history --limit 10
\`\`\`
→ Execute with Bash: \`claude-task history --limit 10\`
5. Archive completed task:
\`\`\`
/task archive
\`\`\`
→ Execute with Bash: \`claude-task archive\`
## Important Notes
- **All commands must be executed using the Bash tool to run actual \`claude-task\` CLI commands**
- Task names with spaces must be enclosed in quotes
- Only \`/task run\` requires special processing (reference .md and execute)`;
}
async getTaskContent() {
if (!await fs.pathExists(this.config.taskFile)) {
throw new types_1.TaskManagerError(this.i18n.t('errors.noTaskFile'), 'NO_TASK_FILE');
}
return await fs.readFile(this.config.taskFile, 'utf8');
}
async getLanguage() {
const config = await this.getConfig();
return config.language || 'en';
}
async setLanguage(lang) {
await this.updateConfig({ language: lang });
}
async updateGitignore() {
const gitignorePath = path.join(this.config.workingDir, '.gitignore');
const entriesToAdd = [
'# Claude Task Manager',
'task.md',
'archive/',
'.claude-tasks/',
'',
'# Temporary task files',
'*.tmp.md',
'task.*.md',
'',
'# Claude Code custom commands (if you want to exclude them)',
'# .claude/commands/task.md'
];
try {
let content = '';
let existingEntries = new Set();
// Read existing .gitignore if it exists
if (await fs.pathExists(gitignorePath)) {
content = await fs.readFile(gitignorePath, 'utf8');
// Parse existing entries (normalize by removing comments and whitespace)
existingEntries = new Set(content.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#')));
}
// Check if we need to add any entries
const newEntries = [];
let needsUpdate = false;
for (const entry of entriesToAdd) {
if (entry.startsWith('#') || entry === '') {
// Always add comments and empty lines for formatting
newEntries.push(entry);
}
else if (!existingEntries.has(entry)) {
// Only add if not already present
newEntries.push(entry);
needsUpdate = true;
}
}
// Only update if there are new entries to add
if (needsUpdate) {
// Add a newline before our section if file exists and doesn't end with newline
if (content && !content.endsWith('\n')) {
content += '\n';
}
// Add another newline if file has content to separate our section
if (content) {
content += '\n';
}
content += newEntries.join('\n');
// Ensure file ends with newline
if (!content.endsWith('\n')) {
content += '\n';
}
await fs.writeFile(gitignorePath, content);
console.log(this.i18n.t('commands.init.gitignoreUpdated') || '✅ Updated .gitignore with task-related entries');
}
}
catch (error) {
// Don't fail init if .gitignore update fails
console.warn('Warning: Could not update .gitignore:', error);
}
}
}
exports.TaskManager = TaskManager;
//# sourceMappingURL=TaskManager.js.map