steroids-api 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/API/src/index.d.ts +10 -0
- package/dist/API/src/index.d.ts.map +1 -0
- package/dist/API/src/index.js +130 -0
- package/dist/API/src/index.js.map +1 -0
- package/dist/API/src/routes/activity.d.ts +7 -0
- package/dist/API/src/routes/activity.d.ts.map +1 -0
- package/dist/API/src/routes/activity.js +252 -0
- package/dist/API/src/routes/activity.js.map +1 -0
- package/dist/API/src/routes/config.d.ts +7 -0
- package/dist/API/src/routes/config.d.ts.map +1 -0
- package/dist/API/src/routes/config.js +521 -0
- package/dist/API/src/routes/config.js.map +1 -0
- package/dist/API/src/routes/health.d.ts +7 -0
- package/dist/API/src/routes/health.d.ts.map +1 -0
- package/dist/API/src/routes/health.js +172 -0
- package/dist/API/src/routes/health.js.map +1 -0
- package/dist/API/src/routes/incidents.d.ts +7 -0
- package/dist/API/src/routes/incidents.d.ts.map +1 -0
- package/dist/API/src/routes/incidents.js +117 -0
- package/dist/API/src/routes/incidents.js.map +1 -0
- package/dist/API/src/routes/projects.d.ts +7 -0
- package/dist/API/src/routes/projects.d.ts.map +1 -0
- package/dist/API/src/routes/projects.js +398 -0
- package/dist/API/src/routes/projects.js.map +1 -0
- package/dist/API/src/routes/runners.d.ts +7 -0
- package/dist/API/src/routes/runners.d.ts.map +1 -0
- package/dist/API/src/routes/runners.js +242 -0
- package/dist/API/src/routes/runners.js.map +1 -0
- package/dist/API/src/routes/tasks.d.ts +7 -0
- package/dist/API/src/routes/tasks.d.ts.map +1 -0
- package/dist/API/src/routes/tasks.js +1007 -0
- package/dist/API/src/routes/tasks.js.map +1 -0
- package/dist/API/src/utils/validation.d.ts +22 -0
- package/dist/API/src/utils/validation.d.ts.map +1 -0
- package/dist/API/src/utils/validation.js +50 -0
- package/dist/API/src/utils/validation.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +184 -0
- package/dist/index.js.map +1 -0
- package/dist/routes/activity.d.ts +7 -0
- package/dist/routes/activity.d.ts.map +1 -0
- package/dist/routes/activity.js +252 -0
- package/dist/routes/activity.js.map +1 -0
- package/dist/routes/config.d.ts +7 -0
- package/dist/routes/config.d.ts.map +1 -0
- package/dist/routes/config.js +647 -0
- package/dist/routes/config.js.map +1 -0
- package/dist/routes/credit-alerts.d.ts +2 -0
- package/dist/routes/credit-alerts.d.ts.map +1 -0
- package/dist/routes/credit-alerts.js +97 -0
- package/dist/routes/credit-alerts.js.map +1 -0
- package/dist/routes/health.d.ts +7 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +200 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/incidents.d.ts +7 -0
- package/dist/routes/incidents.d.ts.map +1 -0
- package/dist/routes/incidents.js +117 -0
- package/dist/routes/incidents.js.map +1 -0
- package/dist/routes/projects.d.ts +7 -0
- package/dist/routes/projects.d.ts.map +1 -0
- package/dist/routes/projects.js +643 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/runners.d.ts +7 -0
- package/dist/routes/runners.d.ts.map +1 -0
- package/dist/routes/runners.js +299 -0
- package/dist/routes/runners.js.map +1 -0
- package/dist/routes/skills.d.ts +3 -0
- package/dist/routes/skills.d.ts.map +1 -0
- package/dist/routes/skills.js +109 -0
- package/dist/routes/skills.js.map +1 -0
- package/dist/routes/storage.d.ts +7 -0
- package/dist/routes/storage.d.ts.map +1 -0
- package/dist/routes/storage.js +93 -0
- package/dist/routes/storage.js.map +1 -0
- package/dist/routes/tasks.d.ts +7 -0
- package/dist/routes/tasks.d.ts.map +1 -0
- package/dist/routes/tasks.js +1145 -0
- package/dist/routes/tasks.js.map +1 -0
- package/dist/src/cleanup/invocation-logs.d.ts +30 -0
- package/dist/src/cleanup/invocation-logs.d.ts.map +1 -0
- package/dist/src/cleanup/invocation-logs.js +66 -0
- package/dist/src/cleanup/invocation-logs.js.map +1 -0
- package/dist/src/commands/loop-phases.d.ts +11 -0
- package/dist/src/commands/loop-phases.d.ts.map +1 -0
- package/dist/src/commands/loop-phases.js +304 -0
- package/dist/src/commands/loop-phases.js.map +1 -0
- package/dist/src/config/loader.d.ts +160 -0
- package/dist/src/config/loader.d.ts.map +1 -0
- package/dist/src/config/loader.js +276 -0
- package/dist/src/config/loader.js.map +1 -0
- package/dist/src/database/connection.d.ts +35 -0
- package/dist/src/database/connection.d.ts.map +1 -0
- package/dist/src/database/connection.js +197 -0
- package/dist/src/database/connection.js.map +1 -0
- package/dist/src/database/queries.d.ts +220 -0
- package/dist/src/database/queries.d.ts.map +1 -0
- package/dist/src/database/queries.js +589 -0
- package/dist/src/database/queries.js.map +1 -0
- package/dist/src/database/schema.d.ts +8 -0
- package/dist/src/database/schema.d.ts.map +1 -0
- package/dist/src/database/schema.js +184 -0
- package/dist/src/database/schema.js.map +1 -0
- package/dist/src/git/push.d.ts +26 -0
- package/dist/src/git/push.d.ts.map +1 -0
- package/dist/src/git/push.js +91 -0
- package/dist/src/git/push.js.map +1 -0
- package/dist/src/git/status.d.ts +83 -0
- package/dist/src/git/status.d.ts.map +1 -0
- package/dist/src/git/status.js +315 -0
- package/dist/src/git/status.js.map +1 -0
- package/dist/src/health/stuck-task-detector.d.ts +131 -0
- package/dist/src/health/stuck-task-detector.d.ts.map +1 -0
- package/dist/src/health/stuck-task-detector.js +233 -0
- package/dist/src/health/stuck-task-detector.js.map +1 -0
- package/dist/src/health/stuck-task-recovery.d.ts +45 -0
- package/dist/src/health/stuck-task-recovery.d.ts.map +1 -0
- package/dist/src/health/stuck-task-recovery.js +309 -0
- package/dist/src/health/stuck-task-recovery.js.map +1 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +130 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/locking/queries.d.ts +116 -0
- package/dist/src/locking/queries.d.ts.map +1 -0
- package/dist/src/locking/queries.js +232 -0
- package/dist/src/locking/queries.js.map +1 -0
- package/dist/src/locking/section-lock.d.ts +74 -0
- package/dist/src/locking/section-lock.d.ts.map +1 -0
- package/dist/src/locking/section-lock.js +196 -0
- package/dist/src/locking/section-lock.js.map +1 -0
- package/dist/src/locking/task-lock.d.ts +92 -0
- package/dist/src/locking/task-lock.d.ts.map +1 -0
- package/dist/src/locking/task-lock.js +233 -0
- package/dist/src/locking/task-lock.js.map +1 -0
- package/dist/src/migrations/index.d.ts +7 -0
- package/dist/src/migrations/index.d.ts.map +1 -0
- package/dist/src/migrations/index.js +9 -0
- package/dist/src/migrations/index.js.map +1 -0
- package/dist/src/migrations/manifest.d.ts +92 -0
- package/dist/src/migrations/manifest.d.ts.map +1 -0
- package/dist/src/migrations/manifest.js +255 -0
- package/dist/src/migrations/manifest.js.map +1 -0
- package/dist/src/migrations/runner.d.ts +84 -0
- package/dist/src/migrations/runner.d.ts.map +1 -0
- package/dist/src/migrations/runner.js +338 -0
- package/dist/src/migrations/runner.js.map +1 -0
- package/dist/src/orchestrator/coder.d.ts +32 -0
- package/dist/src/orchestrator/coder.d.ts.map +1 -0
- package/dist/src/orchestrator/coder.js +170 -0
- package/dist/src/orchestrator/coder.js.map +1 -0
- package/dist/src/orchestrator/coordinator.d.ts +28 -0
- package/dist/src/orchestrator/coordinator.d.ts.map +1 -0
- package/dist/src/orchestrator/coordinator.js +252 -0
- package/dist/src/orchestrator/coordinator.js.map +1 -0
- package/dist/src/orchestrator/fallback-handler.d.ts +24 -0
- package/dist/src/orchestrator/fallback-handler.d.ts.map +1 -0
- package/dist/src/orchestrator/fallback-handler.js +280 -0
- package/dist/src/orchestrator/fallback-handler.js.map +1 -0
- package/dist/src/orchestrator/invoke.d.ts +14 -0
- package/dist/src/orchestrator/invoke.d.ts.map +1 -0
- package/dist/src/orchestrator/invoke.js +76 -0
- package/dist/src/orchestrator/invoke.js.map +1 -0
- package/dist/src/orchestrator/post-coder.d.ts +10 -0
- package/dist/src/orchestrator/post-coder.d.ts.map +1 -0
- package/dist/src/orchestrator/post-coder.js +198 -0
- package/dist/src/orchestrator/post-coder.js.map +1 -0
- package/dist/src/orchestrator/post-reviewer.d.ts +10 -0
- package/dist/src/orchestrator/post-reviewer.d.ts.map +1 -0
- package/dist/src/orchestrator/post-reviewer.js +199 -0
- package/dist/src/orchestrator/post-reviewer.js.map +1 -0
- package/dist/src/orchestrator/reviewer.d.ts +35 -0
- package/dist/src/orchestrator/reviewer.d.ts.map +1 -0
- package/dist/src/orchestrator/reviewer.js +237 -0
- package/dist/src/orchestrator/reviewer.js.map +1 -0
- package/dist/src/orchestrator/schemas.d.ts +10 -0
- package/dist/src/orchestrator/schemas.d.ts.map +1 -0
- package/dist/src/orchestrator/schemas.js +81 -0
- package/dist/src/orchestrator/schemas.js.map +1 -0
- package/dist/src/orchestrator/task-selector.d.ts +102 -0
- package/dist/src/orchestrator/task-selector.d.ts.map +1 -0
- package/dist/src/orchestrator/task-selector.js +326 -0
- package/dist/src/orchestrator/task-selector.js.map +1 -0
- package/dist/src/orchestrator/types.d.ts +74 -0
- package/dist/src/orchestrator/types.d.ts.map +1 -0
- package/dist/src/orchestrator/types.js +5 -0
- package/dist/src/orchestrator/types.js.map +1 -0
- package/dist/src/prompts/coder.d.ts +36 -0
- package/dist/src/prompts/coder.d.ts.map +1 -0
- package/dist/src/prompts/coder.js +303 -0
- package/dist/src/prompts/coder.js.map +1 -0
- package/dist/src/prompts/prompt-helpers.d.ts +51 -0
- package/dist/src/prompts/prompt-helpers.d.ts.map +1 -0
- package/dist/src/prompts/prompt-helpers.js +299 -0
- package/dist/src/prompts/prompt-helpers.js.map +1 -0
- package/dist/src/prompts/reviewer.d.ts +40 -0
- package/dist/src/prompts/reviewer.d.ts.map +1 -0
- package/dist/src/prompts/reviewer.js +416 -0
- package/dist/src/prompts/reviewer.js.map +1 -0
- package/dist/src/providers/claude.d.ts +53 -0
- package/dist/src/providers/claude.d.ts.map +1 -0
- package/dist/src/providers/claude.js +227 -0
- package/dist/src/providers/claude.js.map +1 -0
- package/dist/src/providers/codex.d.ts +53 -0
- package/dist/src/providers/codex.d.ts.map +1 -0
- package/dist/src/providers/codex.js +253 -0
- package/dist/src/providers/codex.js.map +1 -0
- package/dist/src/providers/gemini.d.ts +58 -0
- package/dist/src/providers/gemini.d.ts.map +1 -0
- package/dist/src/providers/gemini.js +240 -0
- package/dist/src/providers/gemini.js.map +1 -0
- package/dist/src/providers/interface.d.ts +185 -0
- package/dist/src/providers/interface.d.ts.map +1 -0
- package/dist/src/providers/interface.js +92 -0
- package/dist/src/providers/interface.js.map +1 -0
- package/dist/src/providers/invocation-logger.d.ts +97 -0
- package/dist/src/providers/invocation-logger.d.ts.map +1 -0
- package/dist/src/providers/invocation-logger.js +378 -0
- package/dist/src/providers/invocation-logger.js.map +1 -0
- package/dist/src/providers/openai.d.ts +53 -0
- package/dist/src/providers/openai.d.ts.map +1 -0
- package/dist/src/providers/openai.js +230 -0
- package/dist/src/providers/openai.js.map +1 -0
- package/dist/src/providers/registry.d.ts +100 -0
- package/dist/src/providers/registry.d.ts.map +1 -0
- package/dist/src/providers/registry.js +170 -0
- package/dist/src/providers/registry.js.map +1 -0
- package/dist/src/routes/activity.d.ts +7 -0
- package/dist/src/routes/activity.d.ts.map +1 -0
- package/dist/src/routes/activity.js +252 -0
- package/dist/src/routes/activity.js.map +1 -0
- package/dist/src/routes/config.d.ts +7 -0
- package/dist/src/routes/config.d.ts.map +1 -0
- package/dist/src/routes/config.js +521 -0
- package/dist/src/routes/config.js.map +1 -0
- package/dist/src/routes/health.d.ts +7 -0
- package/dist/src/routes/health.d.ts.map +1 -0
- package/dist/src/routes/health.js +172 -0
- package/dist/src/routes/health.js.map +1 -0
- package/dist/src/routes/incidents.d.ts +7 -0
- package/dist/src/routes/incidents.d.ts.map +1 -0
- package/dist/src/routes/incidents.js +117 -0
- package/dist/src/routes/incidents.js.map +1 -0
- package/dist/src/routes/projects.d.ts +7 -0
- package/dist/src/routes/projects.d.ts.map +1 -0
- package/dist/src/routes/projects.js +398 -0
- package/dist/src/routes/projects.js.map +1 -0
- package/dist/src/routes/runners.d.ts +7 -0
- package/dist/src/routes/runners.d.ts.map +1 -0
- package/dist/src/routes/runners.js +242 -0
- package/dist/src/routes/runners.js.map +1 -0
- package/dist/src/routes/tasks.d.ts +7 -0
- package/dist/src/routes/tasks.d.ts.map +1 -0
- package/dist/src/routes/tasks.js +1007 -0
- package/dist/src/routes/tasks.js.map +1 -0
- package/dist/src/runners/activity-log.d.ts +65 -0
- package/dist/src/runners/activity-log.d.ts.map +1 -0
- package/dist/src/runners/activity-log.js +140 -0
- package/dist/src/runners/activity-log.js.map +1 -0
- package/dist/src/runners/cron.d.ts +30 -0
- package/dist/src/runners/cron.d.ts.map +1 -0
- package/dist/src/runners/cron.js +333 -0
- package/dist/src/runners/cron.js.map +1 -0
- package/dist/src/runners/daemon.d.ts +71 -0
- package/dist/src/runners/daemon.d.ts.map +1 -0
- package/dist/src/runners/daemon.js +233 -0
- package/dist/src/runners/daemon.js.map +1 -0
- package/dist/src/runners/global-db.d.ts +31 -0
- package/dist/src/runners/global-db.d.ts.map +1 -0
- package/dist/src/runners/global-db.js +220 -0
- package/dist/src/runners/global-db.js.map +1 -0
- package/dist/src/runners/hang-detector.d.ts +38 -0
- package/dist/src/runners/hang-detector.d.ts.map +1 -0
- package/dist/src/runners/hang-detector.js +130 -0
- package/dist/src/runners/hang-detector.js.map +1 -0
- package/dist/src/runners/heartbeat.d.ts +39 -0
- package/dist/src/runners/heartbeat.d.ts.map +1 -0
- package/dist/src/runners/heartbeat.js +71 -0
- package/dist/src/runners/heartbeat.js.map +1 -0
- package/dist/src/runners/lock.d.ts +47 -0
- package/dist/src/runners/lock.d.ts.map +1 -0
- package/dist/src/runners/lock.js +140 -0
- package/dist/src/runners/lock.js.map +1 -0
- package/dist/src/runners/orchestrator-loop.d.ts +20 -0
- package/dist/src/runners/orchestrator-loop.d.ts.map +1 -0
- package/dist/src/runners/orchestrator-loop.js +208 -0
- package/dist/src/runners/orchestrator-loop.js.map +1 -0
- package/dist/src/runners/projects.d.ts +96 -0
- package/dist/src/runners/projects.d.ts.map +1 -0
- package/dist/src/runners/projects.js +243 -0
- package/dist/src/runners/projects.js.map +1 -0
- package/dist/src/runners/wakeup.d.ts +37 -0
- package/dist/src/runners/wakeup.d.ts.map +1 -0
- package/dist/src/runners/wakeup.js +355 -0
- package/dist/src/runners/wakeup.js.map +1 -0
- package/dist/src/utils/validation.d.ts +22 -0
- package/dist/src/utils/validation.d.ts.map +1 -0
- package/dist/src/utils/validation.js +50 -0
- package/dist/src/utils/validation.js.map +1 -0
- package/dist/utils/sqlite.d.ts +17 -0
- package/dist/utils/sqlite.d.ts.map +1 -0
- package/dist/utils/sqlite.js +27 -0
- package/dist/utils/sqlite.js.map +1 -0
- package/dist/utils/storage-cache.d.ts +33 -0
- package/dist/utils/storage-cache.d.ts.map +1 -0
- package/dist/utils/storage-cache.js +81 -0
- package/dist/utils/storage-cache.js.map +1 -0
- package/dist/utils/validation.d.ts +22 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +51 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +39 -0
- package/src/index.ts +199 -0
- package/src/routes/activity.ts +302 -0
- package/src/routes/config.ts +723 -0
- package/src/routes/credit-alerts.ts +73 -0
- package/src/routes/health.ts +219 -0
- package/src/routes/incidents.ts +131 -0
- package/src/routes/projects.ts +854 -0
- package/src/routes/runners.ts +357 -0
- package/src/routes/skills.ts +127 -0
- package/src/routes/storage.ts +108 -0
- package/src/routes/tasks.ts +1372 -0
- package/src/utils/sqlite.ts +36 -0
- package/src/utils/storage-cache.ts +107 -0
- package/src/utils/validation.ts +61 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projects API routes
|
|
3
|
+
* Manages global project registry
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Router, Request, Response } from 'express';
|
|
7
|
+
import { execSync } from 'node:child_process';
|
|
8
|
+
import { existsSync, readdirSync, statSync, realpathSync } from 'node:fs';
|
|
9
|
+
import Database from 'better-sqlite3';
|
|
10
|
+
import { join, relative, resolve, sep } from 'node:path';
|
|
11
|
+
import {
|
|
12
|
+
getRegisteredProjects,
|
|
13
|
+
registerProject,
|
|
14
|
+
unregisterProject,
|
|
15
|
+
enableProject,
|
|
16
|
+
disableProject,
|
|
17
|
+
pruneProjects,
|
|
18
|
+
getRegisteredProject,
|
|
19
|
+
} from '../../../dist/runners/projects.js';
|
|
20
|
+
import { openGlobalDatabase } from '../../../dist/runners/global-db.js';
|
|
21
|
+
import { hasActiveParallelSessionForProjectDb } from '../../../dist/runners/parallel-session-state.js';
|
|
22
|
+
import { isValidProjectPath, validatePathRequest } from '../utils/validation.js';
|
|
23
|
+
import { openSqliteForRead } from '../utils/sqlite.js';
|
|
24
|
+
import { getCachedListStorage } from '../utils/storage-cache.js';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
const router = Router();
|
|
28
|
+
|
|
29
|
+
interface ProjectLiveData {
|
|
30
|
+
stats: {
|
|
31
|
+
pending: number;
|
|
32
|
+
in_progress: number;
|
|
33
|
+
review: number;
|
|
34
|
+
completed: number;
|
|
35
|
+
failed: number;
|
|
36
|
+
disputed: number;
|
|
37
|
+
skipped: number;
|
|
38
|
+
};
|
|
39
|
+
last_task_added_at: string | null;
|
|
40
|
+
isBlocked: boolean;
|
|
41
|
+
isUnreachable: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Query live task stats and last task added from a project's local database
|
|
46
|
+
*/
|
|
47
|
+
function getProjectLiveData(projectPath: string): ProjectLiveData {
|
|
48
|
+
const empty: ProjectLiveData = {
|
|
49
|
+
stats: { pending: 0, in_progress: 0, review: 0, completed: 0, failed: 0, disputed: 0, skipped: 0 },
|
|
50
|
+
last_task_added_at: null,
|
|
51
|
+
isBlocked: false,
|
|
52
|
+
isUnreachable: true,
|
|
53
|
+
};
|
|
54
|
+
const dbPath = join(projectPath, '.steroids', 'steroids.db');
|
|
55
|
+
if (!existsSync(dbPath)) return empty;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const projectDb = openSqliteForRead(dbPath, { timeoutMs: 500 });
|
|
59
|
+
try {
|
|
60
|
+
const row = projectDb
|
|
61
|
+
.prepare(
|
|
62
|
+
`SELECT
|
|
63
|
+
COALESCE(SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END), 0) as pending,
|
|
64
|
+
COALESCE(SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END), 0) as in_progress,
|
|
65
|
+
COALESCE(SUM(CASE WHEN status = 'review' THEN 1 ELSE 0 END), 0) as review,
|
|
66
|
+
COALESCE(SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END), 0) as completed,
|
|
67
|
+
COALESCE(SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END), 0) as failed,
|
|
68
|
+
COALESCE(SUM(CASE WHEN status = 'skipped' THEN 1 ELSE 0 END), 0) as skipped,
|
|
69
|
+
COALESCE(SUM(CASE WHEN status = 'disputed' THEN 1 ELSE 0 END), 0) as disputed,
|
|
70
|
+
COALESCE(SUM(CASE WHEN failure_count >= 3 THEN 1 ELSE 0 END), 0) as high_failures,
|
|
71
|
+
MAX(created_at) as last_task_added_at
|
|
72
|
+
FROM tasks`
|
|
73
|
+
)
|
|
74
|
+
.get() as {
|
|
75
|
+
pending: number;
|
|
76
|
+
in_progress: number;
|
|
77
|
+
review: number;
|
|
78
|
+
completed: number;
|
|
79
|
+
failed: number;
|
|
80
|
+
disputed: number;
|
|
81
|
+
skipped: number;
|
|
82
|
+
high_failures: number;
|
|
83
|
+
last_task_added_at: string | null;
|
|
84
|
+
} | undefined;
|
|
85
|
+
|
|
86
|
+
const failedCount = row?.failed ?? 0;
|
|
87
|
+
const disputedCount = row?.disputed ?? 0;
|
|
88
|
+
const skippedCount = row?.skipped ?? 0;
|
|
89
|
+
const highFailuresCount = row?.high_failures ?? 0;
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
stats: {
|
|
93
|
+
pending: row?.pending ?? 0,
|
|
94
|
+
in_progress: row?.in_progress ?? 0,
|
|
95
|
+
review: row?.review ?? 0,
|
|
96
|
+
completed: row?.completed ?? 0,
|
|
97
|
+
failed: failedCount,
|
|
98
|
+
disputed: disputedCount,
|
|
99
|
+
skipped: skippedCount,
|
|
100
|
+
},
|
|
101
|
+
last_task_added_at: row?.last_task_added_at ?? null,
|
|
102
|
+
isBlocked: failedCount > 0 || disputedCount > 0 || skippedCount > 0 || highFailuresCount > 0,
|
|
103
|
+
isUnreachable: false,
|
|
104
|
+
};
|
|
105
|
+
} finally {
|
|
106
|
+
projectDb.close();
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
return empty;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface ProjectResponse {
|
|
114
|
+
path: string;
|
|
115
|
+
name: string | null;
|
|
116
|
+
enabled: boolean;
|
|
117
|
+
registered_at: string;
|
|
118
|
+
last_seen_at: string;
|
|
119
|
+
last_activity_at: string | null; // Runner heartbeat or null if no active runner
|
|
120
|
+
last_task_added_at: string | null; // Most recent task created_at
|
|
121
|
+
isBlocked: boolean;
|
|
122
|
+
isUnreachable: boolean;
|
|
123
|
+
stats?: {
|
|
124
|
+
pending: number;
|
|
125
|
+
in_progress: number;
|
|
126
|
+
review: number;
|
|
127
|
+
completed: number;
|
|
128
|
+
failed: number;
|
|
129
|
+
disputed: number;
|
|
130
|
+
skipped: number;
|
|
131
|
+
};
|
|
132
|
+
runner?: {
|
|
133
|
+
id: string;
|
|
134
|
+
status: string;
|
|
135
|
+
pid: number | null;
|
|
136
|
+
current_task_id: string | null;
|
|
137
|
+
heartbeat_at: string | null;
|
|
138
|
+
} | null;
|
|
139
|
+
storage_bytes: number | null;
|
|
140
|
+
storage_human: string | null;
|
|
141
|
+
storage_warning: 'orange' | 'red' | null;
|
|
142
|
+
orphaned_in_progress: number;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* GET /api/projects
|
|
147
|
+
* List all registered projects with stats and runner info
|
|
148
|
+
*/
|
|
149
|
+
router.get('/projects', (req: Request, res: Response) => {
|
|
150
|
+
try {
|
|
151
|
+
const includeDisabled = req.query.include_disabled === 'true';
|
|
152
|
+
const projects = getRegisteredProjects(includeDisabled);
|
|
153
|
+
|
|
154
|
+
// Get runner info and stats for each project
|
|
155
|
+
const { db, close } = openGlobalDatabase();
|
|
156
|
+
try {
|
|
157
|
+
const projectsWithData: ProjectResponse[] = projects.map((project) => {
|
|
158
|
+
// Get runner info (including heartbeat)
|
|
159
|
+
const runner = db
|
|
160
|
+
.prepare('SELECT id, status, pid, current_task_id, heartbeat_at FROM runners WHERE project_path = ?')
|
|
161
|
+
.get(project.path) as {
|
|
162
|
+
id: string;
|
|
163
|
+
status: string;
|
|
164
|
+
pid: number | null;
|
|
165
|
+
current_task_id: string | null;
|
|
166
|
+
heartbeat_at: string | null;
|
|
167
|
+
} | undefined;
|
|
168
|
+
|
|
169
|
+
// Get live stats + last task added from project-local database
|
|
170
|
+
const liveData = getProjectLiveData(project.path);
|
|
171
|
+
|
|
172
|
+
// Lightweight storage info (non-blocking, 5-min cache)
|
|
173
|
+
const storageInfo = getCachedListStorage(project.path);
|
|
174
|
+
|
|
175
|
+
// Inline SQL against the already-open db — no extra DB connections
|
|
176
|
+
const hasStandaloneRunner = db.prepare(
|
|
177
|
+
`SELECT 1 FROM runners WHERE project_path = ? AND status != 'stopped'
|
|
178
|
+
AND heartbeat_at > datetime('now', '-5 minutes') AND parallel_session_id IS NULL`
|
|
179
|
+
).get(project.path) !== undefined;
|
|
180
|
+
// Cast: DbLike's run signature uses unknown[] but better-sqlite3 uses {} — runtime-compatible
|
|
181
|
+
const hasParallelSession = hasActiveParallelSessionForProjectDb(db as never, project.path);
|
|
182
|
+
const orphanedInProgress = (hasStandaloneRunner || hasParallelSession)
|
|
183
|
+
? 0
|
|
184
|
+
: (liveData.stats.in_progress ?? 0);
|
|
185
|
+
|
|
186
|
+
const response: ProjectResponse = {
|
|
187
|
+
path: project.path,
|
|
188
|
+
name: project.name,
|
|
189
|
+
enabled: project.enabled,
|
|
190
|
+
registered_at: project.registered_at,
|
|
191
|
+
last_seen_at: project.last_seen_at,
|
|
192
|
+
last_activity_at: runner?.heartbeat_at || null,
|
|
193
|
+
last_task_added_at: liveData.last_task_added_at,
|
|
194
|
+
isBlocked: liveData.isBlocked,
|
|
195
|
+
isUnreachable: liveData.isUnreachable,
|
|
196
|
+
stats: liveData.stats,
|
|
197
|
+
runner: runner
|
|
198
|
+
? {
|
|
199
|
+
id: runner.id,
|
|
200
|
+
status: runner.status,
|
|
201
|
+
pid: runner.pid,
|
|
202
|
+
current_task_id: runner.current_task_id,
|
|
203
|
+
heartbeat_at: runner.heartbeat_at,
|
|
204
|
+
}
|
|
205
|
+
: null,
|
|
206
|
+
storage_bytes: storageInfo?.storage_bytes ?? null,
|
|
207
|
+
storage_human: storageInfo?.storage_human ?? null,
|
|
208
|
+
storage_warning: storageInfo?.storage_warning ?? null,
|
|
209
|
+
orphaned_in_progress: orphanedInProgress,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
return response;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Sort by most recently modified: last task added or project enabled, most recent first
|
|
216
|
+
projectsWithData.sort((a, b) => {
|
|
217
|
+
const aTime = a.last_task_added_at || a.last_seen_at || a.registered_at;
|
|
218
|
+
const bTime = b.last_task_added_at || b.last_seen_at || b.registered_at;
|
|
219
|
+
return new Date(bTime).getTime() - new Date(aTime).getTime();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
res.json({
|
|
223
|
+
success: true,
|
|
224
|
+
projects: projectsWithData,
|
|
225
|
+
count: projectsWithData.length,
|
|
226
|
+
});
|
|
227
|
+
} finally {
|
|
228
|
+
close();
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.error('Error listing projects:', error);
|
|
232
|
+
res.status(500).json({
|
|
233
|
+
success: false,
|
|
234
|
+
error: 'Failed to list projects',
|
|
235
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* POST /api/projects
|
|
242
|
+
* Register a new project
|
|
243
|
+
* Body: { path: string, name?: string }
|
|
244
|
+
*/
|
|
245
|
+
router.post('/projects', (req: Request, res: Response) => {
|
|
246
|
+
try {
|
|
247
|
+
const validation = validatePathRequest(req.body);
|
|
248
|
+
if (!validation.valid) {
|
|
249
|
+
res.status(400).json({
|
|
250
|
+
success: false,
|
|
251
|
+
error: validation.error,
|
|
252
|
+
});
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const { path } = validation;
|
|
257
|
+
const { name } = req.body as { name?: string };
|
|
258
|
+
|
|
259
|
+
// Validate path is a valid project
|
|
260
|
+
if (!isValidProjectPath(path!)) {
|
|
261
|
+
res.status(400).json({
|
|
262
|
+
success: false,
|
|
263
|
+
error: 'Invalid project path - must contain .steroids/steroids.db and not be a system directory',
|
|
264
|
+
});
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
registerProject(path!, name);
|
|
269
|
+
|
|
270
|
+
// Fetch the registered project to return
|
|
271
|
+
const project = getRegisteredProject(path!);
|
|
272
|
+
|
|
273
|
+
res.status(201).json({
|
|
274
|
+
success: true,
|
|
275
|
+
message: 'Project registered successfully',
|
|
276
|
+
project: { ...project, orphaned_in_progress: 0 },
|
|
277
|
+
});
|
|
278
|
+
} catch (error) {
|
|
279
|
+
console.error('Error registering project:', error);
|
|
280
|
+
res.status(500).json({
|
|
281
|
+
success: false,
|
|
282
|
+
error: 'Failed to register project',
|
|
283
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* POST /api/projects/remove
|
|
290
|
+
* Unregister a project
|
|
291
|
+
* Body: { path: string }
|
|
292
|
+
*/
|
|
293
|
+
router.post('/projects/remove', (req: Request, res: Response) => {
|
|
294
|
+
try {
|
|
295
|
+
const validation = validatePathRequest(req.body);
|
|
296
|
+
if (!validation.valid) {
|
|
297
|
+
res.status(400).json({
|
|
298
|
+
success: false,
|
|
299
|
+
error: validation.error,
|
|
300
|
+
});
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const { path } = validation;
|
|
305
|
+
|
|
306
|
+
// Check if project exists
|
|
307
|
+
const project = getRegisteredProject(path!);
|
|
308
|
+
if (!project) {
|
|
309
|
+
res.status(404).json({
|
|
310
|
+
success: false,
|
|
311
|
+
error: 'Project not found in registry',
|
|
312
|
+
});
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
unregisterProject(path!);
|
|
317
|
+
|
|
318
|
+
res.json({
|
|
319
|
+
success: true,
|
|
320
|
+
message: 'Project unregistered successfully',
|
|
321
|
+
});
|
|
322
|
+
} catch (error) {
|
|
323
|
+
console.error('Error unregistering project:', error);
|
|
324
|
+
res.status(500).json({
|
|
325
|
+
success: false,
|
|
326
|
+
error: 'Failed to unregister project',
|
|
327
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* POST /api/projects/enable
|
|
334
|
+
* Enable a project for wakeup
|
|
335
|
+
* Body: { path: string }
|
|
336
|
+
*/
|
|
337
|
+
router.post('/projects/enable', (req: Request, res: Response) => {
|
|
338
|
+
try {
|
|
339
|
+
const validation = validatePathRequest(req.body);
|
|
340
|
+
if (!validation.valid) {
|
|
341
|
+
res.status(400).json({
|
|
342
|
+
success: false,
|
|
343
|
+
error: validation.error,
|
|
344
|
+
});
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const { path } = validation;
|
|
349
|
+
|
|
350
|
+
// Check if project exists
|
|
351
|
+
const project = getRegisteredProject(path!);
|
|
352
|
+
if (!project) {
|
|
353
|
+
res.status(404).json({
|
|
354
|
+
success: false,
|
|
355
|
+
error: 'Project not found in registry',
|
|
356
|
+
});
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
enableProject(path!);
|
|
361
|
+
|
|
362
|
+
res.json({
|
|
363
|
+
success: true,
|
|
364
|
+
message: 'Project enabled successfully',
|
|
365
|
+
});
|
|
366
|
+
} catch (error) {
|
|
367
|
+
console.error('Error enabling project:', error);
|
|
368
|
+
res.status(500).json({
|
|
369
|
+
success: false,
|
|
370
|
+
error: 'Failed to enable project',
|
|
371
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* POST /api/projects/reset
|
|
378
|
+
* Reset failed, skipped, and disputed tasks for a project, and re-enable it.
|
|
379
|
+
* Body: { path: string }
|
|
380
|
+
*/
|
|
381
|
+
router.post('/projects/reset', (req: Request, res: Response) => {
|
|
382
|
+
try {
|
|
383
|
+
const validation = validatePathRequest(req.body);
|
|
384
|
+
if (!validation.valid) {
|
|
385
|
+
res.status(400).json({
|
|
386
|
+
success: false,
|
|
387
|
+
error: 'Invalid request',
|
|
388
|
+
message: validation.error,
|
|
389
|
+
});
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const projectPath = validation.path!;
|
|
394
|
+
|
|
395
|
+
if (!isValidProjectPath(projectPath)) {
|
|
396
|
+
res.status(403).json({
|
|
397
|
+
success: false,
|
|
398
|
+
error: 'Access denied',
|
|
399
|
+
message: 'Invalid project path',
|
|
400
|
+
});
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Re-enable project first
|
|
405
|
+
enableProject(projectPath);
|
|
406
|
+
|
|
407
|
+
// Run the CLI reset command
|
|
408
|
+
const cliBin = fileURLToPath(new URL('../../../dist/index.js', import.meta.url));
|
|
409
|
+
execSync(`node "${cliBin}" tasks reset --all`, { cwd: projectPath, stdio: 'pipe' });
|
|
410
|
+
|
|
411
|
+
// Reset orphaned in_progress tasks — only when no active runner exists
|
|
412
|
+
// Uses inline SQL against the already-open globalDb — same pattern as detection
|
|
413
|
+
const { db: globalDb, close: closeGlobalDb } = openGlobalDatabase();
|
|
414
|
+
try {
|
|
415
|
+
const hasStandaloneRunner = globalDb.prepare(
|
|
416
|
+
`SELECT 1 FROM runners WHERE project_path = ? AND status != 'stopped'
|
|
417
|
+
AND heartbeat_at > datetime('now', '-5 minutes') AND parallel_session_id IS NULL`
|
|
418
|
+
).get(projectPath) !== undefined;
|
|
419
|
+
// Cast: DbLike's run signature uses unknown[] but better-sqlite3 uses {} — runtime-compatible
|
|
420
|
+
const hasParallelSession = hasActiveParallelSessionForProjectDb(globalDb as never, projectPath);
|
|
421
|
+
|
|
422
|
+
if (!hasStandaloneRunner && !hasParallelSession) {
|
|
423
|
+
const dbPath = join(projectPath, '.steroids', 'steroids.db');
|
|
424
|
+
if (existsSync(dbPath)) {
|
|
425
|
+
// Declare before try so finally can safely reference it (even if constructor throws)
|
|
426
|
+
let projectDb: Database.Database | undefined;
|
|
427
|
+
try {
|
|
428
|
+
projectDb = new Database(dbPath, { fileMustExist: true });
|
|
429
|
+
projectDb.transaction(() => {
|
|
430
|
+
// Clear locks first — 60-min TTL would block new runner pickup otherwise
|
|
431
|
+
projectDb!
|
|
432
|
+
.prepare(`DELETE FROM task_locks WHERE task_id IN (SELECT id FROM tasks WHERE status = 'in_progress')`)
|
|
433
|
+
.run();
|
|
434
|
+
projectDb!
|
|
435
|
+
.prepare(`UPDATE tasks SET status = 'pending', updated_at = datetime('now') WHERE status = 'in_progress'`)
|
|
436
|
+
.run();
|
|
437
|
+
})();
|
|
438
|
+
} finally {
|
|
439
|
+
projectDb?.close();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// No wakeup() call — its blast radius covers ALL projects, not just this one.
|
|
444
|
+
// The cron daemon picks up newly-pending tasks on its next cycle.
|
|
445
|
+
// Users wanting immediate pickup can hit "Start Daemon."
|
|
446
|
+
} finally {
|
|
447
|
+
closeGlobalDb();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
res.json({
|
|
451
|
+
success: true,
|
|
452
|
+
message: 'Project tasks reset and project enabled successfully'
|
|
453
|
+
});
|
|
454
|
+
} catch (error) {
|
|
455
|
+
console.error('Error resetting project:', error);
|
|
456
|
+
res.status(500).json({
|
|
457
|
+
success: false,
|
|
458
|
+
error: 'Failed to reset project',
|
|
459
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* POST /api/projects/disable
|
|
466
|
+
* Disable a project (skip in wakeup)
|
|
467
|
+
* Body: { path: string }
|
|
468
|
+
*/
|
|
469
|
+
router.post('/projects/disable', (req: Request, res: Response) => {
|
|
470
|
+
try {
|
|
471
|
+
const validation = validatePathRequest(req.body);
|
|
472
|
+
if (!validation.valid) {
|
|
473
|
+
res.status(400).json({
|
|
474
|
+
success: false,
|
|
475
|
+
error: validation.error,
|
|
476
|
+
});
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const { path } = validation;
|
|
481
|
+
|
|
482
|
+
// Check if project exists
|
|
483
|
+
const project = getRegisteredProject(path!);
|
|
484
|
+
if (!project) {
|
|
485
|
+
res.status(404).json({
|
|
486
|
+
success: false,
|
|
487
|
+
error: 'Project not found in registry',
|
|
488
|
+
});
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
disableProject(path!);
|
|
493
|
+
|
|
494
|
+
res.json({
|
|
495
|
+
success: true,
|
|
496
|
+
message: 'Project disabled successfully',
|
|
497
|
+
});
|
|
498
|
+
} catch (error) {
|
|
499
|
+
console.error('Error disabling project:', error);
|
|
500
|
+
res.status(500).json({
|
|
501
|
+
success: false,
|
|
502
|
+
error: 'Failed to disable project',
|
|
503
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* POST /api/projects/prune
|
|
510
|
+
* Remove stale projects (directories that no longer exist)
|
|
511
|
+
*/
|
|
512
|
+
router.post('/projects/prune', (req: Request, res: Response) => {
|
|
513
|
+
try {
|
|
514
|
+
const removedCount = pruneProjects();
|
|
515
|
+
|
|
516
|
+
res.json({
|
|
517
|
+
success: true,
|
|
518
|
+
message: `Pruned ${removedCount} stale project(s)`,
|
|
519
|
+
removed_count: removedCount,
|
|
520
|
+
});
|
|
521
|
+
} catch (error) {
|
|
522
|
+
console.error('Error pruning projects:', error);
|
|
523
|
+
res.status(500).json({
|
|
524
|
+
success: false,
|
|
525
|
+
error: 'Failed to prune projects',
|
|
526
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* GET /api/projects/status
|
|
533
|
+
* Get a single project's status by path (query param)
|
|
534
|
+
*/
|
|
535
|
+
router.get('/projects/status', (req: Request, res: Response) => {
|
|
536
|
+
try {
|
|
537
|
+
const path = req.query.path as string;
|
|
538
|
+
|
|
539
|
+
if (!path) {
|
|
540
|
+
res.status(400).json({
|
|
541
|
+
success: false,
|
|
542
|
+
error: 'Path query parameter is required',
|
|
543
|
+
});
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const project = getRegisteredProject(path);
|
|
548
|
+
if (!project) {
|
|
549
|
+
res.status(404).json({
|
|
550
|
+
success: false,
|
|
551
|
+
error: 'Project not found in registry',
|
|
552
|
+
});
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Get runner info
|
|
557
|
+
const { db, close } = openGlobalDatabase();
|
|
558
|
+
try {
|
|
559
|
+
const runner = db
|
|
560
|
+
.prepare('SELECT id, status, pid, current_task_id, heartbeat_at FROM runners WHERE project_path = ?')
|
|
561
|
+
.get(path) as {
|
|
562
|
+
id: string;
|
|
563
|
+
status: string;
|
|
564
|
+
pid: number | null;
|
|
565
|
+
current_task_id: string | null;
|
|
566
|
+
heartbeat_at: string | null;
|
|
567
|
+
} | undefined;
|
|
568
|
+
|
|
569
|
+
const storageInfo = getCachedListStorage(project.path);
|
|
570
|
+
const liveData = getProjectLiveData(project.path);
|
|
571
|
+
|
|
572
|
+
// Inline SQL against the already-open db — no extra DB connections
|
|
573
|
+
const hasStandaloneRunner = db.prepare(
|
|
574
|
+
`SELECT 1 FROM runners WHERE project_path = ? AND status != 'stopped'
|
|
575
|
+
AND heartbeat_at > datetime('now', '-5 minutes') AND parallel_session_id IS NULL`
|
|
576
|
+
).get(path) !== undefined;
|
|
577
|
+
// Cast: DbLike's run signature uses unknown[] but better-sqlite3 uses {} — runtime-compatible
|
|
578
|
+
const hasParallelSession = hasActiveParallelSessionForProjectDb(db as never, path);
|
|
579
|
+
const orphanedInProgress = (hasStandaloneRunner || hasParallelSession)
|
|
580
|
+
? 0
|
|
581
|
+
: (liveData.stats.in_progress ?? 0);
|
|
582
|
+
|
|
583
|
+
const response: ProjectResponse = {
|
|
584
|
+
path: project.path,
|
|
585
|
+
name: project.name,
|
|
586
|
+
enabled: project.enabled,
|
|
587
|
+
registered_at: project.registered_at,
|
|
588
|
+
last_seen_at: project.last_seen_at,
|
|
589
|
+
last_activity_at: runner?.heartbeat_at || null,
|
|
590
|
+
last_task_added_at: liveData.last_task_added_at,
|
|
591
|
+
isBlocked: liveData.isBlocked,
|
|
592
|
+
isUnreachable: liveData.isUnreachable,
|
|
593
|
+
stats: liveData.stats,
|
|
594
|
+
runner: runner
|
|
595
|
+
? {
|
|
596
|
+
id: runner.id,
|
|
597
|
+
status: runner.status,
|
|
598
|
+
pid: runner.pid,
|
|
599
|
+
current_task_id: runner.current_task_id,
|
|
600
|
+
heartbeat_at: runner.heartbeat_at,
|
|
601
|
+
}
|
|
602
|
+
: null,
|
|
603
|
+
storage_bytes: storageInfo?.storage_bytes ?? null,
|
|
604
|
+
storage_human: storageInfo?.storage_human ?? null,
|
|
605
|
+
storage_warning: storageInfo?.storage_warning ?? null,
|
|
606
|
+
orphaned_in_progress: orphanedInProgress,
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
res.json({
|
|
610
|
+
success: true,
|
|
611
|
+
project: response,
|
|
612
|
+
});
|
|
613
|
+
} finally {
|
|
614
|
+
close();
|
|
615
|
+
}
|
|
616
|
+
} catch (error) {
|
|
617
|
+
console.error('Error getting project status:', error);
|
|
618
|
+
res.status(500).json({
|
|
619
|
+
success: false,
|
|
620
|
+
error: 'Failed to get project status',
|
|
621
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
/** POST /api/projects/open - Open project folder in Finder */
|
|
627
|
+
router.post('/projects/open', (req: Request, res: Response) => {
|
|
628
|
+
try {
|
|
629
|
+
const validation = validatePathRequest(req.body);
|
|
630
|
+
if (!validation.valid) {
|
|
631
|
+
res.status(400).json({ success: false, error: validation.error });
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const { path } = validation;
|
|
635
|
+
if (!existsSync(path!)) {
|
|
636
|
+
res.status(404).json({ success: false, error: 'Path does not exist' });
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
execSync(`open "${path}"`, { encoding: 'utf-8' });
|
|
640
|
+
res.json({ success: true, message: 'Folder opened in Finder' });
|
|
641
|
+
} catch (error) {
|
|
642
|
+
console.error('Error opening project folder:', error);
|
|
643
|
+
res.status(500).json({
|
|
644
|
+
success: false, error: 'Failed to open project folder',
|
|
645
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* GET /api/projects/logs
|
|
652
|
+
* List all available log files for a project
|
|
653
|
+
*/
|
|
654
|
+
router.get('/projects/logs', (req: Request, res: Response) => {
|
|
655
|
+
try {
|
|
656
|
+
const projectPath = req.query.path as string;
|
|
657
|
+
if (!projectPath) {
|
|
658
|
+
res.status(400).json({ success: false, error: 'Path query parameter is required' });
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (!isValidProjectPath(projectPath)) {
|
|
663
|
+
res.status(403).json({ success: false, error: 'Invalid project path' });
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const logsDir = join(projectPath, '.steroids', 'logs');
|
|
668
|
+
const invocationsDir = join(projectPath, '.steroids', 'invocations');
|
|
669
|
+
|
|
670
|
+
const logs: { name: string; path: string; size: number; mtime: Date; type: 'log' | 'invocation' }[] = [];
|
|
671
|
+
|
|
672
|
+
[ { dir: logsDir, type: 'log' as const }, { dir: invocationsDir, type: 'invocation' as const } ].forEach(({ dir, type }) => {
|
|
673
|
+
if (existsSync(dir)) {
|
|
674
|
+
const files = readdirSync(dir);
|
|
675
|
+
for (const file of files) {
|
|
676
|
+
if (file.endsWith('.log') || file.endsWith('.jsonl') || file.endsWith('.txt')) {
|
|
677
|
+
const filePath = join(dir, file);
|
|
678
|
+
const stats = statSync(filePath);
|
|
679
|
+
if (stats.isFile()) {
|
|
680
|
+
logs.push({
|
|
681
|
+
name: file,
|
|
682
|
+
path: relative(projectPath, filePath),
|
|
683
|
+
size: stats.size,
|
|
684
|
+
mtime: stats.mtime,
|
|
685
|
+
type
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// Sort by modified time, newest first
|
|
694
|
+
logs.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
695
|
+
|
|
696
|
+
res.json({ success: true, logs });
|
|
697
|
+
} catch (error) {
|
|
698
|
+
console.error('Error listing project logs:', error);
|
|
699
|
+
res.status(500).json({ success: false, error: 'Failed to list project logs' });
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* GET /api/projects/logs/content
|
|
705
|
+
* Get the content of a specific log file
|
|
706
|
+
*/
|
|
707
|
+
router.get('/projects/logs/content', (req: Request, res: Response) => {
|
|
708
|
+
try {
|
|
709
|
+
const projectPath = req.query.path as string;
|
|
710
|
+
const logFile = req.query.file as string;
|
|
711
|
+
|
|
712
|
+
if (!projectPath || !logFile) {
|
|
713
|
+
res.status(400).json({ success: false, error: 'Path and file query parameters are required' });
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (!isValidProjectPath(projectPath)) {
|
|
718
|
+
res.status(403).json({ success: false, error: 'Invalid project path' });
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const realProjectPath = realpathSync(projectPath);
|
|
723
|
+
let fullLogPath = resolve(realProjectPath, logFile);
|
|
724
|
+
|
|
725
|
+
if (!existsSync(fullLogPath)) {
|
|
726
|
+
res.status(404).json({ success: false, error: 'Log file not found' });
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
fullLogPath = realpathSync(fullLogPath);
|
|
731
|
+
|
|
732
|
+
// Security (Path Traversal Guard): Strict canonicalization and root path verification
|
|
733
|
+
if (!fullLogPath.startsWith(realProjectPath + sep)) {
|
|
734
|
+
res.status(403).json({ success: false, error: 'Access denied: Path traversal detected' });
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Only allow access to .steroids/logs and .steroids/invocations
|
|
739
|
+
const allowedLogsDir = join(realProjectPath, '.steroids', 'logs') + sep;
|
|
740
|
+
const allowedInvocationsDir = join(realProjectPath, '.steroids', 'invocations') + sep;
|
|
741
|
+
|
|
742
|
+
if (!fullLogPath.startsWith(allowedLogsDir) && !fullLogPath.startsWith(allowedInvocationsDir)) {
|
|
743
|
+
res.status(403).json({ success: false, error: 'Access denied: Only log directories are allowed' });
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Since files can be large, we use sendFile
|
|
748
|
+
res.sendFile(fullLogPath);
|
|
749
|
+
} catch (error) {
|
|
750
|
+
console.error('Error reading log file:', error);
|
|
751
|
+
res.status(500).json({ success: false, error: 'Failed to read log file' });
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* GET /api/projects/instructions?path=<projectPath>
|
|
757
|
+
* Returns instruction files (AGENTS.md, CLAUDE.md, GEMINI.md) with existence, enabled state, and content.
|
|
758
|
+
* Also returns customInstructions string.
|
|
759
|
+
*/
|
|
760
|
+
router.get('/projects/instructions', (req: Request, res: Response) => {
|
|
761
|
+
try {
|
|
762
|
+
const projectPath = req.query.path as string;
|
|
763
|
+
if (!projectPath) {
|
|
764
|
+
res.status(400).json({ success: false, error: 'Path query parameter is required' });
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (!isValidProjectPath(projectPath)) {
|
|
768
|
+
res.status(403).json({ success: false, error: 'Invalid project path' });
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Dynamically import from compiled dist to avoid circular build issues
|
|
773
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
774
|
+
const { getInstructionFilesList, readInstructionOverrides } = require('../../../dist/prompts/instruction-files.js');
|
|
775
|
+
const files = getInstructionFilesList(projectPath);
|
|
776
|
+
const overrides = readInstructionOverrides(projectPath);
|
|
777
|
+
|
|
778
|
+
res.json({
|
|
779
|
+
success: true,
|
|
780
|
+
files,
|
|
781
|
+
customInstructions: overrides.customInstructions ?? '',
|
|
782
|
+
});
|
|
783
|
+
} catch (error) {
|
|
784
|
+
console.error('Error getting project instructions:', error);
|
|
785
|
+
res.status(500).json({
|
|
786
|
+
success: false,
|
|
787
|
+
error: 'Failed to get project instructions',
|
|
788
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* POST /api/projects/instructions
|
|
795
|
+
* Toggle a specific instruction file on/off, or save custom instructions.
|
|
796
|
+
* Body (file toggle): { path: string, key: "agentsMd" | "claudeMd" | "geminiMd", enabled: boolean }
|
|
797
|
+
* Body (custom instructions): { path: string, customInstructions: string }
|
|
798
|
+
*/
|
|
799
|
+
router.post('/projects/instructions', (req: Request, res: Response) => {
|
|
800
|
+
try {
|
|
801
|
+
const { path: projectPath, key, enabled, customInstructions } = req.body as {
|
|
802
|
+
path: string;
|
|
803
|
+
key?: string;
|
|
804
|
+
enabled?: boolean;
|
|
805
|
+
customInstructions?: string;
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
if (!projectPath) {
|
|
809
|
+
res.status(400).json({ success: false, error: 'path is required' });
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
if (!isValidProjectPath(projectPath)) {
|
|
813
|
+
res.status(403).json({ success: false, error: 'Invalid project path' });
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const validKeys = ['agentsMd', 'claudeMd', 'geminiMd'];
|
|
818
|
+
const updatingFile = key !== undefined;
|
|
819
|
+
const updatingCustom = customInstructions !== undefined;
|
|
820
|
+
|
|
821
|
+
if (updatingFile && !validKeys.includes(key!)) {
|
|
822
|
+
res.status(400).json({ success: false, error: `key must be one of: ${validKeys.join(', ')}` });
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
if (!updatingFile && !updatingCustom) {
|
|
826
|
+
res.status(400).json({ success: false, error: 'Provide either key+enabled or customInstructions' });
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
831
|
+
const { readInstructionOverrides, writeInstructionOverrides } = require('../../../dist/prompts/instruction-files.js');
|
|
832
|
+
const overrides = readInstructionOverrides(projectPath);
|
|
833
|
+
|
|
834
|
+
if (updatingFile) {
|
|
835
|
+
overrides[key!] = enabled;
|
|
836
|
+
}
|
|
837
|
+
if (updatingCustom) {
|
|
838
|
+
overrides.customInstructions = customInstructions;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
writeInstructionOverrides(projectPath, overrides);
|
|
842
|
+
|
|
843
|
+
res.json({ success: true, message: 'Instructions updated' });
|
|
844
|
+
} catch (error) {
|
|
845
|
+
console.error('Error updating project instructions:', error);
|
|
846
|
+
res.status(500).json({
|
|
847
|
+
success: false,
|
|
848
|
+
error: 'Failed to update project instructions',
|
|
849
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
export default router;
|