@assistkick/create 1.2.0 → 1.4.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 (54) hide show
  1. package/package.json +2 -1
  2. package/templates/assistkick-product-system/GITHUB_APP_SETUP.md +88 -0
  3. package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +231 -0
  4. package/templates/assistkick-product-system/packages/backend/src/routes/kanban.ts +4 -4
  5. package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +49 -2
  6. package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +82 -0
  7. package/templates/assistkick-product-system/packages/backend/src/server.ts +19 -6
  8. package/templates/assistkick-product-system/packages/backend/src/services/github_app_service.ts +146 -0
  9. package/templates/assistkick-product-system/packages/backend/src/services/init.ts +69 -2
  10. package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +71 -0
  11. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +87 -0
  12. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +194 -0
  13. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +88 -17
  14. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +114 -39
  15. package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +28 -14
  16. package/templates/assistkick-product-system/packages/frontend/src/App.tsx +1 -1
  17. package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +151 -0
  18. package/templates/assistkick-product-system/packages/frontend/src/components/GitRepoModal.tsx +352 -0
  19. package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +208 -95
  20. package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +17 -1
  21. package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +238 -105
  22. package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +15 -13
  23. package/templates/assistkick-product-system/packages/frontend/src/constants/graph.ts +1 -0
  24. package/templates/assistkick-product-system/packages/frontend/src/hooks/useProjects.ts +4 -0
  25. package/templates/assistkick-product-system/packages/frontend/src/routes/dashboard.tsx +22 -4
  26. package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +486 -38
  27. package/templates/assistkick-product-system/packages/shared/db/migrations/0001_vengeful_wallop.sql +1 -0
  28. package/templates/assistkick-product-system/packages/shared/db/migrations/0002_greedy_excalibur.sql +4 -0
  29. package/templates/assistkick-product-system/packages/shared/db/migrations/0003_lonely_cyclops.sql +17 -0
  30. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0001_snapshot.json +826 -0
  31. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0002_snapshot.json +854 -0
  32. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0003_snapshot.json +862 -0
  33. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +21 -0
  34. package/templates/assistkick-product-system/packages/shared/db/schema.ts +10 -3
  35. package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +54 -1
  36. package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +25 -0
  37. package/templates/assistkick-product-system/packages/shared/lib/pipeline-state-store.ts +4 -0
  38. package/templates/assistkick-product-system/packages/shared/lib/pipeline.ts +329 -89
  39. package/templates/assistkick-product-system/packages/shared/lib/pipeline_orchestrator.ts +186 -0
  40. package/templates/assistkick-product-system/packages/shared/lib/session.ts +10 -6
  41. package/templates/assistkick-product-system/packages/shared/tools/db_explorer.ts +275 -0
  42. package/templates/assistkick-product-system/packages/shared/tools/end_session.ts +2 -2
  43. package/templates/assistkick-product-system/packages/shared/tools/get_kanban.ts +2 -1
  44. package/templates/assistkick-product-system/packages/shared/tools/move_card.ts +3 -2
  45. package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -2
  46. package/templates/assistkick-product-system/tests/kanban.test.ts +1 -1
  47. package/templates/assistkick-product-system/tests/pipeline_stats_all_cards.test.ts +1 -1
  48. package/templates/assistkick-product-system/tests/web_terminal.test.ts +189 -150
  49. package/templates/skills/assistkick-bootstrap/SKILL.md +33 -25
  50. package/templates/skills/assistkick-code-reviewer/SKILL.md +23 -15
  51. package/templates/skills/assistkick-db-explorer/SKILL.md +86 -0
  52. package/templates/skills/assistkick-debugger/SKILL.md +30 -22
  53. package/templates/skills/assistkick-developer/SKILL.md +37 -29
  54. package/templates/skills/assistkick-interview/SKILL.md +34 -26
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistkick/create",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Scaffold assistkick-product-system into any project",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,6 +14,7 @@
14
14
  "build": "tsc",
15
15
  "prepare_templates": "bash scripts/prepare_templates.sh",
16
16
  "prepublishOnly": "bash scripts/prepare_templates.sh && pnpm build",
17
+ "publish": "npm publish",
17
18
  "test": "tsx --test tests/**/*.test.ts"
18
19
  },
