Files
claude-task-master/packages/tm-core/src/utils/id-generator.ts
Ralph Khreish 19ec52181d feat: create tm-core and apps/cli (#1093)
- add typescript
- add npm workspaces
2025-09-01 21:44:43 +02:00

143 lines
3.9 KiB
TypeScript

/**
* @fileoverview ID generation utilities for Task Master
* Provides functions to generate unique identifiers for tasks and subtasks
*/
import { randomBytes } from 'node:crypto';
/**
* Generates a unique task ID using the format: TASK-{timestamp}-{random}
*
* @returns A unique task ID string
* @example
* ```typescript
* const taskId = generateTaskId();
* // Returns something like: "TASK-1704067200000-A7B3"
* ```
*/
export function generateTaskId(): string {
const timestamp = Date.now();
const random = generateRandomString(4);
return `TASK-${timestamp}-${random}`;
}
/**
* Generates a subtask ID using the format: {parentId}.{sequential}
*
* @param parentId - The ID of the parent task
* @param existingSubtasks - Array of existing subtask IDs to determine the next sequential number
* @returns A unique subtask ID string
* @example
* ```typescript
* const subtaskId = generateSubtaskId("TASK-123-A7B3", ["TASK-123-A7B3.1"]);
* // Returns: "TASK-123-A7B3.2"
* ```
*/
export function generateSubtaskId(
parentId: string,
existingSubtasks: string[] = []
): string {
// Find existing subtasks for this parent
const parentSubtasks = existingSubtasks.filter((id) =>
id.startsWith(`${parentId}.`)
);
// Extract sequential numbers and find the highest
const sequentialNumbers = parentSubtasks
.map((id) => {
const parts = id.split('.');
const lastPart = parts[parts.length - 1];
return Number.parseInt(lastPart, 10);
})
.filter((num) => !Number.isNaN(num))
.sort((a, b) => a - b);
// Determine the next sequential number
const nextSequential =
sequentialNumbers.length > 0 ? Math.max(...sequentialNumbers) + 1 : 1;
return `${parentId}.${nextSequential}`;
}
/**
* Generates a random alphanumeric string of specified length
* Uses crypto.randomBytes for cryptographically secure randomness
*
* @param length - The desired length of the random string
* @returns A random alphanumeric string
* @internal
*/
function generateRandomString(length: number): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const bytes = randomBytes(length);
let result = '';
for (let i = 0; i < length; i++) {
result += chars[bytes[i] % chars.length];
}
return result;
}
/**
* Validates a task ID format
*
* @param id - The ID to validate
* @returns True if the ID matches the expected task ID format
* @example
* ```typescript
* isValidTaskId("TASK-1704067200000-A7B3"); // true
* isValidTaskId("invalid-id"); // false
* ```
*/
export function isValidTaskId(id: string): boolean {
const taskIdRegex = /^TASK-\d{13}-[A-Z0-9]{4}$/;
return taskIdRegex.test(id);
}
/**
* Validates a subtask ID format
*
* @param id - The ID to validate
* @returns True if the ID matches the expected subtask ID format
* @example
* ```typescript
* isValidSubtaskId("TASK-1704067200000-A7B3.1"); // true
* isValidSubtaskId("TASK-1704067200000-A7B3.1.2"); // true (nested subtask)
* isValidSubtaskId("invalid.id"); // false
* ```
*/
export function isValidSubtaskId(id: string): boolean {
const parts = id.split('.');
if (parts.length < 2) return false;
// First part should be a valid task ID
const taskIdPart = parts[0];
if (!isValidTaskId(taskIdPart)) return false;
// Remaining parts should be positive integers
const sequentialParts = parts.slice(1);
return sequentialParts.every((part) => {
const num = Number.parseInt(part, 10);
return !Number.isNaN(num) && num > 0 && part === num.toString();
});
}
/**
* Extracts the parent task ID from a subtask ID
*
* @param subtaskId - The subtask ID
* @returns The parent task ID, or null if the input is not a valid subtask ID
* @example
* ```typescript
* getParentTaskId("TASK-1704067200000-A7B3.1.2"); // "TASK-1704067200000-A7B3"
* getParentTaskId("TASK-1704067200000-A7B3"); // null (not a subtask)
* ```
*/
export function getParentTaskId(subtaskId: string): string | null {
if (!isValidSubtaskId(subtaskId)) return null;
const parts = subtaskId.split('.');
return parts[0];
}