@todoist/doistbot-cli 0.0.0-stage → 1.0.16
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/dist/actions/auth.js +225 -0
- package/dist/actions/doctor.js +39 -0
- package/dist/actions/review-usage.js +25 -0
- package/dist/actions/review.js +311 -0
- package/dist/actions/setup.js +14 -0
- package/dist/auth.js +202 -0
- package/dist/config.js +145 -0
- package/dist/env.js +10 -0
- package/dist/file-store.js +112 -0
- package/dist/index.js +164 -0
- package/dist/input.js +40 -0
- package/dist/runtime.js +105 -0
- package/dist/secure-store.js +148 -0
- package/dist/terminal.js +185 -0
- package/dist/test-utils/secure-store-mocks.js +29 -0
- package/dist/update-cache-entrypoint.js +16 -0
- package/dist/update-check.js +235 -0
- package/package.json +65 -5
- package/sandbox/_node_modules/doistbot-logging/dist/index.d.ts +23 -0
- package/sandbox/_node_modules/doistbot-logging/dist/index.js +51 -0
- package/sandbox/_node_modules/doistbot-logging/package.json +12 -0
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +93 -0
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +197 -0
- package/sandbox/_node_modules/doistbot-repo-config/package.json +12 -0
- package/sandbox/dist/core/autofix-resources.js +58 -0
- package/sandbox/dist/core/autofix-service-query.js +69 -0
- package/sandbox/dist/core/autofix-test-policy.js +11 -0
- package/sandbox/dist/core/autofix-verification.js +355 -0
- package/sandbox/dist/core/bot-logins.js +14 -0
- package/sandbox/dist/core/check-run.js +158 -0
- package/sandbox/dist/core/datadog-metrics.js +113 -0
- package/sandbox/dist/core/environment.js +49 -0
- package/sandbox/dist/core/git-auth.js +17 -0
- package/sandbox/dist/core/github-history.js +4 -0
- package/sandbox/dist/core/github-issues.js +18 -0
- package/sandbox/dist/core/label-parsing.js +1 -0
- package/sandbox/dist/core/langfuse-observation.js +33 -0
- package/sandbox/dist/core/langfuse-session.js +39 -0
- package/sandbox/dist/core/logger-span-processor.js +90 -0
- package/sandbox/dist/core/logging.js +18 -0
- package/sandbox/dist/core/pi-autofix-extension.js +188 -0
- package/sandbox/dist/core/pi-execution-limits-extension.js +79 -0
- package/sandbox/dist/core/pi-progress-logging.js +51 -0
- package/sandbox/dist/core/pi-tool-output-filter-extension.js +65 -0
- package/sandbox/dist/core/pi.js +870 -0
- package/sandbox/dist/core/pii-scrubber.js +31 -0
- package/sandbox/dist/core/prompt-template.js +63 -0
- package/sandbox/dist/core/reactions.js +36 -0
- package/sandbox/dist/core/regexp.js +3 -0
- package/sandbox/dist/core/repository-map.js +1 -0
- package/sandbox/dist/core/repository-resolver.js +1 -0
- package/sandbox/dist/core/retry.js +29 -0
- package/sandbox/dist/core/s3-utils.js +42 -0
- package/sandbox/dist/core/shared.js +124 -0
- package/sandbox/dist/core/snapshot-helpers.js +17 -0
- package/sandbox/dist/core/span-test-helpers.js +13 -0
- package/sandbox/dist/core/tool-environment.js +83 -0
- package/sandbox/dist/core/tool-output.js +18 -0
- package/sandbox/dist/core/tracing-setup.js +92 -0
- package/sandbox/dist/core/tracing.js +81 -0
- package/sandbox/dist/main.js +51 -0
- package/sandbox/dist/providers/config.js +23 -0
- package/sandbox/dist/providers/credentials.js +40 -0
- package/sandbox/dist/providers/gemini.js +28 -0
- package/sandbox/dist/providers/helpers.js +253 -0
- package/sandbox/dist/providers/index.js +27 -0
- package/sandbox/dist/providers/openai.js +23 -0
- package/sandbox/dist/providers/openrouter-pi.js +25 -0
- package/sandbox/dist/providers/pi-review.js +301 -0
- package/sandbox/dist/providers/process.js +34 -0
- package/sandbox/dist/providers/schemas.js +61 -0
- package/sandbox/dist/providers/types.js +4 -0
- package/sandbox/dist/tasks/chat/chat.js +440 -0
- package/sandbox/dist/tasks/chat/prompt.js +20 -0
- package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +304 -0
- package/sandbox/dist/tasks/issue-summarize/context.js +21 -0
- package/sandbox/dist/tasks/issue-summarize/media.js +335 -0
- package/sandbox/dist/tasks/issue-summarize/model.js +230 -0
- package/sandbox/dist/tasks/issue-summarize/output.js +45 -0
- package/sandbox/dist/tasks/issue-summarize/prompt.js +11 -0
- package/sandbox/dist/tasks/issue-summarize/summarize.js +238 -0
- package/sandbox/dist/tasks/issue-summarize/thread.js +123 -0
- package/sandbox/dist/tasks/issue-summarize/types.js +1 -0
- package/sandbox/dist/tasks/issue-triage/assignee-review.js +114 -0
- package/sandbox/dist/tasks/issue-triage/autofix-capabilities.js +22 -0
- package/sandbox/dist/tasks/issue-triage/autofix-checkpoint.js +122 -0
- package/sandbox/dist/tasks/issue-triage/autofix-policy.js +36 -0
- package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
- package/sandbox/dist/tasks/issue-triage/context.js +57 -0
- package/sandbox/dist/tasks/issue-triage/diff-validator.js +102 -0
- package/sandbox/dist/tasks/issue-triage/fix-attempt.js +617 -0
- package/sandbox/dist/tasks/issue-triage/fix-context.js +5 -0
- package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +263 -0
- package/sandbox/dist/tasks/issue-triage/fix-failure-reasons.js +2 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/effects.js +526 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/effects.types.js +1 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/fake-effects.js +65 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/promotion-task.js +108 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/recovery.js +512 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +233 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/state-machine.js +171 -0
- package/sandbox/dist/tasks/issue-triage/github-request.js +2 -0
- package/sandbox/dist/tasks/issue-triage/hero-group-map.js +206 -0
- package/sandbox/dist/tasks/issue-triage/markers.js +6 -0
- package/sandbox/dist/tasks/issue-triage/model.js +124 -0
- package/sandbox/dist/tasks/issue-triage/output.js +164 -0
- package/sandbox/dist/tasks/issue-triage/pr-creator.js +585 -0
- package/sandbox/dist/tasks/issue-triage/pr-readiness.js +25 -0
- package/sandbox/dist/tasks/issue-triage/prompt.js +63 -0
- package/sandbox/dist/tasks/issue-triage/repo-config.js +47 -0
- package/sandbox/dist/tasks/issue-triage/repo-prompts.js +77 -0
- package/sandbox/dist/tasks/issue-triage/report-fixture.js +53 -0
- package/sandbox/dist/tasks/issue-triage/report-history.js +113 -0
- package/sandbox/dist/tasks/issue-triage/report-schema.js +63 -0
- package/sandbox/dist/tasks/issue-triage/report.js +111 -0
- package/sandbox/dist/tasks/issue-triage/resolution.js +30 -0
- package/sandbox/dist/tasks/issue-triage/routing.js +169 -0
- package/sandbox/dist/tasks/issue-triage/sandbox-environment.js +59 -0
- package/sandbox/dist/tasks/issue-triage/shared.js +86 -0
- package/sandbox/dist/tasks/issue-triage/side-effects.js +102 -0
- package/sandbox/dist/tasks/issue-triage/triage.js +442 -0
- package/sandbox/dist/tasks/issue-triage/types.js +1 -0
- package/sandbox/dist/tasks/persist-logs/context.js +19 -0
- package/sandbox/dist/tasks/persist-logs/dd-proxy.js +116 -0
- package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +156 -0
- package/sandbox/dist/tasks/persist-logs/persist-logs.js +491 -0
- package/sandbox/dist/tasks/refresh-s3-urls/refresh-s3-urls.js +214 -0
- package/sandbox/dist/tasks/review/check-run.js +326 -0
- package/sandbox/dist/tasks/review/comment-mapper.js +46 -0
- package/sandbox/dist/tasks/review/conversation-context.js +477 -0
- package/sandbox/dist/tasks/review/diff.js +95 -0
- package/sandbox/dist/tasks/review/engines/dedupe.js +291 -0
- package/sandbox/dist/tasks/review/engines/multi-focus.js +419 -0
- package/sandbox/dist/tasks/review/engines/shared.js +87 -0
- package/sandbox/dist/tasks/review/engines/types.js +1 -0
- package/sandbox/dist/tasks/review/finding-output.js +18 -0
- package/sandbox/dist/tasks/review/format.js +17 -0
- package/sandbox/dist/tasks/review/github-comment-format.js +94 -0
- package/sandbox/dist/tasks/review/local-git.js +374 -0
- package/sandbox/dist/tasks/review/local.js +118 -0
- package/sandbox/dist/tasks/review/multi-focus-prompt.js +59 -0
- package/sandbox/dist/tasks/review/prompt.js +81 -0
- package/sandbox/dist/tasks/review/review-feedback.js +48 -0
- package/sandbox/dist/tasks/review/review-mode-github.js +58 -0
- package/sandbox/dist/tasks/review/review-mode-policy.js +232 -0
- package/sandbox/dist/tasks/review/review-mode-types.js +1 -0
- package/sandbox/dist/tasks/review/review-mode.js +296 -0
- package/sandbox/dist/tasks/review/review-summary.js +123 -0
- package/sandbox/dist/tasks/review/review.js +391 -0
- package/sandbox/dist/tasks/review/runner.js +255 -0
- package/sandbox/dist/tasks/review/severity.js +5 -0
- package/sandbox/dist/tasks/review/summary-model.js +271 -0
- package/sandbox/dist/tasks/review/thinking-level.js +14 -0
- package/sandbox/dist/tasks/review/translation-review-policy.js +37 -0
- package/sandbox/dist/tasks/review/types.js +1 -0
- package/sandbox/dist/tasks/review-slice-plan/context.js +66 -0
- package/sandbox/dist/tasks/review-slice-plan/plan.js +210 -0
- package/sandbox/dist/tasks/review-slice-plan/prompt.js +59 -0
- package/sandbox/dist/tasks/review-slice-plan/review-slice-plan.js +367 -0
- package/sandbox/dist/tasks/sentry-triage/store.js +133 -0
- package/sandbox/dist/tasks/sentry-triage/triage.js +217 -0
- package/sandbox/node_modules/doistbot-logging/dist/index.d.ts +23 -0
- package/sandbox/node_modules/doistbot-logging/dist/index.js +51 -0
- package/sandbox/node_modules/doistbot-logging/package.json +12 -0
- package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +93 -0
- package/sandbox/node_modules/doistbot-repo-config/dist/index.js +197 -0
- package/sandbox/node_modules/doistbot-repo-config/package.json +12 -0
- package/sandbox/src/review/prompts/ai-internal-tools.md +254 -0
- package/sandbox/src/review/prompts/backend-repository-standards.md +102 -0
- package/sandbox/src/review/prompts/observability-standard.md +169 -0
- package/sandbox/src/review/prompts/outline-map.json +26 -0
- package/sandbox/src/review/prompts/secrets-management.md +286 -0
- package/sandbox/src/review/prompts/service-production-readiness.md +122 -0
- package/sandbox/src/review/prompts/workload-placement.md +345 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/efficiency.md +16 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/general.md +32 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/quality.md +20 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/reuse.md +13 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/security.md +18 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/standards.md +21 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/tests.md +19 -0
- package/sandbox/src/tasks/review/prompts/review-mode.md +40 -0
- package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +142 -0
- package/scripts/restore-runtime-assets.mjs +21 -0
- package/README.md +0 -3
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { isInteractiveOutputAllowed, writeLine } from './terminal.js';
|
|
7
|
+
const PACKAGE_NAME = '@todoist/doistbot-cli';
|
|
8
|
+
const REGISTRY_LATEST_URL = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`;
|
|
9
|
+
const BACKGROUND_FETCH_TIMEOUT_MS = 5_000;
|
|
10
|
+
const FOREGROUND_FETCH_TIMEOUT_MS = 10_000;
|
|
11
|
+
const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1_000;
|
|
12
|
+
export function getDefaultUpdateCheckCachePath() {
|
|
13
|
+
const configRoot = process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), '.cache');
|
|
14
|
+
return path.join(configRoot, 'doistbot-cli', 'update-check.json');
|
|
15
|
+
}
|
|
16
|
+
export function isNewerVersion(latestVersion, currentVersion) {
|
|
17
|
+
const latest = parseVersion(latestVersion);
|
|
18
|
+
const current = parseVersion(currentVersion);
|
|
19
|
+
if (!latest || !current) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
for (const [index, latestPart] of latest.entries()) {
|
|
23
|
+
const currentPart = current[index] ?? 0;
|
|
24
|
+
if (latestPart > currentPart) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
if (latestPart < currentPart) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
export async function notifyIfCliUpdateAvailable({ cachePath = getDefaultUpdateCheckCachePath(), currentVersion, fetchLatestVersion = fetchLatestPackageVersion, timeoutMs = FOREGROUND_FETCH_TIMEOUT_MS, writeLine: write = writeLine, }) {
|
|
34
|
+
const latestVersion = await refreshCliUpdateCache({
|
|
35
|
+
cachePath,
|
|
36
|
+
currentVersion,
|
|
37
|
+
fetchLatestVersion,
|
|
38
|
+
timeoutMs,
|
|
39
|
+
});
|
|
40
|
+
if (!latestVersion) {
|
|
41
|
+
write('Unable to check for Doistbot CLI updates.');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (!isNewerVersion(latestVersion, currentVersion)) {
|
|
45
|
+
write(`doistbot ${currentVersion} is up to date.`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
write(formatCliUpdateNotice({ currentVersion, latestVersion }));
|
|
49
|
+
}
|
|
50
|
+
export async function notifyAndRefreshCliUpdateCache({ cachePath = getDefaultUpdateCheckCachePath(), currentVersion, env = process.env, now = new Date(), spawnProcess = spawn, stdout = process.stdout, updateCacheEntrypointPath, writeLine: write = writeLine, }) {
|
|
51
|
+
if (!isUpdateCheckEnabled({ env, stdout })) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const cache = await readUpdateCheckCache(cachePath);
|
|
55
|
+
writeCachedCliUpdateNotice({ cache, currentVersion, writeLine: write });
|
|
56
|
+
if (shouldRefreshCliUpdateCache({ cache, now })) {
|
|
57
|
+
refreshCliUpdateCacheInBackground({
|
|
58
|
+
cachePath,
|
|
59
|
+
currentVersion,
|
|
60
|
+
env,
|
|
61
|
+
spawnProcess,
|
|
62
|
+
updateCacheEntrypointPath,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export function isUpdateCheckEnabled({ env = process.env, stdout = process.stdout, } = {}) {
|
|
67
|
+
return (env.DOISTBOT_DISABLE_UPDATE_CHECK !== 'true' &&
|
|
68
|
+
env.DOISTBOT_DISABLE_UPDATE_CHECK !== '1' &&
|
|
69
|
+
isInteractiveOutputAllowed({ env, stream: stdout }));
|
|
70
|
+
}
|
|
71
|
+
export async function refreshCliUpdateCache({ cachePath = getDefaultUpdateCheckCachePath(), fetchLatestVersion = fetchLatestPackageVersion, now = new Date(), timeoutMs = BACKGROUND_FETCH_TIMEOUT_MS, }) {
|
|
72
|
+
const latestVersion = await fetchLatestVersionWithTimeout(fetchLatestVersion, timeoutMs);
|
|
73
|
+
const previousCache = await readUpdateCheckCache(cachePath);
|
|
74
|
+
try {
|
|
75
|
+
await writeUpdateCheckCache({
|
|
76
|
+
cachePath,
|
|
77
|
+
checkedAt: now,
|
|
78
|
+
latestVersion: latestVersion ?? previousCache?.latestVersion,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Cache writes are best-effort; a successful registry lookup should still be usable.
|
|
83
|
+
}
|
|
84
|
+
return latestVersion;
|
|
85
|
+
}
|
|
86
|
+
export function formatCliUpdateNotice({ currentVersion, latestVersion, }) {
|
|
87
|
+
const homebrewCommand = 'brew update && brew upgrade doistbot-cli';
|
|
88
|
+
const npmCommand = `npm install -g ${PACKAGE_NAME}@latest`;
|
|
89
|
+
const plainLines = [
|
|
90
|
+
`Update available: doistbot ${currentVersion} -> ${latestVersion}`,
|
|
91
|
+
'',
|
|
92
|
+
'Homebrew',
|
|
93
|
+
` ${homebrewCommand}`,
|
|
94
|
+
'',
|
|
95
|
+
'npm',
|
|
96
|
+
` ${npmCommand}`,
|
|
97
|
+
];
|
|
98
|
+
const innerWidth = Math.max(...plainLines.map((line) => line.length)) + 2;
|
|
99
|
+
const border = colorizeUpdateNotice(`┌${'─'.repeat(innerWidth + 2)}┐`);
|
|
100
|
+
const footer = colorizeUpdateNotice(`└${'─'.repeat(innerWidth + 2)}┘`);
|
|
101
|
+
const renderedLines = plainLines.map((line, index) => renderBoxLine(line, index === 0 ? renderUpdateTitle(currentVersion, latestVersion) : renderUpdateLine(line), innerWidth));
|
|
102
|
+
return ['', '', border, ...renderedLines, footer].join('\n');
|
|
103
|
+
}
|
|
104
|
+
function renderUpdateTitle(currentVersion, latestVersion) {
|
|
105
|
+
return [
|
|
106
|
+
chalk.yellow('Update available: doistbot '),
|
|
107
|
+
chalk.bold.yellow(currentVersion),
|
|
108
|
+
chalk.yellow(' -> '),
|
|
109
|
+
chalk.bold.yellow(latestVersion),
|
|
110
|
+
].join('');
|
|
111
|
+
}
|
|
112
|
+
function renderUpdateLine(line) {
|
|
113
|
+
if (line.startsWith(' ')) {
|
|
114
|
+
return ` ${chalk.italic.white(line.trim())}`;
|
|
115
|
+
}
|
|
116
|
+
return colorizeUpdateNotice(line);
|
|
117
|
+
}
|
|
118
|
+
function renderBoxLine(plainLine, renderedLine, innerWidth) {
|
|
119
|
+
const padding = ' '.repeat(innerWidth - plainLine.length);
|
|
120
|
+
const border = colorizeUpdateNotice('│');
|
|
121
|
+
return `${border} ${renderedLine}${padding} ${border}`;
|
|
122
|
+
}
|
|
123
|
+
function colorizeUpdateNotice(value) {
|
|
124
|
+
return chalk.yellow(value);
|
|
125
|
+
}
|
|
126
|
+
async function fetchLatestVersionWithTimeout(fetchLatestVersion, timeoutMs) {
|
|
127
|
+
try {
|
|
128
|
+
return await fetchLatestVersion(AbortSignal.timeout(timeoutMs));
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function shouldRefreshCliUpdateCache({ cache, now, }) {
|
|
135
|
+
if (!cache?.checkedAt) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
return now.getTime() - cache.checkedAt.getTime() >= UPDATE_CHECK_INTERVAL_MS;
|
|
139
|
+
}
|
|
140
|
+
function refreshCliUpdateCacheInBackground({ cachePath, currentVersion, env, spawnProcess, updateCacheEntrypointPath, }) {
|
|
141
|
+
try {
|
|
142
|
+
const child = spawnProcess(process.execPath, [
|
|
143
|
+
updateCacheEntrypointPath,
|
|
144
|
+
'--cache-path',
|
|
145
|
+
cachePath,
|
|
146
|
+
'--current-version',
|
|
147
|
+
currentVersion,
|
|
148
|
+
], {
|
|
149
|
+
detached: true,
|
|
150
|
+
env: createUpdateCacheRefreshEnv(env),
|
|
151
|
+
stdio: 'ignore',
|
|
152
|
+
});
|
|
153
|
+
child.on('error', () => undefined);
|
|
154
|
+
child.unref();
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Background refresh must never fail the foreground CLI command.
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function createUpdateCacheRefreshEnv(env) {
|
|
161
|
+
const allowedEnvKeys = [
|
|
162
|
+
'HOME',
|
|
163
|
+
'HTTPS_PROXY',
|
|
164
|
+
'HTTP_PROXY',
|
|
165
|
+
'LOGNAME',
|
|
166
|
+
'NODE_EXTRA_CA_CERTS',
|
|
167
|
+
'NO_PROXY',
|
|
168
|
+
'PATH',
|
|
169
|
+
'SHELL',
|
|
170
|
+
'SSL_CERT_DIR',
|
|
171
|
+
'SSL_CERT_FILE',
|
|
172
|
+
'TEMP',
|
|
173
|
+
'TMP',
|
|
174
|
+
'TMPDIR',
|
|
175
|
+
'USER',
|
|
176
|
+
'XDG_CACHE_HOME',
|
|
177
|
+
'https_proxy',
|
|
178
|
+
'http_proxy',
|
|
179
|
+
'no_proxy',
|
|
180
|
+
];
|
|
181
|
+
const minimalEnv = {};
|
|
182
|
+
for (const key of allowedEnvKeys) {
|
|
183
|
+
if (env[key] !== undefined) {
|
|
184
|
+
minimalEnv[key] = env[key];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return minimalEnv;
|
|
188
|
+
}
|
|
189
|
+
function writeCachedCliUpdateNotice({ cache, currentVersion, writeLine: write, }) {
|
|
190
|
+
if (!cache?.latestVersion || !isNewerVersion(cache.latestVersion, currentVersion)) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
write(formatCliUpdateNotice({ currentVersion, latestVersion: cache.latestVersion }));
|
|
194
|
+
}
|
|
195
|
+
async function readUpdateCheckCache(cachePath) {
|
|
196
|
+
try {
|
|
197
|
+
const cache = JSON.parse(await readFile(cachePath, 'utf8'));
|
|
198
|
+
const checkedAt = typeof cache.checkedAt === 'string' ? new Date(cache.checkedAt) : undefined;
|
|
199
|
+
return {
|
|
200
|
+
checkedAt: checkedAt && Number.isFinite(checkedAt.getTime()) ? checkedAt : undefined,
|
|
201
|
+
latestVersion: typeof cache.latestVersion === 'string' ? cache.latestVersion : undefined,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async function writeUpdateCheckCache({ cachePath, checkedAt, latestVersion, }) {
|
|
209
|
+
await mkdir(path.dirname(cachePath), { recursive: true });
|
|
210
|
+
const cache = {
|
|
211
|
+
checkedAt: checkedAt.toISOString(),
|
|
212
|
+
};
|
|
213
|
+
if (latestVersion) {
|
|
214
|
+
cache.latestVersion = latestVersion;
|
|
215
|
+
}
|
|
216
|
+
await writeFile(cachePath, `${JSON.stringify(cache, null, 4)}\n`);
|
|
217
|
+
}
|
|
218
|
+
async function fetchLatestPackageVersion(signal) {
|
|
219
|
+
const response = await fetch(REGISTRY_LATEST_URL, {
|
|
220
|
+
headers: { accept: 'application/json' },
|
|
221
|
+
signal,
|
|
222
|
+
});
|
|
223
|
+
if (!response.ok) {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
const body = (await response.json());
|
|
227
|
+
return typeof body.version === 'string' ? body.version : undefined;
|
|
228
|
+
}
|
|
229
|
+
function parseVersion(version) {
|
|
230
|
+
const match = version.match(/^v?(\d+)\.(\d+)\.(\d+)/);
|
|
231
|
+
if (!match) {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
235
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,66 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@todoist/doistbot-cli",
|
|
3
|
+
"version": "1.0.16",
|
|
4
|
+
"description": "Local self-review CLI for Doistbot",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"db": "dist/index.js",
|
|
9
|
+
"doistbot": "dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"doist",
|
|
13
|
+
"doistbot",
|
|
14
|
+
"cli",
|
|
15
|
+
"review"
|
|
16
|
+
],
|
|
17
|
+
"author": "Doist",
|
|
18
|
+
"license": "UNLICENSED",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/todoist/doistbot-app.git",
|
|
22
|
+
"directory": "apps/cli"
|
|
23
|
+
},
|
|
24
|
+
"preferGlobal": true,
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=24"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"!dist/**/*.test.js",
|
|
34
|
+
"!dist/pr-review-dry-run.*",
|
|
35
|
+
"sandbox",
|
|
36
|
+
"!sandbox/**/*.test.d.ts",
|
|
37
|
+
"!sandbox/**/*.test.js",
|
|
38
|
+
"!sandbox/dist/tasks/review/pr-dry-run*",
|
|
39
|
+
"scripts/restore-runtime-assets.mjs",
|
|
40
|
+
"package.json"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc -p tsconfig.build.json && node scripts/copy-runtime-assets.mjs && node scripts/mark-bin-executable.mjs",
|
|
44
|
+
"postinstall": "node scripts/restore-runtime-assets.mjs",
|
|
45
|
+
"prepublishOnly": "npm run build && npm test",
|
|
46
|
+
"typecheck": "tsc --noEmit",
|
|
47
|
+
"test": "vitest run src scripts --config ../../config/vitest.base.ts"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@earendil-works/pi-ai": "0.99.2",
|
|
51
|
+
"@earendil-works/pi-coding-agent": "0.99.2",
|
|
52
|
+
"@inquirer/checkbox": "5.2.8",
|
|
53
|
+
"@inquirer/password": "5.2.5",
|
|
54
|
+
"@napi-rs/keyring": "1.3.0",
|
|
55
|
+
"@opentelemetry/api": "1.10.0",
|
|
56
|
+
"ajv": "8.20.0",
|
|
57
|
+
"chalk": "4.1.2",
|
|
58
|
+
"commander": "14.0.3",
|
|
59
|
+
"js-yaml": "4.3.2",
|
|
60
|
+
"undici": "8.12.4",
|
|
61
|
+
"yocto-spinner": "1.2.2"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"vitest": "4.1.12"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type LogLevel = 'info' | 'warn' | 'error';
|
|
2
|
+
export type LogMeta = Record<string, unknown>;
|
|
3
|
+
export type LoggerOptions = {
|
|
4
|
+
baseContext?: LogMeta;
|
|
5
|
+
getContext?: () => LogMeta | undefined;
|
|
6
|
+
getTraceInfo?: () => LogMeta | undefined;
|
|
7
|
+
suppressLogs?: (level: LogLevel) => boolean;
|
|
8
|
+
suppressInfoLogs?: () => boolean;
|
|
9
|
+
includeLogField?: boolean;
|
|
10
|
+
/** Receives serialized JSON without a trailing newline. The writer must add one. */
|
|
11
|
+
write?: (line: string) => void;
|
|
12
|
+
buildInfo?: {
|
|
13
|
+
version: number;
|
|
14
|
+
commit: string;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export type Logger = {
|
|
18
|
+
info: (message: string, meta?: LogMeta) => void;
|
|
19
|
+
warn: (message: string, meta?: LogMeta) => void;
|
|
20
|
+
error: (message: string, meta?: LogMeta) => void;
|
|
21
|
+
setBaseContext: (meta: LogMeta) => void;
|
|
22
|
+
};
|
|
23
|
+
export declare function createLogger(options?: LoggerOptions): Logger;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function createLogger(options = {}) {
|
|
2
|
+
let baseContext = options.baseContext ?? {};
|
|
3
|
+
const includeLogField = options.includeLogField ?? false;
|
|
4
|
+
const buildInfo = options.buildInfo;
|
|
5
|
+
function setBaseContext(meta) {
|
|
6
|
+
baseContext = meta;
|
|
7
|
+
}
|
|
8
|
+
function log(level, message, meta) {
|
|
9
|
+
if (options.suppressLogs?.(level) === true) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (level === 'info' && options.suppressInfoLogs?.() === true) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const context = options.getContext?.() ?? {};
|
|
16
|
+
const traceInfo = options.getTraceInfo?.() ?? {};
|
|
17
|
+
const metaInfo = {
|
|
18
|
+
...context,
|
|
19
|
+
...baseContext,
|
|
20
|
+
...(meta ?? {}),
|
|
21
|
+
};
|
|
22
|
+
const payload = {
|
|
23
|
+
level,
|
|
24
|
+
message,
|
|
25
|
+
timestamp: new Date().toISOString(),
|
|
26
|
+
};
|
|
27
|
+
if (includeLogField) {
|
|
28
|
+
payload.log = message;
|
|
29
|
+
}
|
|
30
|
+
if (buildInfo) {
|
|
31
|
+
payload.version = buildInfo.version;
|
|
32
|
+
payload.commit = buildInfo.commit;
|
|
33
|
+
}
|
|
34
|
+
Object.assign(payload, traceInfo);
|
|
35
|
+
payload.meta = metaInfo;
|
|
36
|
+
const line = JSON.stringify(payload);
|
|
37
|
+
if (options.write) {
|
|
38
|
+
options.write(line);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// eslint-disable-next-line no-console
|
|
42
|
+
console.log(line);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
info: (message, meta) => log('info', message, meta),
|
|
47
|
+
warn: (message, meta) => log('warn', message, meta),
|
|
48
|
+
error: (message, meta) => log('error', message, meta),
|
|
49
|
+
setBaseContext,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Logger } from 'doistbot-logging';
|
|
2
|
+
import type { Octokit } from 'octokit';
|
|
3
|
+
export declare const CONFIG_FILE_PATH = ".github/doistbot.yml";
|
|
4
|
+
export declare const DEFAULT_REVIEW_MODELS: readonly ["openai", "deepseek", "zai"];
|
|
5
|
+
export type DefaultReviewModel = (typeof DEFAULT_REVIEW_MODELS)[number];
|
|
6
|
+
export type AutofixPolicy = {
|
|
7
|
+
guidance?: string;
|
|
8
|
+
forbidden_test_paths?: string[];
|
|
9
|
+
verification: {
|
|
10
|
+
checks: {
|
|
11
|
+
name: string;
|
|
12
|
+
command: string;
|
|
13
|
+
use_test_selectors?: boolean;
|
|
14
|
+
}[];
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export type ReviewModeConfig = {
|
|
18
|
+
guidance_path?: string;
|
|
19
|
+
labels?: {
|
|
20
|
+
ask?: string[];
|
|
21
|
+
show?: string[];
|
|
22
|
+
ship?: string[];
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
export type RepoConfig = {
|
|
26
|
+
review?: {
|
|
27
|
+
skip_forks?: boolean;
|
|
28
|
+
skip_drafts?: boolean;
|
|
29
|
+
skip_title_pattern?: string;
|
|
30
|
+
skip_branch_pattern?: string;
|
|
31
|
+
mode?: ReviewModeConfig;
|
|
32
|
+
/**
|
|
33
|
+
* @deprecated Ignored as of #274. Doistbot now posts a GitHub Check
|
|
34
|
+
* Run for every `/review` (always-on; no opt-in) when `HEAD_SHA` is
|
|
35
|
+
* available. Kept here for back-compat: any existing
|
|
36
|
+
* `.github/doistbot.yml` setting this field will parse fine but the
|
|
37
|
+
* value has no effect.
|
|
38
|
+
*/
|
|
39
|
+
post_commit_status?: boolean;
|
|
40
|
+
};
|
|
41
|
+
issue_triage?: {
|
|
42
|
+
enable_auto_triage?: boolean;
|
|
43
|
+
prompts?: {
|
|
44
|
+
triage?: string;
|
|
45
|
+
autofix?: string;
|
|
46
|
+
};
|
|
47
|
+
/** @deprecated Ignored. Every actionable triage report starts a fix. */
|
|
48
|
+
enable_auto_fix?: boolean;
|
|
49
|
+
/** @deprecated Ignored. Every issue fix uses the review and CI loop. */
|
|
50
|
+
use_fix_loop?: boolean;
|
|
51
|
+
max_changed_files?: number;
|
|
52
|
+
max_changed_lines?: number;
|
|
53
|
+
autofix?: AutofixPolicy;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
type FetchFileParams = {
|
|
57
|
+
octokit: Octokit;
|
|
58
|
+
owner: string;
|
|
59
|
+
repo: string;
|
|
60
|
+
path: string;
|
|
61
|
+
ref?: string;
|
|
62
|
+
signal?: AbortSignal;
|
|
63
|
+
logger?: Logger;
|
|
64
|
+
strict?: boolean;
|
|
65
|
+
maxBytes?: number;
|
|
66
|
+
};
|
|
67
|
+
export declare class RepoFileContentError extends Error {
|
|
68
|
+
readonly reason: 'not_file' | 'too_large' | 'unavailable';
|
|
69
|
+
constructor(reason: 'not_file' | 'too_large' | 'unavailable');
|
|
70
|
+
}
|
|
71
|
+
/** Review-mode validation is opt-in so it cannot block unrelated config callers. */
|
|
72
|
+
export declare function parseRepoConfigContent(content: string, { validateReviewMode }?: {
|
|
73
|
+
validateReviewMode?: boolean;
|
|
74
|
+
}): RepoConfig;
|
|
75
|
+
export declare function fetchFileContent({ octokit, owner, repo, path, ref, signal, logger, strict, maxBytes, }: FetchFileParams): Promise<string | null>;
|
|
76
|
+
/**
|
|
77
|
+
* Fetches the repository-specific configuration from .github/doistbot.yml.
|
|
78
|
+
* Returns null when the file is genuinely absent (404) or the path is not a file.
|
|
79
|
+
* Throws on transport / authorization errors so callers can distinguish "no
|
|
80
|
+
* config" from "could not load config" — important when an opt-out flag would
|
|
81
|
+
* otherwise be silently ignored.
|
|
82
|
+
*/
|
|
83
|
+
export declare function getRepoConfig({ octokit, owner, repo, ref, signal, logger, }: Omit<FetchFileParams, 'path' | 'strict' | 'maxBytes'> & {
|
|
84
|
+
logger: Logger;
|
|
85
|
+
}): Promise<RepoConfig | null>;
|
|
86
|
+
export declare function getLocalRepoConfig({ workspaceDir, logger, strict, }: {
|
|
87
|
+
workspaceDir: string;
|
|
88
|
+
logger: Logger;
|
|
89
|
+
strict?: boolean;
|
|
90
|
+
}): RepoConfig | null;
|
|
91
|
+
export declare function getDefaultReviewModels(): DefaultReviewModel[];
|
|
92
|
+
export declare function parseReviewModelsEnabled(value: string | undefined): string[];
|
|
93
|
+
export {};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { load as yamlLoad } from 'js-yaml';
|
|
4
|
+
export const CONFIG_FILE_PATH = '.github/doistbot.yml';
|
|
5
|
+
export const DEFAULT_REVIEW_MODELS = ['openai', 'deepseek', 'zai'];
|
|
6
|
+
export class RepoFileContentError extends Error {
|
|
7
|
+
reason;
|
|
8
|
+
constructor(reason) {
|
|
9
|
+
super(`Repository file content is ${reason}`);
|
|
10
|
+
this.reason = reason;
|
|
11
|
+
this.name = 'RepoFileContentError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function isMapping(value) {
|
|
15
|
+
return (typeof value === 'object' &&
|
|
16
|
+
value !== null &&
|
|
17
|
+
(Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null));
|
|
18
|
+
}
|
|
19
|
+
function hasControlCharacters(value) {
|
|
20
|
+
return [...value].some((character) => {
|
|
21
|
+
const code = character.charCodeAt(0);
|
|
22
|
+
return code < 0x20 || code === 0x7f;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function parseReviewModeConfig(value) {
|
|
26
|
+
if (!isMapping(value)) {
|
|
27
|
+
throw new Error('review.mode must be a mapping');
|
|
28
|
+
}
|
|
29
|
+
const mode = {};
|
|
30
|
+
if (value.guidance_path !== undefined) {
|
|
31
|
+
const guidancePath = value.guidance_path;
|
|
32
|
+
if (typeof guidancePath !== 'string' ||
|
|
33
|
+
!guidancePath.trim() ||
|
|
34
|
+
path.posix.isAbsolute(guidancePath.trim()) ||
|
|
35
|
+
path.win32.isAbsolute(guidancePath.trim()) ||
|
|
36
|
+
/^[a-z][a-z\d+.-]*:/i.test(guidancePath.trim()) ||
|
|
37
|
+
guidancePath.includes('\\') ||
|
|
38
|
+
hasControlCharacters(guidancePath) ||
|
|
39
|
+
guidancePath.trim().split('/').includes('..') ||
|
|
40
|
+
!/\.(md|markdown)$/i.test(guidancePath.trim())) {
|
|
41
|
+
throw new Error('review.mode.guidance_path must be a repository-relative Markdown path');
|
|
42
|
+
}
|
|
43
|
+
mode.guidance_path = path.posix.normalize(guidancePath.trim());
|
|
44
|
+
}
|
|
45
|
+
if (value.labels !== undefined) {
|
|
46
|
+
if (!isMapping(value.labels)) {
|
|
47
|
+
throw new Error('review.mode.labels must be a mapping');
|
|
48
|
+
}
|
|
49
|
+
mode.labels = {};
|
|
50
|
+
for (const category of ['ask', 'show', 'ship']) {
|
|
51
|
+
const labels = value.labels[category];
|
|
52
|
+
if (labels === undefined)
|
|
53
|
+
continue;
|
|
54
|
+
if (!Array.isArray(labels) ||
|
|
55
|
+
labels.some((label) => typeof label !== 'string' || !label.trim() || hasControlCharacters(label))) {
|
|
56
|
+
throw new Error(`review.mode.labels.${category} must contain non-empty label names`);
|
|
57
|
+
}
|
|
58
|
+
mode.labels[category] = labels.map((label) => label.trim());
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return mode;
|
|
62
|
+
}
|
|
63
|
+
/** Review-mode validation is opt-in so it cannot block unrelated config callers. */
|
|
64
|
+
export function parseRepoConfigContent(content, { validateReviewMode = false } = {}) {
|
|
65
|
+
const config = yamlLoad(content);
|
|
66
|
+
if (!validateReviewMode)
|
|
67
|
+
return config;
|
|
68
|
+
if (!isMapping(config)) {
|
|
69
|
+
throw new Error('Repository configuration must be a mapping');
|
|
70
|
+
}
|
|
71
|
+
if (config.review !== undefined) {
|
|
72
|
+
if (!isMapping(config.review)) {
|
|
73
|
+
throw new Error('review must be a mapping');
|
|
74
|
+
}
|
|
75
|
+
if (config.review.mode !== undefined) {
|
|
76
|
+
config.review.mode = parseReviewModeConfig(config.review.mode);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return config;
|
|
80
|
+
}
|
|
81
|
+
export async function fetchFileContent({ octokit, owner, repo, path, ref, signal, logger, strict = false, maxBytes, }) {
|
|
82
|
+
const params = {
|
|
83
|
+
owner,
|
|
84
|
+
repo,
|
|
85
|
+
path,
|
|
86
|
+
};
|
|
87
|
+
if (ref) {
|
|
88
|
+
params.ref = ref;
|
|
89
|
+
}
|
|
90
|
+
if (signal) {
|
|
91
|
+
params.request = { signal };
|
|
92
|
+
}
|
|
93
|
+
let response;
|
|
94
|
+
try {
|
|
95
|
+
response = await octokit.rest.repos.getContent(params);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (error instanceof Error && 'status' in error && error.status === 404) {
|
|
99
|
+
logger?.info('File not found', {
|
|
100
|
+
component: 'repo-config',
|
|
101
|
+
repository: `${owner}/${repo}`,
|
|
102
|
+
path,
|
|
103
|
+
});
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
const { data } = response;
|
|
109
|
+
if (!('type' in data) || data.type !== 'file' || (strict && 'target' in data)) {
|
|
110
|
+
if (strict)
|
|
111
|
+
throw new RepoFileContentError('not_file');
|
|
112
|
+
logger?.warn('Path exists but is not a file', {
|
|
113
|
+
component: 'repo-config',
|
|
114
|
+
repository: `${owner}/${repo}`,
|
|
115
|
+
path,
|
|
116
|
+
});
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
if (maxBytes !== undefined && 'size' in data && data.size > maxBytes) {
|
|
120
|
+
throw new RepoFileContentError('too_large');
|
|
121
|
+
}
|
|
122
|
+
if (!('content' in data) ||
|
|
123
|
+
typeof data.content !== 'string' ||
|
|
124
|
+
(strict && 'encoding' in data && data.encoding !== 'base64')) {
|
|
125
|
+
if (strict)
|
|
126
|
+
throw new RepoFileContentError('unavailable');
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
if (!strict && !data.content)
|
|
130
|
+
return null;
|
|
131
|
+
const content = Buffer.from(data.content, 'base64').toString('utf8');
|
|
132
|
+
if (maxBytes !== undefined && Buffer.byteLength(content, 'utf8') > maxBytes) {
|
|
133
|
+
throw new RepoFileContentError('too_large');
|
|
134
|
+
}
|
|
135
|
+
return content;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Fetches the repository-specific configuration from .github/doistbot.yml.
|
|
139
|
+
* Returns null when the file is genuinely absent (404) or the path is not a file.
|
|
140
|
+
* Throws on transport / authorization errors so callers can distinguish "no
|
|
141
|
+
* config" from "could not load config" — important when an opt-out flag would
|
|
142
|
+
* otherwise be silently ignored.
|
|
143
|
+
*/
|
|
144
|
+
export async function getRepoConfig({ octokit, owner, repo, ref, signal, logger, }) {
|
|
145
|
+
const content = await fetchFileContent({
|
|
146
|
+
octokit,
|
|
147
|
+
owner,
|
|
148
|
+
repo,
|
|
149
|
+
path: CONFIG_FILE_PATH,
|
|
150
|
+
ref,
|
|
151
|
+
signal,
|
|
152
|
+
logger,
|
|
153
|
+
});
|
|
154
|
+
if (content === null) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
const config = parseRepoConfigContent(content);
|
|
158
|
+
logger.info('Loaded repo config', {
|
|
159
|
+
component: 'repo-config',
|
|
160
|
+
repository: `${owner}/${repo}`,
|
|
161
|
+
path: CONFIG_FILE_PATH,
|
|
162
|
+
});
|
|
163
|
+
return config;
|
|
164
|
+
}
|
|
165
|
+
export function getLocalRepoConfig({ workspaceDir, logger, strict = false, }) {
|
|
166
|
+
const configPath = path.join(workspaceDir, CONFIG_FILE_PATH);
|
|
167
|
+
try {
|
|
168
|
+
const config = parseRepoConfigContent(fs.readFileSync(configPath, 'utf8'));
|
|
169
|
+
logger.info('Loaded local repo config', {
|
|
170
|
+
component: 'repo-config',
|
|
171
|
+
path: CONFIG_FILE_PATH,
|
|
172
|
+
});
|
|
173
|
+
return config;
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
|
|
177
|
+
return null;
|
|
178
|
+
if (strict)
|
|
179
|
+
throw error;
|
|
180
|
+
logger.warn('Failed to parse local repo config', {
|
|
181
|
+
component: 'repo-config',
|
|
182
|
+
path: CONFIG_FILE_PATH,
|
|
183
|
+
error: error instanceof Error ? error.message : String(error),
|
|
184
|
+
});
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
export function getDefaultReviewModels() {
|
|
189
|
+
return [...DEFAULT_REVIEW_MODELS];
|
|
190
|
+
}
|
|
191
|
+
export function parseReviewModelsEnabled(value) {
|
|
192
|
+
const models = value
|
|
193
|
+
?.split(',')
|
|
194
|
+
.map((model) => model.trim())
|
|
195
|
+
.filter(Boolean) ?? [];
|
|
196
|
+
return models.length > 0 ? [...new Set(models)] : getDefaultReviewModels();
|
|
197
|
+
}
|