@thxgg/steward 0.1.16 → 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/{wbjFvimm.js → 4r0X30JV.js} +1 -1
- package/.output/public/_nuxt/{ynmyrfyT.js → BMdjSp24.js} +1 -1
- package/.output/public/_nuxt/{BWVTacYj.js → BSZqAKg4.js} +1 -1
- package/.output/public/_nuxt/{Dum5qplW.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/{Bibm_IDv.js → BlTKcjLJ.js} +2 -2
- package/.output/public/_nuxt/{CxHOXVf6.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/{BSA0RJ-H.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-CUfU85GY.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_-CeVnQzOV.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-CHNl58mY.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 +1106 -677
- 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/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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thxgg/steward-prod",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": true,
|
|
6
6
|
"dependencies": {
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
"vue-demi": "0.14.10",
|
|
77
77
|
"vue-router": "4.6.4",
|
|
78
78
|
"vue-sonner": "1.3.2",
|
|
79
|
+
"zod": "3.25.76",
|
|
79
80
|
"zwitch": "2.0.4"
|
|
80
81
|
}
|
|
81
82
|
}
|
package/README.md
CHANGED
|
@@ -55,6 +55,7 @@ Note: `execute` runs in a VM sandbox by design, so globals like `process` are in
|
|
|
55
55
|
```bash
|
|
56
56
|
prd ui
|
|
57
57
|
prd ui --port 3100 --host 127.0.0.1
|
|
58
|
+
prd ui --host 0.0.0.0 --allow-remote
|
|
58
59
|
prd mcp
|
|
59
60
|
```
|
|
60
61
|
|
|
@@ -143,8 +144,10 @@ Detailed API docs and examples: `docs/MCP.md`
|
|
|
143
144
|
|
|
144
145
|
Steward reads local filesystem and git metadata by design.
|
|
145
146
|
|
|
146
|
-
-
|
|
147
|
-
-
|
|
147
|
+
- By default, UI/API only accept loopback requests
|
|
148
|
+
- Non-loopback access requires explicit opt-in (`--allow-remote` or `STEWARD_ALLOW_REMOTE=1`)
|
|
149
|
+
- Optional token auth for remote API access: set `STEWARD_API_TOKEN`
|
|
150
|
+
- For browser sessions, you can initialize the auth cookie with `?token=<STEWARD_API_TOKEN>`
|
|
148
151
|
- Treat as a workstation tool, not a hosted multi-user service
|
|
149
152
|
|
|
150
153
|
## Storage
|
|
@@ -171,6 +174,8 @@ npm run build
|
|
|
171
174
|
| `PRD_STATE_DB_PATH` | Absolute path to SQLite DB file |
|
|
172
175
|
| `PRD_STATE_HOME` | Base directory for DB (`state.db` inside) |
|
|
173
176
|
| `XDG_DATA_HOME` | Fallback base path for default DB location |
|
|
177
|
+
| `STEWARD_ALLOW_REMOTE` | Set to `1` to allow non-loopback requests |
|
|
178
|
+
| `STEWARD_API_TOKEN` | Optional token required for remote `/api/*` access |
|
|
174
179
|
|
|
175
180
|
## OpenCode Bundle
|
|
176
181
|
|
|
@@ -1,190 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { basename, join } from 'node:path';
|
|
3
|
-
import { resolveCommitRepo } from '../../../server/utils/git.js';
|
|
4
|
-
import { discoverGitRepos, getRepos, saveRepos } from '../../../server/utils/repos.js';
|
|
5
|
-
import { getPrdState, getPrdStateSummaries, migrateLegacyStateForRepo } from '../../../server/utils/prd-state.js';
|
|
1
|
+
import { listPrdDocuments, readPrdDocument, readPrdProgress, readPrdTasks, resolveTaskCommits } from '../../../server/utils/prd-service.js';
|
|
6
2
|
import { requireRepo } from './repo-context.js';
|
|
7
|
-
function parseMetadata(content) {
|
|
8
|
-
const metadata = {};
|
|
9
|
-
const authorMatch = content.match(/\*{0,2}Author\*{0,2}:\*{0,2}\s*(.+?)(?:\n|$)/i);
|
|
10
|
-
if (authorMatch && authorMatch[1]) {
|
|
11
|
-
metadata.author = authorMatch[1].trim();
|
|
12
|
-
}
|
|
13
|
-
const dateMatch = content.match(/\*{0,2}Date\*{0,2}:\*{0,2}\s*(.+?)(?:\n|$)/i);
|
|
14
|
-
if (dateMatch && dateMatch[1]) {
|
|
15
|
-
metadata.date = dateMatch[1].trim();
|
|
16
|
-
}
|
|
17
|
-
const statusMatch = content.match(/\*{0,2}Status\*{0,2}:\*{0,2}\s*(.+?)(?:\n|$)/i);
|
|
18
|
-
if (statusMatch && statusMatch[1]) {
|
|
19
|
-
metadata.status = statusMatch[1].trim();
|
|
20
|
-
}
|
|
21
|
-
const shortcutLinkMatch = content.match(/\[([Ss][Cc]-\d+)\]\(([^)]+)\)/);
|
|
22
|
-
if (shortcutLinkMatch && shortcutLinkMatch[1] && shortcutLinkMatch[2]) {
|
|
23
|
-
metadata.shortcutStory = shortcutLinkMatch[1];
|
|
24
|
-
metadata.shortcutUrl = shortcutLinkMatch[2];
|
|
25
|
-
}
|
|
26
|
-
else {
|
|
27
|
-
const shortcutIdMatch = content.match(/\*{0,2}Shortcut(?:\s+Story)?\*{0,2}:\*{0,2}\s*([Ss][Cc]-\d+)/i);
|
|
28
|
-
if (shortcutIdMatch && shortcutIdMatch[1]) {
|
|
29
|
-
metadata.shortcutStory = shortcutIdMatch[1];
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return metadata;
|
|
33
|
-
}
|
|
34
|
-
async function readPrdFile(repo, prdSlug) {
|
|
35
|
-
const prdPath = join(repo.path, 'docs', 'prd', `${prdSlug}.md`);
|
|
36
|
-
try {
|
|
37
|
-
return await fs.readFile(prdPath, 'utf-8');
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
throw new Error('PRD not found');
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
3
|
export const prds = {
|
|
44
4
|
async list(repoId) {
|
|
45
5
|
const repo = await requireRepo(repoId);
|
|
46
|
-
await
|
|
47
|
-
const prdDir = join(repo.path, 'docs', 'prd');
|
|
48
|
-
let prdFiles = [];
|
|
49
|
-
try {
|
|
50
|
-
const files = await fs.readdir(prdDir);
|
|
51
|
-
prdFiles = files.filter((file) => file.endsWith('.md'));
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
return [];
|
|
55
|
-
}
|
|
56
|
-
const stateSummaries = await getPrdStateSummaries(repo.id);
|
|
57
|
-
const items = await Promise.all(prdFiles.map(async (filename) => {
|
|
58
|
-
const slug = basename(filename, '.md');
|
|
59
|
-
const filePath = join(prdDir, filename);
|
|
60
|
-
let name = slug;
|
|
61
|
-
let modifiedAt = 0;
|
|
62
|
-
try {
|
|
63
|
-
const [content, stat] = await Promise.all([
|
|
64
|
-
fs.readFile(filePath, 'utf-8'),
|
|
65
|
-
fs.stat(filePath)
|
|
66
|
-
]);
|
|
67
|
-
modifiedAt = stat.mtime.getTime();
|
|
68
|
-
const h1Match = content.match(/^#\s+(.+)$/m);
|
|
69
|
-
if (h1Match && h1Match[1]) {
|
|
70
|
-
name = h1Match[1].trim();
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
// Keep default values when a file cannot be read.
|
|
75
|
-
}
|
|
76
|
-
const stateSummary = stateSummaries.get(slug);
|
|
77
|
-
return {
|
|
78
|
-
slug,
|
|
79
|
-
name,
|
|
80
|
-
source: `docs/prd/${filename}`,
|
|
81
|
-
hasState: !!stateSummary?.hasState,
|
|
82
|
-
modifiedAt,
|
|
83
|
-
...(stateSummary?.taskCount !== undefined && { taskCount: stateSummary.taskCount }),
|
|
84
|
-
...(stateSummary?.completedCount !== undefined && { completedCount: stateSummary.completedCount })
|
|
85
|
-
};
|
|
86
|
-
}));
|
|
87
|
-
items.sort((a, b) => b.modifiedAt - a.modifiedAt);
|
|
88
|
-
return items;
|
|
6
|
+
return await listPrdDocuments(repo);
|
|
89
7
|
},
|
|
90
8
|
async getDocument(repoId, prdSlug) {
|
|
91
9
|
const repo = await requireRepo(repoId);
|
|
92
|
-
|
|
93
|
-
let name = prdSlug;
|
|
94
|
-
const h1Match = content.match(/^#\s+(.+)$/m);
|
|
95
|
-
if (h1Match && h1Match[1]) {
|
|
96
|
-
name = h1Match[1].trim();
|
|
97
|
-
}
|
|
98
|
-
return {
|
|
99
|
-
slug: prdSlug,
|
|
100
|
-
name,
|
|
101
|
-
content,
|
|
102
|
-
metadata: parseMetadata(content)
|
|
103
|
-
};
|
|
10
|
+
return await readPrdDocument(repo, prdSlug);
|
|
104
11
|
},
|
|
105
12
|
async getTasks(repoId, prdSlug) {
|
|
106
13
|
const repo = await requireRepo(repoId);
|
|
107
|
-
await
|
|
108
|
-
const state = await getPrdState(repo.id, prdSlug);
|
|
109
|
-
return state?.tasks ?? null;
|
|
14
|
+
return await readPrdTasks(repo, prdSlug);
|
|
110
15
|
},
|
|
111
16
|
async getProgress(repoId, prdSlug) {
|
|
112
17
|
const repo = await requireRepo(repoId);
|
|
113
|
-
await
|
|
114
|
-
const state = await getPrdState(repo.id, prdSlug);
|
|
115
|
-
return state?.progress ?? null;
|
|
18
|
+
return await readPrdProgress(repo, prdSlug);
|
|
116
19
|
},
|
|
117
20
|
async getTaskCommits(repoId, prdSlug, taskId) {
|
|
118
21
|
const repo = await requireRepo(repoId);
|
|
119
|
-
await
|
|
120
|
-
const state = await getPrdState(repo.id, prdSlug);
|
|
121
|
-
const progress = state?.progress ?? null;
|
|
122
|
-
if (!progress) {
|
|
123
|
-
return [];
|
|
124
|
-
}
|
|
125
|
-
const taskLogs = Array.isArray(progress.taskLogs) ? progress.taskLogs : [];
|
|
126
|
-
const taskLog = taskLogs.find((log) => log.taskId === taskId);
|
|
127
|
-
if (!taskLog) {
|
|
128
|
-
return [];
|
|
129
|
-
}
|
|
130
|
-
if (!taskLog.commits || taskLog.commits.length === 0) {
|
|
131
|
-
return [];
|
|
132
|
-
}
|
|
133
|
-
const resolvedCommits = [];
|
|
134
|
-
const failedEntries = [];
|
|
135
|
-
for (const commitEntry of taskLog.commits) {
|
|
136
|
-
try {
|
|
137
|
-
const resolved = await resolveCommitRepo(repo, commitEntry);
|
|
138
|
-
resolvedCommits.push({
|
|
139
|
-
sha: resolved.sha,
|
|
140
|
-
repo: resolved.repoPath
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
catch {
|
|
144
|
-
failedEntries.push(commitEntry);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
if (failedEntries.length > 0) {
|
|
148
|
-
const newGitRepos = await discoverGitRepos(repo.path);
|
|
149
|
-
const existingPaths = new Set((repo.gitRepos || []).map((gitRepo) => gitRepo.relativePath));
|
|
150
|
-
const hasNewRepos = newGitRepos.some((gitRepo) => !existingPaths.has(gitRepo.relativePath));
|
|
151
|
-
let resolvedWithUpdatedRepo = false;
|
|
152
|
-
if (hasNewRepos) {
|
|
153
|
-
const allRepos = await getRepos();
|
|
154
|
-
const repoIndex = allRepos.findIndex((candidate) => candidate.id === repoId);
|
|
155
|
-
if (repoIndex !== -1) {
|
|
156
|
-
const updatedRepo = {
|
|
157
|
-
...allRepos[repoIndex],
|
|
158
|
-
gitRepos: newGitRepos.length > 0 ? newGitRepos : undefined
|
|
159
|
-
};
|
|
160
|
-
allRepos[repoIndex] = updatedRepo;
|
|
161
|
-
await saveRepos(allRepos);
|
|
162
|
-
resolvedWithUpdatedRepo = true;
|
|
163
|
-
for (const commitEntry of failedEntries) {
|
|
164
|
-
try {
|
|
165
|
-
const resolved = await resolveCommitRepo(updatedRepo, commitEntry);
|
|
166
|
-
resolvedCommits.push({
|
|
167
|
-
sha: resolved.sha,
|
|
168
|
-
repo: resolved.repoPath
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
catch {
|
|
172
|
-
const sha = typeof commitEntry === 'string' ? commitEntry : commitEntry.sha;
|
|
173
|
-
resolvedCommits.push({ sha, repo: '' });
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
if (!resolvedWithUpdatedRepo) {
|
|
179
|
-
for (const commitEntry of failedEntries) {
|
|
180
|
-
const sha = typeof commitEntry === 'string' ? commitEntry : commitEntry.sha;
|
|
181
|
-
resolvedCommits.push({
|
|
182
|
-
sha,
|
|
183
|
-
repo: ''
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
return resolvedCommits;
|
|
22
|
+
return await resolveTaskCommits(repo, prdSlug, taskId);
|
|
189
23
|
}
|
|
190
24
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { addRepo, discoverGitRepos, getRepoById, getRepos, removeRepo,
|
|
1
|
+
import { addRepo, discoverGitRepos, getRepoById, getRepos, removeRepo, updateRepoGitRepos, validateRepoPath } from '../../../server/utils/repos.js';
|
|
2
2
|
import { migrateLegacyStateForRepo } from '../../../server/utils/prd-state.js';
|
|
3
3
|
import { requireCurrentRepo, requireRepo } from './repo-context.js';
|
|
4
4
|
export const repos = {
|
|
@@ -29,22 +29,9 @@ export const repos = {
|
|
|
29
29
|
return { removed: true };
|
|
30
30
|
},
|
|
31
31
|
async refreshGitRepos(repoId) {
|
|
32
|
-
await requireRepo(repoId);
|
|
33
|
-
const allRepos = await getRepos();
|
|
34
|
-
const repoIndex = allRepos.findIndex((repo) => repo.id === repoId);
|
|
35
|
-
if (repoIndex === -1) {
|
|
36
|
-
throw new Error('Repository not found');
|
|
37
|
-
}
|
|
38
|
-
const repo = allRepos[repoIndex];
|
|
32
|
+
const repo = await requireRepo(repoId);
|
|
39
33
|
const gitRepos = await discoverGitRepos(repo.path);
|
|
40
|
-
|
|
41
|
-
repo.gitRepos = gitRepos;
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
delete repo.gitRepos;
|
|
45
|
-
}
|
|
46
|
-
allRepos[repoIndex] = repo;
|
|
47
|
-
await saveRepos(allRepos);
|
|
34
|
+
await updateRepoGitRepos(repo.id, gitRepos.length > 0 ? gitRepos : undefined);
|
|
48
35
|
return {
|
|
49
36
|
discovered: gitRepos.length,
|
|
50
37
|
gitRepos
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { getPrdState, getPrdStateSummaries, migrateLegacyStateForRepo, upsertPrdState } from '../../../server/utils/prd-state.js';
|
|
2
|
+
import { parseProgressFile, parseTasksFile } from '../../../server/utils/state-schema.js';
|
|
2
3
|
import { requireCurrentRepo, requireRepo, requireRepoByPath } from './repo-context.js';
|
|
3
4
|
function mapStateUpdate(payload) {
|
|
4
5
|
return {
|
|
5
|
-
...(payload.tasks !== undefined && {
|
|
6
|
-
|
|
6
|
+
...(payload.tasks !== undefined && {
|
|
7
|
+
tasks: payload.tasks === null ? null : parseTasksFile(payload.tasks)
|
|
8
|
+
}),
|
|
9
|
+
...(payload.progress !== undefined && {
|
|
10
|
+
progress: payload.progress === null ? null : parseProgressFile(payload.progress)
|
|
11
|
+
}),
|
|
7
12
|
...(payload.notes !== undefined && { notes: payload.notes })
|
|
8
13
|
};
|
|
9
14
|
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import vm from 'node:vm';
|
|
2
|
+
import { git, prds, repos, state } from './api/index.js';
|
|
3
|
+
import { getStewardHelp } from './help.js';
|
|
4
|
+
const MAX_OUTPUT_SIZE = 50_000;
|
|
5
|
+
const EXECUTION_TIMEOUT_MS = Number.parseInt(process.env.STEWARD_EXECUTION_TIMEOUT_MS || '30000', 10);
|
|
6
|
+
const MAX_TIMERS = 100;
|
|
7
|
+
const MAX_LOG_ENTRIES = 200;
|
|
8
|
+
const MAX_LOG_OUTPUT_SIZE = 20_000;
|
|
9
|
+
const MAX_LOG_ENTRY_SIZE = 2_000;
|
|
10
|
+
class ExecutionError extends Error {
|
|
11
|
+
options;
|
|
12
|
+
constructor(message, options) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.options = options;
|
|
15
|
+
this.name = 'ExecutionError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function deepFreeze(value) {
|
|
19
|
+
if (!value || (typeof value !== 'object' && typeof value !== 'function') || Object.isFrozen(value)) {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
23
|
+
const property = value[key];
|
|
24
|
+
if (property && (typeof property === 'object' || typeof property === 'function')) {
|
|
25
|
+
deepFreeze(property);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return Object.freeze(value);
|
|
29
|
+
}
|
|
30
|
+
function safeJsonStringify(value) {
|
|
31
|
+
const seen = new WeakSet();
|
|
32
|
+
try {
|
|
33
|
+
return JSON.stringify(value, (_key, currentValue) => {
|
|
34
|
+
if (typeof currentValue === 'bigint') {
|
|
35
|
+
return `${currentValue}n`;
|
|
36
|
+
}
|
|
37
|
+
if (typeof currentValue === 'function') {
|
|
38
|
+
const functionName = currentValue.name ? ` ${currentValue.name}` : '';
|
|
39
|
+
return `[Function${functionName}]`;
|
|
40
|
+
}
|
|
41
|
+
if (typeof currentValue === 'symbol') {
|
|
42
|
+
return currentValue.toString();
|
|
43
|
+
}
|
|
44
|
+
if (typeof currentValue === 'object' && currentValue !== null) {
|
|
45
|
+
if (seen.has(currentValue)) {
|
|
46
|
+
return '[Circular]';
|
|
47
|
+
}
|
|
48
|
+
seen.add(currentValue);
|
|
49
|
+
}
|
|
50
|
+
return currentValue;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function formatLogValue(value) {
|
|
58
|
+
if (typeof value === 'string') {
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
const json = safeJsonStringify(value);
|
|
62
|
+
if (json !== undefined) {
|
|
63
|
+
return json;
|
|
64
|
+
}
|
|
65
|
+
return String(value);
|
|
66
|
+
}
|
|
67
|
+
function truncateResult(result) {
|
|
68
|
+
if (result === undefined) {
|
|
69
|
+
return {
|
|
70
|
+
result: null,
|
|
71
|
+
truncatedResult: false,
|
|
72
|
+
resultWasUndefined: true
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const json = safeJsonStringify(result);
|
|
76
|
+
if (json === undefined) {
|
|
77
|
+
return {
|
|
78
|
+
result: {
|
|
79
|
+
_unserializable: true,
|
|
80
|
+
preview: String(result)
|
|
81
|
+
},
|
|
82
|
+
truncatedResult: false,
|
|
83
|
+
resultWasUndefined: false
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (json.length <= MAX_OUTPUT_SIZE) {
|
|
87
|
+
return {
|
|
88
|
+
result,
|
|
89
|
+
truncatedResult: false,
|
|
90
|
+
resultWasUndefined: false
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
result: {
|
|
95
|
+
_truncated: true,
|
|
96
|
+
size: json.length,
|
|
97
|
+
preview: json.slice(0, MAX_OUTPUT_SIZE),
|
|
98
|
+
message: `Output truncated (${json.length} chars, showing first ${MAX_OUTPUT_SIZE})`
|
|
99
|
+
},
|
|
100
|
+
truncatedResult: true,
|
|
101
|
+
resultWasUndefined: false
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function normalizeFailure(error) {
|
|
105
|
+
if (error instanceof ExecutionError) {
|
|
106
|
+
return {
|
|
107
|
+
code: error.options?.code || 'EXECUTION_ERROR',
|
|
108
|
+
message: error.message,
|
|
109
|
+
...(error.options?.stackTrace && { stack: error.options.stackTrace }),
|
|
110
|
+
...(error.options?.details !== undefined && { details: error.options.details })
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (error instanceof Error) {
|
|
114
|
+
const { code, details } = error;
|
|
115
|
+
return {
|
|
116
|
+
code: typeof code === 'string' ? code : 'EXECUTION_ERROR',
|
|
117
|
+
message: error.message,
|
|
118
|
+
...(error.stack && { stack: error.stack }),
|
|
119
|
+
...(details !== undefined && { details })
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
code: 'EXECUTION_ERROR',
|
|
124
|
+
message: String(error)
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
async function execute(code) {
|
|
128
|
+
const startedAt = Date.now();
|
|
129
|
+
const logs = [];
|
|
130
|
+
let totalLogChars = 0;
|
|
131
|
+
let logsTruncated = false;
|
|
132
|
+
const appendLog = (level, args) => {
|
|
133
|
+
if (logs.length >= MAX_LOG_ENTRIES) {
|
|
134
|
+
logsTruncated = true;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
let message = args.map(formatLogValue).join(' ');
|
|
138
|
+
if (message.length > MAX_LOG_ENTRY_SIZE) {
|
|
139
|
+
message = `${message.slice(0, MAX_LOG_ENTRY_SIZE)}...`;
|
|
140
|
+
logsTruncated = true;
|
|
141
|
+
}
|
|
142
|
+
if (totalLogChars + message.length > MAX_LOG_OUTPUT_SIZE) {
|
|
143
|
+
logsTruncated = true;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
totalLogChars += message.length;
|
|
147
|
+
logs.push({
|
|
148
|
+
level,
|
|
149
|
+
message,
|
|
150
|
+
timestamp: new Date().toISOString()
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
const buildEnvelope = (params) => ({
|
|
154
|
+
ok: params.ok,
|
|
155
|
+
result: params.result,
|
|
156
|
+
logs,
|
|
157
|
+
error: params.error,
|
|
158
|
+
meta: {
|
|
159
|
+
timeoutMs: EXECUTION_TIMEOUT_MS,
|
|
160
|
+
durationMs: Date.now() - startedAt,
|
|
161
|
+
truncatedResult: params.truncatedResult,
|
|
162
|
+
truncatedLogs: logsTruncated,
|
|
163
|
+
resultWasUndefined: params.resultWasUndefined
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
if (!code || !code.trim()) {
|
|
167
|
+
const error = normalizeFailure(new ExecutionError('Code cannot be empty', { code: 'EMPTY_CODE' }));
|
|
168
|
+
return buildEnvelope({
|
|
169
|
+
ok: false,
|
|
170
|
+
result: null,
|
|
171
|
+
error,
|
|
172
|
+
truncatedResult: false,
|
|
173
|
+
resultWasUndefined: false
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
const timers = new Set();
|
|
177
|
+
let executionTimeout = null;
|
|
178
|
+
let asyncCallbackError = null;
|
|
179
|
+
const wrapTimerHandler = (handler) => {
|
|
180
|
+
return () => {
|
|
181
|
+
try {
|
|
182
|
+
handler();
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
const normalizedError = error instanceof Error
|
|
186
|
+
? error
|
|
187
|
+
: new Error(String(error));
|
|
188
|
+
asyncCallbackError = normalizedError;
|
|
189
|
+
appendLog('error', ['Timer callback error:', normalizedError.message]);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
};
|
|
193
|
+
const ensureTimerHandler = (handler) => {
|
|
194
|
+
if (typeof handler !== 'function') {
|
|
195
|
+
throw new ExecutionError('Timer handler must be a function', {
|
|
196
|
+
code: 'INVALID_TIMER_HANDLER'
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return wrapTimerHandler(handler);
|
|
200
|
+
};
|
|
201
|
+
const apiSurface = deepFreeze({
|
|
202
|
+
repos,
|
|
203
|
+
prds,
|
|
204
|
+
git,
|
|
205
|
+
state,
|
|
206
|
+
steward: {
|
|
207
|
+
help: () => getStewardHelp()
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
const sandbox = {
|
|
211
|
+
...apiSurface,
|
|
212
|
+
console: deepFreeze({
|
|
213
|
+
log: (...args) => appendLog('log', args),
|
|
214
|
+
info: (...args) => appendLog('info', args),
|
|
215
|
+
warn: (...args) => appendLog('warn', args),
|
|
216
|
+
error: (...args) => appendLog('error', args)
|
|
217
|
+
}),
|
|
218
|
+
setTimeout: (handler, timeout) => {
|
|
219
|
+
if (timers.size >= MAX_TIMERS) {
|
|
220
|
+
throw new ExecutionError(`Timer limit exceeded (max ${MAX_TIMERS})`, {
|
|
221
|
+
code: 'TIMER_LIMIT'
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
const wrappedHandler = ensureTimerHandler(handler);
|
|
225
|
+
const timer = setTimeout(() => {
|
|
226
|
+
timers.delete(timer);
|
|
227
|
+
wrappedHandler();
|
|
228
|
+
}, timeout);
|
|
229
|
+
timers.add(timer);
|
|
230
|
+
return timer;
|
|
231
|
+
},
|
|
232
|
+
clearTimeout: (timer) => {
|
|
233
|
+
timers.delete(timer);
|
|
234
|
+
clearTimeout(timer);
|
|
235
|
+
},
|
|
236
|
+
setInterval: (handler, timeout) => {
|
|
237
|
+
if (timers.size >= MAX_TIMERS) {
|
|
238
|
+
throw new ExecutionError(`Timer limit exceeded (max ${MAX_TIMERS})`, {
|
|
239
|
+
code: 'TIMER_LIMIT'
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
const wrappedHandler = ensureTimerHandler(handler);
|
|
243
|
+
const timer = setInterval(wrappedHandler, timeout);
|
|
244
|
+
timers.add(timer);
|
|
245
|
+
return timer;
|
|
246
|
+
},
|
|
247
|
+
clearInterval: (timer) => {
|
|
248
|
+
timers.delete(timer);
|
|
249
|
+
clearInterval(timer);
|
|
250
|
+
},
|
|
251
|
+
Promise
|
|
252
|
+
};
|
|
253
|
+
const wrappedCode = `
|
|
254
|
+
(async () => {
|
|
255
|
+
${code}
|
|
256
|
+
})()
|
|
257
|
+
`;
|
|
258
|
+
try {
|
|
259
|
+
const script = new vm.Script(wrappedCode, {
|
|
260
|
+
filename: 'codemode.js'
|
|
261
|
+
});
|
|
262
|
+
const context = vm.createContext(sandbox);
|
|
263
|
+
const executionPromise = Promise.resolve(script.runInContext(context, {
|
|
264
|
+
timeout: EXECUTION_TIMEOUT_MS
|
|
265
|
+
}));
|
|
266
|
+
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
267
|
+
executionTimeout = setTimeout(() => {
|
|
268
|
+
reject(new ExecutionError(`Execution timed out after ${EXECUTION_TIMEOUT_MS}ms`, {
|
|
269
|
+
code: 'TIMEOUT'
|
|
270
|
+
}));
|
|
271
|
+
}, EXECUTION_TIMEOUT_MS);
|
|
272
|
+
});
|
|
273
|
+
const rawResult = await Promise.race([executionPromise, timeoutPromise]);
|
|
274
|
+
if (asyncCallbackError instanceof Error) {
|
|
275
|
+
throw new ExecutionError(asyncCallbackError.message, {
|
|
276
|
+
code: 'ASYNC_CALLBACK_ERROR',
|
|
277
|
+
stackTrace: asyncCallbackError.stack
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
const truncated = truncateResult(rawResult);
|
|
281
|
+
return buildEnvelope({
|
|
282
|
+
ok: true,
|
|
283
|
+
result: truncated.result,
|
|
284
|
+
error: null,
|
|
285
|
+
truncatedResult: truncated.truncatedResult,
|
|
286
|
+
resultWasUndefined: truncated.resultWasUndefined
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
const failure = normalizeFailure(error);
|
|
291
|
+
appendLog('error', [`${failure.code}: ${failure.message}`]);
|
|
292
|
+
return buildEnvelope({
|
|
293
|
+
ok: false,
|
|
294
|
+
result: null,
|
|
295
|
+
error: failure,
|
|
296
|
+
truncatedResult: false,
|
|
297
|
+
resultWasUndefined: false
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
if (executionTimeout) {
|
|
302
|
+
clearTimeout(executionTimeout);
|
|
303
|
+
}
|
|
304
|
+
timers.forEach((timer) => {
|
|
305
|
+
clearTimeout(timer);
|
|
306
|
+
clearInterval(timer);
|
|
307
|
+
});
|
|
308
|
+
timers.clear();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async function readStdin() {
|
|
312
|
+
return await new Promise((resolveInput) => {
|
|
313
|
+
let input = '';
|
|
314
|
+
process.stdin.setEncoding('utf-8');
|
|
315
|
+
process.stdin.on('data', (chunk) => {
|
|
316
|
+
input += chunk;
|
|
317
|
+
});
|
|
318
|
+
process.stdin.on('end', () => {
|
|
319
|
+
resolveInput(input);
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
function printEnvelope(envelope) {
|
|
324
|
+
process.stdout.write(JSON.stringify(envelope));
|
|
325
|
+
}
|
|
326
|
+
function buildBootstrapFailure(code, message) {
|
|
327
|
+
return {
|
|
328
|
+
ok: false,
|
|
329
|
+
result: null,
|
|
330
|
+
logs: [],
|
|
331
|
+
error: {
|
|
332
|
+
code,
|
|
333
|
+
message
|
|
334
|
+
},
|
|
335
|
+
meta: {
|
|
336
|
+
timeoutMs: EXECUTION_TIMEOUT_MS,
|
|
337
|
+
durationMs: 0,
|
|
338
|
+
truncatedResult: false,
|
|
339
|
+
truncatedLogs: false,
|
|
340
|
+
resultWasUndefined: false
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
async function main() {
|
|
345
|
+
const rawInput = await readStdin();
|
|
346
|
+
if (!rawInput.trim()) {
|
|
347
|
+
printEnvelope(buildBootstrapFailure('EMPTY_PAYLOAD', 'Execution payload is empty'));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
let payload;
|
|
351
|
+
try {
|
|
352
|
+
payload = JSON.parse(rawInput);
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
printEnvelope(buildBootstrapFailure('INVALID_PAYLOAD', `Failed to parse execution payload: ${String(error)}`));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (typeof payload.code !== 'string') {
|
|
359
|
+
printEnvelope(buildBootstrapFailure('INVALID_PAYLOAD', 'Execution payload must include a string "code" field'));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const envelope = await execute(payload.code);
|
|
363
|
+
printEnvelope(envelope);
|
|
364
|
+
}
|
|
365
|
+
main().catch((error) => {
|
|
366
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
367
|
+
printEnvelope(buildBootstrapFailure('RUNNER_FAILURE', message));
|
|
368
|
+
});
|