opencode-froggy 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/README.md +440 -0
- package/agent/code-reviewer.md +89 -0
- package/agent/code-simplifier.md +77 -0
- package/agent/doc-writer.md +101 -0
- package/command/commit.md +18 -0
- package/command/review-changes.md +28 -0
- package/command/review-pr.md +29 -0
- package/command/simplify-changes.md +26 -0
- package/command/tests-coverage.md +7 -0
- package/dist/bash-executor.d.ts +15 -0
- package/dist/bash-executor.js +45 -0
- package/dist/code-files.d.ts +3 -0
- package/dist/code-files.js +50 -0
- package/dist/code-files.test.d.ts +1 -0
- package/dist/code-files.test.js +22 -0
- package/dist/config-paths.d.ts +11 -0
- package/dist/config-paths.js +32 -0
- package/dist/config-paths.test.d.ts +1 -0
- package/dist/config-paths.test.js +101 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +288 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +808 -0
- package/dist/loaders.d.ts +80 -0
- package/dist/loaders.js +135 -0
- package/dist/logger.d.ts +2 -0
- package/dist/logger.js +15 -0
- package/package.json +51 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Commit and push
|
|
3
|
+
agent: build
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
- Current git status: !`git status`
|
|
9
|
+
- Current git diff (staged and unstaged changes): !`git diff HEAD`
|
|
10
|
+
- Current branch: !`git branch --show-current`
|
|
11
|
+
|
|
12
|
+
## Your task
|
|
13
|
+
|
|
14
|
+
Based on the above changes:
|
|
15
|
+
1. Create a new branch if on main or master
|
|
16
|
+
2. Create a single commit with an appropriate message
|
|
17
|
+
3. Push the branch to origin
|
|
18
|
+
4. You have the capability to call multiple tools in a single response. You MUST do all of the above in a single message. Do not use any other tools or do anything else. Do not send any other text or messages besides these tool calls.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Review uncommitted changes (staged + unstaged, incl. untracked diffs)
|
|
3
|
+
agent: code-reviewer
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Review: Working Tree → HEAD
|
|
7
|
+
|
|
8
|
+
CONSTRAINTS:
|
|
9
|
+
- Base analysis ONLY on git outputs below and AGENTS.md (if present)
|
|
10
|
+
- For more context: request `-U10` or `git show "HEAD:<path>"`
|
|
11
|
+
- Untracked files are shown via `git diff --no-index` (working tree only)
|
|
12
|
+
|
|
13
|
+
## 1. Status overview
|
|
14
|
+
!`git status --porcelain=v1 -uall`
|
|
15
|
+
|
|
16
|
+
## 2. Staged changes (will be committed)
|
|
17
|
+
!`git diff --cached --stat`
|
|
18
|
+
!`git diff --cached --name-status`
|
|
19
|
+
!`git diff --cached -U5 --function-context`
|
|
20
|
+
|
|
21
|
+
## 3. Unstaged changes (won't be committed yet)
|
|
22
|
+
!`git diff --stat`
|
|
23
|
+
!`git diff --name-status`
|
|
24
|
+
!`git diff -U5 --function-context`
|
|
25
|
+
|
|
26
|
+
## 4. Untracked (new) files: list + diff (even if not staged)
|
|
27
|
+
!`git ls-files --others --exclude-standard || true`
|
|
28
|
+
!`git ls-files --others --exclude-standard -z | while IFS= read -r -d '' f; do echo "=== NEW: $f ==="; git diff --no-index -U5 --function-context -- /dev/null "$f" || true; echo; done`
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Review changes from source branch into target branch
|
|
3
|
+
agent: code-reviewer
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Review: origin/$1 → origin/$2
|
|
7
|
+
|
|
8
|
+
CONSTRAINTS:
|
|
9
|
+
- Base analysis ONLY on git outputs below and AGENTS.md (if present)
|
|
10
|
+
- For more context: request `-U10` or `git show "origin/$2:<path>"`
|
|
11
|
+
- Local working tree may differ from origin branches
|
|
12
|
+
|
|
13
|
+
## 1. Fetch latest
|
|
14
|
+
!`git fetch origin "$1" "$2" --prune`
|
|
15
|
+
|
|
16
|
+
## 2. Stats overview
|
|
17
|
+
!`git diff --stat "origin/$2...origin/$1"`
|
|
18
|
+
|
|
19
|
+
## 3. Commits to review
|
|
20
|
+
!`git log --oneline --no-merges "origin/$2..origin/$1"`
|
|
21
|
+
|
|
22
|
+
## 4. Files changed
|
|
23
|
+
!`git diff --name-only "origin/$2...origin/$1"`
|
|
24
|
+
|
|
25
|
+
## 5. Project rules (if any)
|
|
26
|
+
!`git show "origin/$2:AGENTS.md" 2>/dev/null || echo "No AGENTS.md in target branch"`
|
|
27
|
+
|
|
28
|
+
## 6. Full diff
|
|
29
|
+
!`git diff -U5 --function-context "origin/$2...origin/$1"`
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Simplify uncommitted changes (staged + unstaged, incl. untracked diffs)
|
|
3
|
+
agent: code-simplifier
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Review: Working Tree → HEAD
|
|
7
|
+
|
|
8
|
+
CONSTRAINTS:
|
|
9
|
+
- Untracked files are shown via `git diff --no-index` (working tree only)
|
|
10
|
+
|
|
11
|
+
## 1. Status overview
|
|
12
|
+
!`git status --porcelain=v1 -uall`
|
|
13
|
+
|
|
14
|
+
## 2. Staged changes (will be committed)
|
|
15
|
+
!`git diff --cached --stat`
|
|
16
|
+
!`git diff --cached --name-status`
|
|
17
|
+
!`git diff --cached -U5 --function-context`
|
|
18
|
+
|
|
19
|
+
## 3. Unstaged changes (won't be committed yet)
|
|
20
|
+
!`git diff --stat`
|
|
21
|
+
!`git diff --name-status`
|
|
22
|
+
!`git diff -U5 --function-context`
|
|
23
|
+
|
|
24
|
+
## 4. Untracked (new) files: list + diff (even if not staged)
|
|
25
|
+
!`git ls-files --others --exclude-standard || true`
|
|
26
|
+
!`git ls-files --others --exclude-standard -z | while IFS= read -r -d '' f; do echo "=== NEW: $f ==="; git diff --no-index -U5 --function-context -- /dev/null "$f" || true; echo; done`
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface BashContext {
|
|
2
|
+
session_id: string;
|
|
3
|
+
event: string;
|
|
4
|
+
cwd: string;
|
|
5
|
+
files?: string[];
|
|
6
|
+
tool_name?: string;
|
|
7
|
+
tool_args?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
export interface BashResult {
|
|
10
|
+
exitCode: number;
|
|
11
|
+
stdout: string;
|
|
12
|
+
stderr: string;
|
|
13
|
+
}
|
|
14
|
+
export declare const DEFAULT_BASH_TIMEOUT = 60000;
|
|
15
|
+
export declare function executeBashAction(command: string, timeout: number, context: BashContext, projectDir: string): Promise<BashResult>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export const DEFAULT_BASH_TIMEOUT = 60000;
|
|
3
|
+
export function executeBashAction(command, timeout, context, projectDir) {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
const env = {
|
|
6
|
+
...process.env,
|
|
7
|
+
OPENCODE_PROJECT_DIR: projectDir,
|
|
8
|
+
OPENCODE_SESSION_ID: context.session_id,
|
|
9
|
+
};
|
|
10
|
+
const child = spawn("bash", ["-c", command], {
|
|
11
|
+
cwd: context.cwd,
|
|
12
|
+
env,
|
|
13
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
14
|
+
});
|
|
15
|
+
let stdout = "";
|
|
16
|
+
let stderr = "";
|
|
17
|
+
let killed = false;
|
|
18
|
+
const timer = setTimeout(() => {
|
|
19
|
+
killed = true;
|
|
20
|
+
child.kill("SIGTERM");
|
|
21
|
+
}, timeout);
|
|
22
|
+
child.stdout.on("data", (data) => {
|
|
23
|
+
stdout += data.toString();
|
|
24
|
+
});
|
|
25
|
+
child.stderr.on("data", (data) => {
|
|
26
|
+
stderr += data.toString();
|
|
27
|
+
});
|
|
28
|
+
child.stdin.on("error", () => { });
|
|
29
|
+
child.stdin.write(JSON.stringify(context));
|
|
30
|
+
child.stdin.end();
|
|
31
|
+
child.on("close", (code) => {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
if (killed) {
|
|
34
|
+
resolve({ exitCode: 1, stdout, stderr: `Command timed out after ${timeout}ms` });
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
resolve({ exitCode: code ?? 1, stdout, stderr });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
child.on("error", (err) => {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
resolve({ exitCode: 1, stdout, stderr: err.message });
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { extname } from "node:path";
|
|
2
|
+
const CODE_EXTENSIONS = new Set([
|
|
3
|
+
".ts",
|
|
4
|
+
".tsx",
|
|
5
|
+
".js",
|
|
6
|
+
".jsx",
|
|
7
|
+
".mjs",
|
|
8
|
+
".cjs",
|
|
9
|
+
".json",
|
|
10
|
+
".yml",
|
|
11
|
+
".yaml",
|
|
12
|
+
".toml",
|
|
13
|
+
".css",
|
|
14
|
+
".scss",
|
|
15
|
+
".sass",
|
|
16
|
+
".less",
|
|
17
|
+
".html",
|
|
18
|
+
".vue",
|
|
19
|
+
".svelte",
|
|
20
|
+
".go",
|
|
21
|
+
".rs",
|
|
22
|
+
".c",
|
|
23
|
+
".h",
|
|
24
|
+
".cpp",
|
|
25
|
+
".cc",
|
|
26
|
+
".cxx",
|
|
27
|
+
".hpp",
|
|
28
|
+
".java",
|
|
29
|
+
".py",
|
|
30
|
+
".rb",
|
|
31
|
+
".php",
|
|
32
|
+
".sh",
|
|
33
|
+
".bash",
|
|
34
|
+
".kt",
|
|
35
|
+
".kts",
|
|
36
|
+
".swift",
|
|
37
|
+
".m",
|
|
38
|
+
".mm",
|
|
39
|
+
".cs",
|
|
40
|
+
".fs",
|
|
41
|
+
".scala",
|
|
42
|
+
".clj",
|
|
43
|
+
".hs",
|
|
44
|
+
".lua",
|
|
45
|
+
]);
|
|
46
|
+
function hasCodeExtension(filePath) {
|
|
47
|
+
const ext = extname(filePath).toLowerCase();
|
|
48
|
+
return Boolean(ext && CODE_EXTENSIONS.has(ext));
|
|
49
|
+
}
|
|
50
|
+
export { CODE_EXTENSIONS, hasCodeExtension };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { CODE_EXTENSIONS, hasCodeExtension } from "./code-files";
|
|
3
|
+
describe("hasCodeExtension", () => {
|
|
4
|
+
it("should return false for non-code extensions", () => {
|
|
5
|
+
expect(hasCodeExtension("/tmp/README.md")).toBe(false);
|
|
6
|
+
});
|
|
7
|
+
it("should return true for code extensions", () => {
|
|
8
|
+
expect(hasCodeExtension("/tmp/main.ts")).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
it("should handle uppercase extensions", () => {
|
|
11
|
+
expect(hasCodeExtension("/tmp/MAIN.JS")).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
it("should return false when no extension exists", () => {
|
|
14
|
+
expect(hasCodeExtension("/tmp/Makefile")).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
describe("CODE_EXTENSIONS", () => {
|
|
18
|
+
it("should include go and rust extensions", () => {
|
|
19
|
+
expect(CODE_EXTENSIONS.has(".go")).toBe(true);
|
|
20
|
+
expect(CODE_EXTENSIONS.has(".rs")).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the user-level config directory based on the OS.
|
|
3
|
+
* - Linux/macOS: XDG_CONFIG_HOME or ~/.config
|
|
4
|
+
* - Windows: Checks ~/.config first (cross-platform), then %APPDATA% (fallback)
|
|
5
|
+
*
|
|
6
|
+
* On Windows, prioritizes ~/.config for cross-platform consistency.
|
|
7
|
+
* Falls back to %APPDATA% for backward compatibility with existing installations.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getUserConfigDir(): string;
|
|
10
|
+
export declare function getGlobalHookDir(): string;
|
|
11
|
+
export declare function getProjectHookDir(directory: string): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const HOOKS_SUBPATH = join("opencode", "hook", "hooks.md");
|
|
5
|
+
/**
|
|
6
|
+
* Returns the user-level config directory based on the OS.
|
|
7
|
+
* - Linux/macOS: XDG_CONFIG_HOME or ~/.config
|
|
8
|
+
* - Windows: Checks ~/.config first (cross-platform), then %APPDATA% (fallback)
|
|
9
|
+
*
|
|
10
|
+
* On Windows, prioritizes ~/.config for cross-platform consistency.
|
|
11
|
+
* Falls back to %APPDATA% for backward compatibility with existing installations.
|
|
12
|
+
*/
|
|
13
|
+
export function getUserConfigDir() {
|
|
14
|
+
const defaultDir = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
15
|
+
if (process.platform !== "win32") {
|
|
16
|
+
return defaultDir;
|
|
17
|
+
}
|
|
18
|
+
if (existsSync(join(defaultDir, HOOKS_SUBPATH))) {
|
|
19
|
+
return defaultDir;
|
|
20
|
+
}
|
|
21
|
+
const appdataDir = process.env.APPDATA || join(homedir(), "AppData", "Roaming");
|
|
22
|
+
if (existsSync(join(appdataDir, HOOKS_SUBPATH))) {
|
|
23
|
+
return appdataDir;
|
|
24
|
+
}
|
|
25
|
+
return defaultDir;
|
|
26
|
+
}
|
|
27
|
+
export function getGlobalHookDir() {
|
|
28
|
+
return join(getUserConfigDir(), "opencode", "hook");
|
|
29
|
+
}
|
|
30
|
+
export function getProjectHookDir(directory) {
|
|
31
|
+
return join(directory, ".opencode", "hook");
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
describe("config-paths", () => {
|
|
5
|
+
const originalPlatform = process.platform;
|
|
6
|
+
const originalEnv = { ...process.env };
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.resetModules();
|
|
9
|
+
});
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
Object.defineProperty(process, "platform", { value: originalPlatform });
|
|
12
|
+
process.env = { ...originalEnv };
|
|
13
|
+
vi.doUnmock("node:fs");
|
|
14
|
+
});
|
|
15
|
+
describe("getUserConfigDir", () => {
|
|
16
|
+
it("should return XDG_CONFIG_HOME on Linux when set", async () => {
|
|
17
|
+
Object.defineProperty(process, "platform", { value: "linux" });
|
|
18
|
+
process.env.XDG_CONFIG_HOME = "/custom/config";
|
|
19
|
+
const { getUserConfigDir } = await import("./config-paths");
|
|
20
|
+
expect(getUserConfigDir()).toBe("/custom/config");
|
|
21
|
+
});
|
|
22
|
+
it("should return ~/.config on Linux when XDG_CONFIG_HOME is not set", async () => {
|
|
23
|
+
Object.defineProperty(process, "platform", { value: "linux" });
|
|
24
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
25
|
+
const { getUserConfigDir } = await import("./config-paths");
|
|
26
|
+
expect(getUserConfigDir()).toBe(join(homedir(), ".config"));
|
|
27
|
+
});
|
|
28
|
+
it("should return XDG_CONFIG_HOME on macOS when set", async () => {
|
|
29
|
+
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
30
|
+
process.env.XDG_CONFIG_HOME = "/custom/mac/config";
|
|
31
|
+
const { getUserConfigDir } = await import("./config-paths");
|
|
32
|
+
expect(getUserConfigDir()).toBe("/custom/mac/config");
|
|
33
|
+
});
|
|
34
|
+
it("should return ~/.config on macOS when XDG_CONFIG_HOME is not set", async () => {
|
|
35
|
+
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
36
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
37
|
+
const { getUserConfigDir } = await import("./config-paths");
|
|
38
|
+
expect(getUserConfigDir()).toBe(join(homedir(), ".config"));
|
|
39
|
+
});
|
|
40
|
+
it("should return ~/.config on Windows by default", async () => {
|
|
41
|
+
Object.defineProperty(process, "platform", { value: "win32" });
|
|
42
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
43
|
+
process.env.APPDATA = "C:\\Users\\Test\\AppData\\Roaming";
|
|
44
|
+
vi.doMock("node:fs", () => ({
|
|
45
|
+
existsSync: () => false,
|
|
46
|
+
}));
|
|
47
|
+
const { getUserConfigDir } = await import("./config-paths");
|
|
48
|
+
expect(getUserConfigDir()).toBe(join(homedir(), ".config"));
|
|
49
|
+
});
|
|
50
|
+
it("should return APPDATA on Windows when hooks.md exists there but not in ~/.config", async () => {
|
|
51
|
+
Object.defineProperty(process, "platform", { value: "win32" });
|
|
52
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
53
|
+
const appdataDir = "C:\\Users\\Test\\AppData\\Roaming";
|
|
54
|
+
process.env.APPDATA = appdataDir;
|
|
55
|
+
const appdataHooksPath = join(appdataDir, "opencode", "hook", "hooks.md");
|
|
56
|
+
vi.doMock("node:fs", () => ({
|
|
57
|
+
existsSync: (path) => path === appdataHooksPath,
|
|
58
|
+
}));
|
|
59
|
+
const { getUserConfigDir } = await import("./config-paths");
|
|
60
|
+
expect(getUserConfigDir()).toBe(appdataDir);
|
|
61
|
+
});
|
|
62
|
+
it("should prefer ~/.config on Windows when hooks.md exists in both locations", async () => {
|
|
63
|
+
Object.defineProperty(process, "platform", { value: "win32" });
|
|
64
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
65
|
+
const appdataDir = "C:\\Users\\Test\\AppData\\Roaming";
|
|
66
|
+
process.env.APPDATA = appdataDir;
|
|
67
|
+
const crossPlatformHooksPath = join(homedir(), ".config", "opencode", "hook", "hooks.md");
|
|
68
|
+
vi.doMock("node:fs", () => ({
|
|
69
|
+
existsSync: (path) => path === crossPlatformHooksPath,
|
|
70
|
+
}));
|
|
71
|
+
const { getUserConfigDir } = await import("./config-paths");
|
|
72
|
+
expect(getUserConfigDir()).toBe(join(homedir(), ".config"));
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
describe("getGlobalHookDir", () => {
|
|
76
|
+
it("should return correct path on Linux", async () => {
|
|
77
|
+
Object.defineProperty(process, "platform", { value: "linux" });
|
|
78
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
79
|
+
const { getGlobalHookDir } = await import("./config-paths");
|
|
80
|
+
expect(getGlobalHookDir()).toBe(join(homedir(), ".config", "opencode", "hook"));
|
|
81
|
+
});
|
|
82
|
+
it("should return correct path with XDG_CONFIG_HOME", async () => {
|
|
83
|
+
Object.defineProperty(process, "platform", { value: "linux" });
|
|
84
|
+
process.env.XDG_CONFIG_HOME = "/custom/config";
|
|
85
|
+
const { getGlobalHookDir } = await import("./config-paths");
|
|
86
|
+
expect(getGlobalHookDir()).toBe("/custom/config/opencode/hook");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
describe("getProjectHookDir", () => {
|
|
90
|
+
it("should return correct path for project directory", async () => {
|
|
91
|
+
const { getProjectHookDir } = await import("./config-paths");
|
|
92
|
+
expect(getProjectHookDir("/my/project")).toBe("/my/project/.opencode/hook");
|
|
93
|
+
});
|
|
94
|
+
it("should handle Windows-style paths", async () => {
|
|
95
|
+
const { getProjectHookDir } = await import("./config-paths");
|
|
96
|
+
const result = getProjectHookDir("C:\\Users\\Test\\project");
|
|
97
|
+
expect(result).toContain(".opencode");
|
|
98
|
+
expect(result).toContain("hook");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
package/dist/index.d.ts
ADDED