@assistkick/create 1.0.0
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/bin/create.d.ts +2 -0
- package/dist/bin/create.js +25 -0
- package/dist/bin/create.js.map +1 -0
- package/dist/src/scaffolder.d.ts +22 -0
- package/dist/src/scaffolder.js +120 -0
- package/dist/src/scaffolder.js.map +1 -0
- package/package.json +24 -0
- package/templates/product-system/.env.example +8 -0
- package/templates/product-system/CLAUDE.md +45 -0
- package/templates/product-system/package.json +32 -0
- package/templates/product-system/packages/backend/package.json +37 -0
- package/templates/product-system/packages/backend/src/middleware/auth_middleware.test.ts +86 -0
- package/templates/product-system/packages/backend/src/middleware/auth_middleware.ts +35 -0
- package/templates/product-system/packages/backend/src/routes/auth.ts +463 -0
- package/templates/product-system/packages/backend/src/routes/coherence.ts +187 -0
- package/templates/product-system/packages/backend/src/routes/graph.ts +67 -0
- package/templates/product-system/packages/backend/src/routes/kanban.ts +201 -0
- package/templates/product-system/packages/backend/src/routes/pipeline.ts +41 -0
- package/templates/product-system/packages/backend/src/routes/projects.ts +122 -0
- package/templates/product-system/packages/backend/src/routes/users.ts +97 -0
- package/templates/product-system/packages/backend/src/server.ts +159 -0
- package/templates/product-system/packages/backend/src/services/auth_service.test.ts +115 -0
- package/templates/product-system/packages/backend/src/services/auth_service.ts +82 -0
- package/templates/product-system/packages/backend/src/services/coherence-review.ts +339 -0
- package/templates/product-system/packages/backend/src/services/email_service.ts +75 -0
- package/templates/product-system/packages/backend/src/services/init.ts +80 -0
- package/templates/product-system/packages/backend/src/services/invitation_service.test.ts +235 -0
- package/templates/product-system/packages/backend/src/services/invitation_service.ts +193 -0
- package/templates/product-system/packages/backend/src/services/password_reset_service.test.ts +151 -0
- package/templates/product-system/packages/backend/src/services/password_reset_service.ts +135 -0
- package/templates/product-system/packages/backend/src/services/project_service.test.ts +215 -0
- package/templates/product-system/packages/backend/src/services/project_service.ts +171 -0
- package/templates/product-system/packages/backend/src/services/pty_session_manager.test.ts +88 -0
- package/templates/product-system/packages/backend/src/services/pty_session_manager.ts +279 -0
- package/templates/product-system/packages/backend/src/services/terminal_ws_handler.ts +133 -0
- package/templates/product-system/packages/backend/src/services/user_management_service.test.ts +158 -0
- package/templates/product-system/packages/backend/src/services/user_management_service.ts +128 -0
- package/templates/product-system/packages/backend/tsconfig.json +22 -0
- package/templates/product-system/packages/frontend/index.html +13 -0
- package/templates/product-system/packages/frontend/package-lock.json +2666 -0
- package/templates/product-system/packages/frontend/package.json +30 -0
- package/templates/product-system/packages/frontend/public/favicon.svg +16 -0
- package/templates/product-system/packages/frontend/src/App.tsx +29 -0
- package/templates/product-system/packages/frontend/src/api/client.ts +386 -0
- package/templates/product-system/packages/frontend/src/api/client_projects.test.ts +104 -0
- package/templates/product-system/packages/frontend/src/api/client_refresh.test.ts +145 -0
- package/templates/product-system/packages/frontend/src/components/CoherenceView.tsx +414 -0
- package/templates/product-system/packages/frontend/src/components/GraphLegend.tsx +124 -0
- package/templates/product-system/packages/frontend/src/components/GraphSettings.tsx +112 -0
- package/templates/product-system/packages/frontend/src/components/GraphView.tsx +370 -0
- package/templates/product-system/packages/frontend/src/components/InviteUserDialog.tsx +85 -0
- package/templates/product-system/packages/frontend/src/components/KanbanView.tsx +470 -0
- package/templates/product-system/packages/frontend/src/components/LoginPage.tsx +116 -0
- package/templates/product-system/packages/frontend/src/components/ProjectSelector.tsx +187 -0
- package/templates/product-system/packages/frontend/src/components/QaIssueSheet.tsx +192 -0
- package/templates/product-system/packages/frontend/src/components/SidePanel.tsx +231 -0
- package/templates/product-system/packages/frontend/src/components/TerminalView.tsx +200 -0
- package/templates/product-system/packages/frontend/src/components/Toolbar.tsx +84 -0
- package/templates/product-system/packages/frontend/src/components/UsersView.tsx +249 -0
- package/templates/product-system/packages/frontend/src/constants/graph.ts +191 -0
- package/templates/product-system/packages/frontend/src/hooks/useAuth.tsx +54 -0
- package/templates/product-system/packages/frontend/src/hooks/useGraph.ts +27 -0
- package/templates/product-system/packages/frontend/src/hooks/useKanban.ts +21 -0
- package/templates/product-system/packages/frontend/src/hooks/useProjects.ts +86 -0
- package/templates/product-system/packages/frontend/src/hooks/useTheme.ts +26 -0
- package/templates/product-system/packages/frontend/src/hooks/useToast.tsx +62 -0
- package/templates/product-system/packages/frontend/src/hooks/use_projects_logic.test.ts +61 -0
- package/templates/product-system/packages/frontend/src/main.tsx +12 -0
- package/templates/product-system/packages/frontend/src/pages/accept_invitation_page.tsx +167 -0
- package/templates/product-system/packages/frontend/src/pages/forgot_password_page.tsx +100 -0
- package/templates/product-system/packages/frontend/src/pages/register_page.tsx +137 -0
- package/templates/product-system/packages/frontend/src/pages/reset_password_page.tsx +146 -0
- package/templates/product-system/packages/frontend/src/routes/ProtectedRoute.tsx +12 -0
- package/templates/product-system/packages/frontend/src/routes/accept_invitation.tsx +14 -0
- package/templates/product-system/packages/frontend/src/routes/dashboard.tsx +221 -0
- package/templates/product-system/packages/frontend/src/routes/forgot_password.tsx +13 -0
- package/templates/product-system/packages/frontend/src/routes/login.tsx +14 -0
- package/templates/product-system/packages/frontend/src/routes/register.tsx +14 -0
- package/templates/product-system/packages/frontend/src/routes/reset_password.tsx +13 -0
- package/templates/product-system/packages/frontend/src/styles/index.css +3358 -0
- package/templates/product-system/packages/frontend/src/utils/auth_validation.test.ts +51 -0
- package/templates/product-system/packages/frontend/src/utils/auth_validation.ts +19 -0
- package/templates/product-system/packages/frontend/src/utils/login_validation.test.ts +61 -0
- package/templates/product-system/packages/frontend/src/utils/login_validation.ts +24 -0
- package/templates/product-system/packages/frontend/src/utils/logout.test.ts +63 -0
- package/templates/product-system/packages/frontend/src/utils/node_sizing.test.ts +62 -0
- package/templates/product-system/packages/frontend/src/utils/node_sizing.ts +24 -0
- package/templates/product-system/packages/frontend/src/utils/task_status.test.ts +53 -0
- package/templates/product-system/packages/frontend/src/utils/task_status.ts +14 -0
- package/templates/product-system/packages/frontend/tsconfig.json +21 -0
- package/templates/product-system/packages/frontend/vite.config.ts +20 -0
- package/templates/product-system/packages/shared/.env.example +3 -0
- package/templates/product-system/packages/shared/README.md +1 -0
- package/templates/product-system/packages/shared/db/migrate.ts +32 -0
- package/templates/product-system/packages/shared/db/migrations/0000_dashing_gorgon.sql +128 -0
- package/templates/product-system/packages/shared/db/migrations/meta/0000_snapshot.json +819 -0
- package/templates/product-system/packages/shared/db/migrations/meta/_journal.json +13 -0
- package/templates/product-system/packages/shared/db/schema.ts +137 -0
- package/templates/product-system/packages/shared/drizzle.config.js +14 -0
- package/templates/product-system/packages/shared/lib/claude-service.ts +215 -0
- package/templates/product-system/packages/shared/lib/coherence.ts +278 -0
- package/templates/product-system/packages/shared/lib/completeness.ts +30 -0
- package/templates/product-system/packages/shared/lib/constants.ts +327 -0
- package/templates/product-system/packages/shared/lib/db.ts +81 -0
- package/templates/product-system/packages/shared/lib/git_workflow.ts +110 -0
- package/templates/product-system/packages/shared/lib/graph.ts +186 -0
- package/templates/product-system/packages/shared/lib/kanban.ts +161 -0
- package/templates/product-system/packages/shared/lib/markdown.ts +205 -0
- package/templates/product-system/packages/shared/lib/pipeline-state-store.ts +124 -0
- package/templates/product-system/packages/shared/lib/pipeline.ts +489 -0
- package/templates/product-system/packages/shared/lib/prompt_builder.ts +170 -0
- package/templates/product-system/packages/shared/lib/relevance_search.ts +159 -0
- package/templates/product-system/packages/shared/lib/session.ts +152 -0
- package/templates/product-system/packages/shared/lib/validator.ts +117 -0
- package/templates/product-system/packages/shared/lib/work_summary_parser.ts +130 -0
- package/templates/product-system/packages/shared/package.json +30 -0
- package/templates/product-system/packages/shared/scripts/assign-project.ts +52 -0
- package/templates/product-system/packages/shared/tools/add_edge.ts +61 -0
- package/templates/product-system/packages/shared/tools/add_node.ts +101 -0
- package/templates/product-system/packages/shared/tools/end_session.ts +87 -0
- package/templates/product-system/packages/shared/tools/get_gaps.ts +87 -0
- package/templates/product-system/packages/shared/tools/get_kanban.ts +125 -0
- package/templates/product-system/packages/shared/tools/get_node.ts +78 -0
- package/templates/product-system/packages/shared/tools/get_status.ts +98 -0
- package/templates/product-system/packages/shared/tools/migrate_to_turso.ts +385 -0
- package/templates/product-system/packages/shared/tools/move_card.ts +143 -0
- package/templates/product-system/packages/shared/tools/rebuild_index.ts +77 -0
- package/templates/product-system/packages/shared/tools/remove_edge.ts +59 -0
- package/templates/product-system/packages/shared/tools/remove_node.ts +96 -0
- package/templates/product-system/packages/shared/tools/resolve_question.ts +75 -0
- package/templates/product-system/packages/shared/tools/search_nodes.ts +106 -0
- package/templates/product-system/packages/shared/tools/start_session.ts +144 -0
- package/templates/product-system/packages/shared/tools/update_node.ts +133 -0
- package/templates/product-system/packages/shared/tsconfig.json +24 -0
- package/templates/product-system/pnpm-workspace.yaml +2 -0
- package/templates/product-system/smoke_test.ts +219 -0
- package/templates/product-system/tests/coherence_review.test.ts +562 -0
- package/templates/product-system/tests/db_sqlite_fallback.test.ts +75 -0
- package/templates/product-system/tests/edge_type_color_coding.test.ts +147 -0
- package/templates/product-system/tests/emit-tool-use-events.test.ts +85 -0
- package/templates/product-system/tests/feature_kind.test.ts +139 -0
- package/templates/product-system/tests/gap_indicators.test.ts +199 -0
- package/templates/product-system/tests/graceful_init.test.ts +142 -0
- package/templates/product-system/tests/graph_legend.test.ts +314 -0
- package/templates/product-system/tests/graph_settings_sheet.test.ts +804 -0
- package/templates/product-system/tests/hide_defined_filter.test.ts +205 -0
- package/templates/product-system/tests/kanban.test.ts +529 -0
- package/templates/product-system/tests/neighborhood_focus.test.ts +132 -0
- package/templates/product-system/tests/node_search.test.ts +340 -0
- package/templates/product-system/tests/node_sizing.test.ts +170 -0
- package/templates/product-system/tests/node_type_toggle_filters.test.ts +285 -0
- package/templates/product-system/tests/node_type_visual_encoding.test.ts +103 -0
- package/templates/product-system/tests/pipeline-state-store.test.ts +268 -0
- package/templates/product-system/tests/pipeline-unit.test.ts +593 -0
- package/templates/product-system/tests/pipeline.test.ts +195 -0
- package/templates/product-system/tests/pipeline_stats_all_cards.test.ts +193 -0
- package/templates/product-system/tests/play_all.test.ts +296 -0
- package/templates/product-system/tests/qa_issue_sheet.test.ts +464 -0
- package/templates/product-system/tests/relevance_search.test.ts +186 -0
- package/templates/product-system/tests/search_reorder.test.ts +88 -0
- package/templates/product-system/tests/serve_ui.test.ts +281 -0
- package/templates/product-system/tests/serve_ui_drizzle.test.ts +114 -0
- package/templates/product-system/tests/session_context_recall.test.ts +135 -0
- package/templates/product-system/tests/side_panel.test.ts +345 -0
- package/templates/product-system/tests/spec_completeness_label.test.ts +69 -0
- package/templates/product-system/tests/url_routing_test.ts +122 -0
- package/templates/product-system/tests/user_login.test.ts +150 -0
- package/templates/product-system/tests/user_registration.test.ts +205 -0
- package/templates/product-system/tests/web_terminal.test.ts +572 -0
- package/templates/product-system/tests/work_summary.test.ts +211 -0
- package/templates/product-system/tests/zoom_pan.test.ts +43 -0
- package/templates/product-system/tsconfig.json +24 -0
- package/templates/skills/product-bootstrap/SKILL.md +312 -0
- package/templates/skills/product-code-reviewer/SKILL.md +147 -0
- package/templates/skills/product-debugger/SKILL.md +206 -0
- package/templates/skills/product-debugger/references/agent-browser.md +1156 -0
- package/templates/skills/product-developer/SKILL.md +182 -0
- package/templates/skills/product-interview/SKILL.md +220 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph API routes — read graph data and individual nodes.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {Router} from 'express';
|
|
6
|
+
import {readFile} from 'node:fs/promises';
|
|
7
|
+
import {join} from 'node:path';
|
|
8
|
+
import {readGraph} from '@interview-system/shared/lib/graph.js';
|
|
9
|
+
import {getDb} from '@interview-system/shared/lib/db.js';
|
|
10
|
+
import {nodes} from '@interview-system/shared/db/schema.js';
|
|
11
|
+
import {eq} from 'drizzle-orm';
|
|
12
|
+
import matter from 'gray-matter';
|
|
13
|
+
import {log, paths} from '../services/init.js';
|
|
14
|
+
|
|
15
|
+
const router: Router = Router();
|
|
16
|
+
|
|
17
|
+
// GET /api/graph
|
|
18
|
+
router.get('/graph', async (req, res) => {
|
|
19
|
+
const projectId = req.query.project_id as string | undefined;
|
|
20
|
+
log('API', `GET /api/graph${projectId ? ` project_id=${projectId}` : ''}`);
|
|
21
|
+
try {
|
|
22
|
+
const graph = await readGraph(projectId);
|
|
23
|
+
res.json(graph);
|
|
24
|
+
} catch (err: any) {
|
|
25
|
+
log('API', `GET /api/graph FAILED: ${err.message}`, err.cause || err);
|
|
26
|
+
res.status(500).json({error: 'Failed to read graph'});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// GET /api/node/:id
|
|
31
|
+
router.get('/node/:id', async (req, res) => {
|
|
32
|
+
const nodeId = req.params.id;
|
|
33
|
+
log('API', `GET /api/node/${nodeId}`);
|
|
34
|
+
try {
|
|
35
|
+
const db = getDb();
|
|
36
|
+
const rows = await db.select().from(nodes).where(eq(nodes.id, nodeId));
|
|
37
|
+
const row = rows[0];
|
|
38
|
+
if (!row) {
|
|
39
|
+
log('API', `GET /api/node/${nodeId} — not found`);
|
|
40
|
+
return res.status(404).json({error: `Node not found: ${nodeId}`});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let content;
|
|
44
|
+
if (row.body) {
|
|
45
|
+
const frontmatter: any = {
|
|
46
|
+
id: row.id,
|
|
47
|
+
type: row.type,
|
|
48
|
+
name: row.name,
|
|
49
|
+
status: row.status,
|
|
50
|
+
priority: row.priority,
|
|
51
|
+
...(row.kind ? {kind: row.kind} : {}),
|
|
52
|
+
created_at: row.createdAt,
|
|
53
|
+
updated_at: row.updatedAt,
|
|
54
|
+
};
|
|
55
|
+
content = matter.stringify(row.body, frontmatter);
|
|
56
|
+
} else {
|
|
57
|
+
const filePath = join(paths.dataDir, 'nodes', `${nodeId}.md`);
|
|
58
|
+
content = await readFile(filePath, 'utf-8');
|
|
59
|
+
}
|
|
60
|
+
res.json({id: nodeId, content});
|
|
61
|
+
} catch (err: any) {
|
|
62
|
+
log('API', `GET /api/node/${nodeId} FAILED: ${err.message}`);
|
|
63
|
+
res.status(500).json({error: `Failed to read node: ${nodeId}`});
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export default router;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kanban API routes — CRUD for kanban board, notes, pipeline.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Router } from 'express';
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { readGraph } from '@interview-system/shared/lib/graph.js';
|
|
8
|
+
import { loadKanban, getKanbanEntry, saveKanbanEntry } from '@interview-system/shared/lib/kanban.js';
|
|
9
|
+
import { log, pipeline } from '../services/init.js';
|
|
10
|
+
|
|
11
|
+
const router: Router = Router();
|
|
12
|
+
|
|
13
|
+
const VALID_COLUMNS = ['todo', 'in_progress', 'in_review', 'qa', 'done'];
|
|
14
|
+
|
|
15
|
+
// GET /api/kanban
|
|
16
|
+
router.get('/', async (req, res) => {
|
|
17
|
+
const projectId = req.query.project_id as string | undefined;
|
|
18
|
+
log('API', `GET /api/kanban${projectId ? ` project_id=${projectId}` : ''}`);
|
|
19
|
+
try {
|
|
20
|
+
const kanban = await loadKanban(projectId);
|
|
21
|
+
|
|
22
|
+
// Auto-add missing feature nodes to the TODO column
|
|
23
|
+
const graph = await readGraph(projectId);
|
|
24
|
+
const featureNodes = graph.nodes.filter((n: any) => n.type === 'feature');
|
|
25
|
+
for (const node of featureNodes) {
|
|
26
|
+
if (!kanban[node.id]) {
|
|
27
|
+
const newEntry = {
|
|
28
|
+
column: 'todo',
|
|
29
|
+
rejection_count: 0,
|
|
30
|
+
notes: [],
|
|
31
|
+
moved_at: node.created_at || new Date().toISOString(),
|
|
32
|
+
};
|
|
33
|
+
await saveKanbanEntry(node.id, newEntry, projectId);
|
|
34
|
+
kanban[node.id] = newEntry;
|
|
35
|
+
log('KANBAN', `Auto-added ${node.id} to todo`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
res.json(kanban);
|
|
40
|
+
} catch (err: any) {
|
|
41
|
+
log('API', `GET /api/kanban FAILED: ${err.message}`);
|
|
42
|
+
res.status(500).json({ error: 'Failed to read kanban' });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// POST /api/kanban/:id/move
|
|
47
|
+
router.post('/:id/move', async (req, res) => {
|
|
48
|
+
const featureId = req.params.id;
|
|
49
|
+
log('API', `POST /api/kanban/${featureId}/move`);
|
|
50
|
+
try {
|
|
51
|
+
const { column } = req.body;
|
|
52
|
+
log('MOVE', `${featureId} → target: ${column}`);
|
|
53
|
+
|
|
54
|
+
if (!column) {
|
|
55
|
+
log('MOVE', `REJECT: missing column`);
|
|
56
|
+
return res.status(400).json({ error: 'Missing "column" in request body' });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const entry = await getKanbanEntry(featureId);
|
|
60
|
+
|
|
61
|
+
if (!entry) {
|
|
62
|
+
log('MOVE', `REJECT: ${featureId} not found`);
|
|
63
|
+
return res.status(404).json({ error: `Feature "${featureId}" not found in kanban` });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const currentColumn = entry.column;
|
|
67
|
+
log('MOVE', `${featureId}: current=${currentColumn} target=${column}`);
|
|
68
|
+
|
|
69
|
+
if (!VALID_COLUMNS.includes(column)) {
|
|
70
|
+
log('MOVE', `REJECT: invalid column "${column}"`);
|
|
71
|
+
return res.status(400).json({ error: `Invalid column "${column}"` });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (currentColumn === column) {
|
|
75
|
+
log('MOVE', `REJECT: card already in "${column}"`);
|
|
76
|
+
return res.status(400).json({ error: `Card is already in "${column}"` });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
entry.column = column;
|
|
80
|
+
entry.moved_at = new Date().toISOString();
|
|
81
|
+
|
|
82
|
+
// Increment rejection count on QA → Todo
|
|
83
|
+
if (currentColumn === 'qa' && column === 'todo') {
|
|
84
|
+
entry.rejection_count = (entry.rejection_count || 0) + 1;
|
|
85
|
+
log('MOVE', `QA rejection #${entry.rejection_count} for ${featureId}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await saveKanbanEntry(featureId, entry);
|
|
89
|
+
log('MOVE', `OK: ${featureId} ${currentColumn} → ${column}`);
|
|
90
|
+
|
|
91
|
+
res.json({
|
|
92
|
+
feature_id: featureId,
|
|
93
|
+
previous_column: currentColumn,
|
|
94
|
+
new_column: column,
|
|
95
|
+
rejection_count: entry.rejection_count || 0,
|
|
96
|
+
});
|
|
97
|
+
} catch (err: any) {
|
|
98
|
+
log('MOVE', `ERROR: ${err.message}`);
|
|
99
|
+
res.status(400).json({ error: err.message });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// POST /api/kanban/:id/notes
|
|
104
|
+
router.post('/:id/notes', async (req, res) => {
|
|
105
|
+
const featureId = req.params.id;
|
|
106
|
+
log('API', `POST /api/kanban/${featureId}/notes`);
|
|
107
|
+
try {
|
|
108
|
+
const { text } = req.body;
|
|
109
|
+
|
|
110
|
+
if (!text || !text.trim()) {
|
|
111
|
+
log('NOTES', `REJECT: missing text`);
|
|
112
|
+
return res.status(400).json({ error: 'Missing "text" in request body' });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const entry = await getKanbanEntry(featureId);
|
|
116
|
+
|
|
117
|
+
if (!entry) {
|
|
118
|
+
log('NOTES', `REJECT: ${featureId} not found`);
|
|
119
|
+
return res.status(404).json({ error: `Feature "${featureId}" not found` });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const note = {
|
|
123
|
+
id: randomUUID().slice(0, 8),
|
|
124
|
+
text: text.trim(),
|
|
125
|
+
created_at: new Date().toISOString(),
|
|
126
|
+
updated_at: new Date().toISOString(),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
if (!entry.notes) entry.notes = [];
|
|
130
|
+
entry.notes.push(note);
|
|
131
|
+
|
|
132
|
+
await saveKanbanEntry(featureId, entry);
|
|
133
|
+
log('NOTES', `Created note ${note.id} on ${featureId}`);
|
|
134
|
+
res.status(201).json(note);
|
|
135
|
+
} catch (err: any) {
|
|
136
|
+
log('NOTES', `ERROR: ${err.message}`);
|
|
137
|
+
res.status(400).json({ error: err.message });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// PUT /api/kanban/:id/notes/:nid
|
|
142
|
+
router.put('/:id/notes/:nid', async (req, res) => {
|
|
143
|
+
const { id: featureId, nid: noteId } = req.params;
|
|
144
|
+
log('API', `PUT /api/kanban/${featureId}/notes/${noteId}`);
|
|
145
|
+
try {
|
|
146
|
+
const { text } = req.body;
|
|
147
|
+
|
|
148
|
+
if (!text || !text.trim()) {
|
|
149
|
+
return res.status(400).json({ error: 'Missing "text" in request body' });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const entry = await getKanbanEntry(featureId);
|
|
153
|
+
|
|
154
|
+
if (!entry) {
|
|
155
|
+
return res.status(404).json({ error: `Feature "${featureId}" not found` });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const note = (entry.notes || []).find((n: any) => n.id === noteId);
|
|
159
|
+
if (!note) {
|
|
160
|
+
return res.status(404).json({ error: `Note "${noteId}" not found` });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
note.text = text.trim();
|
|
164
|
+
note.updated_at = new Date().toISOString();
|
|
165
|
+
|
|
166
|
+
await saveKanbanEntry(featureId, entry);
|
|
167
|
+
log('NOTES', `Updated note ${noteId} on ${featureId}`);
|
|
168
|
+
res.json(note);
|
|
169
|
+
} catch (err: any) {
|
|
170
|
+
log('NOTES', `ERROR: ${err.message}`);
|
|
171
|
+
res.status(400).json({ error: err.message });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// DELETE /api/kanban/:id/notes/:nid
|
|
176
|
+
router.delete('/:id/notes/:nid', async (req, res) => {
|
|
177
|
+
const { id: featureId, nid: noteId } = req.params;
|
|
178
|
+
log('API', `DELETE /api/kanban/${featureId}/notes/${noteId}`);
|
|
179
|
+
try {
|
|
180
|
+
const entry = await getKanbanEntry(featureId);
|
|
181
|
+
|
|
182
|
+
if (!entry) {
|
|
183
|
+
return res.status(404).json({ error: `Feature "${featureId}" not found` });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const idx = (entry.notes || []).findIndex((n: any) => n.id === noteId);
|
|
187
|
+
if (idx === -1) {
|
|
188
|
+
return res.status(404).json({ error: `Note "${noteId}" not found` });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
entry.notes.splice(idx, 1);
|
|
192
|
+
await saveKanbanEntry(featureId, entry);
|
|
193
|
+
log('NOTES', `Deleted note ${noteId} from ${featureId}`);
|
|
194
|
+
res.json({ deleted: true });
|
|
195
|
+
} catch (err: any) {
|
|
196
|
+
log('NOTES', `ERROR: ${err.message}`);
|
|
197
|
+
res.status(400).json({ error: err.message });
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
export default router;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline API routes — start/status/unblock dev pipeline.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Router } from 'express';
|
|
6
|
+
import { log, pipeline } from '../services/init.js';
|
|
7
|
+
|
|
8
|
+
const router: Router = Router();
|
|
9
|
+
|
|
10
|
+
// POST /api/kanban/:id/develop
|
|
11
|
+
router.post('/:id/develop', async (req, res) => {
|
|
12
|
+
const featureId = req.params.id;
|
|
13
|
+
try {
|
|
14
|
+
const result = await pipeline.start(featureId);
|
|
15
|
+
res.status(result.status).json(result.error ? { error: result.error } : { started: result.started, feature_id: result.feature_id });
|
|
16
|
+
} catch (err: any) {
|
|
17
|
+
log('DEVELOP', `UNEXPECTED ERROR: ${err.message}`);
|
|
18
|
+
res.status(500).json({ error: err.message });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// GET /api/kanban/:id/pipeline
|
|
23
|
+
router.get('/:id/pipeline', async (req, res) => {
|
|
24
|
+
const featureId = req.params.id;
|
|
25
|
+
const result = await pipeline.getStatus(featureId);
|
|
26
|
+
res.json(result);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// POST /api/kanban/:id/unblock
|
|
30
|
+
router.post('/:id/unblock', async (req, res) => {
|
|
31
|
+
const featureId = req.params.id;
|
|
32
|
+
try {
|
|
33
|
+
const result = await pipeline.unblock(featureId);
|
|
34
|
+
res.status(result.status).json(result.error ? { error: result.error } : { unblocked: result.unblocked, feature_id: result.feature_id });
|
|
35
|
+
} catch (err: any) {
|
|
36
|
+
log('UNBLOCK', `ERROR: ${err.message}`);
|
|
37
|
+
res.status(500).json({ error: err.message });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export default router;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project routes — CRUD endpoints for project management.
|
|
3
|
+
* GET /api/projects — list active (non-archived) projects
|
|
4
|
+
* POST /api/projects — create a new project
|
|
5
|
+
* PATCH /api/projects/:id — rename a project
|
|
6
|
+
* POST /api/projects/:id/archive — archive (soft delete) a project
|
|
7
|
+
* POST /api/projects/:id/restore — restore an archived project
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Router } from 'express';
|
|
11
|
+
import type { ProjectService } from '../services/project_service.js';
|
|
12
|
+
|
|
13
|
+
interface ProjectRoutesDeps {
|
|
14
|
+
projectService: ProjectService;
|
|
15
|
+
log: (tag: string, ...args: any[]) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const createProjectRoutes = ({ projectService, log }: ProjectRoutesDeps): Router => {
|
|
19
|
+
const router: Router = Router();
|
|
20
|
+
|
|
21
|
+
// GET /api/projects — list active projects
|
|
22
|
+
router.get('/', async (_req, res) => {
|
|
23
|
+
log('PROJECTS', 'GET /api/projects');
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const projectList = await projectService.listActive();
|
|
27
|
+
res.json({ projects: projectList });
|
|
28
|
+
} catch (err: any) {
|
|
29
|
+
log('PROJECTS', `List projects failed: ${err.message}`, err.cause || err);
|
|
30
|
+
res.status(500).json({ error: 'Failed to list projects' });
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// POST /api/projects — create a new project
|
|
35
|
+
router.post('/', async (req, res) => {
|
|
36
|
+
const { name } = req.body;
|
|
37
|
+
log('PROJECTS', `POST /api/projects name="${name}"`);
|
|
38
|
+
|
|
39
|
+
if (!name || typeof name !== 'string' || !name.trim()) {
|
|
40
|
+
res.status(400).json({ error: 'Project name is required' });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const project = await projectService.create(name.trim());
|
|
46
|
+
res.status(201).json({ project });
|
|
47
|
+
} catch (err: any) {
|
|
48
|
+
log('PROJECTS', `Create project failed: ${err.message}`);
|
|
49
|
+
res.status(500).json({ error: 'Failed to create project' });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// PATCH /api/projects/:id — rename a project
|
|
54
|
+
router.patch('/:id', async (req, res) => {
|
|
55
|
+
const { id } = req.params;
|
|
56
|
+
const { name } = req.body;
|
|
57
|
+
log('PROJECTS', `PATCH /api/projects/${id} name="${name}"`);
|
|
58
|
+
|
|
59
|
+
if (!name || typeof name !== 'string' || !name.trim()) {
|
|
60
|
+
res.status(400).json({ error: 'Project name is required' });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const project = await projectService.rename(id, name.trim());
|
|
66
|
+
res.json({ project });
|
|
67
|
+
} catch (err: any) {
|
|
68
|
+
log('PROJECTS', `Rename project failed: ${err.message}`);
|
|
69
|
+
if (err.message === 'Project not found') {
|
|
70
|
+
res.status(404).json({ error: err.message });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
res.status(500).json({ error: 'Failed to rename project' });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// POST /api/projects/:id/archive — archive a project
|
|
78
|
+
router.post('/:id/archive', async (req, res) => {
|
|
79
|
+
const { id } = req.params;
|
|
80
|
+
log('PROJECTS', `POST /api/projects/${id}/archive`);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const project = await projectService.archive(id);
|
|
84
|
+
res.json({ project });
|
|
85
|
+
} catch (err: any) {
|
|
86
|
+
log('PROJECTS', `Archive project failed: ${err.message}`);
|
|
87
|
+
if (err.message === 'Project not found') {
|
|
88
|
+
res.status(404).json({ error: err.message });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (err.message === 'Cannot archive the default project' || err.message === 'Project is already archived') {
|
|
92
|
+
res.status(400).json({ error: err.message });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
res.status(500).json({ error: 'Failed to archive project' });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// POST /api/projects/:id/restore — restore an archived project
|
|
100
|
+
router.post('/:id/restore', async (req, res) => {
|
|
101
|
+
const { id } = req.params;
|
|
102
|
+
log('PROJECTS', `POST /api/projects/${id}/restore`);
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const project = await projectService.restore(id);
|
|
106
|
+
res.json({ project });
|
|
107
|
+
} catch (err: any) {
|
|
108
|
+
log('PROJECTS', `Restore project failed: ${err.message}`);
|
|
109
|
+
if (err.message === 'Project not found') {
|
|
110
|
+
res.status(404).json({ error: err.message });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (err.message === 'Project is not archived') {
|
|
114
|
+
res.status(400).json({ error: err.message });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
res.status(500).json({ error: 'Failed to restore project' });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return router;
|
|
122
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User management routes — admin-only endpoints for managing users and invitations.
|
|
3
|
+
* GET /api/users — list all users
|
|
4
|
+
* DELETE /api/users/:id — delete a user
|
|
5
|
+
* GET /api/users/invitations — list all invitations
|
|
6
|
+
* DELETE /api/users/invitations/:id — delete an invitation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Router } from 'express';
|
|
10
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
11
|
+
import type { UserManagementService } from '../services/user_management_service.js';
|
|
12
|
+
|
|
13
|
+
interface UserRoutesDeps {
|
|
14
|
+
userManagementService: UserManagementService;
|
|
15
|
+
log: (tag: string, ...args: any[]) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const requireAdmin = (req: Request, res: Response, next: NextFunction): void => {
|
|
19
|
+
const user = (req as any).user;
|
|
20
|
+
if (user?.role !== 'admin') {
|
|
21
|
+
res.status(403).json({ error: 'Admin access required' });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
next();
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const createUserRoutes = ({ userManagementService, log }: UserRoutesDeps): Router => {
|
|
28
|
+
const router: Router = Router();
|
|
29
|
+
|
|
30
|
+
router.use(requireAdmin);
|
|
31
|
+
|
|
32
|
+
// GET /api/users — list all users
|
|
33
|
+
router.get('/', async (req, res) => {
|
|
34
|
+
log('USERS', 'GET /api/users');
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const userList = await userManagementService.listUsers();
|
|
38
|
+
res.json({ users: userList });
|
|
39
|
+
} catch (err: any) {
|
|
40
|
+
log('USERS', `List users failed: ${err.message}`);
|
|
41
|
+
res.status(500).json({ error: 'Failed to list users' });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// DELETE /api/users/:id — delete a user
|
|
46
|
+
router.delete('/:id', async (req, res) => {
|
|
47
|
+
const { id } = req.params;
|
|
48
|
+
log('USERS', `DELETE /api/users/${id}`);
|
|
49
|
+
const user = (req as any).user;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
await userManagementService.deleteUser(id, user.id);
|
|
53
|
+
res.json({ message: 'User deleted' });
|
|
54
|
+
} catch (err: any) {
|
|
55
|
+
log('USERS', `Delete user failed: ${err.message}`);
|
|
56
|
+
if (err.message === 'Cannot delete your own account') {
|
|
57
|
+
return res.status(400).json({ error: err.message });
|
|
58
|
+
}
|
|
59
|
+
if (err.message === 'User not found') {
|
|
60
|
+
return res.status(404).json({ error: err.message });
|
|
61
|
+
}
|
|
62
|
+
res.status(500).json({ error: 'Failed to delete user' });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// GET /api/users/invitations — list all invitations
|
|
67
|
+
router.get('/invitations', async (req, res) => {
|
|
68
|
+
log('USERS', 'GET /api/users/invitations');
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const invitationList = await userManagementService.listInvitations();
|
|
72
|
+
res.json({ invitations: invitationList });
|
|
73
|
+
} catch (err: any) {
|
|
74
|
+
log('USERS', `List invitations failed: ${err.message}`);
|
|
75
|
+
res.status(500).json({ error: 'Failed to list invitations' });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// DELETE /api/users/invitations/:id — delete an invitation
|
|
80
|
+
router.delete('/invitations/:id', async (req, res) => {
|
|
81
|
+
const { id } = req.params;
|
|
82
|
+
log('USERS', `DELETE /api/users/invitations/${id}`);
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
await userManagementService.deleteInvitation(id);
|
|
86
|
+
res.json({ message: 'Invitation deleted' });
|
|
87
|
+
} catch (err: any) {
|
|
88
|
+
log('USERS', `Delete invitation failed: ${err.message}`);
|
|
89
|
+
if (err.message === 'Invitation not found') {
|
|
90
|
+
return res.status(404).json({ error: err.message });
|
|
91
|
+
}
|
|
92
|
+
res.status(500).json({ error: 'Failed to delete invitation' });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return router;
|
|
97
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express server entry point.
|
|
3
|
+
* Serves API routes and static frontend files in production.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { config } from 'dotenv';
|
|
7
|
+
import { resolve } from 'node:path';
|
|
8
|
+
config({ path: resolve(import.meta.dirname, '..', '..', '..', '.env') });
|
|
9
|
+
|
|
10
|
+
import express, {Express} from 'express';
|
|
11
|
+
import cors from 'cors';
|
|
12
|
+
import cookieParser from 'cookie-parser';
|
|
13
|
+
import { createServer } from 'node:http';
|
|
14
|
+
import { join, dirname } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { existsSync } from 'node:fs';
|
|
17
|
+
import { WebSocketServer } from 'ws';
|
|
18
|
+
import * as pty from 'node-pty';
|
|
19
|
+
import { initServices, log } from './services/init.js';
|
|
20
|
+
import { AuthService } from './services/auth_service.js';
|
|
21
|
+
import { EmailService } from './services/email_service.js';
|
|
22
|
+
import { PasswordResetService } from './services/password_reset_service.js';
|
|
23
|
+
import { InvitationService } from './services/invitation_service.js';
|
|
24
|
+
import { PtySessionManager } from './services/pty_session_manager.js';
|
|
25
|
+
import { TerminalWsHandler } from './services/terminal_ws_handler.js';
|
|
26
|
+
import { UserManagementService } from './services/user_management_service.js';
|
|
27
|
+
import { ProjectService } from './services/project_service.js';
|
|
28
|
+
import { AuthMiddleware } from './middleware/auth_middleware.js';
|
|
29
|
+
import { createAuthRoutes } from './routes/auth.js';
|
|
30
|
+
import { createUserRoutes } from './routes/users.js';
|
|
31
|
+
import { createProjectRoutes } from './routes/projects.js';
|
|
32
|
+
import { getDb } from '@interview-system/shared/lib/db.js';
|
|
33
|
+
import graphRoutes from './routes/graph.js';
|
|
34
|
+
import kanbanRoutes from './routes/kanban.js';
|
|
35
|
+
import pipelineRoutes from './routes/pipeline.js';
|
|
36
|
+
import coherenceRoutes from './routes/coherence.js';
|
|
37
|
+
|
|
38
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
39
|
+
const DEFAULT_PORT = 3000;
|
|
40
|
+
|
|
41
|
+
const parseArgs = (argv: string[]) => {
|
|
42
|
+
const args = { port: DEFAULT_PORT, verbose: false };
|
|
43
|
+
for (let i = 2; i < argv.length; i++) {
|
|
44
|
+
if (argv[i] === '--port' && argv[i + 1]) {
|
|
45
|
+
args.port = parseInt(argv[i + 1], 10);
|
|
46
|
+
i++;
|
|
47
|
+
}
|
|
48
|
+
if (argv[i] === '--verbose') {
|
|
49
|
+
args.verbose = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return args;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const args = parseArgs(process.argv);
|
|
56
|
+
|
|
57
|
+
// Initialize services after VERBOSE is set
|
|
58
|
+
initServices(args.verbose);
|
|
59
|
+
|
|
60
|
+
const app: Express = express();
|
|
61
|
+
|
|
62
|
+
app.use(cors());
|
|
63
|
+
app.use(express.json());
|
|
64
|
+
app.use(cookieParser());
|
|
65
|
+
|
|
66
|
+
// Request/response logging
|
|
67
|
+
app.use((req, res, next) => {
|
|
68
|
+
const start = Date.now();
|
|
69
|
+
res.on('finish', () => {
|
|
70
|
+
const duration = Date.now() - start;
|
|
71
|
+
// Skip noisy pipeline polls
|
|
72
|
+
if (req.originalUrl.endsWith('/pipeline')) return;
|
|
73
|
+
log('HTTP', `${req.method} ${req.originalUrl} ${res.statusCode} ${duration}ms`);
|
|
74
|
+
});
|
|
75
|
+
next();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Auth routes (public — no middleware)
|
|
79
|
+
const jwtSecret = process.env.JWT_SECRET || 'dev-secret-change-in-production';
|
|
80
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
81
|
+
const authService = new AuthService({ jwtSecret, isProduction });
|
|
82
|
+
const resendApiKey = process.env.RESEND_API_KEY || '';
|
|
83
|
+
const appBaseUrl = process.env.APP_BASE_URL || 'https://product-system.localhost';
|
|
84
|
+
const emailFromAddress = process.env.EMAIL_FROM || 'noreply@example.com';
|
|
85
|
+
const emailService = new EmailService({ apiKey: resendApiKey, fromAddress: emailFromAddress });
|
|
86
|
+
const passwordResetService = new PasswordResetService({ getDb, emailService, authService, appBaseUrl, log });
|
|
87
|
+
const invitationService = new InvitationService({ getDb, emailService, authService, appBaseUrl, log });
|
|
88
|
+
const authRoutes = createAuthRoutes({ getDb, authService, passwordResetService, invitationService, log });
|
|
89
|
+
app.use('/api/auth', authRoutes);
|
|
90
|
+
|
|
91
|
+
// Auth middleware for protected API routes
|
|
92
|
+
const authMiddleware = new AuthMiddleware({ authService });
|
|
93
|
+
|
|
94
|
+
// User management routes (admin-only)
|
|
95
|
+
const userManagementService = new UserManagementService({ getDb, log });
|
|
96
|
+
const userRoutes = createUserRoutes({ userManagementService, log });
|
|
97
|
+
app.use('/api/users', authMiddleware.requireAuth, userRoutes);
|
|
98
|
+
|
|
99
|
+
// Project management routes (authenticated)
|
|
100
|
+
const projectService = new ProjectService({ getDb, log });
|
|
101
|
+
const projectRoutes = createProjectRoutes({ projectService, log });
|
|
102
|
+
app.use('/api/projects', authMiddleware.requireAuth, projectRoutes);
|
|
103
|
+
|
|
104
|
+
// Ensure default project exists and assign orphan nodes on startup
|
|
105
|
+
projectService.ensureDefaultAndAssignOrphans().catch((err: any) => {
|
|
106
|
+
log('STARTUP', `Failed to ensure default project: ${err.message}`);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Protected API routes — require authentication
|
|
110
|
+
app.use('/api', authMiddleware.requireAuth, graphRoutes);
|
|
111
|
+
app.use('/api/kanban', authMiddleware.requireAuth, kanbanRoutes);
|
|
112
|
+
app.use('/api/kanban', authMiddleware.requireAuth, pipelineRoutes);
|
|
113
|
+
app.use('/api/coherence', authMiddleware.requireAuth, coherenceRoutes);
|
|
114
|
+
|
|
115
|
+
// Redirect favicon.ico to SVG favicon served from static files
|
|
116
|
+
app.get('/favicon.ico', (_req, res) => { res.redirect(301, '/favicon.svg'); });
|
|
117
|
+
|
|
118
|
+
// Serve frontend static files in production
|
|
119
|
+
const FRONTEND_DIST = join(__dirname, '..', '..', 'frontend', 'dist');
|
|
120
|
+
if (existsSync(FRONTEND_DIST)) {
|
|
121
|
+
app.use(express.static(FRONTEND_DIST));
|
|
122
|
+
// SPA fallback — send index.html for all non-API routes
|
|
123
|
+
app.get('/{*path}', (_req, res) => {
|
|
124
|
+
res.sendFile(join(FRONTEND_DIST, 'index.html'));
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Create HTTP server for both Express and WebSocket
|
|
129
|
+
const server = createServer(app);
|
|
130
|
+
|
|
131
|
+
// Set up WebSocket for terminal
|
|
132
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
133
|
+
// Resolve project root: from packages/backend/src → product-system → repo root
|
|
134
|
+
const PROJECT_ROOT = join(__dirname, '..', '..', '..', '..');
|
|
135
|
+
const ptyManager = new PtySessionManager({ spawn: pty.spawn, log, projectRoot: PROJECT_ROOT });
|
|
136
|
+
const terminalHandler = new TerminalWsHandler({ wss, authService, ptyManager, log });
|
|
137
|
+
|
|
138
|
+
server.on('upgrade', (req, socket, head) => {
|
|
139
|
+
terminalHandler.handleUpgrade(req, socket, head);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Clean up PTY sessions on server shutdown
|
|
143
|
+
process.on('SIGTERM', () => {
|
|
144
|
+
ptyManager.destroyAll();
|
|
145
|
+
server.close();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
server.listen(args.port, () => {
|
|
149
|
+
log('SERVER', `API server running at http://localhost:${args.port}`);
|
|
150
|
+
log('SERVER', `Verbose mode: ${args.verbose ? 'ON' : 'OFF'}`);
|
|
151
|
+
log('SERVER', `WebSocket terminal endpoint: ws://localhost:${args.port}/api/terminal`);
|
|
152
|
+
if (existsSync(FRONTEND_DIST)) {
|
|
153
|
+
log('SERVER', `Serving frontend from ${FRONTEND_DIST}`);
|
|
154
|
+
} else {
|
|
155
|
+
log('SERVER', `No frontend dist found — run "pnpm --filter @interview-system/frontend build" first, or use dev mode`);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
export default app;
|