@thxgg/steward 0.1.15 → 0.1.17
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 +6 -0
- package/.output/nitro.json +1 -1
- package/.output/public/_nuxt/{qKRNa41x.js → 4r0X30JV.js} +1 -1
- package/.output/public/_nuxt/{uTyw4SRK.js → BMdjSp24.js} +1 -1
- package/.output/public/_nuxt/{BubpH_wW.js → BSZqAKg4.js} +1 -1
- package/.output/public/_nuxt/{nYTZJhvT.js → BdjPva1I.js} +1 -1
- package/.output/public/_nuxt/Beeir9iR.js +1 -0
- package/.output/public/_nuxt/Bh3vsUvl.js +1 -0
- package/.output/public/_nuxt/{CMu9GKTH.js → BlTKcjLJ.js} +2 -2
- package/.output/public/_nuxt/{BDqHART1.js → By7gAVcL.js} +1 -1
- package/.output/public/_nuxt/CbJfCtEa.js +1 -0
- package/.output/public/_nuxt/CbkpNvIu.js +141 -0
- package/.output/public/_nuxt/CmhLcqDu.js +1 -0
- package/.output/public/_nuxt/DC6iPLz1.js +30 -0
- package/.output/public/_nuxt/{C_NevjZD.js → DD--ojY9.js} +1 -1
- package/.output/public/_nuxt/Detail.DSyVQNdr.css +1 -0
- package/.output/public/_nuxt/DhKWRjCh.js +60 -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/f3f42dbd-d501-442b-871c-3d06157e7aa1.json +1 -0
- package/.output/public/_nuxt/c1sXju8w.js +1 -0
- package/.output/public/_nuxt/eGCjCghR.js +1 -0
- package/.output/public/_nuxt/nX8Sf7cz.js +13 -0
- package/.output/server/chunks/_/git-api.mjs +100 -7
- 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 +234 -0
- package/.output/server/chunks/_/prd-service.mjs.map +1 -0
- package/.output/server/chunks/_/task-graph.mjs +3 -3
- package/.output/server/chunks/_/task-graph.mjs.map +1 -1
- package/.output/server/chunks/_/watcher.mjs +26 -46
- package/.output/server/chunks/_/watcher.mjs.map +1 -1
- package/.output/server/chunks/build/{Detail-DC-KJQ1f.mjs → Detail-MGwP_u2d.mjs} +63 -34
- package/.output/server/chunks/build/Detail-MGwP_u2d.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_-C1C4GAhW.mjs → _prd_-C-Aj4fVa.mjs} +75 -33
- package/.output/server/chunks/build/_prd_-C-Aj4fVa.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} +4 -3
- package/.output/server/chunks/build/default-Cao5eO80.mjs.map +1 -0
- package/.output/server/chunks/build/error-404-Bf6kdO80.mjs +2 -1
- package/.output/server/chunks/build/error-500-D_bcARXN.mjs +2 -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} +8 -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-CTEkxiYd.mjs → repo-graph-EuhMeFt7.mjs} +25 -10
- package/.output/server/chunks/build/repo-graph-EuhMeFt7.mjs.map +1 -0
- package/.output/server/chunks/build/server.mjs +7 -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 +1149 -720
- package/.output/server/chunks/nitro/nitro.mjs.map +1 -1
- package/.output/server/chunks/routes/api/browse.get.mjs +12 -6
- package/.output/server/chunks/routes/api/browse.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/index.get.mjs +2 -1
- package/.output/server/chunks/routes/api/index.get.mjs.map +1 -1
- package/.output/server/chunks/routes/api/index.post.mjs +3 -2
- package/.output/server/chunks/routes/api/index.post.mjs.map +1 -1
- package/.output/server/chunks/routes/api/repos/_repoId/git/commits.get.mjs +11 -13
- 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 +11 -6
- 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 +31 -12
- 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 +13 -13
- 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 +5 -1
- 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 +14 -1
- 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 +20 -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 +20 -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 +20 -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 +30 -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 +19 -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 +2 -1
- package/.output/server/chunks/routes/api/repos/_repoId_.delete.mjs.map +1 -1
- package/.output/server/chunks/routes/api/runtime.get.mjs +3 -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 +3 -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 +7 -2
- package/dist/host/src/api/prds.js +6 -172
- package/dist/host/src/api/repos.js +3 -16
- package/dist/host/src/api/state.js +7 -2
- package/dist/host/src/executor-runner.js +368 -0
- package/dist/host/src/executor.js +138 -260
- package/dist/host/src/index.js +7 -2
- package/dist/host/src/mcp.js +27 -1
- package/dist/host/src/ui.js +18 -3
- package/dist/server/utils/change-events.js +33 -0
- package/dist/server/utils/git.js +11 -16
- package/dist/server/utils/prd-service.js +235 -0
- package/dist/server/utils/prd-state.js +57 -45
- package/dist/server/utils/repos.js +58 -13
- package/dist/server/utils/state-schema.js +61 -0
- package/dist/server/utils/task-graph.js +2 -2
- package/package.json +2 -1
- package/.output/public/_nuxt/-k8zG74W.js +0 -61
- package/.output/public/_nuxt/B-5VWizU.js +0 -1
- package/.output/public/_nuxt/BMAq0QVD.js +0 -42
- package/.output/public/_nuxt/BPeTf9dd.js +0 -1
- package/.output/public/_nuxt/C2HGkiSP.js +0 -1
- package/.output/public/_nuxt/CVvrkZkq.js +0 -1
- package/.output/public/_nuxt/Detail.CzXXlavD.css +0 -1
- package/.output/public/_nuxt/_prd_.KTotLoF_.css +0 -1
- package/.output/public/_nuxt/builds/meta/c1a7997d-8d53-4718-ad03-a977e05e2523.json +0 -1
- package/.output/public/_nuxt/qt5OEWHC.js +0 -1
- package/.output/public/_nuxt/wbj-mIhK.js +0 -1
- package/.output/server/chunks/build/Detail-DC-KJQ1f.mjs.map +0 -1
- package/.output/server/chunks/build/DiffViewer-styles-1.mjs-D0sb4vsK.mjs +0 -4
- package/.output/server/chunks/build/DiffViewer-styles-1.mjs-D0sb4vsK.mjs.map +0 -1
- package/.output/server/chunks/build/DiffViewer-styles.CkSjCQ0r.mjs +0 -10
- package/.output/server/chunks/build/DiffViewer-styles.CkSjCQ0r.mjs.map +0 -1
- package/.output/server/chunks/build/DiffViewer-styles.FJJuYjYB.mjs +0 -8
- package/.output/server/chunks/build/DiffViewer-styles.FJJuYjYB.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_-C1C4GAhW.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-CTEkxiYd.mjs.map +0 -1
- package/.output/server/chunks/build/usePrd-SqcxGyFU.mjs.map +0 -1
- package/.output/server/node_modules/shiki/dist/bundle-web.mjs +0 -366
|
@@ -0,0 +1,235 @@
|
|
|
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, migrateLegacyStateForRepo } 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
|
+
await migrateLegacyStateForRepo(repo);
|
|
95
|
+
const prdDir = join(repo.path, 'docs', 'prd');
|
|
96
|
+
let prdFiles = [];
|
|
97
|
+
try {
|
|
98
|
+
const files = await fs.readdir(prdDir);
|
|
99
|
+
prdFiles = files.filter((file) => {
|
|
100
|
+
if (!file.endsWith('.md')) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
const slug = basename(file, '.md');
|
|
104
|
+
return isValidPrdSlug(slug);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
const stateSummaries = await getPrdStateSummaries(repo.id);
|
|
111
|
+
const items = await Promise.all(prdFiles.map(async (filename) => {
|
|
112
|
+
const slug = basename(filename, '.md');
|
|
113
|
+
const filePath = join(prdDir, filename);
|
|
114
|
+
let name = slug;
|
|
115
|
+
let modifiedAt = 0;
|
|
116
|
+
try {
|
|
117
|
+
const [content, stat] = await Promise.all([
|
|
118
|
+
fs.readFile(filePath, 'utf-8'),
|
|
119
|
+
fs.stat(filePath)
|
|
120
|
+
]);
|
|
121
|
+
modifiedAt = stat.mtime.getTime();
|
|
122
|
+
name = extractPrdTitle(content, slug);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Keep fallback values when file cannot be read.
|
|
126
|
+
}
|
|
127
|
+
const stateSummary = stateSummaries.get(slug);
|
|
128
|
+
return {
|
|
129
|
+
slug,
|
|
130
|
+
name,
|
|
131
|
+
source: `docs/prd/${filename}`,
|
|
132
|
+
hasState: !!stateSummary?.hasState,
|
|
133
|
+
modifiedAt,
|
|
134
|
+
...(stateSummary?.taskCount !== undefined && { taskCount: stateSummary.taskCount }),
|
|
135
|
+
...(stateSummary?.completedCount !== undefined && { completedCount: stateSummary.completedCount })
|
|
136
|
+
};
|
|
137
|
+
}));
|
|
138
|
+
items.sort((a, b) => b.modifiedAt - a.modifiedAt);
|
|
139
|
+
return items;
|
|
140
|
+
}
|
|
141
|
+
export async function readPrdTasks(repo, prdSlug) {
|
|
142
|
+
assertValidPrdSlug(prdSlug);
|
|
143
|
+
await migrateLegacyStateForRepo(repo);
|
|
144
|
+
const state = await getPrdState(repo.id, prdSlug);
|
|
145
|
+
return state?.tasks ?? null;
|
|
146
|
+
}
|
|
147
|
+
export async function readPrdProgress(repo, prdSlug) {
|
|
148
|
+
assertValidPrdSlug(prdSlug);
|
|
149
|
+
await migrateLegacyStateForRepo(repo);
|
|
150
|
+
const state = await getPrdState(repo.id, prdSlug);
|
|
151
|
+
return state?.progress ?? null;
|
|
152
|
+
}
|
|
153
|
+
function hasDiscoveredRepoChanges(repo, discoveredRepos) {
|
|
154
|
+
const normalizedExisting = normalizeGitRepos(repo.gitRepos) || [];
|
|
155
|
+
const normalizedDiscovered = normalizeGitRepos(discoveredRepos) || [];
|
|
156
|
+
if (normalizedExisting.length !== normalizedDiscovered.length) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
const existingPaths = new Set(normalizedExisting.map((gitRepo) => gitRepo.relativePath));
|
|
160
|
+
for (const gitRepo of normalizedDiscovered) {
|
|
161
|
+
if (!existingPaths.has(gitRepo.relativePath)) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
async function refreshDiscoveredGitRepos(repo) {
|
|
168
|
+
const discoveredRepos = normalizeGitRepos(await discoverGitRepos(repo.path));
|
|
169
|
+
if (!hasDiscoveredRepoChanges(repo, discoveredRepos)) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const updated = await updateRepoGitRepos(repo.id, discoveredRepos);
|
|
173
|
+
if (!updated) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
...repo,
|
|
178
|
+
gitRepos: discoveredRepos
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
export async function resolveTaskCommits(repo, prdSlug, taskId) {
|
|
182
|
+
assertValidPrdSlug(prdSlug);
|
|
183
|
+
await migrateLegacyStateForRepo(repo);
|
|
184
|
+
const state = await getPrdState(repo.id, prdSlug);
|
|
185
|
+
const progress = state?.progress ?? null;
|
|
186
|
+
if (!progress) {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
const taskLogs = Array.isArray(progress.taskLogs) ? progress.taskLogs : [];
|
|
190
|
+
const taskLog = taskLogs.find((log) => log.taskId === taskId);
|
|
191
|
+
if (!taskLog || !taskLog.commits || taskLog.commits.length === 0) {
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
const resolvedCommits = [];
|
|
195
|
+
const failedEntries = [];
|
|
196
|
+
for (const commitEntry of taskLog.commits) {
|
|
197
|
+
try {
|
|
198
|
+
const resolved = await resolveCommitRepo(repo, commitEntry);
|
|
199
|
+
resolvedCommits.push({
|
|
200
|
+
sha: resolved.sha,
|
|
201
|
+
repo: resolved.repoPath
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
failedEntries.push(commitEntry);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (failedEntries.length === 0) {
|
|
209
|
+
return resolvedCommits;
|
|
210
|
+
}
|
|
211
|
+
const refreshedRepo = await refreshDiscoveredGitRepos(repo);
|
|
212
|
+
for (const commitEntry of failedEntries) {
|
|
213
|
+
if (!refreshedRepo) {
|
|
214
|
+
resolvedCommits.push({
|
|
215
|
+
sha: extractCommitSha(commitEntry),
|
|
216
|
+
repo: ''
|
|
217
|
+
});
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
const resolved = await resolveCommitRepo(refreshedRepo, commitEntry);
|
|
222
|
+
resolvedCommits.push({
|
|
223
|
+
sha: resolved.sha,
|
|
224
|
+
repo: resolved.repoPath
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
resolvedCommits.push({
|
|
229
|
+
sha: extractCommitSha(commitEntry),
|
|
230
|
+
repo: ''
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return resolvedCommits;
|
|
235
|
+
}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { emitChange } from './change-events.js';
|
|
3
4
|
import { dbAll, dbGet, dbRun } from './db.js';
|
|
5
|
+
import { parseProgressFile, parseTasksFile } from './state-schema.js';
|
|
4
6
|
const LEGACY_STATE_STABLE_MS = 0;
|
|
5
7
|
const migrationInFlight = new Map();
|
|
6
8
|
const cleanupCompletedRepoIds = new Set();
|
|
7
|
-
function parseStoredJson(raw, fieldName) {
|
|
9
|
+
function parseStoredJson(raw, fieldName, parseValue) {
|
|
8
10
|
if (!raw) {
|
|
9
11
|
return null;
|
|
10
12
|
}
|
|
11
13
|
try {
|
|
12
|
-
|
|
14
|
+
const parsed = JSON.parse(raw);
|
|
15
|
+
return parseValue(parsed);
|
|
13
16
|
}
|
|
14
17
|
catch (error) {
|
|
15
18
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -24,25 +27,6 @@ function getTaskCounts(tasksFile) {
|
|
|
24
27
|
const completedCount = tasksFile.tasks.filter(task => task.status === 'completed').length;
|
|
25
28
|
return { taskCount, completedCount };
|
|
26
29
|
}
|
|
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
30
|
export async function getPrdState(repoId, slug) {
|
|
47
31
|
const row = await dbGet(`
|
|
48
32
|
SELECT repo_id, slug, tasks_json, progress_json, notes_md, updated_at
|
|
@@ -52,11 +36,12 @@ export async function getPrdState(repoId, slug) {
|
|
|
52
36
|
if (!row) {
|
|
53
37
|
return null;
|
|
54
38
|
}
|
|
55
|
-
const tasks =
|
|
39
|
+
const tasks = parseStoredJson(row.tasks_json, 'prd_states.tasks_json', parseTasksFile);
|
|
40
|
+
const progress = parseStoredJson(row.progress_json, 'prd_states.progress_json', parseProgressFile);
|
|
56
41
|
return {
|
|
57
42
|
slug: row.slug,
|
|
58
43
|
tasks,
|
|
59
|
-
progress
|
|
44
|
+
progress,
|
|
60
45
|
notes: row.notes_md,
|
|
61
46
|
updatedAt: row.updated_at
|
|
62
47
|
};
|
|
@@ -68,7 +53,7 @@ export async function getPrdStateSummaries(repoId) {
|
|
|
68
53
|
const summary = { hasState: true };
|
|
69
54
|
if (row.tasks_json) {
|
|
70
55
|
try {
|
|
71
|
-
const tasksFile = JSON.parse(row.tasks_json);
|
|
56
|
+
const tasksFile = parseTasksFile(JSON.parse(row.tasks_json));
|
|
72
57
|
const counts = getTaskCounts(tasksFile);
|
|
73
58
|
if (counts) {
|
|
74
59
|
summary.taskCount = counts.taskCount;
|
|
@@ -84,33 +69,60 @@ export async function getPrdStateSummaries(repoId) {
|
|
|
84
69
|
return summaries;
|
|
85
70
|
}
|
|
86
71
|
export async function upsertPrdState(repoId, slug, update) {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
72
|
+
const validatedTasks = update.tasks === undefined
|
|
73
|
+
? undefined
|
|
74
|
+
: (update.tasks === null ? null : parseTasksFile(update.tasks));
|
|
75
|
+
const validatedProgress = update.progress === undefined
|
|
76
|
+
? undefined
|
|
77
|
+
: (update.progress === null ? null : parseProgressFile(update.progress));
|
|
78
|
+
const updateTasks = validatedTasks !== undefined;
|
|
79
|
+
const updateProgress = validatedProgress !== undefined;
|
|
80
|
+
const updateNotes = update.notes !== undefined;
|
|
81
|
+
const tasksJson = validatedTasks === undefined
|
|
82
|
+
? null
|
|
83
|
+
: (validatedTasks === null ? null : JSON.stringify(validatedTasks));
|
|
84
|
+
const progressJson = validatedProgress === undefined
|
|
85
|
+
? null
|
|
86
|
+
: (validatedProgress === null ? null : JSON.stringify(validatedProgress));
|
|
98
87
|
const notesMd = update.notes === undefined
|
|
99
|
-
?
|
|
88
|
+
? null
|
|
100
89
|
: update.notes;
|
|
101
90
|
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
91
|
await dbRun(`
|
|
111
92
|
INSERT INTO prd_states (repo_id, slug, tasks_json, progress_json, notes_md, updated_at)
|
|
112
93
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
113
|
-
|
|
94
|
+
ON CONFLICT(repo_id, slug) DO UPDATE SET
|
|
95
|
+
tasks_json = CASE WHEN ? THEN excluded.tasks_json ELSE prd_states.tasks_json END,
|
|
96
|
+
progress_json = CASE WHEN ? THEN excluded.progress_json ELSE prd_states.progress_json END,
|
|
97
|
+
notes_md = CASE WHEN ? THEN excluded.notes_md ELSE prd_states.notes_md END,
|
|
98
|
+
updated_at = excluded.updated_at
|
|
99
|
+
`, [
|
|
100
|
+
repoId,
|
|
101
|
+
slug,
|
|
102
|
+
tasksJson,
|
|
103
|
+
progressJson,
|
|
104
|
+
notesMd,
|
|
105
|
+
updatedAt,
|
|
106
|
+
updateTasks ? 1 : 0,
|
|
107
|
+
updateProgress ? 1 : 0,
|
|
108
|
+
updateNotes ? 1 : 0
|
|
109
|
+
]);
|
|
110
|
+
if (validatedTasks !== undefined) {
|
|
111
|
+
emitChange({
|
|
112
|
+
type: 'change',
|
|
113
|
+
path: `state://${repoId}/${slug}/tasks.json`,
|
|
114
|
+
repoId,
|
|
115
|
+
category: 'tasks'
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (validatedProgress !== undefined) {
|
|
119
|
+
emitChange({
|
|
120
|
+
type: 'change',
|
|
121
|
+
path: `state://${repoId}/${slug}/progress.json`,
|
|
122
|
+
repoId,
|
|
123
|
+
category: 'progress'
|
|
124
|
+
});
|
|
125
|
+
}
|
|
114
126
|
}
|
|
115
127
|
async function readStableLegacyFile(filePath, minFileAgeMs) {
|
|
116
128
|
try {
|
|
@@ -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]);
|
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
2
|
import { getPrdState, getPrdStateSummaries, migrateLegacyStateForRepo } from './prd-state.js';
|
|
3
|
+
import { resolvePrdMarkdownPath } from './prd-service.js';
|
|
4
4
|
const NODE_SEPARATOR = '::';
|
|
5
5
|
const MISSING_PREFIX = 'missing';
|
|
6
6
|
export function createTaskNodeId(prdSlug, taskId) {
|
|
@@ -39,8 +39,8 @@ function parseDependency(rawDependency, currentPrdSlug) {
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
async function resolvePrdName(repo, prdSlug) {
|
|
42
|
-
const prdPath = join(repo.path, 'docs', 'prd', `${prdSlug}.md`);
|
|
43
42
|
try {
|
|
43
|
+
const prdPath = resolvePrdMarkdownPath(repo.path, prdSlug);
|
|
44
44
|
const content = await fs.readFile(prdPath, 'utf-8');
|
|
45
45
|
const h1Match = content.match(/^#\s+(.+)$/m);
|
|
46
46
|
return h1Match?.[1]?.trim() || prdSlug;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thxgg/steward",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"description": "Local-first PRD workflow steward with codemode MCP and web UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "thxgg",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"preview": "nuxt preview",
|
|
50
50
|
"mcp": "npm run build:host && node ./bin/prd mcp",
|
|
51
51
|
"ui": "npm run build && node ./bin/prd ui",
|
|
52
|
+
"test": "npm run build:host && node --test \"tests/**/*.test.mjs\"",
|
|
52
53
|
"typecheck": "nuxt typecheck && npm run typecheck:host",
|
|
53
54
|
"typecheck:host": "tsc -p tsconfig.host.json",
|
|
54
55
|
"prepack": "npm run build"
|