@thxgg/steward 0.1.16 → 0.1.18
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/.output/nitro.json +1 -1
- package/.output/public/_nuxt/{CxHOXVf6.js → -z_Gr0GN.js} +1 -1
- package/.output/public/_nuxt/5LlyHjkF.js +60 -0
- package/.output/public/_nuxt/BA0u_CRT.js +1 -0
- package/.output/public/_nuxt/{Bibm_IDv.js → BA4e9-N5.js} +2 -2
- package/.output/public/_nuxt/{BSA0RJ-H.js → BO8EM227.js} +1 -1
- package/.output/public/_nuxt/C0XT5P3Q.js +1 -0
- package/.output/public/_nuxt/{BWVTacYj.js → CGzrvVc6.js} +1 -1
- package/.output/public/_nuxt/{Dum5qplW.js → CJlXUkTg.js} +1 -1
- package/.output/public/_nuxt/CZsXZugv.js +1 -0
- package/.output/public/_nuxt/{ynmyrfyT.js → C_HVaH3B.js} +1 -1
- package/.output/public/_nuxt/{wbjFvimm.js → DAnnHVQP.js} +1 -1
- package/.output/public/_nuxt/DEr8q68O.js +141 -0
- package/.output/public/_nuxt/Detail.DSyVQNdr.css +1 -0
- package/.output/public/_nuxt/DrXxYwWw.js +30 -0
- package/.output/public/_nuxt/QAzsKGuP.js +1 -0
- package/.output/public/_nuxt/TSsR_oCL.js +1 -0
- package/.output/public/_nuxt/WUF6Thhn.js +13 -0
- package/.output/public/_nuxt/_prd_.BkpxMFSV.css +1 -0
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/19e0e040-a531-4c25-b46d-a6ca54a1ae3e.json +1 -0
- package/.output/public/_nuxt/i9wn3hS7.js +1 -0
- package/.output/server/chunks/_/git-api.mjs +101 -8
- package/.output/server/chunks/_/git-api.mjs.map +1 -1
- package/.output/server/chunks/_/git.mjs +3 -10
- package/.output/server/chunks/_/git.mjs.map +1 -1
- package/.output/server/chunks/_/prd-service.mjs +366 -0
- package/.output/server/chunks/_/prd-service.mjs.map +1 -0
- package/.output/server/chunks/_/repos.mjs +448 -0
- package/.output/server/chunks/_/repos.mjs.map +1 -0
- package/.output/server/chunks/_/task-graph.mjs +13 -14
- package/.output/server/chunks/_/task-graph.mjs.map +1 -1
- package/.output/server/chunks/_/watcher.mjs +54 -68
- package/.output/server/chunks/_/watcher.mjs.map +1 -1
- package/.output/server/chunks/build/{Detail-CUfU85GY.mjs → Detail-BQSkP9Zm.mjs} +170 -74
- package/.output/server/chunks/build/Detail-BQSkP9Zm.mjs.map +1 -0
- package/.output/server/chunks/build/DiffViewer-styles-1.mjs-BFsE2PCW.mjs +4 -0
- package/.output/server/chunks/build/DiffViewer-styles-1.mjs-BFsE2PCW.mjs.map +1 -0
- package/.output/server/chunks/build/DiffViewer-styles.D2bqX3nK.mjs +8 -0
- package/.output/server/chunks/build/DiffViewer-styles.D2bqX3nK.mjs.map +1 -0
- package/.output/server/chunks/build/DiffViewer-styles.FoV36wuV.mjs +10 -0
- package/.output/server/chunks/build/DiffViewer-styles.FoV36wuV.mjs.map +1 -0
- package/.output/server/chunks/build/Viewer-styles.D6wYWFb1.mjs +8 -0
- package/.output/server/chunks/build/Viewer-styles.D6wYWFb1.mjs.map +1 -0
- package/.output/server/chunks/build/{_prd_-CeVnQzOV.mjs → _prd_-CBR_wm9i.mjs} +73 -33
- package/.output/server/chunks/build/_prd_-CBR_wm9i.mjs.map +1 -0
- package/.output/server/chunks/build/client.precomputed.mjs +1 -1
- package/.output/server/chunks/build/{default-DWCOHHTE.mjs → default-Cao5eO80.mjs} +2 -3
- package/.output/server/chunks/build/default-Cao5eO80.mjs.map +1 -0
- package/.output/server/chunks/build/error-404-Bf6kdO80.mjs +0 -1
- package/.output/server/chunks/build/error-500-D_bcARXN.mjs +0 -1
- package/.output/server/chunks/build/{index-CckL_NBD.mjs → index-ByZO4Bvq.mjs} +2 -2
- package/.output/server/chunks/build/index-ByZO4Bvq.mjs.map +1 -0
- package/.output/server/chunks/build/{index-QVeSHT3L.mjs → index-ljj9uTXI.mjs} +6 -5
- package/.output/server/chunks/build/index-ljj9uTXI.mjs.map +1 -0
- package/.output/server/chunks/build/nuxt-link-SvT1nf8Z.mjs +1 -1
- package/.output/server/chunks/build/{repo-graph-CHNl58mY.mjs → repo-graph-CVnkmn8i.mjs} +23 -10
- package/.output/server/chunks/build/repo-graph-CVnkmn8i.mjs.map +1 -0
- package/.output/server/chunks/build/server.mjs +5 -6
- package/.output/server/chunks/build/styles.mjs +4 -4
- package/.output/server/chunks/build/{usePrd-SqcxGyFU.mjs → usePrd-f7ylhIqs.mjs} +10 -34
- package/.output/server/chunks/build/usePrd-f7ylhIqs.mjs.map +1 -0
- package/.output/server/chunks/nitro/nitro.mjs +614 -1211
- package/.output/server/chunks/nitro/nitro.mjs.map +1 -1
- package/.output/server/chunks/routes/api/browse.get.mjs +34 -10
- package/.output/server/chunks/routes/api/browse.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/index.get.mjs +3 -2
- package/.output/server/chunks/routes/api/index.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/index.post.mjs +22 -7
- package/.output/server/chunks/routes/api/index.post.mjs.map +1 -1
- package/.output/server/chunks/routes/api/repos/_repoId/git/commits.get.mjs +29 -23
- package/.output/server/chunks/routes/api/repos/_repoId/git/commits.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/repos/_repoId/git/diff.get.mjs +12 -7
- package/.output/server/chunks/routes/api/repos/_repoId/git/diff.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/repos/_repoId/git/file-content.get.mjs +32 -13
- package/.output/server/chunks/routes/api/repos/_repoId/git/file-content.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/repos/_repoId/git/file-diff.get.mjs +14 -14
- package/.output/server/chunks/routes/api/repos/_repoId/git/file-diff.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/repos/_repoId/graph.get.mjs +7 -2
- package/.output/server/chunks/routes/api/repos/_repoId/graph.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/graph.get.mjs +16 -2
- package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/graph.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/progress.get.mjs +21 -9
- package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/progress.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/tasks/_taskId/commits.get.mjs +21 -85
- package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/tasks/_taskId/commits.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/tasks.get.mjs +21 -9
- package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug/tasks.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug_.get.mjs +31 -50
- package/.output/server/chunks/routes/api/repos/_repoId/prd/_prdSlug_.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/repos/_repoId/prds.get.mjs +20 -49
- package/.output/server/chunks/routes/api/repos/_repoId/prds.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/repos/_repoId/refresh-git-repos.post.mjs +6 -13
- package/.output/server/chunks/routes/api/repos/_repoId/refresh-git-repos.post.mjs.map +1 -1
- package/.output/server/chunks/routes/api/repos/_repoId_.delete.mjs +3 -2
- package/.output/server/chunks/routes/api/repos/_repoId_.delete.mjs.map +1 -1
- package/.output/server/chunks/routes/api/runtime.get.mjs +1 -2
- package/.output/server/chunks/routes/api/runtime.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/watch.get.mjs +5 -4
- package/.output/server/chunks/routes/api/watch.get.mjs.map +1 -1
- package/.output/server/chunks/routes/renderer.mjs +1 -1
- package/.output/server/index.mjs +1 -2
- package/.output/server/index.mjs.map +1 -1
- package/.output/server/node_modules/zod/index.js +4 -0
- package/.output/server/node_modules/zod/package.json +118 -0
- package/.output/server/node_modules/zod/v3/ZodError.js +133 -0
- package/.output/server/node_modules/zod/v3/errors.js +9 -0
- package/.output/server/node_modules/zod/v3/external.js +6 -0
- package/.output/server/node_modules/zod/v3/helpers/errorUtil.js +6 -0
- package/.output/server/node_modules/zod/v3/helpers/parseUtil.js +109 -0
- package/.output/server/node_modules/zod/v3/helpers/typeAliases.js +1 -0
- package/.output/server/node_modules/zod/v3/helpers/util.js +133 -0
- package/.output/server/node_modules/zod/v3/locales/en.js +109 -0
- package/.output/server/node_modules/zod/v3/types.js +3693 -0
- package/.output/server/package.json +2 -1
- package/README.md +2 -2
- package/dist/host/src/api/prds.js +6 -172
- package/dist/host/src/api/repos.js +3 -18
- package/dist/host/src/api/state.js +8 -9
- package/dist/host/src/executor-runner.js +368 -0
- package/dist/host/src/executor.js +138 -260
- package/dist/host/src/mcp.js +27 -1
- package/dist/host/src/ui.js +14 -4
- package/dist/server/utils/change-events.js +33 -0
- package/dist/server/utils/git.js +11 -16
- package/dist/server/utils/prd-service.js +231 -0
- package/dist/server/utils/prd-state.js +54 -162
- package/dist/server/utils/repos.js +72 -17
- package/dist/server/utils/state-schema.js +61 -0
- package/dist/server/utils/task-graph.js +13 -13
- package/package.json +2 -1
- package/.output/public/_nuxt/CVJh28bx.js +0 -1
- package/.output/public/_nuxt/CyZuidLG.js +0 -60
- package/.output/public/_nuxt/D0op9E2g.js +0 -1
- package/.output/public/_nuxt/DX8awZaa.js +0 -1
- package/.output/public/_nuxt/Detail.z33AHKev.css +0 -1
- package/.output/public/_nuxt/DiTJUZOC.js +0 -1
- package/.output/public/_nuxt/T_3JE9C-.js +0 -1
- package/.output/public/_nuxt/WOI2tLsR.js +0 -42
- package/.output/public/_nuxt/_prd_.KTotLoF_.css +0 -1
- package/.output/public/_nuxt/builds/meta/029070b0-b8e2-4988-84f4-d0c9ff55c998.json +0 -1
- package/.output/public/_nuxt/odRGDGwj.js +0 -1
- package/.output/server/chunks/build/Detail-CUfU85GY.mjs.map +0 -1
- package/.output/server/chunks/build/DiffViewer-styles-1.mjs-CS8FTppg.mjs +0 -4
- package/.output/server/chunks/build/DiffViewer-styles-1.mjs-CS8FTppg.mjs.map +0 -1
- package/.output/server/chunks/build/DiffViewer-styles.AUfwwelI.mjs +0 -10
- package/.output/server/chunks/build/DiffViewer-styles.AUfwwelI.mjs.map +0 -1
- package/.output/server/chunks/build/DiffViewer-styles.D_it8zfk.mjs +0 -8
- package/.output/server/chunks/build/DiffViewer-styles.D_it8zfk.mjs.map +0 -1
- package/.output/server/chunks/build/Viewer-styles.CshnetGw.mjs +0 -8
- package/.output/server/chunks/build/Viewer-styles.CshnetGw.mjs.map +0 -1
- package/.output/server/chunks/build/_prd_-CeVnQzOV.mjs.map +0 -1
- package/.output/server/chunks/build/default-DWCOHHTE.mjs.map +0 -1
- package/.output/server/chunks/build/index-CckL_NBD.mjs.map +0 -1
- package/.output/server/chunks/build/index-QVeSHT3L.mjs.map +0 -1
- package/.output/server/chunks/build/repo-graph-CHNl58mY.mjs.map +0 -1
- package/.output/server/chunks/build/usePrd-SqcxGyFU.mjs.map +0 -1
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { basename, isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
|
+
import { resolveCommitRepo } from './git.js';
|
|
4
|
+
import { getPrdState, getPrdStateSummaries } from './prd-state.js';
|
|
5
|
+
import { discoverGitRepos, updateRepoGitRepos } from './repos.js';
|
|
6
|
+
const PRD_SLUG_PATTERN = /^[A-Za-z0-9][A-Za-z0-9-]*$/;
|
|
7
|
+
function normalizePathSlashes(path) {
|
|
8
|
+
return path.replaceAll('\\', '/');
|
|
9
|
+
}
|
|
10
|
+
function normalizeGitRepos(gitRepos) {
|
|
11
|
+
if (!gitRepos || gitRepos.length === 0) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
return gitRepos.map((gitRepo) => ({
|
|
15
|
+
...gitRepo,
|
|
16
|
+
relativePath: normalizePathSlashes(gitRepo.relativePath)
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
function hasPathTraversal(basePath, candidatePath) {
|
|
20
|
+
const relativePath = relative(resolve(basePath), resolve(candidatePath));
|
|
21
|
+
return relativePath.startsWith('..') || isAbsolute(relativePath);
|
|
22
|
+
}
|
|
23
|
+
function extractCommitSha(entry) {
|
|
24
|
+
return typeof entry === 'string' ? entry : entry.sha;
|
|
25
|
+
}
|
|
26
|
+
function parseMetadata(content) {
|
|
27
|
+
const metadata = {};
|
|
28
|
+
const authorMatch = content.match(/\*{0,2}Author\*{0,2}:\*{0,2}\s*(.+?)(?:\n|$)/i);
|
|
29
|
+
if (authorMatch?.[1]) {
|
|
30
|
+
metadata.author = authorMatch[1].trim();
|
|
31
|
+
}
|
|
32
|
+
const dateMatch = content.match(/\*{0,2}Date\*{0,2}:\*{0,2}\s*(.+?)(?:\n|$)/i);
|
|
33
|
+
if (dateMatch?.[1]) {
|
|
34
|
+
metadata.date = dateMatch[1].trim();
|
|
35
|
+
}
|
|
36
|
+
const statusMatch = content.match(/\*{0,2}Status\*{0,2}:\*{0,2}\s*(.+?)(?:\n|$)/i);
|
|
37
|
+
if (statusMatch?.[1]) {
|
|
38
|
+
metadata.status = statusMatch[1].trim();
|
|
39
|
+
}
|
|
40
|
+
const shortcutLinkMatch = content.match(/\[([Ss][Cc]-\d+)\]\(([^)]+)\)/);
|
|
41
|
+
if (shortcutLinkMatch?.[1] && shortcutLinkMatch[2]) {
|
|
42
|
+
metadata.shortcutStory = shortcutLinkMatch[1];
|
|
43
|
+
metadata.shortcutUrl = shortcutLinkMatch[2];
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const shortcutIdMatch = content.match(/\*{0,2}Shortcut(?:\s+Story)?\*{0,2}:\*{0,2}\s*([Ss][Cc]-\d+)/i);
|
|
47
|
+
if (shortcutIdMatch?.[1]) {
|
|
48
|
+
metadata.shortcutStory = shortcutIdMatch[1];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return metadata;
|
|
52
|
+
}
|
|
53
|
+
export function isValidPrdSlug(prdSlug) {
|
|
54
|
+
return PRD_SLUG_PATTERN.test(prdSlug);
|
|
55
|
+
}
|
|
56
|
+
export function assertValidPrdSlug(prdSlug) {
|
|
57
|
+
if (!isValidPrdSlug(prdSlug)) {
|
|
58
|
+
throw new Error('Invalid PRD slug format');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function resolvePrdMarkdownPath(repoPath, prdSlug) {
|
|
62
|
+
assertValidPrdSlug(prdSlug);
|
|
63
|
+
const prdDir = resolve(repoPath, 'docs', 'prd');
|
|
64
|
+
const prdPath = resolve(prdDir, `${prdSlug}.md`);
|
|
65
|
+
if (hasPathTraversal(prdDir, prdPath)) {
|
|
66
|
+
throw new Error('Invalid PRD slug path traversal');
|
|
67
|
+
}
|
|
68
|
+
return prdPath;
|
|
69
|
+
}
|
|
70
|
+
export function extractPrdTitle(content, fallbackSlug) {
|
|
71
|
+
const h1Match = content.match(/^#\s+(.+)$/m);
|
|
72
|
+
if (h1Match?.[1]) {
|
|
73
|
+
return h1Match[1].trim();
|
|
74
|
+
}
|
|
75
|
+
return fallbackSlug;
|
|
76
|
+
}
|
|
77
|
+
export async function readPrdDocument(repo, prdSlug) {
|
|
78
|
+
const prdPath = resolvePrdMarkdownPath(repo.path, prdSlug);
|
|
79
|
+
let content;
|
|
80
|
+
try {
|
|
81
|
+
content = await fs.readFile(prdPath, 'utf-8');
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
throw new Error('PRD not found');
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
slug: prdSlug,
|
|
88
|
+
name: extractPrdTitle(content, prdSlug),
|
|
89
|
+
content,
|
|
90
|
+
metadata: parseMetadata(content)
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export async function listPrdDocuments(repo) {
|
|
94
|
+
const prdDir = join(repo.path, 'docs', 'prd');
|
|
95
|
+
let prdFiles = [];
|
|
96
|
+
try {
|
|
97
|
+
const files = await fs.readdir(prdDir);
|
|
98
|
+
prdFiles = files.filter((file) => {
|
|
99
|
+
if (!file.endsWith('.md')) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
const slug = basename(file, '.md');
|
|
103
|
+
return isValidPrdSlug(slug);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
const stateSummaries = await getPrdStateSummaries(repo.id);
|
|
110
|
+
const items = await Promise.all(prdFiles.map(async (filename) => {
|
|
111
|
+
const slug = basename(filename, '.md');
|
|
112
|
+
const filePath = join(prdDir, filename);
|
|
113
|
+
let name = slug;
|
|
114
|
+
let modifiedAt = 0;
|
|
115
|
+
try {
|
|
116
|
+
const [content, stat] = await Promise.all([
|
|
117
|
+
fs.readFile(filePath, 'utf-8'),
|
|
118
|
+
fs.stat(filePath)
|
|
119
|
+
]);
|
|
120
|
+
modifiedAt = stat.mtime.getTime();
|
|
121
|
+
name = extractPrdTitle(content, slug);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Keep fallback values when file cannot be read.
|
|
125
|
+
}
|
|
126
|
+
const stateSummary = stateSummaries.get(slug);
|
|
127
|
+
return {
|
|
128
|
+
slug,
|
|
129
|
+
name,
|
|
130
|
+
source: `docs/prd/${filename}`,
|
|
131
|
+
hasState: !!stateSummary?.hasState,
|
|
132
|
+
modifiedAt,
|
|
133
|
+
...(stateSummary?.taskCount !== undefined && { taskCount: stateSummary.taskCount }),
|
|
134
|
+
...(stateSummary?.completedCount !== undefined && { completedCount: stateSummary.completedCount })
|
|
135
|
+
};
|
|
136
|
+
}));
|
|
137
|
+
items.sort((a, b) => b.modifiedAt - a.modifiedAt);
|
|
138
|
+
return items;
|
|
139
|
+
}
|
|
140
|
+
export async function readPrdTasks(repo, prdSlug) {
|
|
141
|
+
assertValidPrdSlug(prdSlug);
|
|
142
|
+
const state = await getPrdState(repo.id, prdSlug);
|
|
143
|
+
return state?.tasks ?? null;
|
|
144
|
+
}
|
|
145
|
+
export async function readPrdProgress(repo, prdSlug) {
|
|
146
|
+
assertValidPrdSlug(prdSlug);
|
|
147
|
+
const state = await getPrdState(repo.id, prdSlug);
|
|
148
|
+
return state?.progress ?? null;
|
|
149
|
+
}
|
|
150
|
+
function hasDiscoveredRepoChanges(repo, discoveredRepos) {
|
|
151
|
+
const normalizedExisting = normalizeGitRepos(repo.gitRepos) || [];
|
|
152
|
+
const normalizedDiscovered = normalizeGitRepos(discoveredRepos) || [];
|
|
153
|
+
if (normalizedExisting.length !== normalizedDiscovered.length) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
const existingPaths = new Set(normalizedExisting.map((gitRepo) => gitRepo.relativePath));
|
|
157
|
+
for (const gitRepo of normalizedDiscovered) {
|
|
158
|
+
if (!existingPaths.has(gitRepo.relativePath)) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
async function refreshDiscoveredGitRepos(repo) {
|
|
165
|
+
const discoveredRepos = normalizeGitRepos(await discoverGitRepos(repo.path));
|
|
166
|
+
if (!hasDiscoveredRepoChanges(repo, discoveredRepos)) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
const updated = await updateRepoGitRepos(repo.id, discoveredRepos);
|
|
170
|
+
if (!updated) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
...repo,
|
|
175
|
+
gitRepos: discoveredRepos
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
export async function resolveTaskCommits(repo, prdSlug, taskId) {
|
|
179
|
+
assertValidPrdSlug(prdSlug);
|
|
180
|
+
const state = await getPrdState(repo.id, prdSlug);
|
|
181
|
+
const progress = state?.progress ?? null;
|
|
182
|
+
if (!progress) {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
const taskLogs = Array.isArray(progress.taskLogs) ? progress.taskLogs : [];
|
|
186
|
+
const taskLog = taskLogs.find((log) => log.taskId === taskId);
|
|
187
|
+
if (!taskLog || !taskLog.commits || taskLog.commits.length === 0) {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
const resolvedCommits = [];
|
|
191
|
+
const failedEntries = [];
|
|
192
|
+
for (const commitEntry of taskLog.commits) {
|
|
193
|
+
try {
|
|
194
|
+
const resolved = await resolveCommitRepo(repo, commitEntry);
|
|
195
|
+
resolvedCommits.push({
|
|
196
|
+
sha: resolved.sha,
|
|
197
|
+
repo: resolved.repoPath
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
failedEntries.push(commitEntry);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (failedEntries.length === 0) {
|
|
205
|
+
return resolvedCommits;
|
|
206
|
+
}
|
|
207
|
+
const refreshedRepo = await refreshDiscoveredGitRepos(repo);
|
|
208
|
+
for (const commitEntry of failedEntries) {
|
|
209
|
+
if (!refreshedRepo) {
|
|
210
|
+
resolvedCommits.push({
|
|
211
|
+
sha: extractCommitSha(commitEntry),
|
|
212
|
+
repo: ''
|
|
213
|
+
});
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
const resolved = await resolveCommitRepo(refreshedRepo, commitEntry);
|
|
218
|
+
resolvedCommits.push({
|
|
219
|
+
sha: resolved.sha,
|
|
220
|
+
repo: resolved.repoPath
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
resolvedCommits.push({
|
|
225
|
+
sha: extractCommitSha(commitEntry),
|
|
226
|
+
repo: ''
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return resolvedCommits;
|
|
231
|
+
}
|
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from 'node:path';
|
|
1
|
+
import { emitChange } from './change-events.js';
|
|
3
2
|
import { dbAll, dbGet, dbRun } from './db.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const cleanupCompletedRepoIds = new Set();
|
|
7
|
-
function parseStoredJson(raw, fieldName) {
|
|
3
|
+
import { parseProgressFile, parseTasksFile } from './state-schema.js';
|
|
4
|
+
function parseStoredJson(raw, fieldName, parseValue) {
|
|
8
5
|
if (!raw) {
|
|
9
6
|
return null;
|
|
10
7
|
}
|
|
11
8
|
try {
|
|
12
|
-
|
|
9
|
+
const parsed = JSON.parse(raw);
|
|
10
|
+
return parseValue(parsed);
|
|
13
11
|
}
|
|
14
12
|
catch (error) {
|
|
15
13
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -24,25 +22,6 @@ function getTaskCounts(tasksFile) {
|
|
|
24
22
|
const completedCount = tasksFile.tasks.filter(task => task.status === 'completed').length;
|
|
25
23
|
return { taskCount, completedCount };
|
|
26
24
|
}
|
|
27
|
-
function normalizeLegacyTasksFile(tasksFile) {
|
|
28
|
-
if (!tasksFile || !Array.isArray(tasksFile.tasks)) {
|
|
29
|
-
return tasksFile;
|
|
30
|
-
}
|
|
31
|
-
const tasks = tasksFile.tasks.map((task) => {
|
|
32
|
-
const passes = task.passes;
|
|
33
|
-
if (Array.isArray(passes)) {
|
|
34
|
-
return task;
|
|
35
|
-
}
|
|
36
|
-
return {
|
|
37
|
-
...task,
|
|
38
|
-
passes: []
|
|
39
|
-
};
|
|
40
|
-
});
|
|
41
|
-
return {
|
|
42
|
-
...tasksFile,
|
|
43
|
-
tasks
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
25
|
export async function getPrdState(repoId, slug) {
|
|
47
26
|
const row = await dbGet(`
|
|
48
27
|
SELECT repo_id, slug, tasks_json, progress_json, notes_md, updated_at
|
|
@@ -52,11 +31,12 @@ export async function getPrdState(repoId, slug) {
|
|
|
52
31
|
if (!row) {
|
|
53
32
|
return null;
|
|
54
33
|
}
|
|
55
|
-
const tasks =
|
|
34
|
+
const tasks = parseStoredJson(row.tasks_json, 'prd_states.tasks_json', parseTasksFile);
|
|
35
|
+
const progress = parseStoredJson(row.progress_json, 'prd_states.progress_json', parseProgressFile);
|
|
56
36
|
return {
|
|
57
37
|
slug: row.slug,
|
|
58
38
|
tasks,
|
|
59
|
-
progress
|
|
39
|
+
progress,
|
|
60
40
|
notes: row.notes_md,
|
|
61
41
|
updatedAt: row.updated_at
|
|
62
42
|
};
|
|
@@ -68,7 +48,7 @@ export async function getPrdStateSummaries(repoId) {
|
|
|
68
48
|
const summary = { hasState: true };
|
|
69
49
|
if (row.tasks_json) {
|
|
70
50
|
try {
|
|
71
|
-
const tasksFile = JSON.parse(row.tasks_json);
|
|
51
|
+
const tasksFile = parseTasksFile(JSON.parse(row.tasks_json));
|
|
72
52
|
const counts = getTaskCounts(tasksFile);
|
|
73
53
|
if (counts) {
|
|
74
54
|
summary.taskCount = counts.taskCount;
|
|
@@ -84,146 +64,58 @@ export async function getPrdStateSummaries(repoId) {
|
|
|
84
64
|
return summaries;
|
|
85
65
|
}
|
|
86
66
|
export async function upsertPrdState(repoId, slug, update) {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
67
|
+
const validatedTasks = update.tasks === undefined
|
|
68
|
+
? undefined
|
|
69
|
+
: (update.tasks === null ? null : parseTasksFile(update.tasks));
|
|
70
|
+
const validatedProgress = update.progress === undefined
|
|
71
|
+
? undefined
|
|
72
|
+
: (update.progress === null ? null : parseProgressFile(update.progress));
|
|
73
|
+
const updateTasks = validatedTasks !== undefined;
|
|
74
|
+
const updateProgress = validatedProgress !== undefined;
|
|
75
|
+
const updateNotes = update.notes !== undefined;
|
|
76
|
+
const tasksJson = validatedTasks === undefined
|
|
77
|
+
? null
|
|
78
|
+
: (validatedTasks === null ? null : JSON.stringify(validatedTasks));
|
|
79
|
+
const progressJson = validatedProgress === undefined
|
|
80
|
+
? null
|
|
81
|
+
: (validatedProgress === null ? null : JSON.stringify(validatedProgress));
|
|
98
82
|
const notesMd = update.notes === undefined
|
|
99
|
-
?
|
|
83
|
+
? null
|
|
100
84
|
: update.notes;
|
|
101
85
|
const updatedAt = new Date().toISOString();
|
|
102
|
-
if (existing) {
|
|
103
|
-
await dbRun(`
|
|
104
|
-
UPDATE prd_states
|
|
105
|
-
SET tasks_json = ?, progress_json = ?, notes_md = ?, updated_at = ?
|
|
106
|
-
WHERE repo_id = ? AND slug = ?
|
|
107
|
-
`, [tasksJson, progressJson, notesMd, updatedAt, repoId, slug]);
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
86
|
await dbRun(`
|
|
111
87
|
INSERT INTO prd_states (repo_id, slug, tasks_json, progress_json, notes_md, updated_at)
|
|
112
88
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
return { value: JSON.parse(content), imported: true };
|
|
137
|
-
}
|
|
138
|
-
catch (error) {
|
|
139
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
140
|
-
console.warn(`[legacy-state] Skipping invalid ${label} at ${filePath}: ${message}`);
|
|
141
|
-
return { value: null, imported: false };
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
async function removeIfExists(filePath) {
|
|
145
|
-
try {
|
|
146
|
-
await fs.unlink(filePath);
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
// File may already be removed.
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
async function removeDirIfEmpty(dirPath) {
|
|
153
|
-
try {
|
|
154
|
-
const entries = await fs.readdir(dirPath);
|
|
155
|
-
if (entries.length === 0) {
|
|
156
|
-
await fs.rmdir(dirPath);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
catch {
|
|
160
|
-
// Directory may not exist or may contain files.
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
async function runLegacyStateMigration(repo, cleanupLegacyFiles, minFileAgeMs) {
|
|
164
|
-
const legacyStateDir = join(repo.path, '.claude', 'state');
|
|
165
|
-
const entries = await fs.readdir(legacyStateDir, { withFileTypes: true, encoding: 'utf8' }).catch(() => null);
|
|
166
|
-
if (!entries) {
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
for (const entry of entries) {
|
|
170
|
-
if (!entry.isDirectory()) {
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
const slug = entry.name;
|
|
174
|
-
const slugDir = join(legacyStateDir, slug);
|
|
175
|
-
const tasksPath = join(slugDir, 'tasks.json');
|
|
176
|
-
const progressPath = join(slugDir, 'progress.json');
|
|
177
|
-
const notesPath = join(slugDir, 'notes.md');
|
|
178
|
-
const [tasksResult, progressResult, notesContent] = await Promise.all([
|
|
179
|
-
readLegacyJsonFile(tasksPath, 'tasks.json', minFileAgeMs),
|
|
180
|
-
readLegacyJsonFile(progressPath, 'progress.json', minFileAgeMs),
|
|
181
|
-
readStableLegacyFile(notesPath, minFileAgeMs)
|
|
182
|
-
]);
|
|
183
|
-
const shouldImportNotes = notesContent !== null;
|
|
184
|
-
const shouldImport = tasksResult.imported || progressResult.imported || shouldImportNotes;
|
|
185
|
-
if (!shouldImport) {
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
await upsertPrdState(repo.id, slug, {
|
|
189
|
-
...(tasksResult.imported && { tasks: tasksResult.value }),
|
|
190
|
-
...(progressResult.imported && { progress: progressResult.value }),
|
|
191
|
-
...(shouldImportNotes && { notes: notesContent })
|
|
89
|
+
ON CONFLICT(repo_id, slug) DO UPDATE SET
|
|
90
|
+
tasks_json = CASE WHEN ? THEN excluded.tasks_json ELSE prd_states.tasks_json END,
|
|
91
|
+
progress_json = CASE WHEN ? THEN excluded.progress_json ELSE prd_states.progress_json END,
|
|
92
|
+
notes_md = CASE WHEN ? THEN excluded.notes_md ELSE prd_states.notes_md END,
|
|
93
|
+
updated_at = excluded.updated_at
|
|
94
|
+
`, [
|
|
95
|
+
repoId,
|
|
96
|
+
slug,
|
|
97
|
+
tasksJson,
|
|
98
|
+
progressJson,
|
|
99
|
+
notesMd,
|
|
100
|
+
updatedAt,
|
|
101
|
+
updateTasks ? 1 : 0,
|
|
102
|
+
updateProgress ? 1 : 0,
|
|
103
|
+
updateNotes ? 1 : 0
|
|
104
|
+
]);
|
|
105
|
+
if (validatedTasks !== undefined) {
|
|
106
|
+
emitChange({
|
|
107
|
+
type: 'change',
|
|
108
|
+
path: `state://${repoId}/${slug}/tasks.json`,
|
|
109
|
+
repoId,
|
|
110
|
+
category: 'tasks'
|
|
192
111
|
});
|
|
193
|
-
if (cleanupLegacyFiles) {
|
|
194
|
-
if (tasksResult.imported) {
|
|
195
|
-
await removeIfExists(tasksPath);
|
|
196
|
-
}
|
|
197
|
-
if (progressResult.imported) {
|
|
198
|
-
await removeIfExists(progressPath);
|
|
199
|
-
}
|
|
200
|
-
if (shouldImportNotes) {
|
|
201
|
-
await removeIfExists(notesPath);
|
|
202
|
-
}
|
|
203
|
-
await removeDirIfEmpty(slugDir);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
if (cleanupLegacyFiles) {
|
|
207
|
-
await removeDirIfEmpty(legacyStateDir);
|
|
208
112
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
return inFlight;
|
|
113
|
+
if (validatedProgress !== undefined) {
|
|
114
|
+
emitChange({
|
|
115
|
+
type: 'change',
|
|
116
|
+
path: `state://${repoId}/${slug}/progress.json`,
|
|
117
|
+
repoId,
|
|
118
|
+
category: 'progress'
|
|
119
|
+
});
|
|
217
120
|
}
|
|
218
|
-
const migrationPromise = runLegacyStateMigration(repo, cleanupLegacyFiles, minFileAgeMs)
|
|
219
|
-
.then(() => {
|
|
220
|
-
if (cleanupLegacyFiles) {
|
|
221
|
-
cleanupCompletedRepoIds.add(repo.id);
|
|
222
|
-
}
|
|
223
|
-
})
|
|
224
|
-
.finally(() => {
|
|
225
|
-
migrationInFlight.delete(repo.id);
|
|
226
|
-
});
|
|
227
|
-
migrationInFlight.set(repo.id, migrationPromise);
|
|
228
|
-
return migrationPromise;
|
|
229
121
|
}
|
|
@@ -1,13 +1,35 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
2
|
-
import { join, basename, resolve, relative } from 'node:path';
|
|
1
|
+
import { existsSync, promises as fs } from 'node:fs';
|
|
2
|
+
import { join, basename, dirname, resolve, relative, isAbsolute } from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { dbAll, dbGet, dbRun } from './db.js';
|
|
5
|
-
|
|
6
|
+
function findPackageRoot(startDir) {
|
|
7
|
+
let currentDir = startDir;
|
|
8
|
+
while (true) {
|
|
9
|
+
if (existsSync(join(currentDir, 'package.json'))) {
|
|
10
|
+
return currentDir;
|
|
11
|
+
}
|
|
12
|
+
const parentDir = dirname(currentDir);
|
|
13
|
+
if (parentDir === currentDir) {
|
|
14
|
+
return startDir;
|
|
15
|
+
}
|
|
16
|
+
currentDir = parentDir;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function normalizePathSlashes(path) {
|
|
20
|
+
return path.replaceAll('\\', '/');
|
|
21
|
+
}
|
|
22
|
+
function isPathWithin(basePath, candidatePath) {
|
|
23
|
+
const relativePath = relative(resolve(basePath), resolve(candidatePath));
|
|
24
|
+
return relativePath === '' || (!relativePath.startsWith('..') && !isAbsolute(relativePath));
|
|
25
|
+
}
|
|
26
|
+
const PACKAGE_ROOT = findPackageRoot(dirname(fileURLToPath(import.meta.url)));
|
|
27
|
+
const LEGACY_REPOS_FILE = join(PACKAGE_ROOT, 'server', 'data', 'repos.json');
|
|
6
28
|
let legacyImportPromise = null;
|
|
7
29
|
function serializeGitRepos(gitRepos) {
|
|
8
30
|
return gitRepos && gitRepos.length > 0 ? JSON.stringify(gitRepos) : null;
|
|
9
31
|
}
|
|
10
|
-
function parseGitRepos(gitReposJson) {
|
|
32
|
+
function parseGitRepos(repoPath, gitReposJson) {
|
|
11
33
|
if (!gitReposJson) {
|
|
12
34
|
return undefined;
|
|
13
35
|
}
|
|
@@ -16,21 +38,39 @@ function parseGitRepos(gitReposJson) {
|
|
|
16
38
|
if (!Array.isArray(parsed)) {
|
|
17
39
|
return undefined;
|
|
18
40
|
}
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
41
|
+
const repoRoot = resolve(repoPath);
|
|
42
|
+
const validRepos = new Map();
|
|
43
|
+
for (const item of parsed) {
|
|
44
|
+
if (!item || typeof item !== 'object') {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const relativePath = item.relativePath;
|
|
48
|
+
const name = item.name;
|
|
49
|
+
if (!relativePath || !name) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const normalizedRelativePath = normalizePathSlashes(relativePath).replace(/^\.\//, '');
|
|
53
|
+
if (!normalizedRelativePath || normalizedRelativePath === '.') {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const absolutePath = resolve(repoRoot, normalizedRelativePath);
|
|
57
|
+
if (!isPathWithin(repoRoot, absolutePath)) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
validRepos.set(normalizedRelativePath, {
|
|
61
|
+
relativePath: normalizedRelativePath,
|
|
62
|
+
absolutePath,
|
|
63
|
+
name
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return validRepos.size > 0 ? Array.from(validRepos.values()) : undefined;
|
|
27
67
|
}
|
|
28
68
|
catch {
|
|
29
69
|
return undefined;
|
|
30
70
|
}
|
|
31
71
|
}
|
|
32
72
|
function rowToRepo(row) {
|
|
33
|
-
const gitRepos = parseGitRepos(row.git_repos_json);
|
|
73
|
+
const gitRepos = parseGitRepos(row.path, row.git_repos_json);
|
|
34
74
|
return {
|
|
35
75
|
id: row.id,
|
|
36
76
|
name: row.name,
|
|
@@ -145,6 +185,11 @@ export async function getRepoById(id) {
|
|
|
145
185
|
const row = await dbGet('SELECT id, name, path, added_at, git_repos_json FROM repos WHERE id = ?', [id]);
|
|
146
186
|
return row ? rowToRepo(row) : undefined;
|
|
147
187
|
}
|
|
188
|
+
export async function updateRepoGitRepos(id, gitRepos) {
|
|
189
|
+
await importLegacyReposIfNeeded();
|
|
190
|
+
const result = await dbRun('UPDATE repos SET git_repos_json = ? WHERE id = ?', [serializeGitRepos(gitRepos), id]);
|
|
191
|
+
return result.changes > 0;
|
|
192
|
+
}
|
|
148
193
|
export async function removeRepo(id) {
|
|
149
194
|
await importLegacyReposIfNeeded();
|
|
150
195
|
const result = await dbRun('DELETE FROM repos WHERE id = ?', [id]);
|
|
@@ -229,12 +274,22 @@ export async function discoverGitRepos(basePath, maxDepth = 4) {
|
|
|
229
274
|
return discovered;
|
|
230
275
|
}
|
|
231
276
|
export async function validateRepoPath(path) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
277
|
+
const trimmedPath = path.trim();
|
|
278
|
+
if (trimmedPath.length === 0) {
|
|
279
|
+
return { valid: false, error: 'Path is required' };
|
|
280
|
+
}
|
|
281
|
+
if (trimmedPath.length > 4096) {
|
|
282
|
+
return { valid: false, error: 'Path is too long' };
|
|
283
|
+
}
|
|
284
|
+
if (trimmedPath.includes('\u0000')) {
|
|
285
|
+
return { valid: false, error: 'Path contains invalid characters' };
|
|
286
|
+
}
|
|
287
|
+
const isWindowsAbsolutePath = /^[A-Za-z]:[\\/]/.test(trimmedPath);
|
|
288
|
+
if (!isAbsolute(trimmedPath) && !isWindowsAbsolutePath) {
|
|
236
289
|
return { valid: false, error: 'Path must be absolute' };
|
|
237
290
|
}
|
|
291
|
+
// Normalize the path
|
|
292
|
+
const resolvedPath = resolve(trimmedPath);
|
|
238
293
|
try {
|
|
239
294
|
const stats = await fs.stat(resolvedPath);
|
|
240
295
|
if (!stats.isDirectory()) {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const taskStatusSchema = z.enum(['pending', 'in_progress', 'completed']);
|
|
3
|
+
const taskCategorySchema = z.enum(['setup', 'feature', 'integration', 'testing', 'documentation']);
|
|
4
|
+
const taskPrioritySchema = z.enum(['critical', 'high', 'medium', 'low']);
|
|
5
|
+
const commitRefSchema = z.object({
|
|
6
|
+
sha: z.string().min(1),
|
|
7
|
+
repo: z.string()
|
|
8
|
+
});
|
|
9
|
+
const taskSchema = z.object({
|
|
10
|
+
id: z.string().min(1),
|
|
11
|
+
category: taskCategorySchema,
|
|
12
|
+
title: z.string(),
|
|
13
|
+
description: z.string(),
|
|
14
|
+
steps: z.array(z.string()).default([]),
|
|
15
|
+
passes: z.array(z.string()).default([]),
|
|
16
|
+
dependencies: z.array(z.string()).default([]),
|
|
17
|
+
priority: taskPrioritySchema,
|
|
18
|
+
status: taskStatusSchema,
|
|
19
|
+
startedAt: z.string().optional(),
|
|
20
|
+
completedAt: z.string().optional()
|
|
21
|
+
});
|
|
22
|
+
const tasksFileSchema = z.object({
|
|
23
|
+
prd: z.object({
|
|
24
|
+
name: z.string(),
|
|
25
|
+
source: z.string(),
|
|
26
|
+
shortcutStory: z.string().optional(),
|
|
27
|
+
createdAt: z.string()
|
|
28
|
+
}),
|
|
29
|
+
tasks: z.array(taskSchema)
|
|
30
|
+
});
|
|
31
|
+
const taskLogSchema = z.object({
|
|
32
|
+
taskId: z.string().min(1),
|
|
33
|
+
status: taskStatusSchema,
|
|
34
|
+
startedAt: z.string(),
|
|
35
|
+
completedAt: z.string().optional(),
|
|
36
|
+
implemented: z.string().optional(),
|
|
37
|
+
filesChanged: z.array(z.string()).optional(),
|
|
38
|
+
learnings: z.string().optional(),
|
|
39
|
+
commits: z.array(z.union([z.string().min(1), commitRefSchema])).optional()
|
|
40
|
+
});
|
|
41
|
+
const progressFileSchema = z.object({
|
|
42
|
+
prdName: z.string(),
|
|
43
|
+
shortcutStory: z.string().optional(),
|
|
44
|
+
totalTasks: z.number(),
|
|
45
|
+
completed: z.number(),
|
|
46
|
+
inProgress: z.number(),
|
|
47
|
+
blocked: z.number(),
|
|
48
|
+
startedAt: z.string().nullable(),
|
|
49
|
+
lastUpdated: z.string(),
|
|
50
|
+
patterns: z.array(z.object({
|
|
51
|
+
name: z.string(),
|
|
52
|
+
description: z.string()
|
|
53
|
+
})),
|
|
54
|
+
taskLogs: z.array(taskLogSchema)
|
|
55
|
+
});
|
|
56
|
+
export function parseTasksFile(value) {
|
|
57
|
+
return tasksFileSchema.parse(value);
|
|
58
|
+
}
|
|
59
|
+
export function parseProgressFile(value) {
|
|
60
|
+
return progressFileSchema.parse(value);
|
|
61
|
+
}
|