@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.
- package/package.json +2 -1
- package/templates/assistkick-product-system/GITHUB_APP_SETUP.md +88 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +231 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/kanban.ts +4 -4
- package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +49 -2
- package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +82 -0
- package/templates/assistkick-product-system/packages/backend/src/server.ts +19 -6
- package/templates/assistkick-product-system/packages/backend/src/services/github_app_service.ts +146 -0
- package/templates/assistkick-product-system/packages/backend/src/services/init.ts +69 -2
- package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +71 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +87 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +194 -0
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +88 -17
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +114 -39
- package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +28 -14
- package/templates/assistkick-product-system/packages/frontend/src/App.tsx +1 -1
- package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +151 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/GitRepoModal.tsx +352 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +208 -95
- package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +17 -1
- package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +238 -105
- package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +15 -13
- package/templates/assistkick-product-system/packages/frontend/src/constants/graph.ts +1 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useProjects.ts +4 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/dashboard.tsx +22 -4
- package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +486 -38
- package/templates/assistkick-product-system/packages/shared/db/migrations/0001_vengeful_wallop.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0002_greedy_excalibur.sql +4 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0003_lonely_cyclops.sql +17 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0001_snapshot.json +826 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0002_snapshot.json +854 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0003_snapshot.json +862 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +21 -0
- package/templates/assistkick-product-system/packages/shared/db/schema.ts +10 -3
- package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +54 -1
- package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +25 -0
- package/templates/assistkick-product-system/packages/shared/lib/pipeline-state-store.ts +4 -0
- package/templates/assistkick-product-system/packages/shared/lib/pipeline.ts +329 -89
- package/templates/assistkick-product-system/packages/shared/lib/pipeline_orchestrator.ts +186 -0
- package/templates/assistkick-product-system/packages/shared/lib/session.ts +10 -6
- package/templates/assistkick-product-system/packages/shared/tools/db_explorer.ts +275 -0
- package/templates/assistkick-product-system/packages/shared/tools/end_session.ts +2 -2
- package/templates/assistkick-product-system/packages/shared/tools/get_kanban.ts +2 -1
- package/templates/assistkick-product-system/packages/shared/tools/move_card.ts +3 -2
- package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -2
- package/templates/assistkick-product-system/tests/kanban.test.ts +1 -1
- package/templates/assistkick-product-system/tests/pipeline_stats_all_cards.test.ts +1 -1
- package/templates/assistkick-product-system/tests/web_terminal.test.ts +189 -150
- package/templates/skills/assistkick-bootstrap/SKILL.md +33 -25
- package/templates/skills/assistkick-code-reviewer/SKILL.md +23 -15
- package/templates/skills/assistkick-db-explorer/SKILL.md +86 -0
- package/templates/skills/assistkick-debugger/SKILL.md +30 -22
- package/templates/skills/assistkick-developer/SKILL.md +37 -29
- 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.
|
|
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
|
|
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: '
|
|
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
|
|
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) => {
|