19
20
  "devDependencies": {
@@ -0,0 +1,88 @@
1
+ # GitHub App Setup Guide
2
+
3
+ This guide walks you through creating a GitHub App and configuring it for use with AssistKick.
4
+
5
+ ## Step 1: Create the GitHub App
6
+
7
+ 1. Go to **GitHub Settings > Developer settings > GitHub Apps** (or navigate to `https://github.com/settings/apps`)
8
+ 2. Click **New GitHub App**
9
+ 3. Fill in the form:
10
+
11
+ | Field | Value |
12
+ |---------------------|------------------------------------------------|
13
+ | **GitHub App name** | Something unique, e.g. `assistkick-<your-org>` |
14
+ | **Homepage URL** | Your AssistKick instance URL (or any URL) |
15
+ | **Webhook** | Uncheck "Active" (webhooks are not needed) |
16
+
17
+ ### Permissions
18
+
19
+ Under **Repository permissions**, grant the following:
20
+
21
+ | Permission | Access |
22
+ |--------------|---------------------------|
23
+ | **Contents** | Read & write |
24
+ | **Metadata** | Read-only (auto-selected) |
25
+
26
+ You can add more permissions later if needed (e.g. Pull requests, Issues).
27
+
28
+ ### Installation access
29
+
30
+ Under **Where can this GitHub App be installed?**, choose:
31
+ - **Only on this account** — if you only need it for your own repos/org
32
+ - **Any account** — if multiple orgs will install it
33
+
34
+ 1. Click **Create GitHub App**
35
+
36
+ ## Step 2: Generate a Private Key
37
+
38
+ 1. After creating the app, you'll be on the app's settings page
39
+ 2. Scroll down to **Private keys**
40
+ 3. Click **Generate a private key**
41
+ 4. A `.pem` file will be downloaded — keep this safe
42
+ 5. **Convert to PKCS#8 and base64-encode** (required — GitHub generates PKCS#1 keys but the backend uses the `jose` library which requires PKCS#8):
43
+
44
+ ```bash
45
+ openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in your-app-name.pem | base64
46
+ ```
47
+
48
+ Use the base64 output as the `GITHUB_APP_PRIVATE_KEY` environment variable in Step 5.
49
+
50
+ ## Step 3: Note Your App ID
51
+
52
+ On the app's settings page, find the **App ID** near the top (it's a numeric value like `123456`).
53
+
54
+ ## Step 4: Install the App
55
+
56
+ 1. From the app's settings page, click **Install App** in the left sidebar
57
+ 2. Choose the organization or account where you want to install it
58
+ 3. Select **All repositories** or pick specific repos
59
+ 4. Click **Install**
60
+
61
+ ## Step 5: Configure Environment Variables
62
+
63
+ Set these two environment variables where AssistKick runs:
64
+
65
+ ```bash
66
+ GITHUB_APP_ID=123456
67
+ GITHUB_APP_PRIVATE_KEY=LS0tLS1CRUdJTi...
68
+ ```
69
+
70
+ The value is the base64-encoded PKCS#8 key from Step 2. The backend auto-detects whether the key is raw PEM or base64-encoded.
71
+
72
+ ## Step 6: Verify
73
+
74
+ 1. Open your project in AssistKick
75
+ 2. Open the **Git Repository** dialog
76
+ 3. Go to the **GitHub** tab
77
+ 4. Click **Test Connection**
78
+ 5. You should see your installations listed — select one, pick a repo, and connect
79
+
80
+ ## Troubleshooting
81
+
82
+ | Problem | Solution |
83
+ |-----------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
84
+ | "GitHub App not configured" | `GITHUB_APP_ID` or `GITHUB_APP_PRIVATE_KEY` is missing from env |
85
+ | "No installations found" | Install the app on your GitHub account/org (Step 4) |
86
+ | 401 / "Bad credentials" | Private key doesn't match the App ID, or the key is malformed |
87
+ | "pkcs8" must be PKCS#8 | Key is in PKCS#1 format — convert it with `openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in key.pem -out key-pkcs8.pem` |
88
+ | No repos listed | The installation doesn't have access to any repos — edit installation permissions on GitHub |
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Git repository routes — connect/manage git repos for projects.
3
+ * POST /api/projects/:id/git/connect — connect a git repo (clone URL + GitHub App)
4
+ * POST /api/projects/:id/git/init — initialize a new local git repo
5
+ * POST /api/projects/:id/git/disconnect — disconnect a git repo
6
+ * GET /api/projects/:id/git/status — get git repo status
7
+ * POST /api/projects/:id/git/test — test GitHub App connection
8
+ * GET /api/projects/:id/git/installations — list GitHub App installations
9
+ * GET /api/projects/:id/git/repos — list repos for an installation
10
+ */
11
+
12
+ import { Router } from 'express';
13
+ import type { ProjectService } from '../services/project_service.js';
14
+ import type { GitHubAppService } from '../services/github_app_service.js';
15
+ import type { ProjectWorkspaceService } from '../services/project_workspace_service.js';
16
+
17
+ interface GitRoutesDeps {
18
+ projectService: ProjectService;
19
+ githubAppService: GitHubAppService;
20
+ workspaceService: ProjectWorkspaceService;
21
+ log: (tag: string, ...args: any[]) => void;
22
+ }
23
+
24
+ export const createGitRoutes = ({ projectService, githubAppService, workspaceService, log }: GitRoutesDeps): Router => {
25
+ const router: Router = Router({ mergeParams: true });
26
+
27
+ // POST /api/projects/:id/git/connect — connect a git repo
28
+ router.post('/connect', async (req, res) => {
29
+ const { id } = req.params;
30
+ const { repoUrl, githubInstallationId, githubRepoFullName, baseBranch } = req.body;
31
+ log('GIT', `POST /api/projects/${id}/git/connect repoUrl="${repoUrl}"`);
32
+
33
+ if (!repoUrl && !githubRepoFullName) {
34
+ res.status(400).json({ error: 'Either repoUrl or githubRepoFullName is required' });
35
+ return;
36
+ }
37
+
38
+ try {
39
+ const project = await projectService.getById(id);
40
+ if (!project) {
41
+ res.status(404).json({ error: 'Project not found' });
42
+ return;
43
+ }
44
+
45
+ // Determine clone URL
46
+ let cloneUrl = repoUrl;
47
+ if (!cloneUrl && githubRepoFullName) {
48
+ if (githubInstallationId && githubAppService.isConfigured()) {
49
+ cloneUrl = await githubAppService.buildAuthenticatedCloneUrl(githubInstallationId, githubRepoFullName);
50
+ } else {
51
+ cloneUrl = `https://github.com/${githubRepoFullName}.git`;
52
+ }
53
+ }
54
+
55
+ // Clone the repo into the workspace
56
+ await workspaceService.cloneRepo(id, cloneUrl);
57
+
58
+ // Detect default branch
59
+ const detectedBranch = await workspaceService.getDefaultBranch(id);
60
+ const effectiveBranch = baseBranch || detectedBranch;
61
+
62
+ // Save repo metadata to project
63
+ const updated = await projectService.connectRepo(id, {
64
+ repoUrl: githubRepoFullName ? `https://github.com/${githubRepoFullName}.git` : repoUrl,
65
+ githubInstallationId,
66
+ githubRepoFullName,
67
+ baseBranch: effectiveBranch,
68
+ });
69
+
70
+ res.json({ project: updated });
71
+ } catch (err: any) {
72
+ log('GIT', `Connect repo failed: ${err.message}`);
73
+ res.status(500).json({ error: `Failed to connect repo: ${err.message}` });
74
+ }
75
+ });
76
+
77
+ // POST /api/projects/:id/git/init — initialize a new local git repo
78
+ router.post('/init', async (req, res) => {
79
+ const { id } = req.params;
80
+ log('GIT', `POST /api/projects/${id}/git/init`);
81
+
82
+ try {
83
+ const project = await projectService.getById(id);
84
+ if (!project) {
85
+ res.status(404).json({ error: 'Project not found' });
86
+ return;
87
+ }
88
+
89
+ await workspaceService.initWorkspace(id);
90
+ const branch = await workspaceService.getDefaultBranch(id);
91
+
92
+ // Update project with base branch (no remote URL since it's local only)
93
+ const updated = await projectService.connectRepo(id, {
94
+ repoUrl: '',
95
+ baseBranch: branch,
96
+ });
97
+
98
+ res.json({ project: updated });
99
+ } catch (err: any) {
100
+ log('GIT', `Init repo failed: ${err.message}`);
101
+ res.status(500).json({ error: `Failed to initialize repo: ${err.message}` });
102
+ }
103
+ });
104
+
105
+ // POST /api/projects/:id/git/disconnect — disconnect a git repo
106
+ router.post('/disconnect', async (req, res) => {
107
+ const { id } = req.params;
108
+ log('GIT', `POST /api/projects/${id}/git/disconnect`);
109
+
110
+ try {
111
+ const updated = await projectService.disconnectRepo(id);
112
+ res.json({ project: updated });
113
+ } catch (err: any) {
114
+ log('GIT', `Disconnect repo failed: ${err.message}`);
115
+ if (err.message === 'Project not found') {
116
+ res.status(404).json({ error: err.message });
117
+ return;
118
+ }
119
+ res.status(500).json({ error: `Failed to disconnect repo: ${err.message}` });
120
+ }
121
+ });
122
+
123
+ // GET /api/projects/:id/git/status — get git repo status
124
+ router.get('/status', async (req, res) => {
125
+ const { id } = req.params;
126
+
127
+ try {
128
+ const project = await projectService.getById(id);
129
+ if (!project) {
130
+ res.status(404).json({ error: 'Project not found' });
131
+ return;
132
+ }
133
+
134
+ const gitStatus = await workspaceService.getStatus(id);
135
+ res.json({
136
+ ...gitStatus,
137
+ repoUrl: project.repoUrl,
138
+ githubInstallationId: project.githubInstallationId,
139
+ githubRepoFullName: project.githubRepoFullName,
140
+ baseBranch: project.baseBranch,
141
+ githubAppConfigured: githubAppService.isConfigured(),
142
+ });
143
+ } catch (err: any) {
144
+ log('GIT', `Get git status failed: ${err.message}`);
145
+ res.status(500).json({ error: `Failed to get git status: ${err.message}` });
146
+ }
147
+ });
148
+
149
+ // POST /api/projects/:id/git/test — test GitHub App connection
150
+ router.post('/test', async (req, res) => {
151
+ const { id } = req.params;
152
+ log('GIT', `POST /api/projects/${id}/git/test`);
153
+
154
+ try {
155
+ if (!githubAppService.isConfigured()) {
156
+ res.json({
157
+ configured: false,
158
+ error: 'GitHub App credentials not configured (GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY)',
159
+ });
160
+ return;
161
+ }
162
+
163
+ const installations = await githubAppService.listInstallations();
164
+ res.json({
165
+ configured: true,
166
+ installations: installations.map(i => ({
167
+ id: i.id,
168
+ account: i.account.login,
169
+ accountType: i.account.type,
170
+ })),
171
+ });
172
+ } catch (err: any) {
173
+ log('GIT', `Test GitHub App connection failed: ${err.message}`);
174
+ res.json({
175
+ configured: true,
176
+ error: `Connection test failed: ${err.message}`,
177
+ });
178
+ }
179
+ });
180
+
181
+ // GET /api/projects/:id/git/installations — list GitHub App installations
182
+ router.get('/installations', async (_req, res) => {
183
+ try {
184
+ if (!githubAppService.isConfigured()) {
185
+ res.json({ installations: [], configured: false });
186
+ return;
187
+ }
188
+
189
+ const installations = await githubAppService.listInstallations();
190
+ res.json({
191
+ configured: true,
192
+ installations: installations.map(i => ({
193
+ id: i.id,
194
+ account: i.account.login,
195
+ accountType: i.account.type,
196
+ })),
197
+ });
198
+ } catch (err: any) {
199
+ log('GIT', `List installations failed: ${err.message}`);
200
+ res.status(500).json({ error: `Failed to list installations: ${err.message}` });
201
+ }
202
+ });
203
+
204
+ // GET /api/projects/:id/git/repos?installation_id=xxx — list repos for an installation
205
+ router.get('/repos', async (req, res) => {
206
+ const installationId = req.query.installation_id as string;
207
+
208
+ if (!installationId) {
209
+ res.status(400).json({ error: 'installation_id query parameter is required' });
210
+ return;
211
+ }
212
+
213
+ try {
214
+ const repos = await githubAppService.listInstallationRepos(installationId);
215
+ res.json({
216
+ repos: repos.map(r => ({
217
+ id: r.id,
218
+ fullName: r.full_name,
219
+ private: r.private,
220
+ defaultBranch: r.default_branch,
221
+ cloneUrl: r.clone_url,
222
+ })),
223
+ });
224
+ } catch (err: any) {
225
+ log('GIT', `List repos failed: ${err.message}`);
226
+ res.status(500).json({ error: `Failed to list repos: ${err.message}` });
227
+ }
228
+ });
229
+
230
+ return router;
231
+ };
@@ -10,7 +10,7 @@ import { log, pipeline } from '../services/init.js';
10
10
 
