pi-onedev-toolkit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +57 -0
- package/extensions/onedev/index.ts +1 -0
- package/package.json +67 -0
- package/skills/onedev-issue-to-pr/SKILL.md +55 -0
- package/skills/onedev-pr-review/SKILL.md +65 -0
- package/src/context.ts +97 -0
- package/src/extension.ts +72 -0
- package/src/tod.ts +103 -0
- package/src/tools/build.ts +170 -0
- package/src/tools/common.ts +102 -0
- package/src/tools/index.ts +126 -0
- package/src/tools/issue.ts +225 -0
- package/src/tools/pull.ts +298 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
- Initial release.
|
|
6
|
+
- `onedev_tools` domain activator with context, login, and attachment-download actions.
|
|
7
|
+
- `onedev_issue`, `onedev_pull`, `onedev_build` tools wrapping the TOD CLI.
|
|
8
|
+
- `/od-context` and `/od-health` commands; session status line with the inferred project.
|
|
9
|
+
- `onedev-issue-to-pr` and `onedev-pr-review` skills.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alper Tarhan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# pi-onedev-toolkit
|
|
2
|
+
|
|
3
|
+
Lightweight [OneDev](https://onedev.io/) integration for [Pi](https://pi.dev/), built on the official [TOD CLI](https://code.onedev.io/onedev/tod): repo-aware issue, pull request, and build tools plus issue-to-PR and PR-review skills.
|
|
4
|
+
|
|
5
|
+
The agent never shells out to `tod` itself and never sees tokens. The extension resolves the OneDev project from the repository's git remotes once per session, and the tools speak OneDev terms (`123`, `PROJ-123`, `project#123` all resolve server-side).
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- OneDev 15.1+
|
|
10
|
+
- The `tod` CLI installed and configured:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
curl -fsSL https://code.onedev.io/onedev/tod/~raw/main/install.sh | bash
|
|
14
|
+
tod config set # server-url + access-token (or set ONEDEV_SERVER_URL / ONEDEV_ACCESS_TOKEN)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pi install npm:pi-onedev-toolkit
|
|
21
|
+
# or from source
|
|
22
|
+
pi install git:github.com/alpertarhan/pi-onedev-toolkit
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then run `/od-health` once. When the current directory is a OneDev repository, the status line shows the active project.
|
|
26
|
+
|
|
27
|
+
## Tools
|
|
28
|
+
|
|
29
|
+
| Tool | Domains | Covers |
|
|
30
|
+
| --- | --- | --- |
|
|
31
|
+
| `onedev_tools` (always active) | — | Activate domains; session context; login name; download issue/PR attachments |
|
|
32
|
+
| `onedev_issue` | issue | Query/get/comment issues, change fields/labels/state, log work, issue branches |
|
|
33
|
+
| `onedev_pull` | pull | PR metadata/comments/code comments/labels/builds/patch, create/edit, approve, request changes, guarded merge/discard |
|
|
34
|
+
| `onedev_build` | build | Build queries, logs, code-problem reports, changes since last success, run jobs (incl. uncommitted local changes), build-spec validation |
|
|
35
|
+
|
|
36
|
+
Lazy by design: domain tools activate only through `onedev_tools`, keeping the default tool list light. `merge` and `discard` additionally require `confirm: true`.
|
|
37
|
+
|
|
38
|
+
## Commands
|
|
39
|
+
|
|
40
|
+
- `/od-context` — active server, inferred project, login
|
|
41
|
+
- `/od-health` — re-check tod configuration and server reachability
|
|
42
|
+
|
|
43
|
+
## Skills
|
|
44
|
+
|
|
45
|
+
- `onedev-issue-to-pr` — gather issue context, branch, implement, present for review, then push and open the PR
|
|
46
|
+
- `onedev-pr-review` — full-context PR review with line-anchored findings; submits only on explicit request
|
|
47
|
+
|
|
48
|
+
## Development
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
bun install
|
|
52
|
+
bun run check # typecheck + vitest
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "../../src/extension.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-onedev-toolkit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Lightweight OneDev integration for Pi via the TOD CLI: repo-aware issue, pull request, and build tools",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Alper Tarhan",
|
|
8
|
+
"url": "https://github.com/alpertarhan"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/alpertarhan/pi-onedev-toolkit#readme",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/alpertarhan/pi-onedev-toolkit.git"
|
|
14
|
+
},
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/alpertarhan/pi-onedev-toolkit/issues"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"pi-package",
|
|
20
|
+
"pi",
|
|
21
|
+
"pi-extension",
|
|
22
|
+
"onedev",
|
|
23
|
+
"tod",
|
|
24
|
+
"coding-agent",
|
|
25
|
+
"developer-tools"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"files": [
|
|
29
|
+
"extensions",
|
|
30
|
+
"skills",
|
|
31
|
+
"src",
|
|
32
|
+
"README.md",
|
|
33
|
+
"CHANGELOG.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"check": "bun run typecheck && bun run test",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"prepack": "bun run check"
|
|
41
|
+
},
|
|
42
|
+
"pi": {
|
|
43
|
+
"extensions": [
|
|
44
|
+
"./extensions"
|
|
45
|
+
],
|
|
46
|
+
"skills": [
|
|
47
|
+
"./skills"
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@earendil-works/pi-ai": "*",
|
|
52
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
53
|
+
"typebox": "*"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@earendil-works/pi-ai": "0.84.1",
|
|
57
|
+
"@earendil-works/pi-coding-agent": "0.84.1",
|
|
58
|
+
"@types/node": "^20.17.0",
|
|
59
|
+
"typebox": "1.3.12",
|
|
60
|
+
"typescript": "^7.0.2",
|
|
61
|
+
"vitest": "^4.1.10"
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=20.3"
|
|
65
|
+
},
|
|
66
|
+
"packageManager": "bun@1.3.14"
|
|
67
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: onedev-issue-to-pr
|
|
3
|
+
description: Implement a OneDev issue end to end - gather issue context, branch, implement, then (after user approval) push and open a linked pull request. Use when the user asks to work on, implement, or fix a OneDev issue.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# OneDev issue to pull request
|
|
7
|
+
|
|
8
|
+
## Prerequisites
|
|
9
|
+
|
|
10
|
+
- Call `onedev_tools` with domains `["issue", "pull"]` first; note your login name from the returned context.
|
|
11
|
+
- The current repository belongs to the issue's project (plain refs like `123` resolve against it).
|
|
12
|
+
|
|
13
|
+
## Workflow
|
|
14
|
+
|
|
15
|
+
1. **Resolve the issue reference.** Use the reference from the prompt, or derive it
|
|
16
|
+
with `onedev_issue` action `current_reference`. If neither works, stop and report.
|
|
17
|
+
|
|
18
|
+
2. **Prepare the checkout** (unless the user says otherwise):
|
|
19
|
+
`onedev_issue` action `checkout` with `ref` and `for_write: true`.
|
|
20
|
+
It creates the server-side issue branch and sets up tracking.
|
|
21
|
+
If the working copy is dirty, report it and stop — do not stash on your own.
|
|
22
|
+
|
|
23
|
+
3. **Read the full issue context** — never work from the prompt alone:
|
|
24
|
+
- `onedev_issue` `get` (title, description, fields, submitter)
|
|
25
|
+
- `onedev_issue` `get_comments`
|
|
26
|
+
- Match your login name against submitter/comment authors to understand your role.
|
|
27
|
+
- Download embedded resources (screenshots, logs) with `onedev_tools` action
|
|
28
|
+
`download`, passing the URL exactly as it appears in the markdown.
|
|
29
|
+
- If a failed build is part of the issue, gather evidence with `onedev_build`
|
|
30
|
+
(`get`, `get_log`) before planning changes. Follow `Dependency build … failed`
|
|
31
|
+
chains: same-commit dependency failures are the real target; different-commit
|
|
32
|
+
ones mean the failure is inherited.
|
|
33
|
+
|
|
34
|
+
4. **Implement the work.** Code changes, or an explanatory issue comment if no code
|
|
35
|
+
is needed. Run the smallest relevant test. Optionally validate CI locally with
|
|
36
|
+
`onedev_build` `run` (`mode: "local"`) before committing.
|
|
37
|
+
|
|
38
|
+
5. **STOP — present for review.** Summarize the change and the comment you intend
|
|
39
|
+
to post. Do not push or post anything yet. This is the mandatory checkpoint.
|
|
40
|
+
|
|
41
|
+
6. **After explicit user approval, submit:**
|
|
42
|
+
- Compose the commit message against the server requirements:
|
|
43
|
+
`onedev_pull` `commit_message_requirement` (and `title_description_requirement`
|
|
44
|
+
before creating the PR). Empty output means no requirement.
|
|
45
|
+
- `git add -A && git commit && git push` on the issue branch.
|
|
46
|
+
- Create the PR: `onedev_pull` `create` with `title`, `description`, and
|
|
47
|
+
`target_branch` (source branch defaults to the current branch). The issue link
|
|
48
|
+
is inferred from the branch; state it in the description if not.
|
|
49
|
+
- Post the drafted comment with `onedev_issue` `add_comment` if one was prepared.
|
|
50
|
+
|
|
51
|
+
## Notes
|
|
52
|
+
|
|
53
|
+
- Write comment text in Markdown.
|
|
54
|
+
- Reference entities as `#123` (current project), `project#123`, or `PROJ-123`.
|
|
55
|
+
- If blocked mid-workflow, report the command and error, and stop.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: onedev-pr-review
|
|
3
|
+
description: Review a OneDev pull request - gather PR context, builds, and fixed issues, read the review-scoped patch, draft findings with line-anchored code comments, and submit the review or merge only on explicit request. Use when the user asks to review, address feedback on, or merge a OneDev pull request.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# OneDev pull request review
|
|
7
|
+
|
|
8
|
+
## Prerequisites
|
|
9
|
+
|
|
10
|
+
- Call `onedev_tools` with domains `["pull", "issue", "build"]`; note your login name.
|
|
11
|
+
- The current repository belongs to the PR's source or target project.
|
|
12
|
+
|
|
13
|
+
## Gather context (never review from the patch alone)
|
|
14
|
+
|
|
15
|
+
1. **Resolve the PR reference** from the prompt or `onedev_pull` `current_reference`.
|
|
16
|
+
|
|
17
|
+
2. **Checkout** for inspection: `onedev_pull` `checkout` (`for_write: true` when you
|
|
18
|
+
will change code). Dirty working copy → report and stop.
|
|
19
|
+
|
|
20
|
+
3. **Read everything:**
|
|
21
|
+
- `onedev_pull` `get` — title, description, source/target, merge strategy, status,
|
|
22
|
+
submitter, reviewers, review state.
|
|
23
|
+
- `onedev_pull` `get_comments` and `get_code_comments` — note code-comment `id`s,
|
|
24
|
+
file, line range, resolution state, replies.
|
|
25
|
+
- Fixed issues: `onedev_issue` `list` with query `fixed in pull request "<ref>"`,
|
|
26
|
+
then `get` / `get_comments` on each. Treat fixed issues as the primary spec
|
|
27
|
+
for what the PR should accomplish.
|
|
28
|
+
- `onedev_pull` `get_builds` — investigate every `FAILED` build with
|
|
29
|
+
`onedev_build` `get` / `get_log` before reviewing code.
|
|
30
|
+
- Download embedded resources via `onedev_tools` `download`.
|
|
31
|
+
- Match your login name against authors to understand your role
|
|
32
|
+
(pending reviewer, prior commenter, outsider).
|
|
33
|
+
|
|
34
|
+
## Review
|
|
35
|
+
|
|
36
|
+
1. Read the review-scoped patch: `onedev_pull` `get_patch` with
|
|
37
|
+
`for_code_review: true`. Read files in the checkout when the patch lacks context.
|
|
38
|
+
Check correctness, edge cases, security, style, and test coverage.
|
|
39
|
+
|
|
40
|
+
2. **Triage prior discussion:** resolve addressed code comments, reply where new
|
|
41
|
+
information is needed, leave unrelated unresolved ones alone. Never modify
|
|
42
|
+
another user's code comments.
|
|
43
|
+
|
|
44
|
+
3. **Draft findings:**
|
|
45
|
+
- Line-anchored findings → `onedev_pull` `add_code_comment` (`file`, `from_line`,
|
|
46
|
+
optional `to_line` — must be an added or context line visible in the patch).
|
|
47
|
+
- Outside-patch findings → one overall PR comment with file paths and lines.
|
|
48
|
+
- Review outcome if you are a pending reviewer: `approve` or `request_changes`
|
|
49
|
+
with a `summary`.
|
|
50
|
+
|
|
51
|
+
4. **STOP — present the draft** (findings, replies, outcomes, merge plan).
|
|
52
|
+
Submit nothing yet.
|
|
53
|
+
|
|
54
|
+
5. **On explicit request only:**
|
|
55
|
+
- Post findings/comments/review outcome.
|
|
56
|
+
- Merge: `merge` requires `confirm: true`. For squash merges, compose the commit
|
|
57
|
+
message from PR title/description + fixed issues, satisfying
|
|
58
|
+
`commit_message_requirement` for the target project/branch.
|
|
59
|
+
- Resolving merge conflicts: fetch the target branch, merge with the PR's
|
|
60
|
+
strategy without committing, resolve, verify clean `git status`, commit.
|
|
61
|
+
For rebase-based strategies, ask for direction before rewriting commits.
|
|
62
|
+
|
|
63
|
+
## Notes
|
|
64
|
+
|
|
65
|
+
- Comment text is Markdown; entity refs are `#123`, `project#123`, or `PROJ-123`.
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { CommandExecutor } from "./tod.js";
|
|
2
|
+
import { runTod } from "./tod.js";
|
|
3
|
+
|
|
4
|
+
export interface OneDevSessionContext {
|
|
5
|
+
readonly cwd: string;
|
|
6
|
+
readonly status: "ready" | "unavailable";
|
|
7
|
+
readonly serverUrl?: string;
|
|
8
|
+
/** Project path inferred from the repository's git remotes (e.g. "group/myproject"). */
|
|
9
|
+
readonly project?: string;
|
|
10
|
+
readonly loginName?: string;
|
|
11
|
+
/** Human-readable setup guidance when status is "unavailable". */
|
|
12
|
+
readonly problem?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const TOD_INSTALL_HINT =
|
|
16
|
+
"tod is not installed. Install it (curl -fsSL https://code.onedev.io/onedev/tod/~raw/main/install.sh | bash), then run `tod config set`.";
|
|
17
|
+
|
|
18
|
+
function unavailable(cwd: string, problem: string): OneDevSessionContext {
|
|
19
|
+
return { cwd, status: "unavailable", problem };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Probe the local OneDev setup once per session:
|
|
24
|
+
* 1. `tod config get server-url` — tod present and configured
|
|
25
|
+
* 2. `tod get-login-name` — server reachable and access token accepted
|
|
26
|
+
* 3. `tod project current` — repo-aware project context (optional)
|
|
27
|
+
*/
|
|
28
|
+
export async function probeOneDevContext(
|
|
29
|
+
exec: CommandExecutor,
|
|
30
|
+
cwd: string,
|
|
31
|
+
signal?: AbortSignal,
|
|
32
|
+
): Promise<OneDevSessionContext> {
|
|
33
|
+
let serverUrl: string;
|
|
34
|
+
try {
|
|
35
|
+
const configured = await runTod(exec, ["config", "get", "server-url"], {
|
|
36
|
+
cwd,
|
|
37
|
+
timeoutMs: 10_000,
|
|
38
|
+
signal,
|
|
39
|
+
maxOutputBytes: 2_000,
|
|
40
|
+
});
|
|
41
|
+
serverUrl = configured.text.trim();
|
|
42
|
+
} catch (cause) {
|
|
43
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
44
|
+
return unavailable(
|
|
45
|
+
cwd,
|
|
46
|
+
message.startsWith("tod is not usable") ? TOD_INSTALL_HINT : message,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (!serverUrl) {
|
|
50
|
+
return unavailable(
|
|
51
|
+
cwd,
|
|
52
|
+
"tod is installed but not configured. Run `tod config set` (server-url and access-token), or set ONEDEV_SERVER_URL and ONEDEV_ACCESS_TOKEN.",
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let loginName: string | undefined;
|
|
57
|
+
try {
|
|
58
|
+
const login = await runTod(exec, ["get-login-name"], {
|
|
59
|
+
cwd,
|
|
60
|
+
timeoutMs: 15_000,
|
|
61
|
+
signal,
|
|
62
|
+
maxOutputBytes: 2_000,
|
|
63
|
+
});
|
|
64
|
+
loginName = login.text.trim() || undefined;
|
|
65
|
+
} catch (cause) {
|
|
66
|
+
return unavailable(
|
|
67
|
+
cwd,
|
|
68
|
+
`OneDev server or access token rejected (${cause instanceof Error ? cause.message : String(cause)}). Run \`tod config set\` to fix credentials.`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let project: string | undefined;
|
|
73
|
+
try {
|
|
74
|
+
const current = await runTod(exec, ["project", "current"], {
|
|
75
|
+
cwd,
|
|
76
|
+
timeoutMs: 15_000,
|
|
77
|
+
signal,
|
|
78
|
+
maxOutputBytes: 2_000,
|
|
79
|
+
});
|
|
80
|
+
project = current.text.trim() || undefined;
|
|
81
|
+
} catch {
|
|
82
|
+
// Not a OneDev repository: fine. Tools accept explicit projects/qualified refs.
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { cwd, status: "ready", serverUrl, loginName, project };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function formatContext(context: OneDevSessionContext): string {
|
|
89
|
+
if (context.status === "unavailable") {
|
|
90
|
+
return `OneDev unavailable: ${context.problem ?? "unknown problem"}`;
|
|
91
|
+
}
|
|
92
|
+
const parts = [`server ${context.serverUrl}`];
|
|
93
|
+
if (context.project) parts.push(`project ${context.project}`);
|
|
94
|
+
else parts.push("no OneDev project inferred from git remotes");
|
|
95
|
+
if (context.loginName) parts.push(`user ${context.loginName}`);
|
|
96
|
+
return `OneDev ready: ${parts.join(" · ")}`;
|
|
97
|
+
}
|
package/src/extension.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
formatContext,
|
|
4
|
+
probeOneDevContext,
|
|
5
|
+
type OneDevSessionContext,
|
|
6
|
+
} from "./context.js";
|
|
7
|
+
import {
|
|
8
|
+
registerOnedevTools,
|
|
9
|
+
type OnedevToolController,
|
|
10
|
+
} from "./tools/index.js";
|
|
11
|
+
import type { ToolDeps } from "./tools/common.js";
|
|
12
|
+
|
|
13
|
+
export default function onedevExtension(pi: ExtensionAPI): void {
|
|
14
|
+
let context: OneDevSessionContext | undefined;
|
|
15
|
+
let controller: OnedevToolController | undefined;
|
|
16
|
+
|
|
17
|
+
const deps: ToolDeps = {
|
|
18
|
+
exec: (command, args, options) => pi.exec(command, args, options),
|
|
19
|
+
context: () => {
|
|
20
|
+
if (!context) throw new Error("OneDev extension is still initializing");
|
|
21
|
+
return context;
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const applyStatus = (ctx: {
|
|
26
|
+
hasUI: boolean;
|
|
27
|
+
ui: { setStatus: (key: string, value: string | undefined) => void };
|
|
28
|
+
}): void => {
|
|
29
|
+
if (!ctx.hasUI) return;
|
|
30
|
+
if (context?.status === "ready" && context.project) {
|
|
31
|
+
ctx.ui.setStatus("onedev", `onedev: ${context.project}`);
|
|
32
|
+
} else {
|
|
33
|
+
ctx.ui.setStatus("onedev", undefined);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
pi.registerCommand("od-context", {
|
|
38
|
+
description: "Show the active OneDev server, project, and user",
|
|
39
|
+
handler: async (_args, ctx) => {
|
|
40
|
+
if (!context) throw new Error("OneDev extension is still initializing");
|
|
41
|
+
ctx.ui.notify(
|
|
42
|
+
formatContext(context),
|
|
43
|
+
context.status === "ready" ? "info" : "warning",
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
pi.registerCommand("od-health", {
|
|
49
|
+
description: "Re-check the tod CLI configuration and OneDev server",
|
|
50
|
+
handler: async (_args, ctx) => {
|
|
51
|
+
context = await probeOneDevContext(deps.exec, ctx.cwd);
|
|
52
|
+
applyStatus(ctx);
|
|
53
|
+
ctx.ui.notify(
|
|
54
|
+
formatContext(context),
|
|
55
|
+
context.status === "ready" ? "info" : "warning",
|
|
56
|
+
);
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
controller = registerOnedevTools(pi, deps);
|
|
61
|
+
|
|
62
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
63
|
+
controller?.reset();
|
|
64
|
+
context = await probeOneDevContext(deps.exec, ctx.cwd);
|
|
65
|
+
applyStatus(ctx);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
69
|
+
if (ctx.hasUI) ctx.ui.setStatus("onedev", undefined);
|
|
70
|
+
context = undefined;
|
|
71
|
+
});
|
|
72
|
+
}
|
package/src/tod.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin executor around the OneDev `tod` CLI.
|
|
3
|
+
*
|
|
4
|
+
* Contract (verified against tod source): every subcommand writes the raw
|
|
5
|
+
* OneDev server payload to stdout and logs progress to stderr, so stdout is
|
|
6
|
+
* the tool result and stderr is error/diagnostic context.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface ExecResult {
|
|
10
|
+
stdout: string;
|
|
11
|
+
stderr: string;
|
|
12
|
+
code: number;
|
|
13
|
+
killed: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type CommandExecutor = (
|
|
17
|
+
command: string,
|
|
18
|
+
args: string[],
|
|
19
|
+
options?: { cwd?: string; timeout?: number; signal?: AbortSignal },
|
|
20
|
+
) => Promise<ExecResult>;
|
|
21
|
+
|
|
22
|
+
export class TodError extends Error {
|
|
23
|
+
constructor(message: string, options?: { cause?: unknown }) {
|
|
24
|
+
super(message, options);
|
|
25
|
+
this.name = "TodError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface TodRunOptions {
|
|
30
|
+
cwd?: string;
|
|
31
|
+
timeoutMs?: number;
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
maxOutputBytes?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface TodOutput {
|
|
37
|
+
text: string;
|
|
38
|
+
truncated: boolean;
|
|
39
|
+
originalBytes: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const DEFAULT_TIMEOUT_MS = 30_000;
|
|
43
|
+
export const BUILD_RUN_TIMEOUT_MS = 10 * 60_000;
|
|
44
|
+
export const DEFAULT_MAX_OUTPUT_BYTES = 48_000;
|
|
45
|
+
|
|
46
|
+
function tail(value: string, maxChars = 500): string {
|
|
47
|
+
const cleaned = value.replace(/\s+/g, " ").trim();
|
|
48
|
+
return cleaned.length > maxChars ? `…${cleaned.slice(-maxChars)}` : cleaned;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Byte-bounded view keeping the head (metadata) and tail (newest entries,
|
|
53
|
+
* log ends) of a large payload.
|
|
54
|
+
*/
|
|
55
|
+
export function truncateMiddle(text: string, maxBytes: number): TodOutput {
|
|
56
|
+
const buffer = Buffer.from(text, "utf8");
|
|
57
|
+
const originalBytes = buffer.length;
|
|
58
|
+
if (originalBytes <= maxBytes) {
|
|
59
|
+
return { text, truncated: false, originalBytes };
|
|
60
|
+
}
|
|
61
|
+
const head = Math.floor(maxBytes * 0.6);
|
|
62
|
+
const tailBytes = maxBytes - head;
|
|
63
|
+
const omitted = originalBytes - maxBytes;
|
|
64
|
+
return {
|
|
65
|
+
text:
|
|
66
|
+
buffer.subarray(0, head).toString("utf8") +
|
|
67
|
+
`\n[… truncated ${omitted} bytes …]\n` +
|
|
68
|
+
buffer.subarray(originalBytes - tailBytes).toString("utf8"),
|
|
69
|
+
truncated: true,
|
|
70
|
+
originalBytes,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function runTod(
|
|
75
|
+
exec: CommandExecutor,
|
|
76
|
+
args: readonly string[],
|
|
77
|
+
options: TodRunOptions = {},
|
|
78
|
+
): Promise<TodOutput> {
|
|
79
|
+
let result: ExecResult;
|
|
80
|
+
try {
|
|
81
|
+
result = await exec("tod", [...args], {
|
|
82
|
+
cwd: options.cwd,
|
|
83
|
+
timeout: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
84
|
+
signal: options.signal,
|
|
85
|
+
});
|
|
86
|
+
} catch (cause) {
|
|
87
|
+
throw new TodError(
|
|
88
|
+
`tod is not usable: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
89
|
+
{ cause },
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (result.code !== 0) {
|
|
93
|
+
const scope = args.slice(0, 2).join(" ");
|
|
94
|
+
const detail = tail(result.stderr || result.stdout);
|
|
95
|
+
throw new TodError(
|
|
96
|
+
`tod ${scope} failed (exit ${result.code})${detail ? `: ${detail}` : ""}`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return truncateMiddle(
|
|
100
|
+
result.stdout.trimEnd(),
|
|
101
|
+
options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES,
|
|
102
|
+
);
|
|
103
|
+
}
|