@thxgg/steward 0.1.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/.env.example +7 -0
- package/LICENSE +21 -0
- package/README.md +175 -0
- package/app/app.vue +14 -0
- package/app/assets/css/main.css +129 -0
- package/app/components/CommandPalette.vue +182 -0
- package/app/components/ShortcutsHelp.vue +85 -0
- package/app/components/git/ChangesMinimap.vue +143 -0
- package/app/components/git/CommitList.vue +224 -0
- package/app/components/git/DiffPanel.vue +402 -0
- package/app/components/git/DiffViewer.vue +803 -0
- package/app/components/layout/RepoSelector.vue +358 -0
- package/app/components/layout/Sidebar.vue +91 -0
- package/app/components/prd/Meta.vue +69 -0
- package/app/components/prd/Viewer.vue +285 -0
- package/app/components/tasks/Board.vue +86 -0
- package/app/components/tasks/Card.vue +108 -0
- package/app/components/tasks/Column.vue +108 -0
- package/app/components/tasks/Detail.vue +291 -0
- package/app/components/ui/badge/Badge.vue +26 -0
- package/app/components/ui/badge/index.ts +26 -0
- package/app/components/ui/button/Button.vue +29 -0
- package/app/components/ui/button/index.ts +38 -0
- package/app/components/ui/card/Card.vue +22 -0
- package/app/components/ui/card/CardAction.vue +17 -0
- package/app/components/ui/card/CardContent.vue +17 -0
- package/app/components/ui/card/CardDescription.vue +17 -0
- package/app/components/ui/card/CardFooter.vue +17 -0
- package/app/components/ui/card/CardHeader.vue +17 -0
- package/app/components/ui/card/CardTitle.vue +17 -0
- package/app/components/ui/card/index.ts +7 -0
- package/app/components/ui/combobox/Combobox.vue +19 -0
- package/app/components/ui/combobox/ComboboxAnchor.vue +23 -0
- package/app/components/ui/combobox/ComboboxEmpty.vue +21 -0
- package/app/components/ui/combobox/ComboboxGroup.vue +27 -0
- package/app/components/ui/combobox/ComboboxInput.vue +42 -0
- package/app/components/ui/combobox/ComboboxItem.vue +24 -0
- package/app/components/ui/combobox/ComboboxItemIndicator.vue +23 -0
- package/app/components/ui/combobox/ComboboxList.vue +33 -0
- package/app/components/ui/combobox/ComboboxSeparator.vue +21 -0
- package/app/components/ui/combobox/ComboboxTrigger.vue +24 -0
- package/app/components/ui/combobox/ComboboxViewport.vue +23 -0
- package/app/components/ui/combobox/index.ts +13 -0
- package/app/components/ui/command/Command.vue +103 -0
- package/app/components/ui/command/CommandDialog.vue +33 -0
- package/app/components/ui/command/CommandEmpty.vue +27 -0
- package/app/components/ui/command/CommandGroup.vue +45 -0
- package/app/components/ui/command/CommandInput.vue +54 -0
- package/app/components/ui/command/CommandItem.vue +76 -0
- package/app/components/ui/command/CommandList.vue +25 -0
- package/app/components/ui/command/CommandSeparator.vue +21 -0
- package/app/components/ui/command/CommandShortcut.vue +17 -0
- package/app/components/ui/command/index.ts +25 -0
- package/app/components/ui/dialog/Dialog.vue +19 -0
- package/app/components/ui/dialog/DialogClose.vue +15 -0
- package/app/components/ui/dialog/DialogContent.vue +53 -0
- package/app/components/ui/dialog/DialogDescription.vue +23 -0
- package/app/components/ui/dialog/DialogFooter.vue +15 -0
- package/app/components/ui/dialog/DialogHeader.vue +17 -0
- package/app/components/ui/dialog/DialogOverlay.vue +21 -0
- package/app/components/ui/dialog/DialogScrollContent.vue +59 -0
- package/app/components/ui/dialog/DialogTitle.vue +23 -0
- package/app/components/ui/dialog/DialogTrigger.vue +15 -0
- package/app/components/ui/dialog/index.ts +10 -0
- package/app/components/ui/input/Input.vue +33 -0
- package/app/components/ui/input/index.ts +1 -0
- package/app/components/ui/scroll-area/ScrollArea.vue +33 -0
- package/app/components/ui/scroll-area/ScrollBar.vue +32 -0
- package/app/components/ui/scroll-area/index.ts +2 -0
- package/app/components/ui/separator/Separator.vue +29 -0
- package/app/components/ui/separator/index.ts +1 -0
- package/app/components/ui/sheet/Sheet.vue +19 -0
- package/app/components/ui/sheet/SheetClose.vue +15 -0
- package/app/components/ui/sheet/SheetContent.vue +62 -0
- package/app/components/ui/sheet/SheetDescription.vue +21 -0
- package/app/components/ui/sheet/SheetFooter.vue +16 -0
- package/app/components/ui/sheet/SheetHeader.vue +15 -0
- package/app/components/ui/sheet/SheetOverlay.vue +21 -0
- package/app/components/ui/sheet/SheetTitle.vue +21 -0
- package/app/components/ui/sheet/SheetTrigger.vue +15 -0
- package/app/components/ui/sheet/index.ts +8 -0
- package/app/components/ui/tabs/Tabs.vue +24 -0
- package/app/components/ui/tabs/TabsContent.vue +21 -0
- package/app/components/ui/tabs/TabsList.vue +24 -0
- package/app/components/ui/tabs/TabsTrigger.vue +26 -0
- package/app/components/ui/tabs/index.ts +4 -0
- package/app/components/ui/tooltip/Tooltip.vue +19 -0
- package/app/components/ui/tooltip/TooltipContent.vue +34 -0
- package/app/components/ui/tooltip/TooltipProvider.vue +14 -0
- package/app/components/ui/tooltip/TooltipTrigger.vue +15 -0
- package/app/components/ui/tooltip/index.ts +4 -0
- package/app/composables/useFileWatch.ts +78 -0
- package/app/composables/useGit.ts +180 -0
- package/app/composables/useKeyboard.ts +180 -0
- package/app/composables/usePrd.ts +86 -0
- package/app/composables/useRepos.ts +108 -0
- package/app/composables/useThemeMode.ts +38 -0
- package/app/composables/useToast.ts +31 -0
- package/app/layouts/default.vue +197 -0
- package/app/lib/utils.ts +7 -0
- package/app/pages/[repo]/[prd].vue +263 -0
- package/app/pages/index.vue +257 -0
- package/app/types/git.ts +81 -0
- package/app/types/index.ts +29 -0
- package/app/types/prd.ts +49 -0
- package/app/types/repo.ts +37 -0
- package/app/types/task.ts +134 -0
- package/bin/prd +21 -0
- package/components.json +21 -0
- package/dist/app/types/git.js +1 -0
- package/dist/app/types/prd.js +1 -0
- package/dist/app/types/repo.js +1 -0
- package/dist/app/types/task.js +1 -0
- package/dist/host/src/api/git.js +96 -0
- package/dist/host/src/api/index.js +4 -0
- package/dist/host/src/api/prds.js +195 -0
- package/dist/host/src/api/repos.js +47 -0
- package/dist/host/src/api/state.js +63 -0
- package/dist/host/src/executor.js +109 -0
- package/dist/host/src/index.js +95 -0
- package/dist/host/src/mcp.js +62 -0
- package/dist/host/src/ui.js +64 -0
- package/dist/server/utils/db.js +125 -0
- package/dist/server/utils/git.js +396 -0
- package/dist/server/utils/prd-state.js +229 -0
- package/dist/server/utils/repos.js +256 -0
- package/docs/MCP.md +180 -0
- package/nuxt.config.ts +34 -0
- package/package.json +88 -0
- package/public/favicon.ico +0 -0
- package/public/robots.txt +1 -0
- package/server/api/browse.get.ts +52 -0
- package/server/api/repos/[repoId]/git/commits.get.ts +103 -0
- package/server/api/repos/[repoId]/git/diff.get.ts +77 -0
- package/server/api/repos/[repoId]/git/file-content.get.ts +66 -0
- package/server/api/repos/[repoId]/git/file-diff.get.ts +109 -0
- package/server/api/repos/[repoId]/prd/[prdSlug]/progress.get.ts +36 -0
- package/server/api/repos/[repoId]/prd/[prdSlug]/tasks/[taskId]/commits.get.ts +146 -0
- package/server/api/repos/[repoId]/prd/[prdSlug]/tasks.get.ts +36 -0
- package/server/api/repos/[repoId]/prd/[prdSlug].get.ts +97 -0
- package/server/api/repos/[repoId]/prds.get.ts +85 -0
- package/server/api/repos/[repoId]/refresh-git-repos.post.ts +42 -0
- package/server/api/repos/[repoId].delete.ts +27 -0
- package/server/api/repos/index.get.ts +5 -0
- package/server/api/repos/index.post.ts +39 -0
- package/server/api/watch.get.ts +63 -0
- package/server/plugins/migrate-legacy-state.ts +19 -0
- package/server/tsconfig.json +3 -0
- package/server/utils/db.ts +169 -0
- package/server/utils/git.ts +478 -0
- package/server/utils/prd-state.ts +335 -0
- package/server/utils/repos.ts +322 -0
- package/server/utils/watcher.ts +179 -0
- package/tsconfig.json +4 -0
package/package.json
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thxgg/steward",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local-first PRD workflow steward with codemode MCP and web UI.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "thxgg",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/thxgg/prd-viewer.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/thxgg/prd-viewer/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/thxgg/prd-viewer#readme",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"prd",
|
|
18
|
+
"mcp",
|
|
19
|
+
"workflow",
|
|
20
|
+
"nuxt",
|
|
21
|
+
"sqlite",
|
|
22
|
+
"developer-tools"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=22.6.0",
|
|
26
|
+
"npm": ">=10"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public",
|
|
30
|
+
"provenance": true
|
|
31
|
+
},
|
|
32
|
+
"bin": {
|
|
33
|
+
"prd": "./bin/prd"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"bin/",
|
|
37
|
+
"dist/",
|
|
38
|
+
"app/",
|
|
39
|
+
"server/",
|
|
40
|
+
"public/",
|
|
41
|
+
"docs/MCP.md",
|
|
42
|
+
"nuxt.config.ts",
|
|
43
|
+
"components.json",
|
|
44
|
+
".env.example",
|
|
45
|
+
"README.md",
|
|
46
|
+
"LICENSE",
|
|
47
|
+
"tsconfig.json"
|
|
48
|
+
],
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "npm run build:host && nuxt build",
|
|
51
|
+
"build:host": "tsc -p tsconfig.host.build.json",
|
|
52
|
+
"dev": "nuxt dev",
|
|
53
|
+
"generate": "nuxt generate",
|
|
54
|
+
"preview": "nuxt preview",
|
|
55
|
+
"mcp": "npm run build:host && node ./bin/prd mcp",
|
|
56
|
+
"ui": "npm run build:host && node ./bin/prd ui",
|
|
57
|
+
"typecheck": "nuxt typecheck && npm run typecheck:host",
|
|
58
|
+
"typecheck:host": "tsc -p tsconfig.host.json",
|
|
59
|
+
"prepack": "npm run build:host"
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
63
|
+
"@vueuse/core": "^14.1.0",
|
|
64
|
+
"chokidar": "^5.0.0",
|
|
65
|
+
"class-variance-authority": "^0.7.1",
|
|
66
|
+
"clsx": "^2.1.1",
|
|
67
|
+
"dompurify": "^3.3.1",
|
|
68
|
+
"lucide-vue-next": "^0.563.0",
|
|
69
|
+
"marked": "^17.0.1",
|
|
70
|
+
"nuxt": "^3.21.0",
|
|
71
|
+
"reka-ui": "^2.7.0",
|
|
72
|
+
"shiki": "^3.21.0",
|
|
73
|
+
"tailwind-merge": "^3.4.0",
|
|
74
|
+
"vue": "latest",
|
|
75
|
+
"vue-sonner": "^1.3.2",
|
|
76
|
+
"zod": "^3.25.76"
|
|
77
|
+
},
|
|
78
|
+
"devDependencies": {
|
|
79
|
+
"@nuxtjs/color-mode": "^4.0.0",
|
|
80
|
+
"@tailwindcss/vite": "^4.1.18",
|
|
81
|
+
"@types/dompurify": "^3.2.0",
|
|
82
|
+
"@types/node": "^25.0.10",
|
|
83
|
+
"tailwindcss": "^4.0.0",
|
|
84
|
+
"typescript": "^5.9.2",
|
|
85
|
+
"tw-animate-css": "^1.4.0",
|
|
86
|
+
"vue-tsc": "^3.2.3"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readdir, stat } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const query = getQuery(event)
|
|
7
|
+
let path = (query.path as string) || homedir()
|
|
8
|
+
|
|
9
|
+
// Expand ~ to home directory
|
|
10
|
+
if (path.startsWith('~')) {
|
|
11
|
+
path = path.replace('~', homedir())
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const stats = await stat(path)
|
|
16
|
+
if (!stats.isDirectory()) {
|
|
17
|
+
throw createError({
|
|
18
|
+
statusCode: 400,
|
|
19
|
+
message: 'Path is not a directory'
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const entries = await readdir(path, { withFileTypes: true })
|
|
24
|
+
const directories = entries
|
|
25
|
+
.filter(entry => entry.isDirectory() && !entry.name.startsWith('.'))
|
|
26
|
+
.map(entry => ({
|
|
27
|
+
name: entry.name,
|
|
28
|
+
path: join(path, entry.name)
|
|
29
|
+
}))
|
|
30
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
current: path,
|
|
34
|
+
parent: join(path, '..'),
|
|
35
|
+
directories
|
|
36
|
+
}
|
|
37
|
+
} catch (err: any) {
|
|
38
|
+
if (err.code === 'ENOENT') {
|
|
39
|
+
throw createError({
|
|
40
|
+
statusCode: 404,
|
|
41
|
+
message: 'Directory not found'
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
if (err.code === 'EACCES') {
|
|
45
|
+
throw createError({
|
|
46
|
+
statusCode: 403,
|
|
47
|
+
message: 'Permission denied'
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
throw err
|
|
51
|
+
}
|
|
52
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { getRepos } from '~~/server/utils/repos'
|
|
2
|
+
import { isGitRepo, getCommitInfo } from '~~/server/utils/git'
|
|
3
|
+
import type { GitCommit } from '~/types/git'
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const repoId = getRouterParam(event, 'repoId')
|
|
7
|
+
|
|
8
|
+
if (!repoId) {
|
|
9
|
+
throw createError({
|
|
10
|
+
statusCode: 400,
|
|
11
|
+
statusMessage: 'Repository ID is required',
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const repos = await getRepos()
|
|
16
|
+
const repo = repos.find(r => r.id === repoId)
|
|
17
|
+
|
|
18
|
+
if (!repo) {
|
|
19
|
+
throw createError({
|
|
20
|
+
statusCode: 404,
|
|
21
|
+
statusMessage: 'Repository not found',
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Get query parameters
|
|
26
|
+
const query = getQuery(event)
|
|
27
|
+
const shasParam = query.shas as string | undefined
|
|
28
|
+
const repoPath = query.repo as string | undefined
|
|
29
|
+
|
|
30
|
+
if (!shasParam) {
|
|
31
|
+
throw createError({
|
|
32
|
+
statusCode: 400,
|
|
33
|
+
statusMessage: 'shas query parameter is required',
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const shas = shasParam.split(',').map(s => s.trim()).filter(Boolean)
|
|
38
|
+
|
|
39
|
+
if (shas.length === 0) {
|
|
40
|
+
throw createError({
|
|
41
|
+
statusCode: 400,
|
|
42
|
+
statusMessage: 'At least one SHA is required',
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Resolve the git repo path
|
|
47
|
+
let gitRepoPath = repo.path
|
|
48
|
+
|
|
49
|
+
if (repoPath) {
|
|
50
|
+
// Validate that repoPath is within discovered gitRepos
|
|
51
|
+
if (!repo.gitRepos || repo.gitRepos.length === 0) {
|
|
52
|
+
throw createError({
|
|
53
|
+
statusCode: 400,
|
|
54
|
+
statusMessage: 'repo parameter provided but no git repos discovered in this repository',
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const matchedRepo = repo.gitRepos.find(gr => gr.relativePath === repoPath)
|
|
59
|
+
if (!matchedRepo) {
|
|
60
|
+
throw createError({
|
|
61
|
+
statusCode: 400,
|
|
62
|
+
statusMessage: `repo "${repoPath}" is not a discovered git repo. Available: ${repo.gitRepos.map(gr => gr.relativePath).join(', ')}`,
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
gitRepoPath = matchedRepo.absolutePath
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Check if resolved path is a git repository
|
|
70
|
+
if (!await isGitRepo(gitRepoPath)) {
|
|
71
|
+
throw createError({
|
|
72
|
+
statusCode: 400,
|
|
73
|
+
statusMessage: 'Resolved path is not a git repository',
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Fetch commit info for each SHA
|
|
78
|
+
const commits: GitCommit[] = []
|
|
79
|
+
const errors: string[] = []
|
|
80
|
+
|
|
81
|
+
for (const sha of shas) {
|
|
82
|
+
try {
|
|
83
|
+
const commit = await getCommitInfo(gitRepoPath, sha)
|
|
84
|
+
// Add repoPath to the response
|
|
85
|
+
commits.push({
|
|
86
|
+
...commit,
|
|
87
|
+
repoPath: repoPath || '',
|
|
88
|
+
})
|
|
89
|
+
} catch (error) {
|
|
90
|
+
errors.push(`${sha}: ${(error as Error).message}`)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// If all commits failed, throw error
|
|
95
|
+
if (commits.length === 0 && errors.length > 0) {
|
|
96
|
+
throw createError({
|
|
97
|
+
statusCode: 404,
|
|
98
|
+
statusMessage: `No valid commits found: ${errors.join('; ')}`,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return commits
|
|
103
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { getRepos } from '~~/server/utils/repos'
|
|
3
|
+
import { isGitRepo, getCommitDiff } from '~~/server/utils/git'
|
|
4
|
+
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
const repoId = getRouterParam(event, 'repoId')
|
|
7
|
+
|
|
8
|
+
if (!repoId) {
|
|
9
|
+
throw createError({
|
|
10
|
+
statusCode: 400,
|
|
11
|
+
statusMessage: 'Repository ID is required',
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const repos = await getRepos()
|
|
16
|
+
const repo = repos.find(r => r.id === repoId)
|
|
17
|
+
|
|
18
|
+
if (!repo) {
|
|
19
|
+
throw createError({
|
|
20
|
+
statusCode: 404,
|
|
21
|
+
statusMessage: 'Repository not found',
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Get query parameters
|
|
26
|
+
const query = getQuery(event)
|
|
27
|
+
const commit = query.commit as string | undefined
|
|
28
|
+
const repoPath = query.repo as string | undefined
|
|
29
|
+
|
|
30
|
+
if (!commit) {
|
|
31
|
+
throw createError({
|
|
32
|
+
statusCode: 400,
|
|
33
|
+
statusMessage: 'commit query parameter is required',
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Resolve the git repo path
|
|
38
|
+
let gitRepoPath = repo.path
|
|
39
|
+
|
|
40
|
+
if (repoPath) {
|
|
41
|
+
// Validate that repoPath is within discovered gitRepos
|
|
42
|
+
if (!repo.gitRepos || repo.gitRepos.length === 0) {
|
|
43
|
+
throw createError({
|
|
44
|
+
statusCode: 400,
|
|
45
|
+
statusMessage: 'repo parameter provided but no git repos discovered in this repository',
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const matchedRepo = repo.gitRepos.find(gr => gr.relativePath === repoPath)
|
|
50
|
+
if (!matchedRepo) {
|
|
51
|
+
throw createError({
|
|
52
|
+
statusCode: 400,
|
|
53
|
+
statusMessage: `repo "${repoPath}" is not a discovered git repo. Available: ${repo.gitRepos.map(gr => gr.relativePath).join(', ')}`,
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
gitRepoPath = matchedRepo.absolutePath
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check if resolved path is a git repository
|
|
61
|
+
if (!await isGitRepo(gitRepoPath)) {
|
|
62
|
+
throw createError({
|
|
63
|
+
statusCode: 400,
|
|
64
|
+
statusMessage: 'Resolved path is not a git repository',
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const files = await getCommitDiff(gitRepoPath, commit)
|
|
70
|
+
return files
|
|
71
|
+
} catch (error) {
|
|
72
|
+
throw createError({
|
|
73
|
+
statusCode: 404,
|
|
74
|
+
statusMessage: `Commit not found or invalid: ${(error as Error).message}`,
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { getRepos } from '~~/server/utils/repos'
|
|
2
|
+
import { isGitRepo, getFileContent } from '~~/server/utils/git'
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
const repoId = getRouterParam(event, 'repoId')
|
|
6
|
+
const query = getQuery(event)
|
|
7
|
+
const commit = query.commit as string | undefined
|
|
8
|
+
const file = query.file as string | undefined
|
|
9
|
+
const repoPath = query.repo as string | undefined
|
|
10
|
+
|
|
11
|
+
if (!repoId) {
|
|
12
|
+
throw createError({ statusCode: 400, message: 'Repository ID is required' })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!commit) {
|
|
16
|
+
throw createError({ statusCode: 400, message: 'Commit SHA is required' })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!file) {
|
|
20
|
+
throw createError({ statusCode: 400, message: 'File path is required' })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const repos = await getRepos()
|
|
24
|
+
const repo = repos.find(r => r.id === repoId)
|
|
25
|
+
|
|
26
|
+
if (!repo) {
|
|
27
|
+
throw createError({ statusCode: 404, message: 'Repository not found' })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Resolve the git repo path
|
|
31
|
+
let gitRepoPath = repo.path
|
|
32
|
+
|
|
33
|
+
if (repoPath) {
|
|
34
|
+
// Validate that repoPath is within discovered gitRepos
|
|
35
|
+
if (!repo.gitRepos || repo.gitRepos.length === 0) {
|
|
36
|
+
throw createError({
|
|
37
|
+
statusCode: 400,
|
|
38
|
+
message: 'repo parameter provided but no git repos discovered in this repository',
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const matchedRepo = repo.gitRepos.find(gr => gr.relativePath === repoPath)
|
|
43
|
+
if (!matchedRepo) {
|
|
44
|
+
throw createError({
|
|
45
|
+
statusCode: 400,
|
|
46
|
+
message: `repo "${repoPath}" is not a discovered git repo. Available: ${repo.gitRepos.map(gr => gr.relativePath).join(', ')}`,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
gitRepoPath = matchedRepo.absolutePath
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!await isGitRepo(gitRepoPath)) {
|
|
54
|
+
throw createError({ statusCode: 400, message: 'Not a git repository' })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const content = await getFileContent(gitRepoPath, commit, file)
|
|
59
|
+
return { content }
|
|
60
|
+
} catch (error) {
|
|
61
|
+
throw createError({
|
|
62
|
+
statusCode: 404,
|
|
63
|
+
message: error instanceof Error ? error.message : 'Failed to get file content'
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { getRepos } from '~~/server/utils/repos'
|
|
2
|
+
import { isGitRepo, getFileDiff, validatePathInRepo } from '~~/server/utils/git'
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
const repoId = getRouterParam(event, 'repoId')
|
|
6
|
+
|
|
7
|
+
if (!repoId) {
|
|
8
|
+
throw createError({
|
|
9
|
+
statusCode: 400,
|
|
10
|
+
statusMessage: 'Repository ID is required',
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const repos = await getRepos()
|
|
15
|
+
const repo = repos.find(r => r.id === repoId)
|
|
16
|
+
|
|
17
|
+
if (!repo) {
|
|
18
|
+
throw createError({
|
|
19
|
+
statusCode: 404,
|
|
20
|
+
statusMessage: 'Repository not found',
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Get query parameters
|
|
25
|
+
const query = getQuery(event)
|
|
26
|
+
const commit = query.commit as string | undefined
|
|
27
|
+
const file = query.file as string | undefined
|
|
28
|
+
const repoPath = query.repo as string | undefined
|
|
29
|
+
|
|
30
|
+
if (!commit) {
|
|
31
|
+
throw createError({
|
|
32
|
+
statusCode: 400,
|
|
33
|
+
statusMessage: 'commit query parameter is required',
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!file) {
|
|
38
|
+
throw createError({
|
|
39
|
+
statusCode: 400,
|
|
40
|
+
statusMessage: 'file query parameter is required',
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Resolve the git repo path
|
|
45
|
+
let gitRepoPath = repo.path
|
|
46
|
+
|
|
47
|
+
if (repoPath) {
|
|
48
|
+
// Validate that repoPath is within discovered gitRepos
|
|
49
|
+
if (!repo.gitRepos || repo.gitRepos.length === 0) {
|
|
50
|
+
throw createError({
|
|
51
|
+
statusCode: 400,
|
|
52
|
+
statusMessage: 'repo parameter provided but no git repos discovered in this repository',
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const matchedRepo = repo.gitRepos.find(gr => gr.relativePath === repoPath)
|
|
57
|
+
if (!matchedRepo) {
|
|
58
|
+
throw createError({
|
|
59
|
+
statusCode: 400,
|
|
60
|
+
statusMessage: `repo "${repoPath}" is not a discovered git repo. Available: ${repo.gitRepos.map(gr => gr.relativePath).join(', ')}`,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
gitRepoPath = matchedRepo.absolutePath
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check if resolved path is a git repository
|
|
68
|
+
if (!await isGitRepo(gitRepoPath)) {
|
|
69
|
+
throw createError({
|
|
70
|
+
statusCode: 400,
|
|
71
|
+
statusMessage: 'Resolved path is not a git repository',
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Validate file path is within repo
|
|
76
|
+
if (!validatePathInRepo(gitRepoPath, file)) {
|
|
77
|
+
throw createError({
|
|
78
|
+
statusCode: 400,
|
|
79
|
+
statusMessage: 'Invalid file path: path traversal not allowed',
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const hunks = await getFileDiff(gitRepoPath, commit, file)
|
|
85
|
+
return hunks
|
|
86
|
+
} catch (error) {
|
|
87
|
+
const message = (error as Error).message
|
|
88
|
+
|
|
89
|
+
// Determine appropriate error code
|
|
90
|
+
if (message.includes('Invalid commit SHA')) {
|
|
91
|
+
throw createError({
|
|
92
|
+
statusCode: 400,
|
|
93
|
+
statusMessage: message,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (message.includes('outside repository')) {
|
|
98
|
+
throw createError({
|
|
99
|
+
statusCode: 400,
|
|
100
|
+
statusMessage: message,
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
throw createError({
|
|
105
|
+
statusCode: 404,
|
|
106
|
+
statusMessage: `File not found in commit or commit invalid: ${message}`,
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { getRepos } from '~~/server/utils/repos'
|
|
2
|
+
import { getPrdState, migrateLegacyStateForRepo } from '~~/server/utils/prd-state'
|
|
3
|
+
|
|
4
|
+
export default defineEventHandler(async (event) => {
|
|
5
|
+
const repoId = getRouterParam(event, 'repoId')
|
|
6
|
+
const prdSlug = getRouterParam(event, 'prdSlug')
|
|
7
|
+
|
|
8
|
+
if (!repoId || !prdSlug) {
|
|
9
|
+
throw createError({
|
|
10
|
+
statusCode: 400,
|
|
11
|
+
statusMessage: 'Repository ID and PRD slug are required'
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const repos = await getRepos()
|
|
16
|
+
const repo = repos.find(r => r.id === repoId)
|
|
17
|
+
|
|
18
|
+
if (!repo) {
|
|
19
|
+
throw createError({
|
|
20
|
+
statusCode: 404,
|
|
21
|
+
statusMessage: 'Repository not found'
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
await migrateLegacyStateForRepo(repo)
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const state = await getPrdState(repo.id, prdSlug)
|
|
29
|
+
return state?.progress ?? null
|
|
30
|
+
} catch (error) {
|
|
31
|
+
throw createError({
|
|
32
|
+
statusCode: 500,
|
|
33
|
+
statusMessage: `Failed to read progress state: ${(error as Error).message}`
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
})
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { getRepos, saveRepos, discoverGitRepos } from '~~/server/utils/repos'
|
|
2
|
+
import { getPrdState, migrateLegacyStateForRepo } from '~~/server/utils/prd-state'
|
|
3
|
+
import { resolveCommitRepo } from '~~/server/utils/git'
|
|
4
|
+
import type { ProgressFile, CommitRef } from '~/types/task'
|
|
5
|
+
import type { RepoConfig } from '~/types/repo'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Response format for resolved commits
|
|
9
|
+
*/
|
|
10
|
+
interface ResolvedCommitResponse {
|
|
11
|
+
sha: string
|
|
12
|
+
repo: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default defineEventHandler(async (event) => {
|
|
16
|
+
const repoId = getRouterParam(event, 'repoId')
|
|
17
|
+
const prdSlug = getRouterParam(event, 'prdSlug')
|
|
18
|
+
const taskId = getRouterParam(event, 'taskId')
|
|
19
|
+
|
|
20
|
+
if (!repoId || !prdSlug || !taskId) {
|
|
21
|
+
throw createError({
|
|
22
|
+
statusCode: 400,
|
|
23
|
+
statusMessage: 'Repository ID, PRD slug, and task ID are required'
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const repos = await getRepos()
|
|
28
|
+
const repo = repos.find(r => r.id === repoId)
|
|
29
|
+
|
|
30
|
+
if (!repo) {
|
|
31
|
+
throw createError({
|
|
32
|
+
statusCode: 404,
|
|
33
|
+
statusMessage: 'Repository not found'
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await migrateLegacyStateForRepo(repo)
|
|
38
|
+
|
|
39
|
+
let progress: ProgressFile | null = null
|
|
40
|
+
try {
|
|
41
|
+
const state = await getPrdState(repo.id, prdSlug)
|
|
42
|
+
progress = state?.progress ?? null
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw createError({
|
|
45
|
+
statusCode: 500,
|
|
46
|
+
statusMessage: `Failed to read progress state: ${(error as Error).message}`
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!progress) {
|
|
51
|
+
return []
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Find task log entry matching taskId
|
|
55
|
+
const taskLog = progress.taskLogs.find(log => log.taskId === taskId)
|
|
56
|
+
|
|
57
|
+
if (!taskLog) {
|
|
58
|
+
throw createError({
|
|
59
|
+
statusCode: 404,
|
|
60
|
+
statusMessage: `Task "${taskId}" not found in progress state`
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// No commits recorded
|
|
65
|
+
if (!taskLog.commits || taskLog.commits.length === 0) {
|
|
66
|
+
return []
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Resolve all commits to normalized format with repo context
|
|
70
|
+
const resolvedCommits: ResolvedCommitResponse[] = []
|
|
71
|
+
const failedEntries: (string | CommitRef)[] = []
|
|
72
|
+
|
|
73
|
+
// First pass: try to resolve commits with current repo config
|
|
74
|
+
for (const commitEntry of taskLog.commits) {
|
|
75
|
+
try {
|
|
76
|
+
const resolved = await resolveCommitRepo(repo, commitEntry)
|
|
77
|
+
resolvedCommits.push({
|
|
78
|
+
sha: resolved.sha,
|
|
79
|
+
repo: resolved.repoPath,
|
|
80
|
+
})
|
|
81
|
+
} catch {
|
|
82
|
+
failedEntries.push(commitEntry)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// If some commits failed and this is a pseudo-monorepo, try re-discovering git repos
|
|
87
|
+
if (failedEntries.length > 0) {
|
|
88
|
+
const newGitRepos = await discoverGitRepos(repo.path)
|
|
89
|
+
|
|
90
|
+
// Check if we discovered any new repos
|
|
91
|
+
const existingPaths = new Set((repo.gitRepos || []).map(gr => gr.relativePath))
|
|
92
|
+
const hasNewRepos = newGitRepos.some(gr => !existingPaths.has(gr.relativePath))
|
|
93
|
+
|
|
94
|
+
if (hasNewRepos) {
|
|
95
|
+
// Update repo config with newly discovered repos
|
|
96
|
+
const repos = await getRepos()
|
|
97
|
+
const repoIndex = repos.findIndex(r => r.id === repoId)
|
|
98
|
+
if (repoIndex !== -1) {
|
|
99
|
+
const updatedRepo: RepoConfig = {
|
|
100
|
+
...repos[repoIndex]!,
|
|
101
|
+
gitRepos: newGitRepos.length > 0 ? newGitRepos : undefined,
|
|
102
|
+
}
|
|
103
|
+
repos[repoIndex] = updatedRepo
|
|
104
|
+
await saveRepos(repos)
|
|
105
|
+
|
|
106
|
+
// Retry failed commits with updated config
|
|
107
|
+
for (const commitEntry of failedEntries) {
|
|
108
|
+
try {
|
|
109
|
+
const resolved = await resolveCommitRepo(updatedRepo, commitEntry)
|
|
110
|
+
resolvedCommits.push({
|
|
111
|
+
sha: resolved.sha,
|
|
112
|
+
repo: resolved.repoPath,
|
|
113
|
+
})
|
|
114
|
+
} catch {
|
|
115
|
+
// Still failed after re-discovery, add with empty repo
|
|
116
|
+
const sha = typeof commitEntry === 'string' ? commitEntry : commitEntry.sha
|
|
117
|
+
resolvedCommits.push({
|
|
118
|
+
sha,
|
|
119
|
+
repo: '',
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
// Repo not found in list, add failed entries with empty repo
|
|
125
|
+
for (const commitEntry of failedEntries) {
|
|
126
|
+
const sha = typeof commitEntry === 'string' ? commitEntry : commitEntry.sha
|
|
127
|
+
resolvedCommits.push({
|
|
128
|
+
sha,
|
|
129
|
+
repo: '',
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
// No new repos discovered, add failed entries with empty repo
|
|
135
|
+
for (const commitEntry of failedEntries) {
|
|
136
|
+
const sha = typeof commitEntry === 'string' ? commitEntry : commitEntry.sha
|
|
137
|
+
resolvedCommits.push({
|
|
138
|
+
sha,
|
|
139
|
+
repo: '',
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return resolvedCommits
|
|
146
|
+
})
|