@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.
Files changed (178) hide show
  1. package/dist/bin/create.d.ts +2 -0
  2. package/dist/bin/create.js +25 -0
  3. package/dist/bin/create.js.map +1 -0
  4. package/dist/src/scaffolder.d.ts +22 -0
  5. package/dist/src/scaffolder.js +120 -0
  6. package/dist/src/scaffolder.js.map +1 -0
  7. package/package.json +24 -0
  8. package/templates/product-system/.env.example +8 -0
  9. package/templates/product-system/CLAUDE.md +45 -0
  10. package/templates/product-system/package.json +32 -0
  11. package/templates/product-system/packages/backend/package.json +37 -0
  12. package/templates/product-system/packages/backend/src/middleware/auth_middleware.test.ts +86 -0
  13. package/templates/product-system/packages/backend/src/middleware/auth_middleware.ts +35 -0
  14. package/templates/product-system/packages/backend/src/routes/auth.ts +463 -0
  15. package/templates/product-system/packages/backend/src/routes/coherence.ts +187 -0
  16. package/templates/product-system/packages/backend/src/routes/graph.ts +67 -0
  17. package/templates/product-system/packages/backend/src/routes/kanban.ts +201 -0
  18. package/templates/product-system/packages/backend/src/routes/pipeline.ts +41 -0
  19. package/templates/product-system/packages/backend/src/routes/projects.ts +122 -0
  20. package/templates/product-system/packages/backend/src/routes/users.ts +97 -0
  21. package/templates/product-system/packages/backend/src/server.ts +159 -0
  22. package/templates/product-system/packages/backend/src/services/auth_service.test.ts +115 -0
  23. package/templates/product-system/packages/backend/src/services/auth_service.ts +82 -0
  24. package/templates/product-system/packages/backend/src/services/coherence-review.ts +339 -0
  25. package/templates/product-system/packages/backend/src/services/email_service.ts +75 -0
  26. package/templates/product-system/packages/backend/src/services/init.ts +80 -0
  27. package/templates/product-system/packages/backend/src/services/invitation_service.test.ts +235 -0
  28. package/templates/product-system/packages/backend/src/services/invitation_service.ts +193 -0
  29. package/templates/product-system/packages/backend/src/services/password_reset_service.test.ts +151 -0
  30. package/templates/product-system/packages/backend/src/services/password_reset_service.ts +135 -0
  31. package/templates/product-system/packages/backend/src/services/project_service.test.ts +215 -0
  32. package/templates/product-system/packages/backend/src/services/project_service.ts +171 -0
  33. package/templates/product-system/packages/backend/src/services/pty_session_manager.test.ts +88 -0
  34. package/templates/product-system/packages/backend/src/services/pty_session_manager.ts +279 -0
  35. package/templates/product-system/packages/backend/src/services/terminal_ws_handler.ts +133 -0
  36. package/templates/product-system/packages/backend/src/services/user_management_service.test.ts +158 -0
  37. package/templates/product-system/packages/backend/src/services/user_management_service.ts +128 -0
  38. package/templates/product-system/packages/backend/tsconfig.json +22 -0
  39. package/templates/product-system/packages/frontend/index.html +13 -0
  40. package/templates/product-system/packages/frontend/package-lock.json +2666 -0
  41. package/templates/product-system/packages/frontend/package.json +30 -0
  42. package/templates/product-system/packages/frontend/public/favicon.svg +16 -0
  43. package/templates/product-system/packages/frontend/src/App.tsx +29 -0
  44. package/templates/product-system/packages/frontend/src/api/client.ts +386 -0
  45. package/templates/product-system/packages/frontend/src/api/client_projects.test.ts +104 -0
  46. package/templates/product-system/packages/frontend/src/api/client_refresh.test.ts +145 -0
  47. package/templates/product-system/packages/frontend/src/components/CoherenceView.tsx +414 -0
  48. package/templates/product-system/packages/frontend/src/components/GraphLegend.tsx +124 -0
  49. package/templates/product-system/packages/frontend/src/components/GraphSettings.tsx +112 -0
  50. package/templates/product-system/packages/frontend/src/components/GraphView.tsx +370 -0
  51. package/templates/product-system/packages/frontend/src/components/InviteUserDialog.tsx +85 -0
  52. package/templates/product-system/packages/frontend/src/components/KanbanView.tsx +470 -0
  53. package/templates/product-system/packages/frontend/src/components/LoginPage.tsx +116 -0
  54. package/templates/product-system/packages/frontend/src/components/ProjectSelector.tsx +187 -0
  55. package/templates/product-system/packages/frontend/src/components/QaIssueSheet.tsx +192 -0
  56. package/templates/product-system/packages/frontend/src/components/SidePanel.tsx +231 -0
  57. package/templates/product-system/packages/frontend/src/components/TerminalView.tsx +200 -0
  58. package/templates/product-system/packages/frontend/src/components/Toolbar.tsx +84 -0
  59. package/templates/product-system/packages/frontend/src/components/UsersView.tsx +249 -0
  60. package/templates/product-system/packages/frontend/src/constants/graph.ts +191 -0
  61. package/templates/product-system/packages/frontend/src/hooks/useAuth.tsx +54 -0
  62. package/templates/product-system/packages/frontend/src/hooks/useGraph.ts +27 -0
  63. package/templates/product-system/packages/frontend/src/hooks/useKanban.ts +21 -0
  64. package/templates/product-system/packages/frontend/src/hooks/useProjects.ts +86 -0
  65. package/templates/product-system/packages/frontend/src/hooks/useTheme.ts +26 -0
  66. package/templates/product-system/packages/frontend/src/hooks/useToast.tsx +62 -0
  67. package/templates/product-system/packages/frontend/src/hooks/use_projects_logic.test.ts +61 -0
  68. package/templates/product-system/packages/frontend/src/main.tsx +12 -0
  69. package/templates/product-system/packages/frontend/src/pages/accept_invitation_page.tsx +167 -0
  70. package/templates/product-system/packages/frontend/src/pages/forgot_password_page.tsx +100 -0
  71. package/templates/product-system/packages/frontend/src/pages/register_page.tsx +137 -0
  72. package/templates/product-system/packages/frontend/src/pages/reset_password_page.tsx +146 -0
  73. package/templates/product-system/packages/frontend/src/routes/ProtectedRoute.tsx +12 -0
  74. package/templates/product-system/packages/frontend/src/routes/accept_invitation.tsx +14 -0
  75. package/templates/product-system/packages/frontend/src/routes/dashboard.tsx +221 -0
  76. package/templates/product-system/packages/frontend/src/routes/forgot_password.tsx +13 -0
  77. package/templates/product-system/packages/frontend/src/routes/login.tsx +14 -0
  78. package/templates/product-system/packages/frontend/src/routes/register.tsx +14 -0
  79. package/templates/product-system/packages/frontend/src/routes/reset_password.tsx +13 -0
  80. package/templates/product-system/packages/frontend/src/styles/index.css +3358 -0
  81. package/templates/product-system/packages/frontend/src/utils/auth_validation.test.ts +51 -0
  82. package/templates/product-system/packages/frontend/src/utils/auth_validation.ts +19 -0
  83. package/templates/product-system/packages/frontend/src/utils/login_validation.test.ts +61 -0
  84. package/templates/product-system/packages/frontend/src/utils/login_validation.ts +24 -0
  85. package/templates/product-system/packages/frontend/src/utils/logout.test.ts +63 -0
  86. package/templates/product-system/packages/frontend/src/utils/node_sizing.test.ts +62 -0
  87. package/templates/product-system/packages/frontend/src/utils/node_sizing.ts +24 -0
  88. package/templates/product-system/packages/frontend/src/utils/task_status.test.ts +53 -0
  89. package/templates/product-system/packages/frontend/src/utils/task_status.ts +14 -0
  90. package/templates/product-system/packages/frontend/tsconfig.json +21 -0
  91. package/templates/product-system/packages/frontend/vite.config.ts +20 -0
  92. package/templates/product-system/packages/shared/.env.example +3 -0
  93. package/templates/product-system/packages/shared/README.md +1 -0
  94. package/templates/product-system/packages/shared/db/migrate.ts +32 -0
  95. package/templates/product-system/packages/shared/db/migrations/0000_dashing_gorgon.sql +128 -0
  96. package/templates/product-system/packages/shared/db/migrations/meta/0000_snapshot.json +819 -0
  97. package/templates/product-system/packages/shared/db/migrations/meta/_journal.json +13 -0
  98. package/templates/product-system/packages/shared/db/schema.ts +137 -0
  99. package/templates/product-system/packages/shared/drizzle.config.js +14 -0
  100. package/templates/product-system/packages/shared/lib/claude-service.ts +215 -0
  101. package/templates/product-system/packages/shared/lib/coherence.ts +278 -0
  102. package/templates/product-system/packages/shared/lib/completeness.ts +30 -0
  103. package/templates/product-system/packages/shared/lib/constants.ts +327 -0
  104. package/templates/product-system/packages/shared/lib/db.ts +81 -0
  105. package/templates/product-system/packages/shared/lib/git_workflow.ts +110 -0
  106. package/templates/product-system/packages/shared/lib/graph.ts +186 -0
  107. package/templates/product-system/packages/shared/lib/kanban.ts +161 -0
  108. package/templates/product-system/packages/shared/lib/markdown.ts +205 -0
  109. package/templates/product-system/packages/shared/lib/pipeline-state-store.ts +124 -0
  110. package/templates/product-system/packages/shared/lib/pipeline.ts +489 -0
  111. package/templates/product-system/packages/shared/lib/prompt_builder.ts +170 -0
  112. package/templates/product-system/packages/shared/lib/relevance_search.ts +159 -0
  113. package/templates/product-system/packages/shared/lib/session.ts +152 -0
  114. package/templates/product-system/packages/shared/lib/validator.ts +117 -0
  115. package/templates/product-system/packages/shared/lib/work_summary_parser.ts +130 -0
  116. package/templates/product-system/packages/shared/package.json +30 -0
  117. package/templates/product-system/packages/shared/scripts/assign-project.ts +52 -0
  118. package/templates/product-system/packages/shared/tools/add_edge.ts +61 -0
  119. package/templates/product-system/packages/shared/tools/add_node.ts +101 -0
  120. package/templates/product-system/packages/shared/tools/end_session.ts +87 -0
  121. package/templates/product-system/packages/shared/tools/get_gaps.ts +87 -0
  122. package/templates/product-system/packages/shared/tools/get_kanban.ts +125 -0
  123. package/templates/product-system/packages/shared/tools/get_node.ts +78 -0
  124. package/templates/product-system/packages/shared/tools/get_status.ts +98 -0
  125. package/templates/product-system/packages/shared/tools/migrate_to_turso.ts +385 -0
  126. package/templates/product-system/packages/shared/tools/move_card.ts +143 -0
  127. package/templates/product-system/packages/shared/tools/rebuild_index.ts +77 -0
  128. package/templates/product-system/packages/shared/tools/remove_edge.ts +59 -0
  129. package/templates/product-system/packages/shared/tools/remove_node.ts +96 -0
  130. package/templates/product-system/packages/shared/tools/resolve_question.ts +75 -0
  131. package/templates/product-system/packages/shared/tools/search_nodes.ts +106 -0
  132. package/templates/product-system/packages/shared/tools/start_session.ts +144 -0
  133. package/templates/product-system/packages/shared/tools/update_node.ts +133 -0
  134. package/templates/product-system/packages/shared/tsconfig.json +24 -0
  135. package/templates/product-system/pnpm-workspace.yaml +2 -0
  136. package/templates/product-system/smoke_test.ts +219 -0
  137. package/templates/product-system/tests/coherence_review.test.ts +562 -0
  138. package/templates/product-system/tests/db_sqlite_fallback.test.ts +75 -0
  139. package/templates/product-system/tests/edge_type_color_coding.test.ts +147 -0
  140. package/templates/product-system/tests/emit-tool-use-events.test.ts +85 -0
  141. package/templates/product-system/tests/feature_kind.test.ts +139 -0
  142. package/templates/product-system/tests/gap_indicators.test.ts +199 -0
  143. package/templates/product-system/tests/graceful_init.test.ts +142 -0
  144. package/templates/product-system/tests/graph_legend.test.ts +314 -0
  145. package/templates/product-system/tests/graph_settings_sheet.test.ts +804 -0
  146. package/templates/product-system/tests/hide_defined_filter.test.ts +205 -0
  147. package/templates/product-system/tests/kanban.test.ts +529 -0
  148. package/templates/product-system/tests/neighborhood_focus.test.ts +132 -0
  149. package/templates/product-system/tests/node_search.test.ts +340 -0
  150. package/templates/product-system/tests/node_sizing.test.ts +170 -0
  151. package/templates/product-system/tests/node_type_toggle_filters.test.ts +285 -0
  152. package/templates/product-system/tests/node_type_visual_encoding.test.ts +103 -0
  153. package/templates/product-system/tests/pipeline-state-store.test.ts +268 -0
  154. package/templates/product-system/tests/pipeline-unit.test.ts +593 -0
  155. package/templates/product-system/tests/pipeline.test.ts +195 -0
  156. package/templates/product-system/tests/pipeline_stats_all_cards.test.ts +193 -0
  157. package/templates/product-system/tests/play_all.test.ts +296 -0
  158. package/templates/product-system/tests/qa_issue_sheet.test.ts +464 -0
  159. package/templates/product-system/tests/relevance_search.test.ts +186 -0
  160. package/templates/product-system/tests/search_reorder.test.ts +88 -0
  161. package/templates/product-system/tests/serve_ui.test.ts +281 -0
  162. package/templates/product-system/tests/serve_ui_drizzle.test.ts +114 -0
  163. package/templates/product-system/tests/session_context_recall.test.ts +135 -0
  164. package/templates/product-system/tests/side_panel.test.ts +345 -0
  165. package/templates/product-system/tests/spec_completeness_label.test.ts +69 -0
  166. package/templates/product-system/tests/url_routing_test.ts +122 -0
  167. package/templates/product-system/tests/user_login.test.ts +150 -0
  168. package/templates/product-system/tests/user_registration.test.ts +205 -0
  169. package/templates/product-system/tests/web_terminal.test.ts +572 -0
  170. package/templates/product-system/tests/work_summary.test.ts +211 -0
  171. package/templates/product-system/tests/zoom_pan.test.ts +43 -0
  172. package/templates/product-system/tsconfig.json +24 -0
  173. package/templates/skills/product-bootstrap/SKILL.md +312 -0
  174. package/templates/skills/product-code-reviewer/SKILL.md +147 -0
  175. package/templates/skills/product-debugger/SKILL.md +206 -0
  176. package/templates/skills/product-debugger/references/agent-browser.md +1156 -0
  177. package/templates/skills/product-developer/SKILL.md +182 -0
  178. 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;