Compare commits
2 Commits
fix/update
...
feat/bundl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83c0eec982 | ||
|
|
a2a3229fd0 |
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
"task-master-ai": patch
|
|
||||||
---
|
|
||||||
|
|
||||||
Fix issues with task creation/update where subtasks are being created like id: <parent_task>.<subtask> instead if just id: <subtask>
|
|
||||||
22
.changeset/vast-plants-exist.md
Normal file
22
.changeset/vast-plants-exist.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
"task-master-ai": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
- **Git Worktree Detection:**
|
||||||
|
- Now properly skips Git initialization when inside existing Git worktree
|
||||||
|
- Prevents accidental nested repository creation
|
||||||
|
- **Flag System Overhaul:**
|
||||||
|
- `--git`/`--no-git` controls repository initialization
|
||||||
|
- `--aliases`/`--no-aliases` consistently manages shell alias creation
|
||||||
|
- `--git-tasks`/`--no-git-tasks` controls whether task files are stored in Git
|
||||||
|
- `--dry-run` accurately previews all initialization behaviors
|
||||||
|
- **GitTasks Functionality:**
|
||||||
|
- New `--git-tasks` flag includes task files in Git (comments them out in .gitignore)
|
||||||
|
- New `--no-git-tasks` flag excludes task files from Git (default behavior)
|
||||||
|
- Supports both CLI and MCP interfaces with proper parameter passing
|
||||||
|
|
||||||
|
**Implementation Details:**
|
||||||
|
- Added explicit Git worktree detection before initialization
|
||||||
|
- Refactored flag processing to ensure consistent behavior
|
||||||
|
|
||||||
|
- Fixes #734
|
||||||
@@ -26,6 +26,7 @@ This document provides a detailed reference for interacting with Taskmaster, cov
|
|||||||
* `--name <name>`: `Set the name for your project in Taskmaster's configuration.`
|
* `--name <name>`: `Set the name for your project in Taskmaster's configuration.`
|
||||||
* `--description <text>`: `Provide a brief description for your project.`
|
* `--description <text>`: `Provide a brief description for your project.`
|
||||||
* `--version <version>`: `Set the initial version for your project, e.g., '0.1.0'.`
|
* `--version <version>`: `Set the initial version for your project, e.g., '0.1.0'.`
|
||||||
|
* `--no-git`: `Skip initializing a Git repository entirely.`
|
||||||
* `-y, --yes`: `Initialize Taskmaster quickly using default settings without interactive prompts.`
|
* `-y, --yes`: `Initialize Taskmaster quickly using default settings without interactive prompts.`
|
||||||
* **Usage:** Run this once at the beginning of a new project.
|
* **Usage:** Run this once at the beginning of a new project.
|
||||||
* **MCP Variant Description:** `Set up the basic Taskmaster file structure and configuration in the current directory for a new project by running the 'task-master init' command.`
|
* **MCP Variant Description:** `Set up the basic Taskmaster file structure and configuration in the current directory for a new project by running the 'task-master init' command.`
|
||||||
@@ -36,6 +37,7 @@ This document provides a detailed reference for interacting with Taskmaster, cov
|
|||||||
* `authorName`: `Author name.` (CLI: `--author <author>`)
|
* `authorName`: `Author name.` (CLI: `--author <author>`)
|
||||||
* `skipInstall`: `Skip installing dependencies. Default is false.` (CLI: `--skip-install`)
|
* `skipInstall`: `Skip installing dependencies. Default is false.` (CLI: `--skip-install`)
|
||||||
* `addAliases`: `Add shell aliases tm and taskmaster. Default is false.` (CLI: `--aliases`)
|
* `addAliases`: `Add shell aliases tm and taskmaster. Default is false.` (CLI: `--aliases`)
|
||||||
|
* `noGit`: `Skip initializing a Git repository entirely. Default is false.` (CLI: `--no-git`)
|
||||||
* `yes`: `Skip prompts and use defaults/provided arguments. Default is false.` (CLI: `-y, --yes`)
|
* `yes`: `Skip prompts and use defaults/provided arguments. Default is false.` (CLI: `-y, --yes`)
|
||||||
* **Usage:** Run this once at the beginning of a new project, typically via an integrated tool like Cursor. Operates on the current working directory of the MCP server.
|
* **Usage:** Run this once at the beginning of a new project, typically via an integrated tool like Cursor. Operates on the current working directory of the MCP server.
|
||||||
* **Important:** Once complete, you *MUST* parse a prd in order to generate tasks. There will be no tasks files until then. The next step after initializing should be to create a PRD using the example PRD in .taskmaster/templates/example_prd.txt.
|
* **Important:** Once complete, you *MUST* parse a prd in order to generate tasks. There will be no tasks files until then. The next step after initializing should be to create a PRD using the example PRD in .taskmaster/templates/example_prd.txt.
|
||||||
|
|||||||
5
index.js
5
index.js
@@ -83,6 +83,11 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|||||||
.option('--skip-install', 'Skip installing dependencies')
|
.option('--skip-install', 'Skip installing dependencies')
|
||||||
.option('--dry-run', 'Show what would be done without making changes')
|
.option('--dry-run', 'Show what would be done without making changes')
|
||||||
.option('--aliases', 'Add shell aliases (tm, taskmaster)')
|
.option('--aliases', 'Add shell aliases (tm, taskmaster)')
|
||||||
|
.option('--no-aliases', 'Skip shell aliases (tm, taskmaster)')
|
||||||
|
.option('--git', 'Initialize Git repository')
|
||||||
|
.option('--no-git', 'Skip Git repository initialization')
|
||||||
|
.option('--git-tasks', 'Store tasks in Git')
|
||||||
|
.option('--no-git-tasks', 'No Git storage of tasks')
|
||||||
.action(async (cmdOptions) => {
|
.action(async (cmdOptions) => {
|
||||||
try {
|
try {
|
||||||
await runInitCLI(cmdOptions);
|
await runInitCLI(cmdOptions);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { convertAllRulesToProfileRules } from '../../../../src/utils/rule-transf
|
|||||||
/**
|
/**
|
||||||
* Direct function wrapper for initializing a project.
|
* Direct function wrapper for initializing a project.
|
||||||
* Derives target directory from session, sets CWD, and calls core init logic.
|
* Derives target directory from session, sets CWD, and calls core init logic.
|
||||||
* @param {object} args - Arguments containing initialization options (addAliases, skipInstall, yes, projectRoot, rules)
|
* @param {object} args - Arguments containing initialization options (addAliases, initGit, storeTasksInGit, skipInstall, yes, projectRoot, rules)
|
||||||
* @param {object} log - The FastMCP logger instance.
|
* @param {object} log - The FastMCP logger instance.
|
||||||
* @param {object} context - The context object, must contain { session }.
|
* @param {object} context - The context object, must contain { session }.
|
||||||
* @returns {Promise<{success: boolean, data?: any, error?: {code: string, message: string}}>} - Standard result object.
|
* @returns {Promise<{success: boolean, data?: any, error?: {code: string, message: string}}>} - Standard result object.
|
||||||
@@ -65,7 +65,9 @@ export async function initializeProjectDirect(args, log, context = {}) {
|
|||||||
// Construct options ONLY from the relevant flags in args
|
// Construct options ONLY from the relevant flags in args
|
||||||
// The core initializeProject operates in the current CWD, which we just set
|
// The core initializeProject operates in the current CWD, which we just set
|
||||||
const options = {
|
const options = {
|
||||||
aliases: args.addAliases,
|
addAliases: args.addAliases,
|
||||||
|
initGit: args.initGit,
|
||||||
|
storeTasksInGit: args.storeTasksInGit,
|
||||||
skipInstall: args.skipInstall,
|
skipInstall: args.skipInstall,
|
||||||
yes: true // Force yes mode
|
yes: true // Force yes mode
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,8 +23,18 @@ export function registerInitializeProjectTool(server) {
|
|||||||
addAliases: z
|
addAliases: z
|
||||||
.boolean()
|
.boolean()
|
||||||
.optional()
|
.optional()
|
||||||
.default(false)
|
.default(true)
|
||||||
.describe('Add shell aliases (tm, taskmaster) to shell config file.'),
|
.describe('Add shell aliases (tm, taskmaster) to shell config file.'),
|
||||||
|
initGit: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.default(true)
|
||||||
|
.describe('Initialize Git repository in project root.'),
|
||||||
|
storeTasksInGit: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.default(false)
|
||||||
|
.describe('Store tasks in Git (tasks.json and tasks/ directory).'),
|
||||||
yes: z
|
yes: z
|
||||||
.boolean()
|
.boolean()
|
||||||
.optional()
|
.optional()
|
||||||
|
|||||||
1673
package-lock.json
generated
1673
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
35
package.json
35
package.json
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.17.1",
|
"version": "0.17.1-test",
|
||||||
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
"task-master": "bin/task-master.js",
|
"task-master": "dist/task-master.cjs",
|
||||||
"task-master-mcp": "mcp-server/server.js",
|
"task-master-mcp": "dist/task-master-mcp.cjs",
|
||||||
"task-master-ai": "mcp-server/server.js"
|
"task-master-ai": "dist/task-master-mcp.cjs"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node --experimental-vm-modules node_modules/.bin/jest",
|
"test": "node --experimental-vm-modules node_modules/.bin/jest",
|
||||||
@@ -22,7 +22,13 @@
|
|||||||
"inspector": "npx @modelcontextprotocol/inspector node mcp-server/server.js",
|
"inspector": "npx @modelcontextprotocol/inspector node mcp-server/server.js",
|
||||||
"mcp-server": "node mcp-server/server.js",
|
"mcp-server": "node mcp-server/server.js",
|
||||||
"format-check": "biome format .",
|
"format-check": "biome format .",
|
||||||
"format": "biome format . --write"
|
"format": "biome format . --write",
|
||||||
|
"prebuild": "npm test",
|
||||||
|
"build": "vite build && npm run postbuild",
|
||||||
|
"postbuild": "node scripts/add-shebang.js",
|
||||||
|
"build:bundle": "npm run build",
|
||||||
|
"build:watch": "vite build --watch",
|
||||||
|
"test:build": "echo 'Testing bundled binaries:' && echo '🔧 CLI (task-master):' && node dist/task-master.cjs --version && echo '🔌 MCP (task-master-ai):' && node dist/task-master-mcp.cjs --help | head -5 && echo '✅ Both bundles work!'"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"claude",
|
"claude",
|
||||||
@@ -91,14 +97,11 @@
|
|||||||
"url": "https://github.com/eyaltoledano/claude-task-master/issues"
|
"url": "https://github.com/eyaltoledano/claude-task-master/issues"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"scripts/**",
|
"dist/**",
|
||||||
"assets/**",
|
|
||||||
".cursor/**",
|
|
||||||
"README-task-master.md",
|
|
||||||
"index.js",
|
|
||||||
"bin/**",
|
|
||||||
"mcp-server/**",
|
"mcp-server/**",
|
||||||
"src/**"
|
"README-task-master.md",
|
||||||
|
".cursor/**",
|
||||||
|
"assets/**"
|
||||||
],
|
],
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"node-fetch": "^2.6.12",
|
"node-fetch": "^2.6.12",
|
||||||
@@ -108,15 +111,19 @@
|
|||||||
"@biomejs/biome": "^1.9.4",
|
"@biomejs/biome": "^1.9.4",
|
||||||
"@changesets/changelog-github": "^0.5.1",
|
"@changesets/changelog-github": "^0.5.1",
|
||||||
"@changesets/cli": "^2.28.1",
|
"@changesets/cli": "^2.28.1",
|
||||||
|
"@rollup/plugin-json": "^6.1.0",
|
||||||
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"execa": "^8.0.1",
|
"execa": "^8.0.1",
|
||||||
"ink": "^5.0.1",
|
"ink": "^5.0.1",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-node": "^29.7.0",
|
"jest-environment-node": "^29.7.0",
|
||||||
"mock-fs": "^5.5.0",
|
"mock-fs": "^5.5.0",
|
||||||
|
"pkg": "^5.8.1",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"supertest": "^7.1.0",
|
"supertest": "^7.1.0",
|
||||||
"tsx": "^4.16.2"
|
"tsx": "^4.16.2",
|
||||||
|
"vite": "^6.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
82
scripts/add-shebang.js
Normal file
82
scripts/add-shebang.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import {
|
||||||
|
readFileSync,
|
||||||
|
writeFileSync,
|
||||||
|
chmodSync,
|
||||||
|
copyFileSync,
|
||||||
|
mkdirSync,
|
||||||
|
existsSync
|
||||||
|
} from 'fs';
|
||||||
|
import { join, dirname } from 'path';
|
||||||
|
|
||||||
|
const bundlePaths = [
|
||||||
|
join(process.cwd(), 'dist/task-master.cjs'), // CLI tool
|
||||||
|
join(process.cwd(), 'dist/task-master-mcp.cjs') // MCP server
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Copy necessary asset files to dist
|
||||||
|
const assetsToCopy = [
|
||||||
|
{
|
||||||
|
src: 'scripts/modules/supported-models.json',
|
||||||
|
dest: 'dist/supported-models.json'
|
||||||
|
},
|
||||||
|
{ src: 'README-task-master.md', dest: 'dist/README-task-master.md' }
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log('📁 Copying assets...');
|
||||||
|
for (const asset of assetsToCopy) {
|
||||||
|
const srcPath = join(process.cwd(), asset.src);
|
||||||
|
const destPath = join(process.cwd(), asset.dest);
|
||||||
|
|
||||||
|
if (existsSync(srcPath)) {
|
||||||
|
// Ensure destination directory exists
|
||||||
|
const destDir = dirname(destPath);
|
||||||
|
if (!existsSync(destDir)) {
|
||||||
|
mkdirSync(destDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
copyFileSync(srcPath, destPath);
|
||||||
|
console.log(` ✅ Copied ${asset.src} → ${asset.dest}`);
|
||||||
|
} else {
|
||||||
|
console.log(` ⚠️ Source not found: ${asset.src}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process each bundle file
|
||||||
|
for (const bundlePath of bundlePaths) {
|
||||||
|
const fileName = bundlePath.split('/').pop();
|
||||||
|
|
||||||
|
if (!existsSync(bundlePath)) {
|
||||||
|
console.log(`⚠️ Bundle not found: ${fileName}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the existing bundle
|
||||||
|
const bundleContent = readFileSync(bundlePath, 'utf8');
|
||||||
|
|
||||||
|
// Add shebang if it doesn't already exist
|
||||||
|
if (!bundleContent.startsWith('#!/usr/bin/env node')) {
|
||||||
|
const contentWithShebang = '#!/usr/bin/env node\n' + bundleContent;
|
||||||
|
writeFileSync(bundlePath, contentWithShebang);
|
||||||
|
console.log(`✅ Added shebang to ${fileName}`);
|
||||||
|
} else {
|
||||||
|
console.log(`✅ Shebang already exists in ${fileName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make it executable
|
||||||
|
chmodSync(bundlePath, 0o755);
|
||||||
|
console.log(`✅ Made ${fileName} executable`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('📦 Both bundles ready:');
|
||||||
|
console.log(' 🔧 CLI tool: dist/task-master.cjs');
|
||||||
|
console.log(' 🔌 MCP server: dist/task-master-mcp.cjs');
|
||||||
|
console.log('🧪 Test with:');
|
||||||
|
console.log(' node dist/task-master.cjs --version');
|
||||||
|
console.log(' node dist/task-master-mcp.cjs --help');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Post-build failed:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
196
scripts/init.js
196
scripts/init.js
@@ -23,6 +23,8 @@ import figlet from 'figlet';
|
|||||||
import boxen from 'boxen';
|
import boxen from 'boxen';
|
||||||
import gradient from 'gradient-string';
|
import gradient from 'gradient-string';
|
||||||
import { isSilentMode } from './modules/utils.js';
|
import { isSilentMode } from './modules/utils.js';
|
||||||
|
import { insideGitWorkTree } from './modules/utils/git-utils.js';
|
||||||
|
import { manageGitignoreFile } from '../src/utils/manage-gitignore.js';
|
||||||
import { RULE_PROFILES } from '../src/constants/profiles.js';
|
import { RULE_PROFILES } from '../src/constants/profiles.js';
|
||||||
import {
|
import {
|
||||||
convertAllRulesToProfileRules,
|
convertAllRulesToProfileRules,
|
||||||
@@ -320,6 +322,30 @@ async function initializeProject(options = {}) {
|
|||||||
// console.log('==================================================');
|
// console.log('==================================================');
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// Handle boolean aliases flags
|
||||||
|
if (options.aliases === true) {
|
||||||
|
options.addAliases = true; // --aliases flag provided
|
||||||
|
} else if (options.aliases === false) {
|
||||||
|
options.addAliases = false; // --no-aliases flag provided
|
||||||
|
}
|
||||||
|
// If options.aliases and options.noAliases are undefined, we'll prompt for it
|
||||||
|
|
||||||
|
// Handle boolean git flags
|
||||||
|
if (options.git === true) {
|
||||||
|
options.initGit = true; // --git flag provided
|
||||||
|
} else if (options.git === false) {
|
||||||
|
options.initGit = false; // --no-git flag provided
|
||||||
|
}
|
||||||
|
// If options.git and options.noGit are undefined, we'll prompt for it
|
||||||
|
|
||||||
|
// Handle boolean gitTasks flags
|
||||||
|
if (options.gitTasks === true) {
|
||||||
|
options.storeTasksInGit = true; // --git-tasks flag provided
|
||||||
|
} else if (options.gitTasks === false) {
|
||||||
|
options.storeTasksInGit = false; // --no-git-tasks flag provided
|
||||||
|
}
|
||||||
|
// If options.gitTasks and options.noGitTasks are undefined, we'll prompt for it
|
||||||
|
|
||||||
const skipPrompts = options.yes || (options.name && options.description);
|
const skipPrompts = options.yes || (options.name && options.description);
|
||||||
|
|
||||||
// if (!isSilentMode()) {
|
// if (!isSilentMode()) {
|
||||||
@@ -343,21 +369,44 @@ async function initializeProject(options = {}) {
|
|||||||
const projectVersion = options.version || '0.1.0';
|
const projectVersion = options.version || '0.1.0';
|
||||||
const authorName = options.author || 'Vibe coder';
|
const authorName = options.author || 'Vibe coder';
|
||||||
const dryRun = options.dryRun || false;
|
const dryRun = options.dryRun || false;
|
||||||
const addAliases = options.aliases || false;
|
const addAliases =
|
||||||
|
options.addAliases !== undefined ? options.addAliases : true; // Default to true if not specified
|
||||||
|
const initGit = options.initGit !== undefined ? options.initGit : true; // Default to true if not specified
|
||||||
|
const storeTasksInGit =
|
||||||
|
options.storeTasksInGit !== undefined ? options.storeTasksInGit : false; // Default to false if not specified
|
||||||
|
|
||||||
if (dryRun) {
|
if (dryRun) {
|
||||||
log('info', 'DRY RUN MODE: No files will be modified');
|
log('info', 'DRY RUN MODE: No files will be modified');
|
||||||
log('info', 'Would initialize Task Master project');
|
log('info', 'Would initialize Task Master project');
|
||||||
log('info', 'Would create/update necessary project files');
|
log('info', 'Would create/update necessary project files');
|
||||||
if (addAliases) {
|
|
||||||
log('info', 'Would add shell aliases for task-master');
|
// Show flag-specific behavior
|
||||||
}
|
log(
|
||||||
|
'info',
|
||||||
|
`${addAliases ? 'Would add shell aliases (tm, taskmaster)' : 'Would skip shell aliases'}`
|
||||||
|
);
|
||||||
|
log(
|
||||||
|
'info',
|
||||||
|
`${initGit ? 'Would initialize Git repository' : 'Would skip Git initialization'}`
|
||||||
|
);
|
||||||
|
log(
|
||||||
|
'info',
|
||||||
|
`${storeTasksInGit ? 'Would store tasks in Git' : 'Would exclude tasks from Git'}`
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dryRun: true
|
dryRun: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
createProjectStructure(addAliases, dryRun, options, selectedRuleProfiles);
|
createProjectStructure(
|
||||||
|
addAliases,
|
||||||
|
initGit,
|
||||||
|
storeTasksInGit,
|
||||||
|
dryRun,
|
||||||
|
options,
|
||||||
|
selectedRuleProfiles
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Interactive logic
|
// Interactive logic
|
||||||
log('info', 'Required options not provided, proceeding with prompts.');
|
log('info', 'Required options not provided, proceeding with prompts.');
|
||||||
@@ -367,14 +416,45 @@ async function initializeProject(options = {}) {
|
|||||||
input: process.stdin,
|
input: process.stdin,
|
||||||
output: process.stdout
|
output: process.stdout
|
||||||
});
|
});
|
||||||
// Only prompt for shell aliases
|
// Prompt for shell aliases (skip if --aliases or --no-aliases flag was provided)
|
||||||
const addAliasesInput = await promptQuestion(
|
let addAliasesPrompted = true; // Default to true
|
||||||
rl,
|
if (options.addAliases !== undefined) {
|
||||||
chalk.cyan(
|
addAliasesPrompted = options.addAliases; // Use flag value if provided
|
||||||
'Add shell aliases for task-master? This lets you type "tm" instead of "task-master" (Y/n): '
|
} else {
|
||||||
)
|
const addAliasesInput = await promptQuestion(
|
||||||
);
|
rl,
|
||||||
const addAliasesPrompted = addAliasesInput.trim().toLowerCase() !== 'n';
|
chalk.cyan(
|
||||||
|
'Add shell aliases for task-master? This lets you type "tm" instead of "task-master" (Y/n): '
|
||||||
|
)
|
||||||
|
);
|
||||||
|
addAliasesPrompted = addAliasesInput.trim().toLowerCase() !== 'n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompt for Git initialization (skip if --git or --no-git flag was provided)
|
||||||
|
let initGitPrompted = true; // Default to true
|
||||||
|
if (options.initGit !== undefined) {
|
||||||
|
initGitPrompted = options.initGit; // Use flag value if provided
|
||||||
|
} else {
|
||||||
|
const gitInitInput = await promptQuestion(
|
||||||
|
rl,
|
||||||
|
chalk.cyan('Initialize a Git repository in project root? (Y/n): ')
|
||||||
|
);
|
||||||
|
initGitPrompted = gitInitInput.trim().toLowerCase() !== 'n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompt for Git tasks storage (skip if --git-tasks or --no-git-tasks flag was provided)
|
||||||
|
let storeGitPrompted = false; // Default to false
|
||||||
|
if (options.storeTasksInGit !== undefined) {
|
||||||
|
storeGitPrompted = options.storeTasksInGit; // Use flag value if provided
|
||||||
|
} else {
|
||||||
|
const gitTasksInput = await promptQuestion(
|
||||||
|
rl,
|
||||||
|
chalk.cyan(
|
||||||
|
'Store tasks in Git (tasks.json and tasks/ directory)? (y/N): '
|
||||||
|
)
|
||||||
|
);
|
||||||
|
storeGitPrompted = gitTasksInput.trim().toLowerCase() === 'y';
|
||||||
|
}
|
||||||
|
|
||||||
// Confirm settings...
|
// Confirm settings...
|
||||||
console.log('\nTask Master Project settings:');
|
console.log('\nTask Master Project settings:');
|
||||||
@@ -384,6 +464,14 @@ async function initializeProject(options = {}) {
|
|||||||
),
|
),
|
||||||
chalk.white(addAliasesPrompted ? 'Yes' : 'No')
|
chalk.white(addAliasesPrompted ? 'Yes' : 'No')
|
||||||
);
|
);
|
||||||
|
console.log(
|
||||||
|
chalk.blue('Initialize Git repository in project root:'),
|
||||||
|
chalk.white(initGitPrompted ? 'Yes' : 'No')
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
chalk.blue('Store tasks in Git (tasks.json and tasks/ directory):'),
|
||||||
|
chalk.white(storeGitPrompted ? 'Yes' : 'No')
|
||||||
|
);
|
||||||
|
|
||||||
const confirmInput = await promptQuestion(
|
const confirmInput = await promptQuestion(
|
||||||
rl,
|
rl,
|
||||||
@@ -422,9 +510,21 @@ async function initializeProject(options = {}) {
|
|||||||
log('info', 'DRY RUN MODE: No files will be modified');
|
log('info', 'DRY RUN MODE: No files will be modified');
|
||||||
log('info', 'Would initialize Task Master project');
|
log('info', 'Would initialize Task Master project');
|
||||||
log('info', 'Would create/update necessary project files');
|
log('info', 'Would create/update necessary project files');
|
||||||
if (addAliasesPrompted) {
|
|
||||||
log('info', 'Would add shell aliases for task-master');
|
// Show flag-specific behavior
|
||||||
}
|
log(
|
||||||
|
'info',
|
||||||
|
`${addAliasesPrompted ? 'Would add shell aliases (tm, taskmaster)' : 'Would skip shell aliases'}`
|
||||||
|
);
|
||||||
|
log(
|
||||||
|
'info',
|
||||||
|
`${initGitPrompted ? 'Would initialize Git repository' : 'Would skip Git initialization'}`
|
||||||
|
);
|
||||||
|
log(
|
||||||
|
'info',
|
||||||
|
`${storeGitPrompted ? 'Would store tasks in Git' : 'Would exclude tasks from Git'}`
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dryRun: true
|
dryRun: true
|
||||||
};
|
};
|
||||||
@@ -433,6 +533,8 @@ async function initializeProject(options = {}) {
|
|||||||
// Create structure using only necessary values
|
// Create structure using only necessary values
|
||||||
createProjectStructure(
|
createProjectStructure(
|
||||||
addAliasesPrompted,
|
addAliasesPrompted,
|
||||||
|
initGitPrompted,
|
||||||
|
storeGitPrompted,
|
||||||
dryRun,
|
dryRun,
|
||||||
options,
|
options,
|
||||||
selectedRuleProfiles
|
selectedRuleProfiles
|
||||||
@@ -458,6 +560,8 @@ function promptQuestion(rl, question) {
|
|||||||
// Function to create the project structure
|
// Function to create the project structure
|
||||||
function createProjectStructure(
|
function createProjectStructure(
|
||||||
addAliases,
|
addAliases,
|
||||||
|
initGit,
|
||||||
|
storeTasksInGit,
|
||||||
dryRun,
|
dryRun,
|
||||||
options,
|
options,
|
||||||
selectedRuleProfiles = RULE_PROFILES // Default to all rule profiles
|
selectedRuleProfiles = RULE_PROFILES // Default to all rule profiles
|
||||||
@@ -507,18 +611,55 @@ function createProjectStructure(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Copy .gitignore
|
// Copy .gitignore with GitTasks preference
|
||||||
copyTemplateFile('gitignore', path.join(targetDir, GITIGNORE_FILE));
|
try {
|
||||||
|
const gitignoreTemplatePath = path.join(
|
||||||
|
__dirname,
|
||||||
|
'..',
|
||||||
|
'assets',
|
||||||
|
'gitignore'
|
||||||
|
);
|
||||||
|
const templateContent = fs.readFileSync(gitignoreTemplatePath, 'utf8');
|
||||||
|
manageGitignoreFile(
|
||||||
|
path.join(targetDir, GITIGNORE_FILE),
|
||||||
|
templateContent,
|
||||||
|
storeTasksInGit,
|
||||||
|
log
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
log('error', `Failed to create .gitignore: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Copy example_prd.txt to NEW location
|
// Copy example_prd.txt to NEW location
|
||||||
copyTemplateFile('example_prd.txt', path.join(targetDir, EXAMPLE_PRD_FILE));
|
copyTemplateFile('example_prd.txt', path.join(targetDir, EXAMPLE_PRD_FILE));
|
||||||
|
|
||||||
// Initialize git repository if git is available
|
// Initialize git repository if git is available
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(path.join(targetDir, '.git'))) {
|
if (initGit === false) {
|
||||||
log('info', 'Initializing git repository...');
|
log('info', 'Git initialization skipped due to --no-git flag.');
|
||||||
execSync('git init', { stdio: 'ignore' });
|
} else if (initGit === true) {
|
||||||
log('success', 'Git repository initialized');
|
if (insideGitWorkTree()) {
|
||||||
|
log(
|
||||||
|
'info',
|
||||||
|
'Existing Git repository detected – skipping git init despite --git flag.'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log('info', 'Initializing Git repository due to --git flag...');
|
||||||
|
execSync('git init', { cwd: targetDir, stdio: 'ignore' });
|
||||||
|
log('success', 'Git repository initialized');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Default behavior when no flag is provided (from interactive prompt)
|
||||||
|
if (insideGitWorkTree()) {
|
||||||
|
log('info', 'Existing Git repository detected – skipping git init.');
|
||||||
|
} else {
|
||||||
|
log(
|
||||||
|
'info',
|
||||||
|
'No Git repository detected. Initializing one in project root...'
|
||||||
|
);
|
||||||
|
execSync('git init', { cwd: targetDir, stdio: 'ignore' });
|
||||||
|
log('success', 'Git repository initialized');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log('warn', 'Git not available, skipping repository initialization');
|
log('warn', 'Git not available, skipping repository initialization');
|
||||||
@@ -599,6 +740,17 @@ function createProjectStructure(
|
|||||||
}
|
}
|
||||||
// ====================================
|
// ====================================
|
||||||
|
|
||||||
|
// Add shell aliases if requested
|
||||||
|
if (addAliases && !dryRun) {
|
||||||
|
log('info', 'Adding shell aliases...');
|
||||||
|
const aliasResult = addShellAliases();
|
||||||
|
if (aliasResult) {
|
||||||
|
log('success', 'Shell aliases added successfully');
|
||||||
|
}
|
||||||
|
} else if (addAliases && dryRun) {
|
||||||
|
log('info', 'DRY RUN: Would add shell aliases (tm, taskmaster)');
|
||||||
|
}
|
||||||
|
|
||||||
// Display success message
|
// Display success message
|
||||||
if (!isSilentMode()) {
|
if (!isSilentMode()) {
|
||||||
console.log(
|
console.log(
|
||||||
|
|||||||
@@ -3342,6 +3342,11 @@ ${result.result}
|
|||||||
.option('--skip-install', 'Skip installing dependencies')
|
.option('--skip-install', 'Skip installing dependencies')
|
||||||
.option('--dry-run', 'Show what would be done without making changes')
|
.option('--dry-run', 'Show what would be done without making changes')
|
||||||
.option('--aliases', 'Add shell aliases (tm, taskmaster)')
|
.option('--aliases', 'Add shell aliases (tm, taskmaster)')
|
||||||
|
.option('--no-aliases', 'Skip shell aliases (tm, taskmaster)')
|
||||||
|
.option('--git', 'Initialize Git repository')
|
||||||
|
.option('--no-git', 'Skip Git repository initialization')
|
||||||
|
.option('--git-tasks', 'Store tasks in Git')
|
||||||
|
.option('--no-git-tasks', 'No Git storage of tasks')
|
||||||
.action(async (cmdOptions) => {
|
.action(async (cmdOptions) => {
|
||||||
// cmdOptions contains parsed arguments
|
// cmdOptions contains parsed arguments
|
||||||
// Parse rules: accept space or comma separated, default to all available rules
|
// Parse rules: accept space or comma separated, default to all available rules
|
||||||
|
|||||||
@@ -39,23 +39,7 @@ const updatedTaskSchema = z
|
|||||||
priority: z.string().optional(),
|
priority: z.string().optional(),
|
||||||
details: z.string().optional(),
|
details: z.string().optional(),
|
||||||
testStrategy: z.string().optional(),
|
testStrategy: z.string().optional(),
|
||||||
subtasks: z
|
subtasks: z.array(z.any()).optional()
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
id: z
|
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.positive()
|
|
||||||
.describe('Sequential subtask ID starting from 1'),
|
|
||||||
title: z.string(),
|
|
||||||
description: z.string(),
|
|
||||||
status: z.string(),
|
|
||||||
dependencies: z.array(z.number().int()).optional(),
|
|
||||||
details: z.string().optional(),
|
|
||||||
testStrategy: z.string().optional()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.optional()
|
|
||||||
})
|
})
|
||||||
.strip(); // Allows parsing even if AI adds extra fields, but validation focuses on schema
|
.strip(); // Allows parsing even if AI adds extra fields, but validation focuses on schema
|
||||||
|
|
||||||
@@ -457,8 +441,6 @@ Guidelines:
|
|||||||
9. Instead, add a new subtask that clearly indicates what needs to be changed or replaced
|
9. Instead, add a new subtask that clearly indicates what needs to be changed or replaced
|
||||||
10. Use the existence of completed subtasks as an opportunity to make new subtasks more specific and targeted
|
10. Use the existence of completed subtasks as an opportunity to make new subtasks more specific and targeted
|
||||||
11. Ensure any new subtasks have unique IDs that don't conflict with existing ones
|
11. Ensure any new subtasks have unique IDs that don't conflict with existing ones
|
||||||
12. CRITICAL: For subtask IDs, use ONLY numeric values (1, 2, 3, etc.) NOT strings ("1", "2", "3")
|
|
||||||
13. CRITICAL: Subtask IDs should start from 1 and increment sequentially (1, 2, 3...) - do NOT use parent task ID as prefix
|
|
||||||
|
|
||||||
The changes described in the prompt should be thoughtfully applied to make the task more accurate and actionable.`;
|
The changes described in the prompt should be thoughtfully applied to make the task more accurate and actionable.`;
|
||||||
|
|
||||||
@@ -591,37 +573,6 @@ The changes described in the prompt should be thoughtfully applied to make the t
|
|||||||
);
|
);
|
||||||
updatedTask.status = taskToUpdate.status;
|
updatedTask.status = taskToUpdate.status;
|
||||||
}
|
}
|
||||||
// Fix subtask IDs if they exist (ensure they are numeric and sequential)
|
|
||||||
if (updatedTask.subtasks && Array.isArray(updatedTask.subtasks)) {
|
|
||||||
let currentSubtaskId = 1;
|
|
||||||
updatedTask.subtasks = updatedTask.subtasks.map((subtask) => {
|
|
||||||
// Fix AI-generated subtask IDs that might be strings or use parent ID as prefix
|
|
||||||
const correctedSubtask = {
|
|
||||||
...subtask,
|
|
||||||
id: currentSubtaskId, // Override AI-generated ID with correct sequential ID
|
|
||||||
dependencies: Array.isArray(subtask.dependencies)
|
|
||||||
? subtask.dependencies
|
|
||||||
.map((dep) =>
|
|
||||||
typeof dep === 'string' ? parseInt(dep, 10) : dep
|
|
||||||
)
|
|
||||||
.filter(
|
|
||||||
(depId) =>
|
|
||||||
!Number.isNaN(depId) &&
|
|
||||||
depId >= 1 &&
|
|
||||||
depId < currentSubtaskId
|
|
||||||
)
|
|
||||||
: [],
|
|
||||||
status: subtask.status || 'pending'
|
|
||||||
};
|
|
||||||
currentSubtaskId++;
|
|
||||||
return correctedSubtask;
|
|
||||||
});
|
|
||||||
report(
|
|
||||||
'info',
|
|
||||||
`Fixed ${updatedTask.subtasks.length} subtask IDs to be sequential numeric IDs.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preserve completed subtasks (Keep existing logic)
|
// Preserve completed subtasks (Keep existing logic)
|
||||||
if (taskToUpdate.subtasks?.length > 0) {
|
if (taskToUpdate.subtasks?.length > 0) {
|
||||||
if (!updatedTask.subtasks) {
|
if (!updatedTask.subtasks) {
|
||||||
|
|||||||
@@ -349,6 +349,25 @@ function getCurrentBranchSync(projectRoot) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the current working directory is inside a Git work-tree.
|
||||||
|
* Uses `git rev-parse --is-inside-work-tree` which is more specific than --git-dir
|
||||||
|
* for detecting work-trees (excludes bare repos and .git directories).
|
||||||
|
* This is ideal for preventing accidental git init in existing work-trees.
|
||||||
|
* @returns {boolean} True if inside a Git work-tree, false otherwise.
|
||||||
|
*/
|
||||||
|
function insideGitWorkTree() {
|
||||||
|
try {
|
||||||
|
execSync('git rev-parse --is-inside-work-tree', {
|
||||||
|
stdio: 'ignore',
|
||||||
|
cwd: process.cwd()
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Export all functions
|
// Export all functions
|
||||||
export {
|
export {
|
||||||
isGitRepository,
|
isGitRepository,
|
||||||
@@ -366,5 +385,6 @@ export {
|
|||||||
checkAndAutoSwitchGitTag,
|
checkAndAutoSwitchGitTag,
|
||||||
checkAndAutoSwitchGitTagSync,
|
checkAndAutoSwitchGitTagSync,
|
||||||
isGitRepositorySync,
|
isGitRepositorySync,
|
||||||
getCurrentBranchSync
|
getCurrentBranchSync,
|
||||||
|
insideGitWorkTree
|
||||||
};
|
};
|
||||||
|
|||||||
293
src/utils/manage-gitignore.js
Normal file
293
src/utils/manage-gitignore.js
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
// Utility to manage .gitignore files with task file preferences and template merging
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const TASK_FILES_COMMENT = '# Task files';
|
||||||
|
const TASK_JSON_PATTERN = 'tasks.json';
|
||||||
|
const TASK_DIR_PATTERN = 'tasks/';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a line by removing comments and trimming whitespace
|
||||||
|
* @param {string} line - Line to normalize
|
||||||
|
* @returns {string} Normalized line
|
||||||
|
*/
|
||||||
|
function normalizeLine(line) {
|
||||||
|
return line.trim().replace(/^#/, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a line is task-related (tasks.json or tasks/)
|
||||||
|
* @param {string} line - Line to check
|
||||||
|
* @returns {boolean} True if line is task-related
|
||||||
|
*/
|
||||||
|
function isTaskLine(line) {
|
||||||
|
const normalized = normalizeLine(line);
|
||||||
|
return normalized === TASK_JSON_PATTERN || normalized === TASK_DIR_PATTERN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjusts task-related lines in template based on storage preference
|
||||||
|
* @param {string[]} templateLines - Array of template lines
|
||||||
|
* @param {boolean} storeTasksInGit - Whether to comment out task lines
|
||||||
|
* @returns {string[]} Adjusted template lines
|
||||||
|
*/
|
||||||
|
function adjustTaskLinesInTemplate(templateLines, storeTasksInGit) {
|
||||||
|
return templateLines.map((line) => {
|
||||||
|
if (isTaskLine(line)) {
|
||||||
|
const normalized = normalizeLine(line);
|
||||||
|
// Preserve original trailing whitespace from the line
|
||||||
|
const originalTrailingSpace = line.match(/\s*$/)[0];
|
||||||
|
return storeTasksInGit
|
||||||
|
? `# ${normalized}${originalTrailingSpace}`
|
||||||
|
: `${normalized}${originalTrailingSpace}`;
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes existing task files section from content
|
||||||
|
* @param {string[]} existingLines - Existing file lines
|
||||||
|
* @returns {string[]} Lines with task section removed
|
||||||
|
*/
|
||||||
|
function removeExistingTaskSection(existingLines) {
|
||||||
|
const cleanedLines = [];
|
||||||
|
let inTaskSection = false;
|
||||||
|
|
||||||
|
for (const line of existingLines) {
|
||||||
|
// Start of task files section
|
||||||
|
if (line.trim() === TASK_FILES_COMMENT) {
|
||||||
|
inTaskSection = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task lines (commented or not)
|
||||||
|
if (isTaskLine(line)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty lines within task section
|
||||||
|
if (inTaskSection && !line.trim()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// End of task section (any non-empty, non-task line)
|
||||||
|
if (inTaskSection && line.trim() && !isTaskLine(line)) {
|
||||||
|
inTaskSection = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep all other lines
|
||||||
|
if (!inTaskSection) {
|
||||||
|
cleanedLines.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleanedLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters template lines to only include new content not already present
|
||||||
|
* @param {string[]} templateLines - Template lines
|
||||||
|
* @param {Set<string>} existingLinesSet - Set of existing trimmed lines
|
||||||
|
* @returns {string[]} New lines to add
|
||||||
|
*/
|
||||||
|
function filterNewTemplateLines(templateLines, existingLinesSet) {
|
||||||
|
return templateLines.filter((line) => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) return false;
|
||||||
|
|
||||||
|
// Skip task-related lines (handled separately)
|
||||||
|
if (isTaskLine(line) || trimmed === TASK_FILES_COMMENT) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include only if not already present
|
||||||
|
return !existingLinesSet.has(trimmed);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the task files section based on storage preference
|
||||||
|
* @param {boolean} storeTasksInGit - Whether to comment out task lines
|
||||||
|
* @returns {string[]} Task files section lines
|
||||||
|
*/
|
||||||
|
function buildTaskFilesSection(storeTasksInGit) {
|
||||||
|
const section = [TASK_FILES_COMMENT];
|
||||||
|
|
||||||
|
if (storeTasksInGit) {
|
||||||
|
section.push(`# ${TASK_JSON_PATTERN}`, `# ${TASK_DIR_PATTERN} `);
|
||||||
|
} else {
|
||||||
|
section.push(TASK_JSON_PATTERN, `${TASK_DIR_PATTERN} `);
|
||||||
|
}
|
||||||
|
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a separator line if needed (avoids double spacing)
|
||||||
|
* @param {string[]} lines - Current lines array
|
||||||
|
*/
|
||||||
|
function addSeparatorIfNeeded(lines) {
|
||||||
|
if (lines.some((line) => line.trim())) {
|
||||||
|
const lastLine = lines[lines.length - 1];
|
||||||
|
if (lastLine && lastLine.trim()) {
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates input parameters
|
||||||
|
* @param {string} targetPath - Path to .gitignore file
|
||||||
|
* @param {string} content - Template content
|
||||||
|
* @param {boolean} storeTasksInGit - Storage preference
|
||||||
|
* @throws {Error} If validation fails
|
||||||
|
*/
|
||||||
|
function validateInputs(targetPath, content, storeTasksInGit) {
|
||||||
|
if (!targetPath || typeof targetPath !== 'string') {
|
||||||
|
throw new Error('targetPath must be a non-empty string');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetPath.endsWith('.gitignore')) {
|
||||||
|
throw new Error('targetPath must end with .gitignore');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content || typeof content !== 'string') {
|
||||||
|
throw new Error('content must be a non-empty string');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof storeTasksInGit !== 'boolean') {
|
||||||
|
throw new Error('storeTasksInGit must be a boolean');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new .gitignore file from template
|
||||||
|
* @param {string} targetPath - Path to create file at
|
||||||
|
* @param {string[]} templateLines - Adjusted template lines
|
||||||
|
* @param {function} log - Logging function
|
||||||
|
*/
|
||||||
|
function createNewGitignoreFile(targetPath, templateLines, log) {
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(targetPath, templateLines.join('\n'));
|
||||||
|
if (typeof log === 'function') {
|
||||||
|
log('success', `Created ${targetPath} with full template`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (typeof log === 'function') {
|
||||||
|
log('error', `Failed to create ${targetPath}: ${error.message}`);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges template content with existing .gitignore file
|
||||||
|
* @param {string} targetPath - Path to existing file
|
||||||
|
* @param {string[]} templateLines - Adjusted template lines
|
||||||
|
* @param {boolean} storeTasksInGit - Storage preference
|
||||||
|
* @param {function} log - Logging function
|
||||||
|
*/
|
||||||
|
function mergeWithExistingFile(
|
||||||
|
targetPath,
|
||||||
|
templateLines,
|
||||||
|
storeTasksInGit,
|
||||||
|
log
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// Read and process existing file
|
||||||
|
const existingContent = fs.readFileSync(targetPath, 'utf8');
|
||||||
|
const existingLines = existingContent.split('\n');
|
||||||
|
|
||||||
|
// Remove existing task section
|
||||||
|
const cleanedExistingLines = removeExistingTaskSection(existingLines);
|
||||||
|
|
||||||
|
// Find new template lines to add
|
||||||
|
const existingLinesSet = new Set(
|
||||||
|
cleanedExistingLines.map((line) => line.trim()).filter((line) => line)
|
||||||
|
);
|
||||||
|
const newLines = filterNewTemplateLines(templateLines, existingLinesSet);
|
||||||
|
|
||||||
|
// Build final content
|
||||||
|
const finalLines = [...cleanedExistingLines];
|
||||||
|
|
||||||
|
// Add new template content
|
||||||
|
if (newLines.length > 0) {
|
||||||
|
addSeparatorIfNeeded(finalLines);
|
||||||
|
finalLines.push(...newLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add task files section
|
||||||
|
addSeparatorIfNeeded(finalLines);
|
||||||
|
finalLines.push(...buildTaskFilesSection(storeTasksInGit));
|
||||||
|
|
||||||
|
// Write result
|
||||||
|
fs.writeFileSync(targetPath, finalLines.join('\n'));
|
||||||
|
|
||||||
|
if (typeof log === 'function') {
|
||||||
|
const hasNewContent =
|
||||||
|
newLines.length > 0 ? ' and merged new content' : '';
|
||||||
|
log(
|
||||||
|
'success',
|
||||||
|
`Updated ${targetPath} according to user preference${hasNewContent}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (typeof log === 'function') {
|
||||||
|
log(
|
||||||
|
'error',
|
||||||
|
`Failed to merge content with ${targetPath}: ${error.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages .gitignore file creation and updates with task file preferences
|
||||||
|
* @param {string} targetPath - Path to the .gitignore file
|
||||||
|
* @param {string} content - Template content for .gitignore
|
||||||
|
* @param {boolean} storeTasksInGit - Whether to store tasks in git or not
|
||||||
|
* @param {function} log - Logging function (level, message)
|
||||||
|
* @throws {Error} If validation or file operations fail
|
||||||
|
*/
|
||||||
|
function manageGitignoreFile(
|
||||||
|
targetPath,
|
||||||
|
content,
|
||||||
|
storeTasksInGit = false,
|
||||||
|
log = null
|
||||||
|
) {
|
||||||
|
// Validate inputs
|
||||||
|
validateInputs(targetPath, content, storeTasksInGit);
|
||||||
|
|
||||||
|
// Process template with task preference
|
||||||
|
const templateLines = content.split('\n');
|
||||||
|
const adjustedTemplateLines = adjustTaskLinesInTemplate(
|
||||||
|
templateLines,
|
||||||
|
storeTasksInGit
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle file creation or merging
|
||||||
|
if (!fs.existsSync(targetPath)) {
|
||||||
|
createNewGitignoreFile(targetPath, adjustedTemplateLines, log);
|
||||||
|
} else {
|
||||||
|
mergeWithExistingFile(
|
||||||
|
targetPath,
|
||||||
|
adjustedTemplateLines,
|
||||||
|
storeTasksInGit,
|
||||||
|
log
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default manageGitignoreFile;
|
||||||
|
export {
|
||||||
|
manageGitignoreFile,
|
||||||
|
normalizeLine,
|
||||||
|
isTaskLine,
|
||||||
|
buildTaskFilesSection,
|
||||||
|
TASK_FILES_COMMENT,
|
||||||
|
TASK_JSON_PATTERN,
|
||||||
|
TASK_DIR_PATTERN
|
||||||
|
};
|
||||||
581
tests/integration/manage-gitignore.test.js
Normal file
581
tests/integration/manage-gitignore.test.js
Normal file
@@ -0,0 +1,581 @@
|
|||||||
|
/**
|
||||||
|
* Integration tests for manage-gitignore.js module
|
||||||
|
* Tests actual file system operations in a temporary directory
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import os from 'os';
|
||||||
|
import manageGitignoreFile from '../../src/utils/manage-gitignore.js';
|
||||||
|
|
||||||
|
describe('manage-gitignore.js Integration Tests', () => {
|
||||||
|
let tempDir;
|
||||||
|
let testGitignorePath;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Create a temporary directory for each test
|
||||||
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitignore-test-'));
|
||||||
|
testGitignorePath = path.join(tempDir, '.gitignore');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up temporary directory after each test
|
||||||
|
if (fs.existsSync(tempDir)) {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('New File Creation', () => {
|
||||||
|
const templateContent = `# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# Task files
|
||||||
|
tasks.json
|
||||||
|
tasks/ `;
|
||||||
|
|
||||||
|
test('should create new .gitignore file with commented task lines (storeTasksInGit = true)', () => {
|
||||||
|
const logs = [];
|
||||||
|
const mockLog = (level, message) => logs.push({ level, message });
|
||||||
|
|
||||||
|
manageGitignoreFile(testGitignorePath, templateContent, true, mockLog);
|
||||||
|
|
||||||
|
// Verify file was created
|
||||||
|
expect(fs.existsSync(testGitignorePath)).toBe(true);
|
||||||
|
|
||||||
|
// Verify content
|
||||||
|
const content = fs.readFileSync(testGitignorePath, 'utf8');
|
||||||
|
expect(content).toContain('# Logs');
|
||||||
|
expect(content).toContain('logs');
|
||||||
|
expect(content).toContain('# Dependencies');
|
||||||
|
expect(content).toContain('node_modules/');
|
||||||
|
expect(content).toContain('# Task files');
|
||||||
|
expect(content).toContain('tasks.json');
|
||||||
|
expect(content).toContain('tasks/');
|
||||||
|
|
||||||
|
// Verify task lines are commented (storeTasksInGit = true)
|
||||||
|
expect(content).toMatch(
|
||||||
|
/# Task files\s*[\r\n]+# tasks\.json\s*[\r\n]+# tasks\/ /
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify log message
|
||||||
|
expect(logs).toContainEqual({
|
||||||
|
level: 'success',
|
||||||
|
message: expect.stringContaining('Created')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should create new .gitignore file with uncommented task lines (storeTasksInGit = false)', () => {
|
||||||
|
const logs = [];
|
||||||
|
const mockLog = (level, message) => logs.push({ level, message });
|
||||||
|
|
||||||
|
manageGitignoreFile(testGitignorePath, templateContent, false, mockLog);
|
||||||
|
|
||||||
|
// Verify file was created
|
||||||
|
expect(fs.existsSync(testGitignorePath)).toBe(true);
|
||||||
|
|
||||||
|
// Verify content
|
||||||
|
const content = fs.readFileSync(testGitignorePath, 'utf8');
|
||||||
|
expect(content).toContain('# Task files');
|
||||||
|
|
||||||
|
// Verify task lines are uncommented (storeTasksInGit = false)
|
||||||
|
expect(content).toMatch(
|
||||||
|
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify log message
|
||||||
|
expect(logs).toContainEqual({
|
||||||
|
level: 'success',
|
||||||
|
message: expect.stringContaining('Created')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should work without log function', () => {
|
||||||
|
expect(() => {
|
||||||
|
manageGitignoreFile(testGitignorePath, templateContent, false);
|
||||||
|
}).not.toThrow();
|
||||||
|
|
||||||
|
expect(fs.existsSync(testGitignorePath)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('File Merging', () => {
|
||||||
|
const templateContent = `# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Task files
|
||||||
|
tasks.json
|
||||||
|
tasks/ `;
|
||||||
|
|
||||||
|
test('should merge template with existing file content', () => {
|
||||||
|
// Create existing .gitignore file
|
||||||
|
const existingContent = `# Existing content
|
||||||
|
old-files.txt
|
||||||
|
*.backup
|
||||||
|
|
||||||
|
# Old task files (to be replaced)
|
||||||
|
# Task files
|
||||||
|
# tasks.json
|
||||||
|
# tasks/
|
||||||
|
|
||||||
|
# More existing content
|
||||||
|
cache/`;
|
||||||
|
|
||||||
|
fs.writeFileSync(testGitignorePath, existingContent);
|
||||||
|
|
||||||
|
const logs = [];
|
||||||
|
const mockLog = (level, message) => logs.push({ level, message });
|
||||||
|
|
||||||
|
manageGitignoreFile(testGitignorePath, templateContent, false, mockLog);
|
||||||
|
|
||||||
|
// Verify file still exists
|
||||||
|
expect(fs.existsSync(testGitignorePath)).toBe(true);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(testGitignorePath, 'utf8');
|
||||||
|
|
||||||
|
// Should retain existing non-task content
|
||||||
|
expect(content).toContain('# Existing content');
|
||||||
|
expect(content).toContain('old-files.txt');
|
||||||
|
expect(content).toContain('*.backup');
|
||||||
|
expect(content).toContain('# More existing content');
|
||||||
|
expect(content).toContain('cache/');
|
||||||
|
|
||||||
|
// Should add new template content
|
||||||
|
expect(content).toContain('# Logs');
|
||||||
|
expect(content).toContain('logs');
|
||||||
|
expect(content).toContain('# Dependencies');
|
||||||
|
expect(content).toContain('node_modules/');
|
||||||
|
expect(content).toContain('# Environment variables');
|
||||||
|
expect(content).toContain('.env');
|
||||||
|
|
||||||
|
// Should replace task section with new preference (storeTasksInGit = false means uncommented)
|
||||||
|
expect(content).toMatch(
|
||||||
|
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify log message
|
||||||
|
expect(logs).toContainEqual({
|
||||||
|
level: 'success',
|
||||||
|
message: expect.stringContaining('Updated')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle switching task preferences from commented to uncommented', () => {
|
||||||
|
// Create existing file with commented task lines
|
||||||
|
const existingContent = `# Existing
|
||||||
|
existing.txt
|
||||||
|
|
||||||
|
# Task files
|
||||||
|
# tasks.json
|
||||||
|
# tasks/ `;
|
||||||
|
|
||||||
|
fs.writeFileSync(testGitignorePath, existingContent);
|
||||||
|
|
||||||
|
// Update with storeTasksInGit = true (commented)
|
||||||
|
manageGitignoreFile(testGitignorePath, templateContent, true);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(testGitignorePath, 'utf8');
|
||||||
|
|
||||||
|
// Should retain existing content
|
||||||
|
expect(content).toContain('# Existing');
|
||||||
|
expect(content).toContain('existing.txt');
|
||||||
|
|
||||||
|
// Should have commented task lines (storeTasksInGit = true)
|
||||||
|
expect(content).toMatch(
|
||||||
|
/# Task files\s*[\r\n]+# tasks\.json\s*[\r\n]+# tasks\/ /
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle switching task preferences from uncommented to commented', () => {
|
||||||
|
// Create existing file with uncommented task lines
|
||||||
|
const existingContent = `# Existing
|
||||||
|
existing.txt
|
||||||
|
|
||||||
|
# Task files
|
||||||
|
tasks.json
|
||||||
|
tasks/ `;
|
||||||
|
|
||||||
|
fs.writeFileSync(testGitignorePath, existingContent);
|
||||||
|
|
||||||
|
// Update with storeTasksInGit = false (uncommented)
|
||||||
|
manageGitignoreFile(testGitignorePath, templateContent, false);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(testGitignorePath, 'utf8');
|
||||||
|
|
||||||
|
// Should retain existing content
|
||||||
|
expect(content).toContain('# Existing');
|
||||||
|
expect(content).toContain('existing.txt');
|
||||||
|
|
||||||
|
// Should have uncommented task lines (storeTasksInGit = false)
|
||||||
|
expect(content).toMatch(
|
||||||
|
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not duplicate existing template content', () => {
|
||||||
|
// Create existing file that already has some template content
|
||||||
|
const existingContent = `# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Custom content
|
||||||
|
custom.txt
|
||||||
|
|
||||||
|
# Task files
|
||||||
|
# tasks.json
|
||||||
|
# tasks/ `;
|
||||||
|
|
||||||
|
fs.writeFileSync(testGitignorePath, existingContent);
|
||||||
|
|
||||||
|
manageGitignoreFile(testGitignorePath, templateContent, false);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(testGitignorePath, 'utf8');
|
||||||
|
|
||||||
|
// Should not duplicate logs section
|
||||||
|
const logsMatches = content.match(/# Logs/g);
|
||||||
|
expect(logsMatches).toHaveLength(1);
|
||||||
|
|
||||||
|
// Should not duplicate dependencies section
|
||||||
|
const depsMatches = content.match(/# Dependencies/g);
|
||||||
|
expect(depsMatches).toHaveLength(1);
|
||||||
|
|
||||||
|
// Should retain custom content
|
||||||
|
expect(content).toContain('# Custom content');
|
||||||
|
expect(content).toContain('custom.txt');
|
||||||
|
|
||||||
|
// Should add new template content that wasn't present
|
||||||
|
expect(content).toContain('# Environment variables');
|
||||||
|
expect(content).toContain('.env');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle empty existing file', () => {
|
||||||
|
// Create empty file
|
||||||
|
fs.writeFileSync(testGitignorePath, '');
|
||||||
|
|
||||||
|
manageGitignoreFile(testGitignorePath, templateContent, false);
|
||||||
|
|
||||||
|
expect(fs.existsSync(testGitignorePath)).toBe(true);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(testGitignorePath, 'utf8');
|
||||||
|
expect(content).toContain('# Logs');
|
||||||
|
expect(content).toContain('# Task files');
|
||||||
|
expect(content).toMatch(
|
||||||
|
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle file with only whitespace', () => {
|
||||||
|
// Create file with only whitespace
|
||||||
|
fs.writeFileSync(testGitignorePath, ' \n\n \n');
|
||||||
|
|
||||||
|
manageGitignoreFile(testGitignorePath, templateContent, true);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(testGitignorePath, 'utf8');
|
||||||
|
expect(content).toContain('# Logs');
|
||||||
|
expect(content).toContain('# Task files');
|
||||||
|
expect(content).toMatch(
|
||||||
|
/# Task files\s*[\r\n]+# tasks\.json\s*[\r\n]+# tasks\/ /
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Complex Task Section Handling', () => {
|
||||||
|
test('should remove task section with mixed comments and spacing', () => {
|
||||||
|
const existingContent = `# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Task files
|
||||||
|
|
||||||
|
# tasks.json
|
||||||
|
tasks/
|
||||||
|
|
||||||
|
|
||||||
|
# More content
|
||||||
|
more.txt`;
|
||||||
|
|
||||||
|
const templateContent = `# New content
|
||||||
|
new.txt
|
||||||
|
|
||||||
|
# Task files
|
||||||
|
tasks.json
|
||||||
|
tasks/ `;
|
||||||
|
|
||||||
|
fs.writeFileSync(testGitignorePath, existingContent);
|
||||||
|
|
||||||
|
manageGitignoreFile(testGitignorePath, templateContent, false);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(testGitignorePath, 'utf8');
|
||||||
|
|
||||||
|
// Should retain non-task content
|
||||||
|
expect(content).toContain('# Dependencies');
|
||||||
|
expect(content).toContain('node_modules/');
|
||||||
|
expect(content).toContain('# More content');
|
||||||
|
expect(content).toContain('more.txt');
|
||||||
|
|
||||||
|
// Should add new content
|
||||||
|
expect(content).toContain('# New content');
|
||||||
|
expect(content).toContain('new.txt');
|
||||||
|
|
||||||
|
// Should have clean task section (storeTasksInGit = false means uncommented)
|
||||||
|
expect(content).toMatch(
|
||||||
|
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle multiple task file variations', () => {
|
||||||
|
const existingContent = `# Existing
|
||||||
|
existing.txt
|
||||||
|
|
||||||
|
# Task files
|
||||||
|
tasks.json
|
||||||
|
# tasks.json
|
||||||
|
# tasks/
|
||||||
|
tasks/
|
||||||
|
#tasks.json
|
||||||
|
|
||||||
|
# More content
|
||||||
|
more.txt`;
|
||||||
|
|
||||||
|
const templateContent = `# Task files
|
||||||
|
tasks.json
|
||||||
|
tasks/ `;
|
||||||
|
|
||||||
|
fs.writeFileSync(testGitignorePath, existingContent);
|
||||||
|
|
||||||
|
manageGitignoreFile(testGitignorePath, templateContent, true);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(testGitignorePath, 'utf8');
|
||||||
|
|
||||||
|
// Should retain non-task content
|
||||||
|
expect(content).toContain('# Existing');
|
||||||
|
expect(content).toContain('existing.txt');
|
||||||
|
expect(content).toContain('# More content');
|
||||||
|
expect(content).toContain('more.txt');
|
||||||
|
|
||||||
|
// Should have clean task section with preference applied (storeTasksInGit = true means commented)
|
||||||
|
expect(content).toMatch(
|
||||||
|
/# Task files\s*[\r\n]+# tasks\.json\s*[\r\n]+# tasks\/ /
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should not have multiple task sections
|
||||||
|
const taskFileMatches = content.match(/# Task files/g);
|
||||||
|
expect(taskFileMatches).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Error Handling', () => {
|
||||||
|
test('should handle permission errors gracefully', () => {
|
||||||
|
// Create a directory where we would create the file, then remove write permissions
|
||||||
|
const readOnlyDir = path.join(tempDir, 'readonly');
|
||||||
|
fs.mkdirSync(readOnlyDir);
|
||||||
|
fs.chmodSync(readOnlyDir, 0o444); // Read-only
|
||||||
|
|
||||||
|
const readOnlyGitignorePath = path.join(readOnlyDir, '.gitignore');
|
||||||
|
const templateContent = `# Test
|
||||||
|
test.txt
|
||||||
|
|
||||||
|
# Task files
|
||||||
|
tasks.json
|
||||||
|
tasks/ `;
|
||||||
|
|
||||||
|
const logs = [];
|
||||||
|
const mockLog = (level, message) => logs.push({ level, message });
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
manageGitignoreFile(
|
||||||
|
readOnlyGitignorePath,
|
||||||
|
templateContent,
|
||||||
|
false,
|
||||||
|
mockLog
|
||||||
|
);
|
||||||
|
}).toThrow();
|
||||||
|
|
||||||
|
// Verify error was logged
|
||||||
|
expect(logs).toContainEqual({
|
||||||
|
level: 'error',
|
||||||
|
message: expect.stringContaining('Failed to create')
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restore permissions for cleanup
|
||||||
|
fs.chmodSync(readOnlyDir, 0o755);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle read errors on existing files', () => {
|
||||||
|
// Create a file then remove read permissions
|
||||||
|
fs.writeFileSync(testGitignorePath, 'existing content');
|
||||||
|
fs.chmodSync(testGitignorePath, 0o000); // No permissions
|
||||||
|
|
||||||
|
const templateContent = `# Test
|
||||||
|
test.txt
|
||||||
|
|
||||||
|
# Task files
|
||||||
|
tasks.json
|
||||||
|
tasks/ `;
|
||||||
|
|
||||||
|
const logs = [];
|
||||||
|
const mockLog = (level, message) => logs.push({ level, message });
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
manageGitignoreFile(testGitignorePath, templateContent, false, mockLog);
|
||||||
|
}).toThrow();
|
||||||
|
|
||||||
|
// Verify error was logged
|
||||||
|
expect(logs).toContainEqual({
|
||||||
|
level: 'error',
|
||||||
|
message: expect.stringContaining('Failed to merge content')
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restore permissions for cleanup
|
||||||
|
fs.chmodSync(testGitignorePath, 0o644);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Real-world Scenarios', () => {
|
||||||
|
test('should handle typical Node.js project .gitignore', () => {
|
||||||
|
const existingNodeGitignore = `# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# next.js build output
|
||||||
|
.next`;
|
||||||
|
|
||||||
|
const taskMasterTemplate = `# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Task files
|
||||||
|
tasks.json
|
||||||
|
tasks/ `;
|
||||||
|
|
||||||
|
fs.writeFileSync(testGitignorePath, existingNodeGitignore);
|
||||||
|
|
||||||
|
manageGitignoreFile(testGitignorePath, taskMasterTemplate, false);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(testGitignorePath, 'utf8');
|
||||||
|
|
||||||
|
// Should retain existing Node.js specific entries
|
||||||
|
expect(content).toContain('npm-debug.log*');
|
||||||
|
expect(content).toContain('yarn-debug.log*');
|
||||||
|
expect(content).toContain('*.pid');
|
||||||
|
expect(content).toContain('jspm_packages/');
|
||||||
|
expect(content).toContain('.npm');
|
||||||
|
expect(content).toContain('*.tgz');
|
||||||
|
expect(content).toContain('.yarn-integrity');
|
||||||
|
expect(content).toContain('.next');
|
||||||
|
|
||||||
|
// Should add new content from template that wasn't present
|
||||||
|
expect(content).toContain('dist/');
|
||||||
|
expect(content).toContain('build/');
|
||||||
|
|
||||||
|
// Should add task files section with correct preference (storeTasksInGit = false means uncommented)
|
||||||
|
expect(content).toMatch(
|
||||||
|
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should not duplicate common entries
|
||||||
|
const nodeModulesMatches = content.match(/node_modules\//g);
|
||||||
|
expect(nodeModulesMatches).toHaveLength(1);
|
||||||
|
|
||||||
|
const logsMatches = content.match(/# Logs/g);
|
||||||
|
expect(logsMatches).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle project with existing task files in git', () => {
|
||||||
|
const existingContent = `# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Current task setup - keeping in git
|
||||||
|
# Task files
|
||||||
|
tasks.json
|
||||||
|
tasks/
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/`;
|
||||||
|
|
||||||
|
const templateContent = `# New template
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Task files
|
||||||
|
tasks.json
|
||||||
|
tasks/ `;
|
||||||
|
|
||||||
|
fs.writeFileSync(testGitignorePath, existingContent);
|
||||||
|
|
||||||
|
// Change preference to exclude tasks from git (storeTasksInGit = false means uncommented/ignored)
|
||||||
|
manageGitignoreFile(testGitignorePath, templateContent, false);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(testGitignorePath, 'utf8');
|
||||||
|
|
||||||
|
// Should retain existing content
|
||||||
|
expect(content).toContain('# Dependencies');
|
||||||
|
expect(content).toContain('node_modules/');
|
||||||
|
expect(content).toContain('# Logs');
|
||||||
|
expect(content).toContain('*.log');
|
||||||
|
expect(content).toContain('# Build output');
|
||||||
|
expect(content).toContain('dist/');
|
||||||
|
|
||||||
|
// Should update task preference to uncommented (storeTasksInGit = false)
|
||||||
|
expect(content).toMatch(
|
||||||
|
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
538
tests/unit/initialize-project.test.js
Normal file
538
tests/unit/initialize-project.test.js
Normal file
@@ -0,0 +1,538 @@
|
|||||||
|
import { jest } from '@jest/globals';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import os from 'os';
|
||||||
|
|
||||||
|
// Reduce noise in test output
|
||||||
|
process.env.TASKMASTER_LOG_LEVEL = 'error';
|
||||||
|
|
||||||
|
// === Mock everything early ===
|
||||||
|
jest.mock('child_process', () => ({ execSync: jest.fn() }));
|
||||||
|
jest.mock('fs', () => ({
|
||||||
|
...jest.requireActual('fs'),
|
||||||
|
mkdirSync: jest.fn(),
|
||||||
|
writeFileSync: jest.fn(),
|
||||||
|
readFileSync: jest.fn(),
|
||||||
|
appendFileSync: jest.fn(),
|
||||||
|
existsSync: jest.fn(),
|
||||||
|
mkdtempSync: jest.requireActual('fs').mkdtempSync,
|
||||||
|
rmSync: jest.requireActual('fs').rmSync
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock console methods to suppress output
|
||||||
|
const consoleMethods = ['log', 'info', 'warn', 'error', 'clear'];
|
||||||
|
consoleMethods.forEach((method) => {
|
||||||
|
global.console[method] = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock ES modules using unstable_mockModule
|
||||||
|
jest.unstable_mockModule('../../scripts/modules/utils.js', () => ({
|
||||||
|
isSilentMode: jest.fn(() => true),
|
||||||
|
enableSilentMode: jest.fn(),
|
||||||
|
log: jest.fn(),
|
||||||
|
findProjectRoot: jest.fn(() => process.cwd())
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock git-utils module
|
||||||
|
jest.unstable_mockModule('../../scripts/modules/utils/git-utils.js', () => ({
|
||||||
|
insideGitWorkTree: jest.fn(() => false)
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock rule transformer
|
||||||
|
jest.unstable_mockModule('../../src/utils/rule-transformer.js', () => ({
|
||||||
|
convertAllRulesToProfileRules: jest.fn(),
|
||||||
|
getRulesProfile: jest.fn(() => ({
|
||||||
|
conversionConfig: {},
|
||||||
|
globalReplacements: []
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock any other modules that might output or do real operations
|
||||||
|
jest.unstable_mockModule('../../scripts/modules/config-manager.js', () => ({
|
||||||
|
createDefaultConfig: jest.fn(() => ({ models: {}, project: {} })),
|
||||||
|
saveConfig: jest.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock display libraries
|
||||||
|
jest.mock('figlet', () => ({ textSync: jest.fn(() => 'MOCKED BANNER') }));
|
||||||
|
jest.mock('boxen', () => jest.fn(() => 'MOCKED BOX'));
|
||||||
|
jest.mock('gradient-string', () => jest.fn(() => jest.fn((text) => text)));
|
||||||
|
jest.mock('chalk', () => ({
|
||||||
|
blue: jest.fn((text) => text),
|
||||||
|
green: jest.fn((text) => text),
|
||||||
|
red: jest.fn((text) => text),
|
||||||
|
yellow: jest.fn((text) => text),
|
||||||
|
cyan: jest.fn((text) => text),
|
||||||
|
white: jest.fn((text) => text),
|
||||||
|
dim: jest.fn((text) => text),
|
||||||
|
bold: jest.fn((text) => text),
|
||||||
|
underline: jest.fn((text) => text)
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { execSync } = jest.requireMock('child_process');
|
||||||
|
const mockFs = jest.requireMock('fs');
|
||||||
|
|
||||||
|
// Import the mocked modules
|
||||||
|
const mockUtils = await import('../../scripts/modules/utils.js');
|
||||||
|
const mockGitUtils = await import('../../scripts/modules/utils/git-utils.js');
|
||||||
|
const mockRuleTransformer = await import('../../src/utils/rule-transformer.js');
|
||||||
|
|
||||||
|
// Import after mocks
|
||||||
|
const { initializeProject } = await import('../../scripts/init.js');
|
||||||
|
|
||||||
|
describe('initializeProject – Git / Alias flag logic', () => {
|
||||||
|
let tmpDir;
|
||||||
|
const origCwd = process.cwd();
|
||||||
|
|
||||||
|
// Standard non-interactive options for all tests
|
||||||
|
const baseOptions = {
|
||||||
|
yes: true,
|
||||||
|
skipInstall: true,
|
||||||
|
name: 'test-project',
|
||||||
|
description: 'Test project description',
|
||||||
|
version: '1.0.0',
|
||||||
|
author: 'Test Author'
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
// Set up basic fs mocks
|
||||||
|
mockFs.mkdirSync.mockImplementation(() => {});
|
||||||
|
mockFs.writeFileSync.mockImplementation(() => {});
|
||||||
|
mockFs.readFileSync.mockImplementation((filePath) => {
|
||||||
|
if (filePath.includes('assets') || filePath.includes('.cursor/rules')) {
|
||||||
|
return 'mock template content';
|
||||||
|
}
|
||||||
|
if (filePath.includes('.zshrc') || filePath.includes('.bashrc')) {
|
||||||
|
return '# existing config';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
mockFs.appendFileSync.mockImplementation(() => {});
|
||||||
|
mockFs.existsSync.mockImplementation((filePath) => {
|
||||||
|
// Template source files exist
|
||||||
|
if (filePath.includes('assets') || filePath.includes('.cursor/rules')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Shell config files exist by default
|
||||||
|
if (filePath.includes('.zshrc') || filePath.includes('.bashrc')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset utils mocks
|
||||||
|
mockUtils.isSilentMode.mockReturnValue(true);
|
||||||
|
mockGitUtils.insideGitWorkTree.mockReturnValue(false);
|
||||||
|
|
||||||
|
// Default execSync mock
|
||||||
|
execSync.mockImplementation(() => '');
|
||||||
|
|
||||||
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-init-'));
|
||||||
|
process.chdir(tmpDir);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.chdir(origCwd);
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Git Flag Behavior', () => {
|
||||||
|
it('completes successfully with git:false in dry run', async () => {
|
||||||
|
const result = await initializeProject({
|
||||||
|
...baseOptions,
|
||||||
|
git: false,
|
||||||
|
aliases: false,
|
||||||
|
dryRun: true
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.dryRun).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('completes successfully with git:true when not inside repo', async () => {
|
||||||
|
mockGitUtils.insideGitWorkTree.mockReturnValue(false);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
initializeProject({
|
||||||
|
...baseOptions,
|
||||||
|
git: true,
|
||||||
|
aliases: false,
|
||||||
|
dryRun: false
|
||||||
|
})
|
||||||
|
).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('completes successfully when already inside repo', async () => {
|
||||||
|
mockGitUtils.insideGitWorkTree.mockReturnValue(true);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
initializeProject({
|
||||||
|
...baseOptions,
|
||||||
|
git: true,
|
||||||
|
aliases: false,
|
||||||
|
dryRun: false
|
||||||
|
})
|
||||||
|
).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses default git behavior without errors', async () => {
|
||||||
|
mockGitUtils.insideGitWorkTree.mockReturnValue(false);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
initializeProject({
|
||||||
|
...baseOptions,
|
||||||
|
aliases: false,
|
||||||
|
dryRun: false
|
||||||
|
})
|
||||||
|
).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles git command failures gracefully', async () => {
|
||||||
|
mockGitUtils.insideGitWorkTree.mockReturnValue(false);
|
||||||
|
execSync.mockImplementation((cmd) => {
|
||||||
|
if (cmd.includes('git init')) {
|
||||||
|
throw new Error('git not found');
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
initializeProject({
|
||||||
|
...baseOptions,
|
||||||
|
git: true,
|
||||||
|
aliases: false,
|
||||||
|
dryRun: false
|
||||||
|
})
|
||||||
|
).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Alias Flag Behavior', () => {
|
||||||
|
it('completes successfully when aliases:true and environment is set up', async () => {
|
||||||
|
const originalShell = process.env.SHELL;
|
||||||
|
const originalHome = process.env.HOME;
|
||||||
|
|
||||||
|
process.env.SHELL = '/bin/zsh';
|
||||||
|
process.env.HOME = '/mock/home';
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
initializeProject({
|
||||||
|
...baseOptions,
|
||||||
|
git: false,
|
||||||
|
aliases: true,
|
||||||
|
dryRun: false
|
||||||
|
})
|
||||||
|
).resolves.not.toThrow();
|
||||||
|
|
||||||
|
process.env.SHELL = originalShell;
|
||||||
|
process.env.HOME = originalHome;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('completes successfully when aliases:false', async () => {
|
||||||
|
await expect(
|
||||||
|
initializeProject({
|
||||||
|
...baseOptions,
|
||||||
|
git: false,
|
||||||
|
aliases: false,
|
||||||
|
dryRun: false
|
||||||
|
})
|
||||||
|
).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles missing shell gracefully', async () => {
|
||||||
|
const originalShell = process.env.SHELL;
|
||||||
|
const originalHome = process.env.HOME;
|
||||||
|
|
||||||
|
delete process.env.SHELL; // Remove shell env var
|
||||||
|
process.env.HOME = '/mock/home';
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
initializeProject({
|
||||||
|
...baseOptions,
|
||||||
|
git: false,
|
||||||
|
aliases: true,
|
||||||
|
dryRun: false
|
||||||
|
})
|
||||||
|
).resolves.not.toThrow();
|
||||||
|
|
||||||
|
process.env.SHELL = originalShell;
|
||||||
|
process.env.HOME = originalHome;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles missing shell config file gracefully', async () => {
|
||||||
|
const originalShell = process.env.SHELL;
|
||||||
|
const originalHome = process.env.HOME;
|
||||||
|
|
||||||
|
process.env.SHELL = '/bin/zsh';
|
||||||
|
process.env.HOME = '/mock/home';
|
||||||
|
|
||||||
|
// Shell config doesn't exist
|
||||||
|
mockFs.existsSync.mockImplementation((filePath) => {
|
||||||
|
if (filePath.includes('.zshrc') || filePath.includes('.bashrc')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (filePath.includes('assets') || filePath.includes('.cursor/rules')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
initializeProject({
|
||||||
|
...baseOptions,
|
||||||
|
git: false,
|
||||||
|
aliases: true,
|
||||||
|
dryRun: false
|
||||||
|
})
|
||||||
|
).resolves.not.toThrow();
|
||||||
|
|
||||||
|
process.env.SHELL = originalShell;
|
||||||
|
process.env.HOME = originalHome;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Flag Combinations', () => {
|
||||||
|
it.each`
|
||||||
|
git | aliases | description
|
||||||
|
${true} | ${true} | ${'git & aliases enabled'}
|
||||||
|
${true} | ${false} | ${'git enabled, aliases disabled'}
|
||||||
|
${false} | ${true} | ${'git disabled, aliases enabled'}
|
||||||
|
${false} | ${false} | ${'git & aliases disabled'}
|
||||||
|
`('handles $description without errors', async ({ git, aliases }) => {
|
||||||
|
const originalShell = process.env.SHELL;
|
||||||
|
const originalHome = process.env.HOME;
|
||||||
|
|
||||||
|
if (aliases) {
|
||||||
|
process.env.SHELL = '/bin/zsh';
|
||||||
|
process.env.HOME = '/mock/home';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (git) {
|
||||||
|
mockGitUtils.insideGitWorkTree.mockReturnValue(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
initializeProject({
|
||||||
|
...baseOptions,
|
||||||
|
git,
|
||||||
|
aliases,
|
||||||
|
dryRun: false
|
||||||
|
})
|
||||||
|
).resolves.not.toThrow();
|
||||||
|
|
||||||
|
process.env.SHELL = originalShell;
|
||||||
|
process.env.HOME = originalHome;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Dry Run Mode', () => {
|
||||||
|
it('returns dry run result and performs no operations', async () => {
|
||||||
|
const result = await initializeProject({
|
||||||
|
...baseOptions,
|
||||||
|
git: true,
|
||||||
|
aliases: true,
|
||||||
|
dryRun: true
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.dryRun).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
git | aliases | description
|
||||||
|
${true} | ${false} | ${'git-specific behavior'}
|
||||||
|
${false} | ${false} | ${'no-git behavior'}
|
||||||
|
${false} | ${true} | ${'alias behavior'}
|
||||||
|
`('shows $description in dry run', async ({ git, aliases }) => {
|
||||||
|
const result = await initializeProject({
|
||||||
|
...baseOptions,
|
||||||
|
git,
|
||||||
|
aliases,
|
||||||
|
dryRun: true
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.dryRun).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Error Handling', () => {
|
||||||
|
it('handles npm install failures gracefully', async () => {
|
||||||
|
execSync.mockImplementation((cmd) => {
|
||||||
|
if (cmd.includes('npm install')) {
|
||||||
|
throw new Error('npm failed');
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
initializeProject({
|
||||||
|
...baseOptions,
|
||||||
|
git: false,
|
||||||
|
aliases: false,
|
||||||
|
skipInstall: false,
|
||||||
|
dryRun: false
|
||||||
|
})
|
||||||
|
).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles git failures gracefully', async () => {
|
||||||
|
mockGitUtils.insideGitWorkTree.mockReturnValue(false);
|
||||||
|
execSync.mockImplementation((cmd) => {
|
||||||
|
if (cmd.includes('git init')) {
|
||||||
|
throw new Error('git failed');
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
initializeProject({
|
||||||
|
...baseOptions,
|
||||||
|
git: true,
|
||||||
|
aliases: false,
|
||||||
|
dryRun: false
|
||||||
|
})
|
||||||
|
).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles file system errors gracefully', async () => {
|
||||||
|
mockFs.mkdirSync.mockImplementation(() => {
|
||||||
|
throw new Error('Permission denied');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should handle file system errors gracefully
|
||||||
|
await expect(
|
||||||
|
initializeProject({
|
||||||
|
...baseOptions,
|
||||||
|
git: false,
|
||||||
|
aliases: false,
|
||||||
|
dryRun: false
|
||||||
|
})
|
||||||
|
).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Non-Interactive Mode', () => {
|
||||||
|
it('bypasses prompts with yes:true', async () => {
|
||||||
|
const result = await initializeProject({
|
||||||
|
...baseOptions,
|
||||||
|
git: true,
|
||||||
|
aliases: true,
|
||||||
|
dryRun: true
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({ dryRun: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('completes without hanging', async () => {
|
||||||
|
await expect(
|
||||||
|
initializeProject({
|
||||||
|
...baseOptions,
|
||||||
|
git: false,
|
||||||
|
aliases: false,
|
||||||
|
dryRun: false
|
||||||
|
})
|
||||||
|
).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles all flag combinations without hanging', async () => {
|
||||||
|
const flagCombinations = [
|
||||||
|
{ git: true, aliases: true },
|
||||||
|
{ git: true, aliases: false },
|
||||||
|
{ git: false, aliases: true },
|
||||||
|
{ git: false, aliases: false },
|
||||||
|
{} // No flags (uses defaults)
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const flags of flagCombinations) {
|
||||||
|
await expect(
|
||||||
|
initializeProject({
|
||||||
|
...baseOptions,
|
||||||
|
...flags,
|
||||||
|
dryRun: true // Use dry run for speed
|
||||||
|
})
|
||||||
|
).resolves.not.toThrow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts complete project details', async () => {
|
||||||
|
await expect(
|
||||||
|
initializeProject({
|
||||||
|
name: 'test-project',
|
||||||
|
description: 'test description',
|
||||||
|
version: '2.0.0',
|
||||||
|
author: 'Test User',
|
||||||
|
git: false,
|
||||||
|
aliases: false,
|
||||||
|
dryRun: true
|
||||||
|
})
|
||||||
|
).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works with skipInstall option', async () => {
|
||||||
|
await expect(
|
||||||
|
initializeProject({
|
||||||
|
...baseOptions,
|
||||||
|
skipInstall: true,
|
||||||
|
git: false,
|
||||||
|
aliases: false,
|
||||||
|
dryRun: false
|
||||||
|
})
|
||||||
|
).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Function Integration', () => {
|
||||||
|
it('calls utility functions without errors', async () => {
|
||||||
|
await initializeProject({
|
||||||
|
...baseOptions,
|
||||||
|
git: false,
|
||||||
|
aliases: false,
|
||||||
|
dryRun: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify that utility functions were called
|
||||||
|
expect(mockUtils.isSilentMode).toHaveBeenCalled();
|
||||||
|
expect(
|
||||||
|
mockRuleTransformer.convertAllRulesToProfileRules
|
||||||
|
).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles template operations gracefully', async () => {
|
||||||
|
// Make file operations throw errors
|
||||||
|
mockFs.writeFileSync.mockImplementation(() => {
|
||||||
|
throw new Error('Write failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should complete despite file operation failures
|
||||||
|
await expect(
|
||||||
|
initializeProject({
|
||||||
|
...baseOptions,
|
||||||
|
git: false,
|
||||||
|
aliases: false,
|
||||||
|
dryRun: false
|
||||||
|
})
|
||||||
|
).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates boolean flag conversion', async () => {
|
||||||
|
// Test the boolean flag handling specifically
|
||||||
|
await expect(
|
||||||
|
initializeProject({
|
||||||
|
...baseOptions,
|
||||||
|
git: true, // Should convert to initGit: true
|
||||||
|
aliases: false, // Should convert to addAliases: false
|
||||||
|
dryRun: true
|
||||||
|
})
|
||||||
|
).resolves.not.toThrow();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
initializeProject({
|
||||||
|
...baseOptions,
|
||||||
|
git: false, // Should convert to initGit: false
|
||||||
|
aliases: true, // Should convert to addAliases: true
|
||||||
|
dryRun: true
|
||||||
|
})
|
||||||
|
).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
439
tests/unit/manage-gitignore.test.js
Normal file
439
tests/unit/manage-gitignore.test.js
Normal file
@@ -0,0 +1,439 @@
|
|||||||
|
/**
|
||||||
|
* Unit tests for manage-gitignore.js module
|
||||||
|
* Tests the logic with Jest spies instead of mocked modules
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { jest } from '@jest/globals';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import os from 'os';
|
||||||
|
|
||||||
|
// Import the module under test and its exports
|
||||||
|
import manageGitignoreFile, {
|
||||||
|
normalizeLine,
|
||||||
|
isTaskLine,
|
||||||
|
buildTaskFilesSection,
|
||||||
|
TASK_FILES_COMMENT,
|
||||||
|
TASK_JSON_PATTERN,
|
||||||
|
TASK_DIR_PATTERN
|
||||||
|
} from '../../src/utils/manage-gitignore.js';
|
||||||
|
|
||||||
|
describe('manage-gitignore.js Unit Tests', () => {
|
||||||
|
let tempDir;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
// Create a temporary directory for testing
|
||||||
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'manage-gitignore-test-'));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up the temporary directory
|
||||||
|
try {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch (err) {
|
||||||
|
// Ignore cleanup errors
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Constants', () => {
|
||||||
|
test('should have correct constant values', () => {
|
||||||
|
expect(TASK_FILES_COMMENT).toBe('# Task files');
|
||||||
|
expect(TASK_JSON_PATTERN).toBe('tasks.json');
|
||||||
|
expect(TASK_DIR_PATTERN).toBe('tasks/');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('normalizeLine function', () => {
|
||||||
|
test('should remove leading/trailing whitespace', () => {
|
||||||
|
expect(normalizeLine(' test ')).toBe('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should remove comment hash and trim', () => {
|
||||||
|
expect(normalizeLine('# tasks.json')).toBe('tasks.json');
|
||||||
|
expect(normalizeLine('#tasks/')).toBe('tasks/');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle empty strings', () => {
|
||||||
|
expect(normalizeLine('')).toBe('');
|
||||||
|
expect(normalizeLine(' ')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle lines without comments', () => {
|
||||||
|
expect(normalizeLine('tasks.json')).toBe('tasks.json');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isTaskLine function', () => {
|
||||||
|
test('should identify task.json patterns', () => {
|
||||||
|
expect(isTaskLine('tasks.json')).toBe(true);
|
||||||
|
expect(isTaskLine('# tasks.json')).toBe(true);
|
||||||
|
expect(isTaskLine(' # tasks.json ')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should identify tasks/ patterns', () => {
|
||||||
|
expect(isTaskLine('tasks/')).toBe(true);
|
||||||
|
expect(isTaskLine('# tasks/')).toBe(true);
|
||||||
|
expect(isTaskLine(' # tasks/ ')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should reject non-task patterns', () => {
|
||||||
|
expect(isTaskLine('node_modules/')).toBe(false);
|
||||||
|
expect(isTaskLine('# Some comment')).toBe(false);
|
||||||
|
expect(isTaskLine('')).toBe(false);
|
||||||
|
expect(isTaskLine('tasks.txt')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buildTaskFilesSection function', () => {
|
||||||
|
test('should build commented section when storeTasksInGit is true (tasks stored in git)', () => {
|
||||||
|
const result = buildTaskFilesSection(true);
|
||||||
|
expect(result).toEqual(['# Task files', '# tasks.json', '# tasks/ ']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should build uncommented section when storeTasksInGit is false (tasks ignored)', () => {
|
||||||
|
const result = buildTaskFilesSection(false);
|
||||||
|
expect(result).toEqual(['# Task files', 'tasks.json', 'tasks/ ']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('manageGitignoreFile function - Input Validation', () => {
|
||||||
|
test('should throw error for invalid targetPath', () => {
|
||||||
|
expect(() => {
|
||||||
|
manageGitignoreFile('', 'content', false);
|
||||||
|
}).toThrow('targetPath must be a non-empty string');
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
manageGitignoreFile(null, 'content', false);
|
||||||
|
}).toThrow('targetPath must be a non-empty string');
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
manageGitignoreFile('invalid.txt', 'content', false);
|
||||||
|
}).toThrow('targetPath must end with .gitignore');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw error for invalid content', () => {
|
||||||
|
expect(() => {
|
||||||
|
manageGitignoreFile('.gitignore', '', false);
|
||||||
|
}).toThrow('content must be a non-empty string');
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
manageGitignoreFile('.gitignore', null, false);
|
||||||
|
}).toThrow('content must be a non-empty string');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw error for invalid storeTasksInGit', () => {
|
||||||
|
expect(() => {
|
||||||
|
manageGitignoreFile('.gitignore', 'content', 'not-boolean');
|
||||||
|
}).toThrow('storeTasksInGit must be a boolean');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('manageGitignoreFile function - File Operations with Spies', () => {
|
||||||
|
let writeFileSyncSpy;
|
||||||
|
let readFileSyncSpy;
|
||||||
|
let existsSyncSpy;
|
||||||
|
let mockLog;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Set up spies
|
||||||
|
writeFileSyncSpy = jest
|
||||||
|
.spyOn(fs, 'writeFileSync')
|
||||||
|
.mockImplementation(() => {});
|
||||||
|
readFileSyncSpy = jest
|
||||||
|
.spyOn(fs, 'readFileSync')
|
||||||
|
.mockImplementation(() => '');
|
||||||
|
existsSyncSpy = jest
|
||||||
|
.spyOn(fs, 'existsSync')
|
||||||
|
.mockImplementation(() => false);
|
||||||
|
mockLog = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Restore original implementations
|
||||||
|
writeFileSyncSpy.mockRestore();
|
||||||
|
readFileSyncSpy.mockRestore();
|
||||||
|
existsSyncSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('New File Creation', () => {
|
||||||
|
const templateContent = `# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Task files
|
||||||
|
tasks.json
|
||||||
|
tasks/ `;
|
||||||
|
|
||||||
|
test('should create new file with commented task lines when storeTasksInGit is true', () => {
|
||||||
|
existsSyncSpy.mockReturnValue(false); // File doesn't exist
|
||||||
|
|
||||||
|
manageGitignoreFile('.gitignore', templateContent, true, mockLog);
|
||||||
|
|
||||||
|
expect(writeFileSyncSpy).toHaveBeenCalledWith(
|
||||||
|
'.gitignore',
|
||||||
|
`# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Task files
|
||||||
|
# tasks.json
|
||||||
|
# tasks/ `
|
||||||
|
);
|
||||||
|
expect(mockLog).toHaveBeenCalledWith(
|
||||||
|
'success',
|
||||||
|
'Created .gitignore with full template'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should create new file with uncommented task lines when storeTasksInGit is false', () => {
|
||||||
|
existsSyncSpy.mockReturnValue(false); // File doesn't exist
|
||||||
|
|
||||||
|
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
|
||||||
|
|
||||||
|
expect(writeFileSyncSpy).toHaveBeenCalledWith(
|
||||||
|
'.gitignore',
|
||||||
|
`# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Task files
|
||||||
|
tasks.json
|
||||||
|
tasks/ `
|
||||||
|
);
|
||||||
|
expect(mockLog).toHaveBeenCalledWith(
|
||||||
|
'success',
|
||||||
|
'Created .gitignore with full template'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle write errors gracefully', () => {
|
||||||
|
existsSyncSpy.mockReturnValue(false);
|
||||||
|
const writeError = new Error('Permission denied');
|
||||||
|
writeFileSyncSpy.mockImplementation(() => {
|
||||||
|
throw writeError;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
|
||||||
|
}).toThrow('Permission denied');
|
||||||
|
|
||||||
|
expect(mockLog).toHaveBeenCalledWith(
|
||||||
|
'error',
|
||||||
|
'Failed to create .gitignore: Permission denied'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('File Merging', () => {
|
||||||
|
const templateContent = `# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Task files
|
||||||
|
tasks.json
|
||||||
|
tasks/ `;
|
||||||
|
|
||||||
|
test('should merge with existing file and add new content', () => {
|
||||||
|
const existingContent = `# Old content
|
||||||
|
old-file.txt
|
||||||
|
|
||||||
|
# Task files
|
||||||
|
# tasks.json
|
||||||
|
# tasks/`;
|
||||||
|
|
||||||
|
existsSyncSpy.mockReturnValue(true); // File exists
|
||||||
|
readFileSyncSpy.mockReturnValue(existingContent);
|
||||||
|
|
||||||
|
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
|
||||||
|
|
||||||
|
expect(writeFileSyncSpy).toHaveBeenCalledWith(
|
||||||
|
'.gitignore',
|
||||||
|
expect.stringContaining('# Old content')
|
||||||
|
);
|
||||||
|
expect(writeFileSyncSpy).toHaveBeenCalledWith(
|
||||||
|
'.gitignore',
|
||||||
|
expect.stringContaining('# Logs')
|
||||||
|
);
|
||||||
|
expect(writeFileSyncSpy).toHaveBeenCalledWith(
|
||||||
|
'.gitignore',
|
||||||
|
expect.stringContaining('# Dependencies')
|
||||||
|
);
|
||||||
|
expect(writeFileSyncSpy).toHaveBeenCalledWith(
|
||||||
|
'.gitignore',
|
||||||
|
expect.stringContaining('# Task files')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should remove existing task section and replace with new preferences', () => {
|
||||||
|
const existingContent = `# Existing
|
||||||
|
existing.txt
|
||||||
|
|
||||||
|
# Task files
|
||||||
|
tasks.json
|
||||||
|
tasks/
|
||||||
|
|
||||||
|
# More content
|
||||||
|
more.txt`;
|
||||||
|
|
||||||
|
existsSyncSpy.mockReturnValue(true);
|
||||||
|
readFileSyncSpy.mockReturnValue(existingContent);
|
||||||
|
|
||||||
|
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
|
||||||
|
|
||||||
|
const writtenContent = writeFileSyncSpy.mock.calls[0][1];
|
||||||
|
|
||||||
|
// Should contain existing non-task content
|
||||||
|
expect(writtenContent).toContain('# Existing');
|
||||||
|
expect(writtenContent).toContain('existing.txt');
|
||||||
|
expect(writtenContent).toContain('# More content');
|
||||||
|
expect(writtenContent).toContain('more.txt');
|
||||||
|
|
||||||
|
// Should contain new template content
|
||||||
|
expect(writtenContent).toContain('# Logs');
|
||||||
|
expect(writtenContent).toContain('# Dependencies');
|
||||||
|
|
||||||
|
// Should have uncommented task lines (storeTasksInGit = false means ignore tasks)
|
||||||
|
expect(writtenContent).toMatch(
|
||||||
|
/# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle different task preferences correctly', () => {
|
||||||
|
const existingContent = `# Existing
|
||||||
|
existing.txt
|
||||||
|
|
||||||
|
# Task files
|
||||||
|
# tasks.json
|
||||||
|
# tasks/`;
|
||||||
|
|
||||||
|
existsSyncSpy.mockReturnValue(true);
|
||||||
|
readFileSyncSpy.mockReturnValue(existingContent);
|
||||||
|
|
||||||
|
// Test with storeTasksInGit = true (commented)
|
||||||
|
manageGitignoreFile('.gitignore', templateContent, true, mockLog);
|
||||||
|
|
||||||
|
const writtenContent = writeFileSyncSpy.mock.calls[0][1];
|
||||||
|
expect(writtenContent).toMatch(
|
||||||
|
/# Task files\s*[\r\n]+# tasks\.json\s*[\r\n]+# tasks\/ /
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not duplicate existing template content', () => {
|
||||||
|
const existingContent = `# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Task files
|
||||||
|
# tasks.json
|
||||||
|
# tasks/`;
|
||||||
|
|
||||||
|
existsSyncSpy.mockReturnValue(true);
|
||||||
|
readFileSyncSpy.mockReturnValue(existingContent);
|
||||||
|
|
||||||
|
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
|
||||||
|
|
||||||
|
const writtenContent = writeFileSyncSpy.mock.calls[0][1];
|
||||||
|
|
||||||
|
// Should not duplicate the logs section
|
||||||
|
const logsCount = (writtenContent.match(/# Logs/g) || []).length;
|
||||||
|
expect(logsCount).toBe(1);
|
||||||
|
|
||||||
|
// Should not duplicate dependencies
|
||||||
|
const depsCount = (writtenContent.match(/# Dependencies/g) || [])
|
||||||
|
.length;
|
||||||
|
expect(depsCount).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle read errors gracefully', () => {
|
||||||
|
existsSyncSpy.mockReturnValue(true);
|
||||||
|
const readError = new Error('File not readable');
|
||||||
|
readFileSyncSpy.mockImplementation(() => {
|
||||||
|
throw readError;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
|
||||||
|
}).toThrow('File not readable');
|
||||||
|
|
||||||
|
expect(mockLog).toHaveBeenCalledWith(
|
||||||
|
'error',
|
||||||
|
'Failed to merge content with .gitignore: File not readable'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle write errors during merge gracefully', () => {
|
||||||
|
existsSyncSpy.mockReturnValue(true);
|
||||||
|
readFileSyncSpy.mockReturnValue('existing content');
|
||||||
|
|
||||||
|
const writeError = new Error('Disk full');
|
||||||
|
writeFileSyncSpy.mockImplementation(() => {
|
||||||
|
throw writeError;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
|
||||||
|
}).toThrow('Disk full');
|
||||||
|
|
||||||
|
expect(mockLog).toHaveBeenCalledWith(
|
||||||
|
'error',
|
||||||
|
'Failed to merge content with .gitignore: Disk full'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Edge Cases', () => {
|
||||||
|
test('should work without log function', () => {
|
||||||
|
existsSyncSpy.mockReturnValue(false);
|
||||||
|
const templateContent = `# Test
|
||||||
|
test.txt
|
||||||
|
|
||||||
|
# Task files
|
||||||
|
tasks.json
|
||||||
|
tasks/`;
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
manageGitignoreFile('.gitignore', templateContent, false);
|
||||||
|
}).not.toThrow();
|
||||||
|
|
||||||
|
expect(writeFileSyncSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle empty existing file', () => {
|
||||||
|
existsSyncSpy.mockReturnValue(true);
|
||||||
|
readFileSyncSpy.mockReturnValue('');
|
||||||
|
|
||||||
|
const templateContent = `# Task files
|
||||||
|
tasks.json
|
||||||
|
tasks/`;
|
||||||
|
|
||||||
|
manageGitignoreFile('.gitignore', templateContent, false, mockLog);
|
||||||
|
|
||||||
|
expect(writeFileSyncSpy).toHaveBeenCalled();
|
||||||
|
const writtenContent = writeFileSyncSpy.mock.calls[0][1];
|
||||||
|
expect(writtenContent).toContain('# Task files');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle template with only task files', () => {
|
||||||
|
existsSyncSpy.mockReturnValue(false);
|
||||||
|
const templateContent = `# Task files
|
||||||
|
tasks.json
|
||||||
|
tasks/ `;
|
||||||
|
|
||||||
|
manageGitignoreFile('.gitignore', templateContent, true, mockLog);
|
||||||
|
|
||||||
|
const writtenContent = writeFileSyncSpy.mock.calls[0][1];
|
||||||
|
expect(writtenContent).toBe(`# Task files
|
||||||
|
# tasks.json
|
||||||
|
# tasks/ `);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
72
vite.config.js
Normal file
72
vite.config.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
ssr: true, // Use SSR mode for Node.js
|
||||||
|
rollupOptions: {
|
||||||
|
// Multiple entry points for different applications
|
||||||
|
input: {
|
||||||
|
'task-master': resolve(__dirname, 'bin/task-master.js'), // CLI tool
|
||||||
|
'task-master-mcp': resolve(__dirname, 'mcp-server/server.js') // MCP server
|
||||||
|
},
|
||||||
|
// Bundle everything except Node.js built-ins
|
||||||
|
external: [
|
||||||
|
// Node.js built-in modules
|
||||||
|
'fs',
|
||||||
|
'fs/promises',
|
||||||
|
'path',
|
||||||
|
'os',
|
||||||
|
'crypto',
|
||||||
|
'http',
|
||||||
|
'https',
|
||||||
|
'net',
|
||||||
|
'tls',
|
||||||
|
'child_process',
|
||||||
|
'util',
|
||||||
|
'events',
|
||||||
|
'stream',
|
||||||
|
'url',
|
||||||
|
'querystring',
|
||||||
|
'buffer',
|
||||||
|
'module',
|
||||||
|
'worker_threads',
|
||||||
|
'readline',
|
||||||
|
'process',
|
||||||
|
'assert',
|
||||||
|
'zlib',
|
||||||
|
'dns',
|
||||||
|
'perf_hooks',
|
||||||
|
// Optional dependencies that might not be available
|
||||||
|
'@anthropic-ai/claude-code'
|
||||||
|
],
|
||||||
|
output: {
|
||||||
|
// Generate separate files for each entry
|
||||||
|
dir: 'dist',
|
||||||
|
format: 'cjs', // CommonJS for Node.js compatibility
|
||||||
|
entryFileNames: '[name].cjs',
|
||||||
|
chunkFileNames: 'chunks/[name]-[hash].cjs',
|
||||||
|
assetFileNames: 'assets/[name].[ext]'
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
nodeResolve({
|
||||||
|
preferBuiltins: true,
|
||||||
|
exportConditions: ['node']
|
||||||
|
})
|
||||||
|
]
|
||||||
|
},
|
||||||
|
target: 'node18',
|
||||||
|
outDir: 'dist',
|
||||||
|
minify: false, // Keep readable for debugging
|
||||||
|
sourcemap: false
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
// Define any environment variables if needed
|
||||||
|
'process.env.NODE_ENV': '"production"'
|
||||||
|
},
|
||||||
|
ssr: {
|
||||||
|
// Don't externalize any dependencies - bundle them all
|
||||||
|
noExternal: true
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user