11
11
  const router: Router = Router();
12
12
 
13
- const VALID_COLUMNS = ['todo', 'in_progress', 'in_review', 'qa', 'done'];
13
+ const VALID_COLUMNS = ['backlog', 'todo', 'in_progress', 'in_review', 'qa', 'done'];
14
14
 
15
15
  // GET /api/kanban
16
16
  router.get('/', async (req, res) => {
@@ -19,20 +19,20 @@ router.get('/', async (req, res) => {
19
19
  try {
20
20
  const kanban = await loadKanban(projectId);
21
21
 
22
- // Auto-add missing feature nodes to the TODO column
22
+ // Auto-add missing feature nodes to the Backlog column
23
23
  const graph = await readGraph(projectId);
24
24
  const featureNodes = graph.nodes.filter((n: any) => n.type === 'feature');
25
25
  for (const node of featureNodes) {
26
26
  if (!kanban[node.id]) {
27
27
  const newEntry = {
28
- column: 'todo',
28
+ column: 'backlog',
29
29
  rejection_count: 0,
30
30
  notes: [],
31
31
  moved_at: node.created_at || new Date().toISOString(),
32
32
  };
33
33
  await saveKanbanEntry(node.id, newEntry, projectId);
34
34
  kanban[node.id] = newEntry;
35
- log('KANBAN', `Auto-added ${node.id} to todo`);
35
+ log('KANBAN', `Auto-added ${node.id} to backlog`);
36
36
  }
37
37
  }
38
38
 
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Pipeline API routes — start/status/unblock dev pipeline.
2
+ * Pipeline API routes — start/status/unblock dev pipeline + orchestrator.
3
3
  */
4
4
 
5
5
  import { Router } from 'express';
6
- import { log, pipeline } from '../services/init.js';
6
+ import { log, pipeline, orchestrator } from '../services/init.js';
7
7
 
8
8
  const router: Router = Router();
9
9
 
@@ -26,6 +26,18 @@ router.get('/:id/pipeline', async (req, res) => {
26
26
  res.json(result);
27
27
  });
28
28
 
29
+ // POST /api/kanban/:id/resume
30
+ router.post('/:id/resume', async (req, res) => {
31
+ const featureId = req.params.id;
32
+ try {
33
+ const result = await pipeline.resume(featureId);
34
+ res.status(result.status).json(result.error ? { error: result.error } : { resumed: result.resumed, feature_id: result.feature_id });
35
+ } catch (err: any) {
36
+ log('RESUME', `UNEXPECTED ERROR: ${err.message}`);
37
+ res.status(500).json({ error: err.message });
38
+ }
39
+ });
40
+
29
41
  // POST /api/kanban/:id/unblock
30
42
  router.post('/:id/unblock', async (req, res) => {
31
43
  const featureId = req.params.id;
@@ -38,4 +50,39 @@ router.post('/:id/unblock', async (req, res) => {
38
50
  }
39
51
  });
40
52
 
53
+ // --- Orchestrator (Play All) endpoints ---
54
+
55
+ // POST /api/pipeline/play-all
56
+ router.post('/play-all', async (req, res) => {
57
+ const projectId = req.query.project_id as string | undefined;
58
+ try {
59
+ const result = orchestrator.startPlayAll(projectId);
60
+ const resolved = await result;
61
+ res.status(resolved.status).json(
62
+ resolved.error ? { error: resolved.error } : { started: resolved.started }
63
+ );
64
+ } catch (err: any) {
65
+ log('ORCHESTRATOR', `UNEXPECTED ERROR: ${err.message}`);
66
+ res.status(500).json({ error: err.message });
67
+ }
68
+ });
69
+
70
+ // POST /api/pipeline/stop-all
71
+ router.post('/stop-all', (_req, res) => {
72
+ try {
73
+ const result = orchestrator.stopPlayAll();
74
+ res.status(result.status).json(
75
+ result.error ? { error: result.error } : { stopped: result.stopped }
76
+ );
77
+ } catch (err: any) {
78
+ log('ORCHESTRATOR', `UNEXPECTED ERROR: ${err.message}`);
79
+ res.status(500).json({ error: err.message });
80
+ }
81
+ });
82
+
83
+ // GET /api/pipeline/orchestrator-status
84
+ router.get('/orchestrator-status', (_req, res) => {
85
+ res.json(orchestrator.getStatus());
86
+ });
87
+
41
88
  export default router;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Terminal session routes — REST API for managing named PTY sessions.
3
+ * GET /api/terminal/sessions — list all active sessions
4
+ * POST /api/terminal/sessions — create a new named session
5
+ * DELETE /api/terminal/sessions/:id — kill and remove a session
6
+ *
7
+ * Admin-only: all routes require admin role.
8
+ */
9
+
10
+ import { Router } from 'express';
11
+ import type { Request, Response } from 'express';
12
+ import type { PtySessionManager } from '../services/pty_session_manager.js';
13
+
14
+ interface TerminalRoutesDeps {
15
+ ptyManager: PtySessionManager;
16
+ log: (tag: string, ...args: unknown[]) => void;
17
+ }
18
+
19
+ const requireAdmin = (req: Request, res: Response, next: () => void): void => {
20
+ const user = (req as any).user;
21
+ if (!user || user.role !== 'admin') {
22
+ res.status(403).json({ error: 'Admin privileges required' });
23
+ return;
24
+ }
25
+ next();
26
+ };
27
+
28
+ export const createTerminalRoutes = ({ ptyManager, log }: TerminalRoutesDeps): Router => {
29
+ const router: Router = Router();
30
+
31
+ router.use(requireAdmin as any);
32
+
33
+ // GET /api/terminal/sessions
34
+ router.get('/sessions', (_req, res) => {
35
+ const sessions = ptyManager.listSessions();
36
+ res.json({ sessions });
37
+ });
38
+
39
+ // POST /api/terminal/sessions
40
+ router.post('/sessions', (req, res) => {
41
+ const { projectId, projectName } = req.body;
42
+
43
+ if (!projectId || typeof projectId !== 'string') {
44
+ res.status(400).json({ error: 'projectId is required' });
45
+ return;
46
+ }
47
+ if (!projectName || typeof projectName !== 'string') {
48
+ res.status(400).json({ error: 'projectName is required' });
49
+ return;
50
+ }
51
+
52
+ const session = ptyManager.createSession(projectId.trim(), projectName.trim(), 80, 24);
53
+ log('TERMINAL', `Created session "${session.name}" for project ${projectId}`);
54
+ res.status(201).json({
55
+ session: {
56
+ id: session.id,
57
+ name: session.name,
58
+ projectId: session.projectId,
59
+ projectName: session.projectName,
60
+ state: session.state,
61
+ createdAt: session.createdAt.toISOString(),
62
+ },
63
+ });
64
+ });
65
+
66
+ // DELETE /api/terminal/sessions/:id
67
+ router.delete('/sessions/:id', (req, res) => {
68
+ const { id } = req.params;
69
+ const session = ptyManager.getSession(id);
70
+
71
+ if (!session) {
72
+ res.status(404).json({ error: 'Session not found' });
73
+ return;
74
+ }
75
+
76
+ ptyManager.destroySession(id);
77
+ log('TERMINAL', `Killed session ${id}`);
78
+ res.json({ ok: true });
79
+ });
80
+
81
+ return router;
82
+ };
@@ -16,7 +16,7 @@ import { fileURLToPath } from 'node:url';
16
16
  import { existsSync } from 'node:fs';
17
17
  import { WebSocketServer } from 'ws';
18
18
  import * as pty from 'node-pty';
19
- import { initServices, log } from './services/init.js';
19
+ import { initServices, log, githubAppService, workspaceService } from './services/init.js';
20
20
  import { AuthService } from './services/auth_service.js';
21
21
  import { EmailService } from './services/email_service.js';
22
22
  import { PasswordResetService } from './services/password_reset_service.js';
@@ -29,11 +29,13 @@ import { AuthMiddleware } from './middleware/auth_middleware.js';
29
29
  import { createAuthRoutes } from './routes/auth.js';
30
30
  import { createUserRoutes } from './routes/users.js';
31
31
  import { createProjectRoutes } from './routes/projects.js';
32
+ import { createTerminalRoutes } from './routes/terminal.js';
32
33
  import { getDb } from '@assistkick/shared/lib/db.js';
33
34
  import graphRoutes from './routes/graph.js';
34
35
  import kanbanRoutes from './routes/kanban.js';
35
36
  import pipelineRoutes from './routes/pipeline.js';
36
37
  import coherenceRoutes from './routes/coherence.js';
38
+ import { createGitRoutes } from './routes/git.js';
37
39
 
38
40
  const __dirname = dirname(fileURLToPath(import.meta.url));
39
41
  const DEFAULT_PORT = parseInt(process.env.PORT || '3000', 10);
@@ -68,8 +70,8 @@ app.use((req, res, next) => {
68
70
  const start = Date.now();
69
71
  res.on('finish', () => {
70
72
  const duration = Date.now() - start;
71
- // Skip noisy pipeline polls
72
- if (req.originalUrl.endsWith('/pipeline')) return;
73
+ // Skip noisy pipeline and orchestrator status polls
74
+ if (req.originalUrl.endsWith('/pipeline') || req.originalUrl.endsWith('/orchestrator-status')) return;
73
75
  log('HTTP', `${req.method} ${req.originalUrl} ${res.statusCode} ${duration}ms`);
74
76
  });
75
77
  next();
@@ -101,15 +103,29 @@ const projectService = new ProjectService({ getDb, log });
101
103
  const projectRoutes = createProjectRoutes({ projectService, log });
102
104
  app.use('/api/projects', authMiddleware.requireAuth, projectRoutes);
103
105
 
106
+ // Git repository routes (nested under /api/projects/:id/git)
107
+ const gitRoutes = createGitRoutes({ projectService, githubAppService, workspaceService, log });
108
+ app.use('/api/projects/:id/git', authMiddleware.requireAuth, gitRoutes);
109
+
104
110
  // Ensure default project exists and assign orphan nodes on startup
105
111
  projectService.ensureDefaultAndAssignOrphans().catch((err: any) => {
106
112
  log('STARTUP', `Failed to ensure default project: ${err.message}`);
107
113
  });
108
114
 
115
+ // PTY session manager — initialized early so terminal REST routes can reference it
116
+ // Resolve project root: from packages/backend/src → assistkick-product-system → repo root
117
+ const PROJECT_ROOT = join(__dirname, '..', '..', '..', '..');
118
+ const ptyManager = new PtySessionManager({ spawn: pty.spawn, log, projectRoot: PROJECT_ROOT });
119
+
120
+ // Terminal session management REST routes (admin-only, auth required)
121
+ const terminalRoutes = createTerminalRoutes({ ptyManager, log });
122
+ app.use('/api/terminal', authMiddleware.requireAuth, terminalRoutes);
123
+
109
124
  // Protected API routes — require authentication
110
125
  app.use('/api', authMiddleware.requireAuth, graphRoutes);
111
126
  app.use('/api/kanban', authMiddleware.requireAuth, kanbanRoutes);
112
127
  app.use('/api/kanban', authMiddleware.requireAuth, pipelineRoutes);
128
+ app.use('/api/pipeline', authMiddleware.requireAuth, pipelineRoutes);
113
129
  app.use('/api/coherence', authMiddleware.requireAuth, coherenceRoutes);
114
130
 
115
131
  // Redirect favicon.ico to SVG favicon served from static files
@@ -130,9 +146,6 @@ const server = createServer(app);
130
146
 
131
147
  // Set up WebSocket for terminal
132
148
  const wss = new WebSocketServer({ noServer: true });
133
- // Resolve project root: from packages/backend/src → assistkick-product-system → repo root
134
- const PROJECT_ROOT = join(__dirname, '..', '..', '..', '..');
135
- const ptyManager = new PtySessionManager({ spawn: pty.spawn, log, projectRoot: PROJECT_ROOT });
136
149
  const terminalHandler = new TerminalWsHandler({ wss, authService, ptyManager, log });
137
150
 
138
151
  server.on('upgrade', (req, socket, head) => {