New features: - detectProjectFromCwd(): Maps directory paths to project keys - project_context: Returns project, tasks, epics, lock status - Auto-detection for 18 known projects (ST, VPN, OWUI, etc.) - Falls back to extracting from Apps/X or Infrastructure/X paths Use project_context at session start to see only relevant tasks. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
186 lines
4.5 KiB
TypeScript
186 lines
4.5 KiB
TypeScript
import pg from 'pg';
|
|
const { Pool } = pg;
|
|
|
|
// Configuration from environment variables
|
|
const config = {
|
|
host: process.env.DB_HOST || 'localhost',
|
|
port: parseInt(process.env.DB_PORT || '5432'),
|
|
database: process.env.DB_NAME || 'agiliton',
|
|
user: process.env.DB_USER || 'agiliton',
|
|
password: process.env.DB_PASSWORD || 'QtqiwCOAUpQNF6pjzOMAREzUny2bY8V1',
|
|
max: 5,
|
|
idleTimeoutMillis: 30000,
|
|
connectionTimeoutMillis: 5000,
|
|
};
|
|
|
|
// Create connection pool
|
|
const pool = new Pool(config);
|
|
|
|
// Log connection errors
|
|
pool.on('error', (err) => {
|
|
console.error('Unexpected database error:', err);
|
|
});
|
|
|
|
/**
|
|
* Execute a query and return all rows
|
|
*/
|
|
export async function query<T = Record<string, unknown>>(
|
|
text: string,
|
|
params?: unknown[]
|
|
): Promise<T[]> {
|
|
const result = await pool.query(text, params);
|
|
return result.rows as T[];
|
|
}
|
|
|
|
/**
|
|
* Execute a query and return the first row
|
|
*/
|
|
export async function queryOne<T = Record<string, unknown>>(
|
|
text: string,
|
|
params?: unknown[]
|
|
): Promise<T | null> {
|
|
const result = await pool.query(text, params);
|
|
return (result.rows[0] as T) || null;
|
|
}
|
|
|
|
/**
|
|
* Execute a query without returning results (INSERT, UPDATE, DELETE)
|
|
*/
|
|
export async function execute(
|
|
text: string,
|
|
params?: unknown[]
|
|
): Promise<number> {
|
|
const result = await pool.query(text, params);
|
|
return result.rowCount || 0;
|
|
}
|
|
|
|
/**
|
|
* Get the next task ID for a project
|
|
*/
|
|
export async function getNextTaskId(projectKey: string): Promise<string> {
|
|
const result = await queryOne<{ next_id: number }>(
|
|
`INSERT INTO task_sequences (project, next_id) VALUES ($1, 1)
|
|
ON CONFLICT (project) DO UPDATE SET next_id = task_sequences.next_id + 1
|
|
RETURNING next_id`,
|
|
[projectKey]
|
|
);
|
|
return `${projectKey}-${result?.next_id || 1}`;
|
|
}
|
|
|
|
/**
|
|
* Detect project from current working directory
|
|
*/
|
|
export function detectProjectFromCwd(): string | null {
|
|
const cwd = process.cwd();
|
|
|
|
// Known project path patterns
|
|
const patterns: Record<string, string> = {
|
|
'SmartTranslate': 'ST',
|
|
'VPN': 'VPN',
|
|
'BestGPT': 'BGPT',
|
|
'AssistOWUI': 'OWUI',
|
|
'AssistForOpenWebUI': 'OWUI',
|
|
'AssistForJira': 'AFJ',
|
|
'eToroGridbot': 'GB',
|
|
'WildFiles': 'WF',
|
|
'Circles': 'CIR',
|
|
'ClaudeFramework': 'CF',
|
|
'AgilitonScripts': 'AS',
|
|
'WHMCS': 'WHMCS',
|
|
'Cardscanner': 'CS',
|
|
'ZorkiOS': 'ZORK',
|
|
'MeetingMind': 'MM',
|
|
'PropertyMap': 'PM',
|
|
'Rubic': 'RUB',
|
|
'Socialguard': 'SG',
|
|
};
|
|
|
|
// Check each pattern
|
|
for (const [name, key] of Object.entries(patterns)) {
|
|
if (cwd.includes(`/${name}/`) || cwd.endsWith(`/${name}`)) {
|
|
return key;
|
|
}
|
|
}
|
|
|
|
// Try to extract from Apps/X or Libraries/X path
|
|
const appsMatch = cwd.match(/\/Apps\/([^/]+)/);
|
|
if (appsMatch) {
|
|
return appsMatch[1].replace(/[a-z]/g, '').slice(0, 4) || appsMatch[1].slice(0, 3).toUpperCase();
|
|
}
|
|
|
|
const infraMatch = cwd.match(/\/Infrastructure\/([^/]+)/);
|
|
if (infraMatch) {
|
|
return infraMatch[1].replace(/[a-z]/g, '').slice(0, 4) || infraMatch[1].slice(0, 3).toUpperCase();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get project key from name, or generate one
|
|
* If projectName is empty/undefined, tries to detect from CWD
|
|
*/
|
|
export async function getProjectKey(projectName?: string): Promise<string> {
|
|
// Auto-detect if not provided
|
|
if (!projectName) {
|
|
const detected = detectProjectFromCwd();
|
|
if (detected) {
|
|
// Ensure project exists in DB
|
|
await execute(
|
|
`INSERT INTO projects (key, name) VALUES ($1, $1)
|
|
ON CONFLICT (key) DO NOTHING`,
|
|
[detected]
|
|
);
|
|
return detected;
|
|
}
|
|
return 'MISC'; // Fallback for unknown projects
|
|
}
|
|
|
|
// First check if already registered
|
|
const existing = await queryOne<{ key: string }>(
|
|
`SELECT key FROM projects WHERE name = $1 LIMIT 1`,
|
|
[projectName]
|
|
);
|
|
|
|
if (existing) {
|
|
return existing.key;
|
|
}
|
|
|
|
// Generate a key from the name (uppercase first letters)
|
|
let generated = projectName.replace(/[a-z]/g, '').slice(0, 4);
|
|
if (!generated) {
|
|
generated = projectName.slice(0, 3).toUpperCase();
|
|
}
|
|
|
|
// Register the new project
|
|
await execute(
|
|
`INSERT INTO projects (key, name) VALUES ($1, $2)
|
|
ON CONFLICT (key) DO NOTHING`,
|
|
[generated, projectName]
|
|
);
|
|
|
|
return generated;
|
|
}
|
|
|
|
/**
|
|
* Test database connection
|
|
*/
|
|
export async function testConnection(): Promise<boolean> {
|
|
try {
|
|
await query('SELECT 1');
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Database connection failed:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Close the connection pool
|
|
*/
|
|
export async function close(): Promise<void> {
|
|
await pool.end();
|
|
}
|
|
|
|
export default pool;
|