@octavio.bot/review 0.1.3 → 0.1.5
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 +32 -0
- package/dist/index.mjs +401 -127
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -17,6 +17,19 @@ No GitHub review comments are created or updated.
|
|
|
17
17
|
bunx --bun @octavio.bot/review@latest review --owner acme --repo web --pr 123 --workdir .
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
Initialize Octavio in your repository:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
bunx --bun @octavio.bot/review@latest init --workdir .
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
This scaffolds:
|
|
27
|
+
|
|
28
|
+
- `.octavio/review.config.json`
|
|
29
|
+
- `.github/workflows/review-check.yml`
|
|
30
|
+
|
|
31
|
+
The generated workflow defaults to `OPENCODE_MODEL=opencode/minimax-m2.5-free` and sets `OPENCODE_API_KEY` to an empty fallback (`''`). If you move to a non-free model, set repository secret `OPENCODE_API_KEY`.
|
|
32
|
+
|
|
20
33
|
CLI binary name: `octavio-review`.
|
|
21
34
|
|
|
22
35
|
## OpenCode Detection and Install
|
|
@@ -63,6 +76,12 @@ bun install
|
|
|
63
76
|
bun run review-bot --owner acme --repo web --pr 123 --instructions-profile balanced --workdir .
|
|
64
77
|
```
|
|
65
78
|
|
|
79
|
+
Build and packaging notes:
|
|
80
|
+
|
|
81
|
+
- `bun run review-cli:build` builds the CLI bundle and syncs prompt markdown into `apps/review-bot-cli/prompts/`
|
|
82
|
+
- `bun run sync` from repo root runs the CLI `sync` task through Turborepo and refreshes generated prompts
|
|
83
|
+
- Prompt source-of-truth lives in `packages/prompts/prompts/*.md`; the app-level `prompts/` directory is generated for publishing
|
|
84
|
+
|
|
66
85
|
Optional flags:
|
|
67
86
|
|
|
68
87
|
- `--report-output path/to/review.md`
|
|
@@ -72,6 +91,11 @@ Optional flags:
|
|
|
72
91
|
- `--artifact-execution agent|host`
|
|
73
92
|
- `--install-opencode`
|
|
74
93
|
|
|
94
|
+
Init flags:
|
|
95
|
+
|
|
96
|
+
- `--workdir path/to/repo`
|
|
97
|
+
- `--force` (overwrite existing scaffolded files)
|
|
98
|
+
|
|
75
99
|
## Instruction Profiles
|
|
76
100
|
|
|
77
101
|
Instruction resolution order:
|
|
@@ -111,9 +135,17 @@ Default artifact schema writes these files into `artifacts/`:
|
|
|
111
135
|
- Review workflow: `.github/workflows/review-check.yml`
|
|
112
136
|
- Runs profile matrix (`balanced`, `styling`, `security`) with `max-parallel: 1`
|
|
113
137
|
- Uses `bunx --bun @octavio.bot/review@latest`
|
|
138
|
+
- Defaults to `OPENCODE_MODEL=opencode/minimax-m2.5-free`
|
|
139
|
+
- Uses `OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY || '' }}`
|
|
114
140
|
- Uploads `review.md`, `confidence.json`, and `result.json`
|
|
115
141
|
- CI workflow: `.github/workflows/ci.yml`
|
|
116
142
|
- Includes smoke test: `bunx --bun @octavio.bot/review@latest doctor`
|
|
117
143
|
- Publish workflow: `.github/workflows/publish-review.yml`
|
|
118
144
|
- Manual only (`workflow_dispatch`)
|
|
119
145
|
- Publishes from `apps/review-bot-cli` using npm trusted publishing (OIDC)
|
|
146
|
+
|
|
147
|
+
## Troubleshooting
|
|
148
|
+
|
|
149
|
+
- If runs fail after changing models, set repository secret `OPENCODE_API_KEY`.
|
|
150
|
+
- If OpenCode is unreachable, verify `OPENCODE_HOSTNAME` and `OPENCODE_PORT`.
|
|
151
|
+
- If GitHub API requests fail, verify `GITHUB_TOKEN` permissions.
|
package/dist/index.mjs
CHANGED
|
@@ -1694,6 +1694,17 @@ class CodeReviewWorkflow {
|
|
|
1694
1694
|
}
|
|
1695
1695
|
}
|
|
1696
1696
|
|
|
1697
|
+
// ../../packages/prompts/src/index.ts
|
|
1698
|
+
var PROMPT_PROFILES = ["balanced", "styling", "security"];
|
|
1699
|
+
var PROMPT_PATHS = {
|
|
1700
|
+
balanced: "../prompts/code-review.md",
|
|
1701
|
+
security: "../prompts/security-review.md",
|
|
1702
|
+
styling: "../prompts/styling-review.md"
|
|
1703
|
+
};
|
|
1704
|
+
var DEFAULT_PROMPT_PROFILE = "balanced";
|
|
1705
|
+
var isPromptProfile = (value) => PROMPT_PROFILES.some((profile) => profile === value);
|
|
1706
|
+
var resolvePromptPath = (profile) => Bun.fileURLToPath(new URL(PROMPT_PATHS[profile], import.meta.url));
|
|
1707
|
+
|
|
1697
1708
|
// ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
|
|
1698
1709
|
var exports_external = {};
|
|
1699
1710
|
__export(exports_external, {
|
|
@@ -15235,7 +15246,7 @@ var envSchema = exports_external.object({
|
|
|
15235
15246
|
});
|
|
15236
15247
|
var POLICY_RULE_REGEX = /^(any|new):(low|medium|high|critical)$/u;
|
|
15237
15248
|
var artifactExecutionSchema = exports_external.enum(["host", "agent"]);
|
|
15238
|
-
var instructionsPromptSchema = exports_external.enum(
|
|
15249
|
+
var instructionsPromptSchema = exports_external.enum(PROMPT_PROFILES);
|
|
15239
15250
|
var artifactSchemaConfigSchema = exports_external.object({
|
|
15240
15251
|
artifactDir: exports_external.string().min(1).optional(),
|
|
15241
15252
|
confidenceFile: exports_external.string().min(1).optional(),
|
|
@@ -15658,40 +15669,379 @@ class OpenCodeReportRunner {
|
|
|
15658
15669
|
}
|
|
15659
15670
|
}
|
|
15660
15671
|
|
|
15661
|
-
//
|
|
15662
|
-
var
|
|
15663
|
-
|
|
15664
|
-
|
|
15665
|
-
|
|
15672
|
+
// src/init.ts
|
|
15673
|
+
var DEFAULT_REVIEW_CONFIG = {
|
|
15674
|
+
defaultProfile: "balanced",
|
|
15675
|
+
profiles: {
|
|
15676
|
+
balanced: {
|
|
15677
|
+
artifactExecution: "agent",
|
|
15678
|
+
artifactSchema: {
|
|
15679
|
+
artifactDir: "artifacts",
|
|
15680
|
+
confidenceFile: "confidence.json",
|
|
15681
|
+
maxAttempts: 2,
|
|
15682
|
+
reviewFile: "review.md"
|
|
15683
|
+
},
|
|
15684
|
+
instructionsPrompt: "balanced",
|
|
15685
|
+
policy: {
|
|
15686
|
+
failOn: ["new:high", "new:critical"]
|
|
15687
|
+
}
|
|
15688
|
+
},
|
|
15689
|
+
security: {
|
|
15690
|
+
artifactExecution: "agent",
|
|
15691
|
+
artifactSchema: {
|
|
15692
|
+
artifactDir: "artifacts",
|
|
15693
|
+
confidenceFile: "confidence.json",
|
|
15694
|
+
maxAttempts: 2,
|
|
15695
|
+
reviewFile: "review.md"
|
|
15696
|
+
},
|
|
15697
|
+
instructionsPrompt: "security",
|
|
15698
|
+
policy: {
|
|
15699
|
+
failOn: ["new:medium", "new:high", "new:critical"]
|
|
15700
|
+
}
|
|
15701
|
+
},
|
|
15702
|
+
styling: {
|
|
15703
|
+
artifactExecution: "agent",
|
|
15704
|
+
artifactSchema: {
|
|
15705
|
+
artifactDir: "artifacts",
|
|
15706
|
+
confidenceFile: "confidence.json",
|
|
15707
|
+
maxAttempts: 2,
|
|
15708
|
+
reviewFile: "review.md"
|
|
15709
|
+
},
|
|
15710
|
+
instructionsPrompt: "styling",
|
|
15711
|
+
policy: {
|
|
15712
|
+
failOn: ["new:high", "new:critical"]
|
|
15713
|
+
}
|
|
15714
|
+
}
|
|
15715
|
+
}
|
|
15666
15716
|
};
|
|
15667
|
-
var
|
|
15668
|
-
var isPromptProfile = (value) => Object.hasOwn(PROMPT_PATHS, value);
|
|
15669
|
-
var resolvePromptPath = (profile) => Bun.fileURLToPath(new URL(PROMPT_PATHS[profile], import.meta.url));
|
|
15717
|
+
var DEFAULT_REVIEW_WORKFLOW = `name: review-check
|
|
15670
15718
|
|
|
15671
|
-
|
|
15672
|
-
|
|
15719
|
+
on:
|
|
15720
|
+
pull_request:
|
|
15721
|
+
types: [opened, synchronize, reopened, ready_for_review]
|
|
15722
|
+
|
|
15723
|
+
concurrency:
|
|
15724
|
+
group: review-check-pr-\${{ github.event.pull_request.number }}
|
|
15725
|
+
cancel-in-progress: true
|
|
15726
|
+
|
|
15727
|
+
permissions:
|
|
15728
|
+
actions: read
|
|
15729
|
+
contents: read
|
|
15730
|
+
pull-requests: read
|
|
15731
|
+
|
|
15732
|
+
jobs:
|
|
15733
|
+
review:
|
|
15734
|
+
name: review (\${{ matrix.profile }})
|
|
15735
|
+
runs-on: ubuntu-latest
|
|
15736
|
+
timeout-minutes: 10
|
|
15737
|
+
strategy:
|
|
15738
|
+
fail-fast: false
|
|
15739
|
+
max-parallel: 1
|
|
15740
|
+
matrix:
|
|
15741
|
+
profile: [balanced, styling, security]
|
|
15742
|
+
steps:
|
|
15743
|
+
- name: Checkout
|
|
15744
|
+
uses: actions/checkout@v4
|
|
15745
|
+
|
|
15746
|
+
- name: Setup Bun
|
|
15747
|
+
uses: oven-sh/setup-bun@v2
|
|
15748
|
+
with:
|
|
15749
|
+
bun-version: latest
|
|
15750
|
+
|
|
15751
|
+
- name: Run review check
|
|
15752
|
+
timeout-minutes: 8
|
|
15753
|
+
env:
|
|
15754
|
+
GITHUB_TOKEN: \${{ github.token }}
|
|
15755
|
+
OPENCODE_HOSTNAME: \${{ vars.OPENCODE_HOSTNAME || '127.0.0.1' }}
|
|
15756
|
+
OCTAVIO_INSTRUCTIONS_PROFILE: \${{ matrix.profile }}
|
|
15757
|
+
OPENCODE_MODEL: opencode/minimax-m2.5-free
|
|
15758
|
+
OPENCODE_PORT: \${{ vars.OPENCODE_PORT || '4096' }}
|
|
15759
|
+
OPENCODE_API_KEY: \${{ secrets.OPENCODE_API_KEY || '' }}
|
|
15760
|
+
run: |
|
|
15761
|
+
mkdir -p artifacts
|
|
15762
|
+
PROFILE_ARGS=""
|
|
15763
|
+
if [ -n "$OCTAVIO_INSTRUCTIONS_PROFILE" ]; then
|
|
15764
|
+
PROFILE_ARGS="--instructions-profile $OCTAVIO_INSTRUCTIONS_PROFILE"
|
|
15765
|
+
fi
|
|
15766
|
+
bunx --bun @octavio.bot/review@latest review \\
|
|
15767
|
+
--owner "\${{ github.repository_owner }}" \\
|
|
15768
|
+
--repo "\${{ github.event.repository.name }}" \\
|
|
15769
|
+
--pr "\${{ github.event.pull_request.number }}" \\
|
|
15770
|
+
$PROFILE_ARGS \\
|
|
15771
|
+
--install-opencode \\
|
|
15772
|
+
--workdir . \\
|
|
15773
|
+
--report-output artifacts/review.md \\
|
|
15774
|
+
--findings-output artifacts/confidence.json \\
|
|
15775
|
+
--result-output artifacts/result.json
|
|
15776
|
+
|
|
15777
|
+
- name: Publish job summary
|
|
15778
|
+
if: always()
|
|
15779
|
+
env:
|
|
15780
|
+
OCTAVIO_INSTRUCTIONS_PROFILE: \${{ matrix.profile }}
|
|
15781
|
+
run: |
|
|
15782
|
+
if [ -f artifacts/result.json ]; then
|
|
15783
|
+
bun -e 'const result = await Bun.file("artifacts/result.json").json();
|
|
15784
|
+
const lines = [
|
|
15785
|
+
"## Octavio Review",
|
|
15786
|
+
"",
|
|
15787
|
+
"- Profile: " + (process.env.OCTAVIO_INSTRUCTIONS_PROFILE || "balanced"),
|
|
15788
|
+
"- Blocking findings: " + (result.hasBlockingFindings ? "yes" : "no"),
|
|
15789
|
+
"- Policy source: " + result.policy.source,
|
|
15790
|
+
"- Policy rules: " + (result.policy.failOnRules.join(", ") || "none"),
|
|
15791
|
+
"- Matched rules: " + (result.policy.matchedRules.join(", ") || "none"),
|
|
15792
|
+
"",
|
|
15793
|
+
"### Summary",
|
|
15794
|
+
result.summary,
|
|
15795
|
+
];
|
|
15796
|
+
await Bun.write(Bun.stdout, lines.join("\\n") + "\\n");' >> "$GITHUB_STEP_SUMMARY"
|
|
15797
|
+
else
|
|
15798
|
+
printf '## Octavio Review
|
|
15799
|
+
|
|
15800
|
+
Run failed before report generation.
|
|
15801
|
+
|
|
15802
|
+
Troubleshooting:
|
|
15803
|
+
- Confirm workflow env uses OPENCODE_MODEL=opencode/minimax-m2.5-free.
|
|
15804
|
+
- If you switch to a non-free model, set repository secret OPENCODE_API_KEY.
|
|
15805
|
+
- Check run logs for OpenCode connection/auth/model errors.
|
|
15806
|
+
' >> "$GITHUB_STEP_SUMMARY"
|
|
15807
|
+
fi
|
|
15808
|
+
|
|
15809
|
+
- name: Upload report artifact
|
|
15810
|
+
if: always()
|
|
15811
|
+
uses: actions/upload-artifact@v4
|
|
15812
|
+
with:
|
|
15813
|
+
name: octavio-review-report-pr-\${{ github.event.pull_request.number }}-\${{ matrix.profile }}
|
|
15814
|
+
path: artifacts/review.md
|
|
15815
|
+
if-no-files-found: ignore
|
|
15816
|
+
|
|
15817
|
+
- name: Upload confidence artifact
|
|
15818
|
+
if: always()
|
|
15819
|
+
uses: actions/upload-artifact@v4
|
|
15820
|
+
with:
|
|
15821
|
+
name: octavio-confidence-pr-\${{ github.event.pull_request.number }}-\${{ matrix.profile }}
|
|
15822
|
+
path: artifacts/confidence.json
|
|
15823
|
+
if-no-files-found: ignore
|
|
15824
|
+
|
|
15825
|
+
- name: Upload result artifact
|
|
15826
|
+
if: always()
|
|
15827
|
+
uses: actions/upload-artifact@v4
|
|
15828
|
+
with:
|
|
15829
|
+
name: octavio-result-pr-\${{ github.event.pull_request.number }}-\${{ matrix.profile }}
|
|
15830
|
+
path: artifacts/result.json
|
|
15831
|
+
if-no-files-found: ignore
|
|
15832
|
+
`;
|
|
15833
|
+
var scaffoldPlans = () => [
|
|
15834
|
+
{
|
|
15835
|
+
contents: `${JSON.stringify(DEFAULT_REVIEW_CONFIG, null, 2)}
|
|
15836
|
+
`,
|
|
15837
|
+
relativePath: ".octavio/review.config.json"
|
|
15838
|
+
},
|
|
15839
|
+
{
|
|
15840
|
+
contents: DEFAULT_REVIEW_WORKFLOW,
|
|
15841
|
+
relativePath: ".github/workflows/review-check.yml"
|
|
15842
|
+
}
|
|
15843
|
+
];
|
|
15844
|
+
var scaffoldReviewSetup = async (workspaceDirectory, force) => {
|
|
15845
|
+
const results = [];
|
|
15846
|
+
for (const plan of scaffoldPlans()) {
|
|
15847
|
+
const absolutePath = resolvePathFromWorkspace(workspaceDirectory, plan.relativePath);
|
|
15848
|
+
const file2 = Bun.file(absolutePath);
|
|
15849
|
+
const exists = await file2.exists();
|
|
15850
|
+
if (exists && !force) {
|
|
15851
|
+
results.push({
|
|
15852
|
+
relativePath: plan.relativePath,
|
|
15853
|
+
status: "skipped"
|
|
15854
|
+
});
|
|
15855
|
+
continue;
|
|
15856
|
+
}
|
|
15857
|
+
await Bun.write(absolutePath, plan.contents);
|
|
15858
|
+
results.push({
|
|
15859
|
+
relativePath: plan.relativePath,
|
|
15860
|
+
status: exists ? "overwritten" : "created"
|
|
15861
|
+
});
|
|
15862
|
+
}
|
|
15863
|
+
return results;
|
|
15864
|
+
};
|
|
15865
|
+
|
|
15866
|
+
// src/opencode.ts
|
|
15673
15867
|
var OPEN_CODE_INSTALL_COMMAND = "curl -fsSL https://opencode.ai/install | bash";
|
|
15674
15868
|
var OPEN_CODE_INSTALL_COMMAND_NO_PATH = "curl -fsSL https://opencode.ai/install | bash -s -- --no-modify-path";
|
|
15869
|
+
var ensureBinaryDirectoryOnPath = (binaryPath) => {
|
|
15870
|
+
const directoryEnd = binaryPath.lastIndexOf("/");
|
|
15871
|
+
if (directoryEnd <= 0) {
|
|
15872
|
+
return;
|
|
15873
|
+
}
|
|
15874
|
+
const directory = binaryPath.slice(0, directoryEnd);
|
|
15875
|
+
const currentPath = process.env.PATH ?? "";
|
|
15876
|
+
const pathEntries = currentPath.split(":").filter((entry) => entry.length > 0);
|
|
15877
|
+
if (pathEntries.includes(directory)) {
|
|
15878
|
+
return;
|
|
15879
|
+
}
|
|
15880
|
+
process.env.PATH = currentPath.length > 0 ? `${directory}:${currentPath}` : directory;
|
|
15881
|
+
process.stdout.write(`Added ${directory} to PATH for this process.
|
|
15882
|
+
`);
|
|
15883
|
+
};
|
|
15884
|
+
var knownOpenCodePaths = () => {
|
|
15885
|
+
const home = process.env.HOME;
|
|
15886
|
+
return [
|
|
15887
|
+
Bun.which("opencode") ?? "",
|
|
15888
|
+
home ? `${home}/.opencode/bin/opencode` : "",
|
|
15889
|
+
home ? `${home}/.local/bin/opencode` : ""
|
|
15890
|
+
].filter((value, index, values) => value.length > 0 && values.indexOf(value) === index);
|
|
15891
|
+
};
|
|
15892
|
+
var readProcessOutput = async (stream) => stream ? await new Response(stream).text() : "";
|
|
15893
|
+
var getOpenCodeVersion = async (binaryPath) => {
|
|
15894
|
+
const subprocess = Bun.spawn([binaryPath, "--version"], {
|
|
15895
|
+
stderr: "pipe",
|
|
15896
|
+
stdout: "pipe"
|
|
15897
|
+
});
|
|
15898
|
+
const [exitCode, stderr, stdout] = await Promise.all([
|
|
15899
|
+
subprocess.exited,
|
|
15900
|
+
readProcessOutput(subprocess.stderr),
|
|
15901
|
+
readProcessOutput(subprocess.stdout)
|
|
15902
|
+
]);
|
|
15903
|
+
if (exitCode !== 0) {
|
|
15904
|
+
const details = stderr.trim();
|
|
15905
|
+
return details.length > 0 ? details : null;
|
|
15906
|
+
}
|
|
15907
|
+
const version2 = stdout.trim();
|
|
15908
|
+
return version2.length > 0 ? version2 : null;
|
|
15909
|
+
};
|
|
15910
|
+
var detectOpenCode = async () => {
|
|
15911
|
+
for (const candidatePath of knownOpenCodePaths()) {
|
|
15912
|
+
const file2 = Bun.file(candidatePath);
|
|
15913
|
+
if (!await file2.exists()) {
|
|
15914
|
+
continue;
|
|
15915
|
+
}
|
|
15916
|
+
const version2 = await getOpenCodeVersion(candidatePath);
|
|
15917
|
+
return {
|
|
15918
|
+
path: candidatePath,
|
|
15919
|
+
version: version2 ?? "unknown"
|
|
15920
|
+
};
|
|
15921
|
+
}
|
|
15922
|
+
return null;
|
|
15923
|
+
};
|
|
15924
|
+
var runOpenCodeInstall = async () => {
|
|
15925
|
+
process.stdout.write(`OpenCode was not found. Installing now...
|
|
15926
|
+
`);
|
|
15927
|
+
process.stdout.write(`Install command: ${OPEN_CODE_INSTALL_COMMAND_NO_PATH}
|
|
15928
|
+
`);
|
|
15929
|
+
const subprocess = Bun.spawn(["bash", "-lc", OPEN_CODE_INSTALL_COMMAND_NO_PATH], {
|
|
15930
|
+
stderr: "inherit",
|
|
15931
|
+
stdout: "inherit"
|
|
15932
|
+
});
|
|
15933
|
+
const exitCode = await subprocess.exited;
|
|
15934
|
+
if (exitCode !== 0) {
|
|
15935
|
+
throw new Error([
|
|
15936
|
+
"Failed to auto-install OpenCode.",
|
|
15937
|
+
`Run this manually and retry: ${OPEN_CODE_INSTALL_COMMAND}`
|
|
15938
|
+
].join(`
|
|
15939
|
+
`));
|
|
15940
|
+
}
|
|
15941
|
+
};
|
|
15942
|
+
var ensureOpenCodeInstalled = async (forceInstall) => {
|
|
15943
|
+
const detectedBeforeInstall = await detectOpenCode();
|
|
15944
|
+
if (detectedBeforeInstall) {
|
|
15945
|
+
ensureBinaryDirectoryOnPath(detectedBeforeInstall.path);
|
|
15946
|
+
process.stdout.write(`OpenCode detected at ${detectedBeforeInstall.path} (${detectedBeforeInstall.version}).
|
|
15947
|
+
`);
|
|
15948
|
+
return detectedBeforeInstall;
|
|
15949
|
+
}
|
|
15950
|
+
const shouldAutoInstall = forceInstall || process.env.GITHUB_ACTIONS === "true";
|
|
15951
|
+
if (!shouldAutoInstall) {
|
|
15952
|
+
throw new Error([
|
|
15953
|
+
"OpenCode CLI is required but was not found.",
|
|
15954
|
+
`Install it with: ${OPEN_CODE_INSTALL_COMMAND}`,
|
|
15955
|
+
"Then rerun this command, or pass --install-opencode to auto-install."
|
|
15956
|
+
].join(`
|
|
15957
|
+
`));
|
|
15958
|
+
}
|
|
15959
|
+
await runOpenCodeInstall();
|
|
15960
|
+
const detectedAfterInstall = await detectOpenCode();
|
|
15961
|
+
if (!detectedAfterInstall) {
|
|
15962
|
+
throw new Error([
|
|
15963
|
+
"OpenCode install completed but the binary was still not detected.",
|
|
15964
|
+
`Try opening a new shell or run manually: ${OPEN_CODE_INSTALL_COMMAND}`
|
|
15965
|
+
].join(`
|
|
15966
|
+
`));
|
|
15967
|
+
}
|
|
15968
|
+
process.stdout.write(`OpenCode installed at ${detectedAfterInstall.path} (${detectedAfterInstall.version}).
|
|
15969
|
+
`);
|
|
15970
|
+
ensureBinaryDirectoryOnPath(detectedAfterInstall.path);
|
|
15971
|
+
return detectedAfterInstall;
|
|
15972
|
+
};
|
|
15973
|
+
var runDoctor = async () => {
|
|
15974
|
+
const detected = await detectOpenCode();
|
|
15975
|
+
process.stdout.write(`Octavio doctor
|
|
15976
|
+
`);
|
|
15977
|
+
process.stdout.write(`- bun: ${Bun.version}
|
|
15978
|
+
`);
|
|
15979
|
+
if (detected) {
|
|
15980
|
+
process.stdout.write(`- opencode: installed (${detected.version})
|
|
15981
|
+
`);
|
|
15982
|
+
process.stdout.write(`- opencode-path: ${detected.path}
|
|
15983
|
+
`);
|
|
15984
|
+
} else {
|
|
15985
|
+
process.stdout.write(`- opencode: missing
|
|
15986
|
+
`);
|
|
15987
|
+
process.stdout.write(`- install: ${OPEN_CODE_INSTALL_COMMAND}
|
|
15988
|
+
`);
|
|
15989
|
+
}
|
|
15990
|
+
};
|
|
15991
|
+
|
|
15992
|
+
// src/troubleshooting.ts
|
|
15993
|
+
var troubleshootingGuidance = (message) => {
|
|
15994
|
+
const lower = message.toLowerCase();
|
|
15995
|
+
const guidance = new Set;
|
|
15996
|
+
if (lower.includes("github_token is required")) {
|
|
15997
|
+
guidance.add("Set GITHUB_TOKEN before running the review command.");
|
|
15998
|
+
}
|
|
15999
|
+
if (lower.includes("github api request failed (401") || lower.includes("github api request failed (403")) {
|
|
16000
|
+
guidance.add("Check GITHUB_TOKEN permissions: repository contents read and pull requests read.");
|
|
16001
|
+
}
|
|
16002
|
+
if (lower.includes("econnrefused") || lower.includes("enotfound") || lower.includes("connect")) {
|
|
16003
|
+
guidance.add("Verify OpenCode connectivity (OPENCODE_HOSTNAME and OPENCODE_PORT).");
|
|
16004
|
+
}
|
|
16005
|
+
if (lower.includes("opencode_api_key") || lower.includes("api key") || lower.includes("unauthorized")) {
|
|
16006
|
+
guidance.add("If you are not using a free model, set OPENCODE_API_KEY (repository secret in GitHub Actions, env var locally).");
|
|
16007
|
+
}
|
|
16008
|
+
if (lower.includes("model") && (lower.includes("not found") || lower.includes("unsupported") || lower.includes("unavailable"))) {
|
|
16009
|
+
guidance.add("Use OPENCODE_MODEL=opencode/minimax-m2.5-free, or provide OPENCODE_API_KEY for paid/private models.");
|
|
16010
|
+
}
|
|
16011
|
+
if (lower.includes("429") || lower.includes("rate limit")) {
|
|
16012
|
+
guidance.add("You hit provider limits. Retry later or use a model/account with higher quota.");
|
|
16013
|
+
}
|
|
16014
|
+
return [...guidance];
|
|
16015
|
+
};
|
|
16016
|
+
|
|
16017
|
+
// src/index.ts
|
|
16018
|
+
var REPORT_LOG_MAX_CHARS = 8000;
|
|
15675
16019
|
var DEFAULT_ARTIFACT_DIR = "artifacts";
|
|
15676
16020
|
var DEFAULT_REVIEW_FILE = "review.md";
|
|
15677
16021
|
var DEFAULT_CONFIDENCE_FILE = "confidence.json";
|
|
15678
16022
|
var DEFAULT_ARTIFACT_MAX_ATTEMPTS = 2;
|
|
15679
16023
|
var DEFAULT_ARTIFACT_EXECUTION = "agent";
|
|
16024
|
+
var INSTRUCTIONS_PROFILE_LIST = PROMPT_PROFILES.join("|");
|
|
15680
16025
|
var usage = () => [
|
|
15681
16026
|
"Usage:",
|
|
15682
16027
|
" octavio-review review --owner <owner> --repo <repo> --pr <number> [options]",
|
|
16028
|
+
" octavio-review init [options]",
|
|
15683
16029
|
" octavio-review doctor",
|
|
15684
16030
|
" octavio-review install-opencode",
|
|
15685
16031
|
"",
|
|
15686
16032
|
"Options for review:",
|
|
15687
16033
|
" --instructions <path>",
|
|
15688
|
-
|
|
16034
|
+
` --instructions-profile <${INSTRUCTIONS_PROFILE_LIST}>`,
|
|
15689
16035
|
" --artifact-execution <agent|host>",
|
|
15690
16036
|
" --workdir <path>",
|
|
15691
16037
|
" --report-output <path>",
|
|
15692
16038
|
" --findings-output <path>",
|
|
15693
16039
|
" --result-output <path>",
|
|
15694
|
-
" --install-opencode (force auto-install if missing)"
|
|
16040
|
+
" --install-opencode (force auto-install if missing)",
|
|
16041
|
+
"",
|
|
16042
|
+
"Options for init:",
|
|
16043
|
+
" --workdir <path>",
|
|
16044
|
+
" --force (overwrite existing files)"
|
|
15695
16045
|
].join(`
|
|
15696
16046
|
`);
|
|
15697
16047
|
var parseArtifactExecution = (rawValue) => {
|
|
@@ -15767,6 +16117,18 @@ ${usage()}`);
|
|
|
15767
16117
|
workspaceDirectory
|
|
15768
16118
|
};
|
|
15769
16119
|
};
|
|
16120
|
+
var parseInitInput = (argv) => {
|
|
16121
|
+
const { flags, positional } = parseArgs(argv);
|
|
16122
|
+
if (positional.length > 0) {
|
|
16123
|
+
throw new Error(`Unexpected positional arguments for init: ${positional.join(", ")}
|
|
16124
|
+
|
|
16125
|
+
${usage()}`);
|
|
16126
|
+
}
|
|
16127
|
+
return {
|
|
16128
|
+
force: hasBooleanFlag(flags, "force"),
|
|
16129
|
+
workspaceDirectory: resolveWorkspaceDirectory(getStringFlag(flags, "workdir") ?? process.cwd())
|
|
16130
|
+
};
|
|
16131
|
+
};
|
|
15770
16132
|
var resolveInstructions = (cliInput, config2) => {
|
|
15771
16133
|
const selectedProfileName = cliInput.instructionsProfile ?? config2?.defaultProfile;
|
|
15772
16134
|
const selectedProfile = selectedProfileName ? config2?.profiles[selectedProfileName] : undefined;
|
|
@@ -15777,7 +16139,7 @@ var resolveInstructions = (cliInput, config2) => {
|
|
|
15777
16139
|
if (selectedProfile) {
|
|
15778
16140
|
const profilePrompt = selectedProfile.instructionsPrompt;
|
|
15779
16141
|
if (!isPromptProfile(profilePrompt)) {
|
|
15780
|
-
throw new Error(`Unsupported instructionsPrompt '${profilePrompt}'. Supported values:
|
|
16142
|
+
throw new Error(`Unsupported instructionsPrompt '${profilePrompt}'. Supported values: ${PROMPT_PROFILES.join(", ")}.`);
|
|
15781
16143
|
}
|
|
15782
16144
|
resolvedPath = resolvePromptPath(profilePrompt);
|
|
15783
16145
|
}
|
|
@@ -15798,127 +16160,26 @@ var defaultResultPath = (pullNumber) => {
|
|
|
15798
16160
|
};
|
|
15799
16161
|
var truncateForLogs = (value) => value.length > REPORT_LOG_MAX_CHARS ? `${value.slice(0, REPORT_LOG_MAX_CHARS)}
|
|
15800
16162
|
...truncated...` : value;
|
|
15801
|
-
var
|
|
15802
|
-
const
|
|
15803
|
-
|
|
15804
|
-
return;
|
|
15805
|
-
}
|
|
15806
|
-
const directory = binaryPath.slice(0, directoryEnd);
|
|
15807
|
-
const currentPath = process.env.PATH ?? "";
|
|
15808
|
-
const pathEntries = currentPath.split(":").filter((entry) => entry.length > 0);
|
|
15809
|
-
if (pathEntries.includes(directory)) {
|
|
15810
|
-
return;
|
|
15811
|
-
}
|
|
15812
|
-
process.env.PATH = currentPath.length > 0 ? `${directory}:${currentPath}` : directory;
|
|
15813
|
-
process.stdout.write(`Added ${directory} to PATH for this process.
|
|
15814
|
-
`);
|
|
15815
|
-
};
|
|
15816
|
-
var knownOpenCodePaths = () => {
|
|
15817
|
-
const home = process.env.HOME;
|
|
15818
|
-
return [
|
|
15819
|
-
Bun.which("opencode") ?? "",
|
|
15820
|
-
home ? `${home}/.opencode/bin/opencode` : "",
|
|
15821
|
-
home ? `${home}/.local/bin/opencode` : ""
|
|
15822
|
-
].filter((value, index, values) => value.length > 0 && values.indexOf(value) === index);
|
|
15823
|
-
};
|
|
15824
|
-
var readProcessOutput = async (stream) => stream ? await new Response(stream).text() : "";
|
|
15825
|
-
var getOpenCodeVersion = async (binaryPath) => {
|
|
15826
|
-
const subprocess = Bun.spawn([binaryPath, "--version"], {
|
|
15827
|
-
stderr: "pipe",
|
|
15828
|
-
stdout: "pipe"
|
|
15829
|
-
});
|
|
15830
|
-
const [exitCode, stderr, stdout] = await Promise.all([
|
|
15831
|
-
subprocess.exited,
|
|
15832
|
-
readProcessOutput(subprocess.stderr),
|
|
15833
|
-
readProcessOutput(subprocess.stdout)
|
|
15834
|
-
]);
|
|
15835
|
-
if (exitCode !== 0) {
|
|
15836
|
-
const details = stderr.trim();
|
|
15837
|
-
return details.length > 0 ? details : null;
|
|
15838
|
-
}
|
|
15839
|
-
const version2 = stdout.trim();
|
|
15840
|
-
return version2.length > 0 ? version2 : null;
|
|
15841
|
-
};
|
|
15842
|
-
var detectOpenCode = async () => {
|
|
15843
|
-
for (const candidatePath of knownOpenCodePaths()) {
|
|
15844
|
-
const file2 = Bun.file(candidatePath);
|
|
15845
|
-
if (!await file2.exists()) {
|
|
15846
|
-
continue;
|
|
15847
|
-
}
|
|
15848
|
-
const version2 = await getOpenCodeVersion(candidatePath);
|
|
15849
|
-
return {
|
|
15850
|
-
path: candidatePath,
|
|
15851
|
-
version: version2 ?? "unknown"
|
|
15852
|
-
};
|
|
15853
|
-
}
|
|
15854
|
-
return null;
|
|
15855
|
-
};
|
|
15856
|
-
var runOpenCodeInstall = async () => {
|
|
15857
|
-
process.stdout.write(`OpenCode was not found. Installing now...
|
|
16163
|
+
var runInit = async (argv) => {
|
|
16164
|
+
const input = parseInitInput(argv);
|
|
16165
|
+
process.stdout.write(`Initializing Octavio review in ${input.workspaceDirectory}...
|
|
15858
16166
|
`);
|
|
15859
|
-
|
|
16167
|
+
const results = await scaffoldReviewSetup(input.workspaceDirectory, input.force);
|
|
16168
|
+
for (const result of results) {
|
|
16169
|
+
process.stdout.write(`- ${result.status}: ${result.relativePath}
|
|
15860
16170
|
`);
|
|
15861
|
-
const subprocess = Bun.spawn(["bash", "-lc", OPEN_CODE_INSTALL_COMMAND_NO_PATH], {
|
|
15862
|
-
stderr: "inherit",
|
|
15863
|
-
stdout: "inherit"
|
|
15864
|
-
});
|
|
15865
|
-
const exitCode = await subprocess.exited;
|
|
15866
|
-
if (exitCode !== 0) {
|
|
15867
|
-
throw new Error([
|
|
15868
|
-
"Failed to auto-install OpenCode.",
|
|
15869
|
-
`Run this manually and retry: ${OPEN_CODE_INSTALL_COMMAND}`
|
|
15870
|
-
].join(`
|
|
15871
|
-
`));
|
|
15872
16171
|
}
|
|
15873
|
-
|
|
15874
|
-
|
|
15875
|
-
|
|
15876
|
-
if (detectedBeforeInstall) {
|
|
15877
|
-
ensureBinaryDirectoryOnPath(detectedBeforeInstall.path);
|
|
15878
|
-
process.stdout.write(`OpenCode detected at ${detectedBeforeInstall.path} (${detectedBeforeInstall.version}).
|
|
16172
|
+
const skippedCount = results.filter((result) => result.status === "skipped").length;
|
|
16173
|
+
if (skippedCount > 0 && !input.force) {
|
|
16174
|
+
process.stdout.write(`Some files already existed and were skipped. Re-run with --force to overwrite.
|
|
15879
16175
|
`);
|
|
15880
|
-
return detectedBeforeInstall;
|
|
15881
16176
|
}
|
|
15882
|
-
|
|
15883
|
-
if (!shouldAutoInstall) {
|
|
15884
|
-
throw new Error([
|
|
15885
|
-
"OpenCode CLI is required but was not found.",
|
|
15886
|
-
`Install it with: ${OPEN_CODE_INSTALL_COMMAND}`,
|
|
15887
|
-
"Then rerun this command, or pass --install-opencode to auto-install."
|
|
15888
|
-
].join(`
|
|
15889
|
-
`));
|
|
15890
|
-
}
|
|
15891
|
-
await runOpenCodeInstall();
|
|
15892
|
-
const detectedAfterInstall = await detectOpenCode();
|
|
15893
|
-
if (!detectedAfterInstall) {
|
|
15894
|
-
throw new Error([
|
|
15895
|
-
"OpenCode install completed but the binary was still not detected.",
|
|
15896
|
-
`Try opening a new shell or run manually: ${OPEN_CODE_INSTALL_COMMAND}`
|
|
15897
|
-
].join(`
|
|
15898
|
-
`));
|
|
15899
|
-
}
|
|
15900
|
-
process.stdout.write(`OpenCode installed at ${detectedAfterInstall.path} (${detectedAfterInstall.version}).
|
|
15901
|
-
`);
|
|
15902
|
-
ensureBinaryDirectoryOnPath(detectedAfterInstall.path);
|
|
15903
|
-
return detectedAfterInstall;
|
|
15904
|
-
};
|
|
15905
|
-
var runDoctor = async () => {
|
|
15906
|
-
const detected = await detectOpenCode();
|
|
15907
|
-
process.stdout.write(`Octavio doctor
|
|
15908
|
-
`);
|
|
15909
|
-
process.stdout.write(`- bun: ${Bun.version}
|
|
15910
|
-
`);
|
|
15911
|
-
if (detected) {
|
|
15912
|
-
process.stdout.write(`- opencode: installed (${detected.version})
|
|
16177
|
+
process.stdout.write(`Next steps:
|
|
15913
16178
|
`);
|
|
15914
|
-
|
|
16179
|
+
process.stdout.write(`1. Commit .octavio/review.config.json and .github/workflows/review-check.yml.
|
|
15915
16180
|
`);
|
|
15916
|
-
|
|
15917
|
-
process.stdout.write(`- opencode: missing
|
|
16181
|
+
process.stdout.write(`2. Optional: set OPENCODE_API_KEY if you switch away from the default free model.
|
|
15918
16182
|
`);
|
|
15919
|
-
process.stdout.write(`- install: ${OPEN_CODE_INSTALL_COMMAND}
|
|
15920
|
-
`);
|
|
15921
|
-
}
|
|
15922
16183
|
};
|
|
15923
16184
|
var runReview = async (argv) => {
|
|
15924
16185
|
const cliInput = parseReviewInput(argv);
|
|
@@ -16011,6 +16272,10 @@ var run = async () => {
|
|
|
16011
16272
|
await runDoctor();
|
|
16012
16273
|
return;
|
|
16013
16274
|
}
|
|
16275
|
+
if (firstArg === "init") {
|
|
16276
|
+
await runInit(args.slice(1));
|
|
16277
|
+
return;
|
|
16278
|
+
}
|
|
16014
16279
|
if (firstArg === "install-opencode") {
|
|
16015
16280
|
await ensureOpenCodeInstalled(true);
|
|
16016
16281
|
return;
|
|
@@ -16033,6 +16298,15 @@ try {
|
|
|
16033
16298
|
const message = error48 instanceof Error ? error48.message : String(error48);
|
|
16034
16299
|
process.stderr.write(`octavio-review failed: ${message}
|
|
16035
16300
|
`);
|
|
16301
|
+
const guidance = troubleshootingGuidance(message);
|
|
16302
|
+
if (guidance.length > 0) {
|
|
16303
|
+
process.stderr.write(`Troubleshooting:
|
|
16304
|
+
`);
|
|
16305
|
+
for (const line of guidance) {
|
|
16306
|
+
process.stderr.write(`- ${line}
|
|
16307
|
+
`);
|
|
16308
|
+
}
|
|
16309
|
+
}
|
|
16036
16310
|
process.exitCode = 1;
|
|
16037
16311
|
}
|
|
16038
16312
|
await Bun.sleep(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octavio.bot/review",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "CLI for OpenCode-powered pull request review checks",
|
|
6
6
|
"homepage": "https://github.com/rustydotwtf/octavio.bot/tree/main/apps/review-bot-cli",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"directory": "apps/review-bot-cli"
|
|
14
14
|
},
|
|
15
15
|
"bin": {
|
|
16
|
-
"octavio-review": "
|
|
16
|
+
"octavio-review": "dist/index.mjs"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"dist",
|
|
@@ -24,7 +24,10 @@
|
|
|
24
24
|
"access": "public"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
|
-
"build": "bun build ./src/index.ts --outfile ./dist/index.mjs --target bun --banner '#!/usr/bin/env bun' && chmod +x ./dist/index.mjs
|
|
27
|
+
"build:bin": "bun build ./src/index.ts --outfile ./dist/index.mjs --target bun --banner '#!/usr/bin/env bun' && chmod +x ./dist/index.mjs",
|
|
28
|
+
"sync:prompts": "[ -d ../../packages/prompts/prompts ] || (echo 'Missing prompts source: ../../packages/prompts/prompts' && exit 1) && rm -rf ./prompts && mkdir -p ./prompts && cp ../../packages/prompts/prompts/*.md ./prompts/",
|
|
29
|
+
"sync": "bun run sync:prompts",
|
|
30
|
+
"build": "bun run build:bin && bun run sync:prompts",
|
|
28
31
|
"prepack": "bun run build"
|
|
29
32
|
},
|
|
30
33
|
"dependencies": {
|