Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@

**Stop typing code. Start directing AI agents.**

> **[!WARNING]**
>
> **This project is no longer actively maintained.** The codebase is provided as-is. No bug fixes, security updates, or new features are being developed.

<details open>
<summary><h2>Table of Contents</h2></summary>

Expand Down
74 changes: 74 additions & 0 deletions apps/server/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { defineConfig, globalIgnores } from 'eslint/config';
import js from '@eslint/js';
import ts from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';

const eslintConfig = defineConfig([
js.configs.recommended,
{
files: ['**/*.ts'],
languageOptions: {
parser: tsParser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
globals: {
// Node.js globals
console: 'readonly',
process: 'readonly',
Buffer: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
URL: 'readonly',
URLSearchParams: 'readonly',
AbortController: 'readonly',
AbortSignal: 'readonly',
fetch: 'readonly',
Response: 'readonly',
Request: 'readonly',
Headers: 'readonly',
FormData: 'readonly',
RequestInit: 'readonly',
// Timers
setTimeout: 'readonly',
setInterval: 'readonly',
clearTimeout: 'readonly',
clearInterval: 'readonly',
setImmediate: 'readonly',
clearImmediate: 'readonly',
queueMicrotask: 'readonly',
// Node.js types
NodeJS: 'readonly',
},
},
plugins: {
'@typescript-eslint': ts,
},
rules: {
...ts.configs.recommended.rules,
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
'@typescript-eslint/no-explicit-any': 'warn',
// Server code frequently works with terminal output containing ANSI escape codes
'no-control-regex': 'off',
'@typescript-eslint/ban-ts-comment': [
'error',
{
'ts-nocheck': 'allow-with-description',
minimumDescriptionLength: 10,
},
],
},
},
globalIgnores(['dist/**', 'node_modules/**']),
]);

export default eslintConfig;
13 changes: 0 additions & 13 deletions apps/server/src/routes/features/routes/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,6 @@ export function createCreateHandler(featureLoader: FeatureLoader, events?: Event
return;
}

// Check for duplicate title if title is provided
if (feature.title && feature.title.trim()) {
const duplicate = await featureLoader.findDuplicateTitle(projectPath, feature.title);
if (duplicate) {
res.status(409).json({
success: false,
error: `A feature with title "${feature.title}" already exists`,
duplicateFeatureId: duplicate.id,
});
return;
}
}

const created = await featureLoader.create(projectPath, feature);

// Emit feature_created event for hooks
Expand Down
17 changes: 0 additions & 17 deletions apps/server/src/routes/features/routes/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,6 @@ export function createUpdateHandler(featureLoader: FeatureLoader) {
return;
}

// Check for duplicate title if title is being updated
if (updates.title && updates.title.trim()) {
const duplicate = await featureLoader.findDuplicateTitle(
projectPath,
updates.title,
featureId // Exclude the current feature from duplicate check
);
if (duplicate) {
res.status(409).json({
success: false,
error: `A feature with title "${updates.title}" already exists`,
duplicateFeatureId: duplicate.id,
});
return;
}
}

// Get the current feature to detect status changes
const currentFeature = await featureLoader.get(projectPath, featureId);
const previousStatus = currentFeature?.status as FeatureStatus | undefined;
Expand Down
7 changes: 6 additions & 1 deletion apps/server/src/routes/worktree/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,12 @@ export function createWorktreeRoutes(
requireValidWorktree,
createPullHandler()
);
router.post('/checkout-branch', requireValidWorktree, createCheckoutBranchHandler());
router.post(
'/checkout-branch',
validatePathParams('worktreePath'),
requireValidWorktree,
createCheckoutBranchHandler()
);
router.post(
'/list-branches',
validatePathParams('worktreePath'),
Expand Down
60 changes: 39 additions & 21 deletions apps/server/src/routes/worktree/routes/checkout-branch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
* POST /checkout-branch endpoint - Create and checkout a new branch
*
* Note: Git repository validation (isGitRepo, hasCommits) is handled by
* the requireValidWorktree middleware in index.ts
* the requireValidWorktree middleware in index.ts.
* Path validation (ALLOWED_ROOT_DIRECTORY) is handled by validatePathParams
* middleware in index.ts.
*/

import type { Request, Response } from 'express';
import { exec } from 'child_process';
import { promisify } from 'util';
import { getErrorMessage, logError } from '../common.js';

const execAsync = promisify(exec);
import path from 'path';
import { stat } from 'fs/promises';
import { getErrorMessage, logError, isValidBranchName, execGitCommand } from '../common.js';

export function createCheckoutBranchHandler() {
return async (req: Request, res: Response): Promise<void> => {
Expand All @@ -36,27 +36,47 @@ export function createCheckoutBranchHandler() {
return;
}

// Validate branch name (basic validation)
const invalidChars = /[\s~^:?*\[\\]/;
if (invalidChars.test(branchName)) {
// Validate branch name using shared allowlist: /^[a-zA-Z0-9._\-/]+$/
if (!isValidBranchName(branchName)) {
res.status(400).json({
success: false,
error: 'Branch name contains invalid characters',
error:
'Invalid branch name. Must contain only letters, numbers, dots, dashes, underscores, or slashes.',
});
return;
}

// Get current branch for reference
const { stdout: currentBranchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
cwd: worktreePath,
});
// Resolve and validate worktreePath to prevent traversal attacks.
// The validatePathParams middleware checks against ALLOWED_ROOT_DIRECTORY,
// but we also resolve the path and verify it exists as a directory.
const resolvedPath = path.resolve(worktreePath);
try {
const stats = await stat(resolvedPath);
if (!stats.isDirectory()) {
res.status(400).json({
success: false,
error: 'worktreePath is not a directory',
});
return;
}
} catch {
res.status(400).json({
success: false,
error: 'worktreePath does not exist or is not accessible',
});
return;
}

// Get current branch for reference (using argument array to avoid shell injection)
const currentBranchOutput = await execGitCommand(
['rev-parse', '--abbrev-ref', 'HEAD'],
resolvedPath
);
const currentBranch = currentBranchOutput.trim();

// Check if branch already exists
try {
await execAsync(`git rev-parse --verify ${branchName}`, {
cwd: worktreePath,
});
await execGitCommand(['rev-parse', '--verify', branchName], resolvedPath);
// Branch exists
res.status(400).json({
success: false,
Expand All @@ -67,10 +87,8 @@ export function createCheckoutBranchHandler() {
// Branch doesn't exist, good to create
}

// Create and checkout the new branch
await execAsync(`git checkout -b ${branchName}`, {
cwd: worktreePath,
});
// Create and checkout the new branch (using argument array to avoid shell injection)
await execGitCommand(['checkout', '-b', branchName], resolvedPath);

res.json({
success: true,
Expand Down
21 changes: 8 additions & 13 deletions apps/server/src/routes/worktree/routes/open-in-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,19 +125,14 @@ export function createOpenInEditorHandler() {
`Failed to open in editor, falling back to file manager: ${getErrorMessage(editorError)}`
);

try {
const result = await openInFileManager(worktreePath);
res.json({
success: true,
result: {
message: `Opened ${worktreePath} in ${result.editorName}`,
editorName: result.editorName,
},
});
} catch (fallbackError) {
// Both editor and file manager failed
throw fallbackError;
}
const result = await openInFileManager(worktreePath);
res.json({
success: true,
result: {
message: `Opened ${worktreePath} in ${result.editorName}`,
editorName: result.editorName,
},
});
}
} catch (error) {
logError(error, 'Open in editor failed');
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/services/claude-usage-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,7 @@ export class ClaudeUsageService {

resetTime = this.parseResetTime(resetText, type);
// Strip timezone like "(Asia/Dubai)" from the display text
resetText = resetText.replace(/\s*\([A-Za-z_\/]+\)\s*$/, '').trim();
resetText = resetText.replace(/\s*\([A-Za-z_/]+\)\s*$/, '').trim();
}

return { percentage: percentage ?? 0, resetTime, resetText };
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/services/dev-server-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ class DevServerService {
/(?:Local|Network):\s+(https?:\/\/[^\s]+)/i, // Vite format
/(?:ready|started server).*?(?:url:\s*)?(https?:\/\/[^\s,]+)/i, // Next.js format
/(https?:\/\/(?:localhost|127\.0\.0\.1|\[::\]):\d+)/i, // Generic localhost URL
/(https?:\/\/[^\s<>"{}|\\^`\[\]]+)/i, // Any HTTP(S) URL
/(https?:\/\/[^\s<>"{}|\\^`[\]]+)/i, // Any HTTP(S) URL
];

for (const pattern of urlPatterns) {
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/services/ideation-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -888,7 +888,7 @@ ${contextSection}${existingWorkSection}`;

for (const line of lines) {
// Check for numbered items or markdown headers
const titleMatch = line.match(/^(?:\d+[\.\)]\s*\*{0,2}|#{1,3}\s+)(.+)/);
const titleMatch = line.match(/^(?:\d+[.)]\s*\*{0,2}|#{1,3}\s+)(.+)/);

if (titleMatch) {
// Save previous suggestion
Expand Down
10 changes: 9 additions & 1 deletion apps/ui/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,15 @@ const eslintConfig = defineConfig([
},
rules: {
...ts.configs.recommended.rules,
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/ban-ts-comment': [
'error',
Expand Down
5 changes: 5 additions & 0 deletions apps/ui/src/components/views/board-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@ export function BoardView() {
handleForceStopFeature,
handleStartNextFeatures,
handleArchiveAllVerified,
handleDuplicateFeature,
} = useBoardActions({
currentProject,
features: hookFeatures,
Expand Down Expand Up @@ -1503,6 +1504,8 @@ export function BoardView() {
setSpawnParentFeature(feature);
setShowAddDialog(true);
},
onDuplicate: (feature) => handleDuplicateFeature(feature, false),
onDuplicateAsChild: (feature) => handleDuplicateFeature(feature, true),
}}
runningAutoTasks={runningAutoTasksAllWorktrees}
pipelineConfig={pipelineConfig}
Expand Down Expand Up @@ -1542,6 +1545,8 @@ export function BoardView() {
setSpawnParentFeature(feature);
setShowAddDialog(true);
}}
onDuplicate={(feature) => handleDuplicateFeature(feature, false)}
onDuplicateAsChild={(feature) => handleDuplicateFeature(feature, true)}
featuresWithContext={featuresWithContext}
runningAutoTasks={runningAutoTasksAllWorktrees}
onArchiveAllVerified={() => setShowArchiveAllVerifiedDialog(true)}
Expand Down
Loading