@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
package/dist/auth.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import {} from '@earendil-works/pi-ai';
|
|
5
|
+
import { getEnvApiKeys } from './env.js';
|
|
6
|
+
import { createFileStore } from './file-store.js';
|
|
7
|
+
import { promptInput } from './input.js';
|
|
8
|
+
import { createCompositeStore, createKeyringStore, SecureStoreUnavailableError, } from './secure-store.js';
|
|
9
|
+
import { writeLine } from './terminal.js';
|
|
10
|
+
const GEMINI_ACCOUNT_NAME = 'gemini-api-key';
|
|
11
|
+
const OPENROUTER_ACCOUNT_NAME = 'openrouter-api-key';
|
|
12
|
+
export function createGeminiKeyStore() {
|
|
13
|
+
return createCompositeStore(createKeyringStore(GEMINI_ACCOUNT_NAME), createFileStore(GEMINI_ACCOUNT_NAME));
|
|
14
|
+
}
|
|
15
|
+
export function createOpenRouterKeyStore() {
|
|
16
|
+
return createCompositeStore(createKeyringStore(OPENROUTER_ACCOUNT_NAME), createFileStore(OPENROUTER_ACCOUNT_NAME));
|
|
17
|
+
}
|
|
18
|
+
export const OPENAI_CODEX_PROVIDER_ID = 'openai-codex';
|
|
19
|
+
export const PI_AUTH_API_KEY = `pi-auth://${OPENAI_CODEX_PROVIDER_ID}`;
|
|
20
|
+
const OPENAI_CODEX_BROWSER_LOGIN_METHOD = 'browser';
|
|
21
|
+
export async function createPiAuthRuntime() {
|
|
22
|
+
// Pi's own CLI installs its npm Undici implementation before initializing
|
|
23
|
+
// ModelRuntime. SDK consumers must do the same on Node 26 so compressed OAuth
|
|
24
|
+
// responses are decoded before response.json() parses them.
|
|
25
|
+
const { install } = await import('undici');
|
|
26
|
+
install();
|
|
27
|
+
const { ModelRuntime } = await import('@earendil-works/pi-coding-agent');
|
|
28
|
+
return await ModelRuntime.create({
|
|
29
|
+
authPath: path.join(homedir(), '.pi', 'agent', 'auth.json'),
|
|
30
|
+
allowModelNetwork: false,
|
|
31
|
+
modelsPath: null,
|
|
32
|
+
refreshOnCreate: false,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export function getBrowserOpenCommand(platform, url) {
|
|
36
|
+
if (platform === 'darwin') {
|
|
37
|
+
return { command: 'open', args: [url] };
|
|
38
|
+
}
|
|
39
|
+
if (platform === 'win32') {
|
|
40
|
+
return { command: 'cmd', args: ['/c', 'start', '', url] };
|
|
41
|
+
}
|
|
42
|
+
return { command: 'xdg-open', args: [url] };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Check that the stored OAuth session is usable, not just present.
|
|
46
|
+
* The Pi model runtime returns the cached access token when unexpired; when
|
|
47
|
+
* expired it attempts one cross-process-locked refresh and persists the result.
|
|
48
|
+
* Refresh failures preserve the stored credentials. Unexpected storage errors
|
|
49
|
+
* propagate.
|
|
50
|
+
*/
|
|
51
|
+
export async function getOpenAICodexAuthState(runtime) {
|
|
52
|
+
const resolvedRuntime = runtime ?? (await createPiAuthRuntime());
|
|
53
|
+
const credentials = await resolvedRuntime.listCredentials();
|
|
54
|
+
if (!credentials.some(({ providerId, type }) => providerId === OPENAI_CODEX_PROVIDER_ID && type === 'oauth')) {
|
|
55
|
+
return 'missing';
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const auth = await resolvedRuntime.getAuth(OPENAI_CODEX_PROVIDER_ID);
|
|
59
|
+
return auth?.auth.apiKey ? 'ready' : 'refresh-failed';
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (typeof error === 'object' &&
|
|
63
|
+
error !== null &&
|
|
64
|
+
'code' in error &&
|
|
65
|
+
error.code === 'oauth') {
|
|
66
|
+
return 'refresh-failed';
|
|
67
|
+
}
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export async function loginOpenAICodex(runtime) {
|
|
72
|
+
const resolvedRuntime = runtime ?? (await createPiAuthRuntime());
|
|
73
|
+
await resolvedRuntime.login(OPENAI_CODEX_PROVIDER_ID, 'oauth', createOpenAICodexBrowserLoginInteraction());
|
|
74
|
+
}
|
|
75
|
+
export async function logoutOpenAICodex(runtime) {
|
|
76
|
+
const resolvedRuntime = runtime ?? (await createPiAuthRuntime());
|
|
77
|
+
await resolvedRuntime.logout(OPENAI_CODEX_PROVIDER_ID);
|
|
78
|
+
}
|
|
79
|
+
export async function readStoredGeminiApiKey(store = createGeminiKeyStore()) {
|
|
80
|
+
const result = await store.getSecret();
|
|
81
|
+
const trimmed = result.value?.trim();
|
|
82
|
+
return trimmed ? { value: trimmed, backend: result.backend } : { value: undefined };
|
|
83
|
+
}
|
|
84
|
+
export async function getStoredGeminiApiKey(store = createGeminiKeyStore()) {
|
|
85
|
+
return (await readStoredGeminiApiKey(store)).value;
|
|
86
|
+
}
|
|
87
|
+
export async function readStoredOpenRouterApiKey(store = createOpenRouterKeyStore()) {
|
|
88
|
+
const result = await store.getSecret();
|
|
89
|
+
const trimmed = result.value?.trim();
|
|
90
|
+
return trimmed ? { value: trimmed, backend: result.backend } : { value: undefined };
|
|
91
|
+
}
|
|
92
|
+
export async function getStoredOpenRouterApiKey(store = createOpenRouterKeyStore()) {
|
|
93
|
+
return (await readStoredOpenRouterApiKey(store)).value;
|
|
94
|
+
}
|
|
95
|
+
export async function saveGeminiApiKey(apiKey, store = createGeminiKeyStore()) {
|
|
96
|
+
const normalized = apiKey.trim();
|
|
97
|
+
if (!normalized) {
|
|
98
|
+
throw new Error('Gemini API key cannot be empty');
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
return await store.setSecret(normalized);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (error instanceof SecureStoreUnavailableError) {
|
|
105
|
+
throw new SecureStoreUnavailableError(`${error.message} Set GEMINI_API_KEY in your environment instead.`);
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export async function saveOpenRouterApiKey(apiKey, store = createOpenRouterKeyStore()) {
|
|
111
|
+
const normalized = apiKey.trim();
|
|
112
|
+
if (!normalized) {
|
|
113
|
+
throw new Error('OpenRouter API key cannot be empty');
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
return await store.setSecret(normalized);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (error instanceof SecureStoreUnavailableError) {
|
|
120
|
+
throw new SecureStoreUnavailableError(`${error.message} Set OPENROUTER_API_KEY in your environment instead.`);
|
|
121
|
+
}
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export async function deleteOpenRouterApiKey(store = createOpenRouterKeyStore()) {
|
|
126
|
+
return await store.deleteSecret();
|
|
127
|
+
}
|
|
128
|
+
export async function deleteGeminiApiKey(store = createGeminiKeyStore()) {
|
|
129
|
+
return await store.deleteSecret();
|
|
130
|
+
}
|
|
131
|
+
export async function getAuthStatus({ env = process.env, runtime, geminiStore = createGeminiKeyStore(), openRouterStore = createOpenRouterKeyStore(), } = {}) {
|
|
132
|
+
const envApiKeys = getEnvApiKeys(env);
|
|
133
|
+
const geminiStored = envApiKeys.gemini
|
|
134
|
+
? { value: undefined }
|
|
135
|
+
: await readStoredGeminiApiKey(geminiStore).catch(() => ({ value: undefined }));
|
|
136
|
+
const openRouterStored = envApiKeys.openrouter
|
|
137
|
+
? { value: undefined }
|
|
138
|
+
: await readStoredOpenRouterApiKey(openRouterStore).catch(() => ({ value: undefined }));
|
|
139
|
+
const openAICodexAuthState = await getOpenAICodexAuthState(runtime);
|
|
140
|
+
return {
|
|
141
|
+
openai: openAICodexAuthState === 'missing'
|
|
142
|
+
? { configured: false, source: undefined }
|
|
143
|
+
: {
|
|
144
|
+
configured: true,
|
|
145
|
+
source: 'pi-auth',
|
|
146
|
+
refreshFailed: openAICodexAuthState === 'refresh-failed',
|
|
147
|
+
},
|
|
148
|
+
gemini: envApiKeys.gemini
|
|
149
|
+
? { configured: true, source: 'env' }
|
|
150
|
+
: {
|
|
151
|
+
configured: geminiStored.value !== undefined,
|
|
152
|
+
source: geminiStored.value !== undefined
|
|
153
|
+
? geminiStored.backend === 'file'
|
|
154
|
+
? 'file'
|
|
155
|
+
: 'keychain'
|
|
156
|
+
: undefined,
|
|
157
|
+
},
|
|
158
|
+
openrouter: envApiKeys.openrouter
|
|
159
|
+
? { configured: true, source: 'env' }
|
|
160
|
+
: {
|
|
161
|
+
configured: openRouterStored.value !== undefined,
|
|
162
|
+
source: openRouterStored.value !== undefined
|
|
163
|
+
? openRouterStored.backend === 'file'
|
|
164
|
+
? 'file'
|
|
165
|
+
: 'keychain'
|
|
166
|
+
: undefined,
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function createOpenAICodexBrowserLoginInteraction() {
|
|
171
|
+
return {
|
|
172
|
+
notify: (event) => {
|
|
173
|
+
if (event.type === 'auth_url') {
|
|
174
|
+
openUrlInBrowser(event.url);
|
|
175
|
+
writeLine('Open this URL in your browser:');
|
|
176
|
+
writeLine(event.url);
|
|
177
|
+
if (event.instructions) {
|
|
178
|
+
writeLine(event.instructions);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else if (event.type === 'info' || event.type === 'progress') {
|
|
182
|
+
writeLine(event.message);
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
prompt: async (prompt) => {
|
|
186
|
+
if (prompt.type === 'select') {
|
|
187
|
+
const browserOption = prompt.options.find(({ id }) => id === OPENAI_CODEX_BROWSER_LOGIN_METHOD);
|
|
188
|
+
if (!browserOption) {
|
|
189
|
+
throw new Error('OpenAI Codex did not provide a browser login option');
|
|
190
|
+
}
|
|
191
|
+
return browserOption.id;
|
|
192
|
+
}
|
|
193
|
+
return await promptInput(`${prompt.message} `, prompt.signal);
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function openUrlInBrowser(url) {
|
|
198
|
+
const { command, args } = getBrowserOpenCommand(process.platform, url);
|
|
199
|
+
const child = spawn(command, args, { detached: true, stdio: 'ignore' });
|
|
200
|
+
child.on('error', () => undefined);
|
|
201
|
+
child.unref();
|
|
202
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { getOpenAICodexAuthState, getStoredGeminiApiKey, getStoredOpenRouterApiKey, PI_AUTH_API_KEY, } from './auth.js';
|
|
2
|
+
import { getEnvApiKeys } from './env.js';
|
|
3
|
+
export const SUPPORTED_CLI_REVIEW_MODELS = [
|
|
4
|
+
'openai',
|
|
5
|
+
'deepseek',
|
|
6
|
+
'gemini',
|
|
7
|
+
'zai',
|
|
8
|
+
];
|
|
9
|
+
export function parseCliReviewModels(value) {
|
|
10
|
+
const models = value.split(',').map((model) => model.trim().toLowerCase());
|
|
11
|
+
if (models.some((model) => model.length === 0)) {
|
|
12
|
+
throw new Error(`--models requires a comma-separated list of: ${SUPPORTED_CLI_REVIEW_MODELS.join(', ')}`);
|
|
13
|
+
}
|
|
14
|
+
const supportedModels = new Set(SUPPORTED_CLI_REVIEW_MODELS);
|
|
15
|
+
const unsupportedModels = [...new Set(models.filter((model) => !supportedModels.has(model)))];
|
|
16
|
+
if (unsupportedModels.length > 0) {
|
|
17
|
+
throw new Error(`Unsupported review model(s): ${unsupportedModels.join(', ')}. Choose from: ${SUPPORTED_CLI_REVIEW_MODELS.join(', ')}`);
|
|
18
|
+
}
|
|
19
|
+
return [...new Set(models)];
|
|
20
|
+
}
|
|
21
|
+
const REQUIRED_CLI_AUTH_PROVIDERS = [{ provider: 'openai', label: 'OpenAI' }];
|
|
22
|
+
const ALL_REVIEW_FOCUSES = [
|
|
23
|
+
'general',
|
|
24
|
+
'security',
|
|
25
|
+
'reuse',
|
|
26
|
+
'quality',
|
|
27
|
+
'efficiency',
|
|
28
|
+
'tests',
|
|
29
|
+
'standards',
|
|
30
|
+
];
|
|
31
|
+
const OPENROUTER_REVIEW_MODELS = ['deepseek', 'zai'];
|
|
32
|
+
const CLI_GEMINI_REVIEW_MODEL = 'gemini-3.8-flash';
|
|
33
|
+
function createCliGeminiCredential(apiKey) {
|
|
34
|
+
return {
|
|
35
|
+
apiKey,
|
|
36
|
+
modelName: CLI_GEMINI_REVIEW_MODEL,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function createCliGeminiSummaryModel(credential) {
|
|
40
|
+
if (!credential) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
provider: 'gemini',
|
|
45
|
+
credential,
|
|
46
|
+
modelName: CLI_GEMINI_REVIEW_MODEL,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export async function getApiKeys(env = process.env, auth = {
|
|
50
|
+
getStoredGeminiApiKey,
|
|
51
|
+
getStoredOpenRouterApiKey,
|
|
52
|
+
getOpenAICodexAuthState,
|
|
53
|
+
}, providers) {
|
|
54
|
+
const selectedProviders = providers ? new Set(providers) : undefined;
|
|
55
|
+
const needsGemini = !selectedProviders || selectedProviders.has('gemini');
|
|
56
|
+
const needsOpenRouter = !selectedProviders || selectedProviders.has('deepseek') || selectedProviders.has('zai');
|
|
57
|
+
const needsOpenAI = !selectedProviders || selectedProviders.has('openai');
|
|
58
|
+
const envApiKeys = getEnvApiKeys(env);
|
|
59
|
+
const storedGeminiApiKey = needsGemini && !envApiKeys.gemini
|
|
60
|
+
? await auth.getStoredGeminiApiKey().catch(() => undefined)
|
|
61
|
+
: undefined;
|
|
62
|
+
const storedOpenRouterApiKey = needsOpenRouter && !envApiKeys.openrouter
|
|
63
|
+
? await auth.getStoredOpenRouterApiKey().catch(() => undefined)
|
|
64
|
+
: undefined;
|
|
65
|
+
// Explicit non-OpenAI runs must not refresh an unrelated OAuth session.
|
|
66
|
+
const openAICodexAuthState = needsOpenAI ? await auth.getOpenAICodexAuthState() : 'missing';
|
|
67
|
+
const openRouterApiKey = envApiKeys.openrouter || storedOpenRouterApiKey;
|
|
68
|
+
const apiKeys = {};
|
|
69
|
+
const geminiApiKey = needsGemini ? envApiKeys.gemini || storedGeminiApiKey : undefined;
|
|
70
|
+
if (geminiApiKey) {
|
|
71
|
+
apiKeys.gemini = createCliGeminiCredential(geminiApiKey);
|
|
72
|
+
}
|
|
73
|
+
if (needsOpenRouter && openRouterApiKey) {
|
|
74
|
+
if (!selectedProviders || selectedProviders.has('deepseek')) {
|
|
75
|
+
apiKeys.deepseek = { apiKey: openRouterApiKey, piProviderId: 'openrouter' };
|
|
76
|
+
}
|
|
77
|
+
if (!selectedProviders) {
|
|
78
|
+
apiKeys.grok = { apiKey: openRouterApiKey, piProviderId: 'openrouter' };
|
|
79
|
+
}
|
|
80
|
+
if (!selectedProviders || selectedProviders.has('zai')) {
|
|
81
|
+
apiKeys.zai = { apiKey: openRouterApiKey, piProviderId: 'openrouter' };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (openAICodexAuthState === 'ready') {
|
|
85
|
+
apiKeys.openai = PI_AUTH_API_KEY;
|
|
86
|
+
}
|
|
87
|
+
return apiKeys;
|
|
88
|
+
}
|
|
89
|
+
export function getMissingRequiredCliAuthProviders(status) {
|
|
90
|
+
return REQUIRED_CLI_AUTH_PROVIDERS.filter(({ provider }) => {
|
|
91
|
+
const providerStatus = status[provider];
|
|
92
|
+
return !providerStatus.configured || providerStatus.refreshFailed === true;
|
|
93
|
+
}).map(({ label }) => label);
|
|
94
|
+
}
|
|
95
|
+
export function getMissingRequiredCliReviewAuthProviders(apiKeys) {
|
|
96
|
+
return REQUIRED_CLI_AUTH_PROVIDERS.filter(({ provider }) => {
|
|
97
|
+
switch (provider) {
|
|
98
|
+
case 'openai':
|
|
99
|
+
return !apiKeys.openai;
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}).map(({ label }) => label);
|
|
103
|
+
}
|
|
104
|
+
export function formatMissingRequiredCliAuthProviders(missingProviders) {
|
|
105
|
+
return `${missingProviders.join(' and ')} authentication unavailable (not configured, or the session could not be refreshed); run \`doistbot auth login\``;
|
|
106
|
+
}
|
|
107
|
+
export function getCliReviewRouting(apiKeys, selectedModels) {
|
|
108
|
+
if (selectedModels !== undefined) {
|
|
109
|
+
if (selectedModels.length === 0) {
|
|
110
|
+
throw new Error('At least one review model must be selected.');
|
|
111
|
+
}
|
|
112
|
+
const focusModels = [...selectedModels];
|
|
113
|
+
return {
|
|
114
|
+
reviewModelsEnabled: focusModels,
|
|
115
|
+
reviewModelsByFocus: Object.fromEntries(ALL_REVIEW_FOCUSES.map((focus) => [focus, focusModels])),
|
|
116
|
+
geminiFallbackFocuses: [],
|
|
117
|
+
...(selectedModels.includes('gemini') &&
|
|
118
|
+
!selectedModels.includes('zai') &&
|
|
119
|
+
apiKeys.gemini
|
|
120
|
+
? { reviewSummaryModel: createCliGeminiSummaryModel(apiKeys.gemini) }
|
|
121
|
+
: {}),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const hasGemini = Boolean(apiKeys.gemini);
|
|
125
|
+
const hasOpenRouter = Boolean(apiKeys.deepseek && apiKeys.zai);
|
|
126
|
+
if (hasGemini && hasOpenRouter) {
|
|
127
|
+
return {
|
|
128
|
+
reviewModelsEnabled: ['openai', ...OPENROUTER_REVIEW_MODELS],
|
|
129
|
+
geminiFallbackFocuses: ALL_REVIEW_FOCUSES,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const focusModels = [
|
|
133
|
+
'openai',
|
|
134
|
+
...(hasGemini ? ['gemini'] : []),
|
|
135
|
+
...(hasOpenRouter ? OPENROUTER_REVIEW_MODELS : []),
|
|
136
|
+
];
|
|
137
|
+
return {
|
|
138
|
+
reviewModelsEnabled: focusModels,
|
|
139
|
+
reviewModelsByFocus: Object.fromEntries(ALL_REVIEW_FOCUSES.map((focus) => [focus, focusModels])),
|
|
140
|
+
geminiFallbackFocuses: [],
|
|
141
|
+
...(hasGemini && !hasOpenRouter
|
|
142
|
+
? { reviewSummaryModel: createCliGeminiSummaryModel(apiKeys.gemini) }
|
|
143
|
+
: {}),
|
|
144
|
+
};
|
|
145
|
+
}
|
package/dist/env.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
function getEnvValue(value) {
|
|
2
|
+
return value?.trim() || undefined;
|
|
3
|
+
}
|
|
4
|
+
export function getEnvApiKeys(env = process.env) {
|
|
5
|
+
return {
|
|
6
|
+
gemini: getEnvValue(env.GEMINI_API_KEY),
|
|
7
|
+
openai: getEnvValue(env.OPENAI_KEY) || getEnvValue(env.OPENAI_API_KEY),
|
|
8
|
+
openrouter: getEnvValue(env.OPENROUTER_API_KEY),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { describeError, SecureStoreUnavailableError, } from './secure-store.js';
|
|
6
|
+
export const FILE_STORE_PATH = path.join(homedir(), '.doistbot', 'credentials.json');
|
|
7
|
+
const FILE_BACKEND = 'file';
|
|
8
|
+
export function createFileStore(account, filePath = FILE_STORE_PATH) {
|
|
9
|
+
return {
|
|
10
|
+
async getSecret() {
|
|
11
|
+
const data = await readCredentialsFile(filePath);
|
|
12
|
+
const raw = data[account];
|
|
13
|
+
if (typeof raw !== 'string' || raw.length === 0) {
|
|
14
|
+
return { value: null };
|
|
15
|
+
}
|
|
16
|
+
return { value: raw, backend: FILE_BACKEND };
|
|
17
|
+
},
|
|
18
|
+
async setSecret(secret) {
|
|
19
|
+
assertWritableOnThisPlatform();
|
|
20
|
+
const data = await readCredentialsFile(filePath);
|
|
21
|
+
data[account] = secret;
|
|
22
|
+
await writeCredentialsFile(filePath, data);
|
|
23
|
+
return FILE_BACKEND;
|
|
24
|
+
},
|
|
25
|
+
async deleteSecret() {
|
|
26
|
+
const data = await readCredentialsFile(filePath);
|
|
27
|
+
if (!(account in data)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
delete data[account];
|
|
31
|
+
if (Object.keys(data).length === 0) {
|
|
32
|
+
await removeFile(filePath);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
assertWritableOnThisPlatform();
|
|
36
|
+
await writeCredentialsFile(filePath, data);
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function assertWritableOnThisPlatform() {
|
|
43
|
+
if (process.platform === 'win32') {
|
|
44
|
+
throw new SecureStoreUnavailableError('File-based credential fallback is not supported on Windows because POSIX 0600 permissions cannot be enforced. Use Windows Credential Manager or set an API key environment variable.');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function readCredentialsFile(filePath) {
|
|
48
|
+
let raw;
|
|
49
|
+
try {
|
|
50
|
+
raw = await fs.readFile(filePath, 'utf8');
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (isNotFoundError(error)) {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
throw new SecureStoreUnavailableError(`Failed to read credentials file at ${filePath}: ${describeError(error)}`);
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(raw);
|
|
60
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
61
|
+
throw new Error('credentials file is not a JSON object');
|
|
62
|
+
}
|
|
63
|
+
const result = {};
|
|
64
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
65
|
+
if (typeof value === 'string') {
|
|
66
|
+
result[key] = value;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
throw new SecureStoreUnavailableError(`Credentials file at ${filePath} is corrupt: ${describeError(error)}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function writeCredentialsFile(filePath, data) {
|
|
76
|
+
const dir = path.dirname(filePath);
|
|
77
|
+
try {
|
|
78
|
+
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
79
|
+
await fs.chmod(dir, 0o700);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
throw new SecureStoreUnavailableError(`Failed to create credentials directory ${dir}: ${describeError(error)}`);
|
|
83
|
+
}
|
|
84
|
+
const tmpPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
85
|
+
const payload = `${JSON.stringify(data, null, 2)}\n`;
|
|
86
|
+
try {
|
|
87
|
+
await fs.writeFile(tmpPath, payload, { mode: 0o600 });
|
|
88
|
+
await fs.chmod(tmpPath, 0o600);
|
|
89
|
+
await fs.rename(tmpPath, filePath);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
await removeFile(tmpPath).catch(() => undefined);
|
|
93
|
+
throw new SecureStoreUnavailableError(`Failed to write credentials file at ${filePath}: ${describeError(error)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function removeFile(filePath) {
|
|
97
|
+
try {
|
|
98
|
+
await fs.unlink(filePath);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (isNotFoundError(error)) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
throw new SecureStoreUnavailableError(`Failed to remove credentials file at ${filePath}: ${describeError(error)}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function isNotFoundError(error) {
|
|
108
|
+
return (typeof error === 'object' &&
|
|
109
|
+
error !== null &&
|
|
110
|
+
'code' in error &&
|
|
111
|
+
error.code === 'ENOENT');
|
|
112
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { Command, Option } from 'commander';
|
|
6
|
+
import { AUTH_LOGOUT_PROVIDER_USAGE, runAuthLogout, runAuthSetup, runAuthStatus, } from './actions/auth.js';
|
|
7
|
+
import { runDoctor } from './actions/doctor.js';
|
|
8
|
+
import { runReview } from './actions/review.js';
|
|
9
|
+
import { formatReviewUsage } from './actions/review-usage.js';
|
|
10
|
+
import { runSetup } from './actions/setup.js';
|
|
11
|
+
import { parseCliReviewModels, SUPPORTED_CLI_REVIEW_MODELS } from './config.js';
|
|
12
|
+
import { writeErrorLine } from './terminal.js';
|
|
13
|
+
import { notifyAndRefreshCliUpdateCache, notifyIfCliUpdateAvailable } from './update-check.js';
|
|
14
|
+
export { getSandboxRoot } from './runtime.js';
|
|
15
|
+
function getPackageVersion() {
|
|
16
|
+
const packageJsonPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../package.json');
|
|
17
|
+
try {
|
|
18
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
19
|
+
return typeof packageJson.version === 'string' ? packageJson.version : '0.0.0';
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return '0.0.0';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const PACKAGE_VERSION = getPackageVersion();
|
|
26
|
+
const LOCAL_REVIEW_SOURCE_STAGED = 'staged';
|
|
27
|
+
function getUpdateCacheEntrypointPath() {
|
|
28
|
+
return fileURLToPath(new URL('./update-cache-entrypoint.js', import.meta.url));
|
|
29
|
+
}
|
|
30
|
+
function parseReviewInput(sourceArg, options = {}) {
|
|
31
|
+
if (sourceArg !== undefined && sourceArg !== LOCAL_REVIEW_SOURCE_STAGED) {
|
|
32
|
+
throw new Error(formatReviewUsage());
|
|
33
|
+
}
|
|
34
|
+
if (sourceArg === LOCAL_REVIEW_SOURCE_STAGED && options.base) {
|
|
35
|
+
throw new Error(formatReviewUsage());
|
|
36
|
+
}
|
|
37
|
+
const reviewOptions = {
|
|
38
|
+
json: options.json === true,
|
|
39
|
+
...(options.models !== undefined ? { models: parseCliReviewModels(options.models) } : {}),
|
|
40
|
+
};
|
|
41
|
+
if (options.base !== undefined) {
|
|
42
|
+
return { source: { kind: 'base-ref', ref: options.base }, ...reviewOptions };
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
source: sourceArg === LOCAL_REVIEW_SOURCE_STAGED ? LOCAL_REVIEW_SOURCE_STAGED : 'base',
|
|
46
|
+
...reviewOptions,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function createProgram({ runAuthLogout: authLogout = runAuthLogout, runAuthSetup: authSetup = runAuthSetup, runAuthStatus: authStatus = runAuthStatus, runDoctor: doctor = runDoctor, runSetup: setup = runSetup, runReview: review = runReview, runAutomaticUpdateCheck: automaticUpdateCheck = async () => await notifyAndRefreshCliUpdateCache({
|
|
50
|
+
currentVersion: PACKAGE_VERSION,
|
|
51
|
+
updateCacheEntrypointPath: getUpdateCacheEntrypointPath(),
|
|
52
|
+
}), runUpdateCheck: updateCheck = async () => await notifyIfCliUpdateAvailable({ currentVersion: PACKAGE_VERSION }), } = {}) {
|
|
53
|
+
const program = new Command();
|
|
54
|
+
const automaticUpdateCommands = new Set();
|
|
55
|
+
function withAutomaticUpdateCheck(command) {
|
|
56
|
+
automaticUpdateCommands.add(command);
|
|
57
|
+
return command;
|
|
58
|
+
}
|
|
59
|
+
function shouldRunAutomaticUpdateCheck(actionCommand) {
|
|
60
|
+
if (!automaticUpdateCommands.has(actionCommand)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if (actionCommand.name() === 'review' && actionCommand.opts().json) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
program
|
|
69
|
+
.name('doistbot')
|
|
70
|
+
.description('Doistbot local self-review CLI')
|
|
71
|
+
.version(PACKAGE_VERSION)
|
|
72
|
+
.hook('postAction', async (_thisCommand, actionCommand) => {
|
|
73
|
+
if (shouldRunAutomaticUpdateCheck(actionCommand)) {
|
|
74
|
+
await automaticUpdateCheck();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
withAutomaticUpdateCheck(program
|
|
78
|
+
.command('doctor')
|
|
79
|
+
.description('Diagnose local Doistbot CLI setup')
|
|
80
|
+
.action(async () => await doctor()));
|
|
81
|
+
withAutomaticUpdateCheck(program
|
|
82
|
+
.command('setup')
|
|
83
|
+
.description('Check prerequisites and configure local provider auth')
|
|
84
|
+
.action(async () => await setup()));
|
|
85
|
+
const authCommand = withAutomaticUpdateCheck(program
|
|
86
|
+
.command('auth')
|
|
87
|
+
.description('Authenticate Doistbot CLI with local review providers')
|
|
88
|
+
.action(async () => await authSetup()));
|
|
89
|
+
withAutomaticUpdateCheck(authCommand
|
|
90
|
+
.command('status')
|
|
91
|
+
.description('Show provider authentication status')
|
|
92
|
+
.action(async () => await authStatus()));
|
|
93
|
+
withAutomaticUpdateCheck(authCommand
|
|
94
|
+
.command('login')
|
|
95
|
+
.description('Configure provider authentication')
|
|
96
|
+
.action(async () => await authSetup()));
|
|
97
|
+
withAutomaticUpdateCheck(authCommand
|
|
98
|
+
.command('logout [provider]')
|
|
99
|
+
.description(`Clear stored Doistbot CLI auth for ${AUTH_LOGOUT_PROVIDER_USAGE}`)
|
|
100
|
+
.action(async (provider) => await authLogout(provider)));
|
|
101
|
+
withAutomaticUpdateCheck(program
|
|
102
|
+
.command('review [source]')
|
|
103
|
+
.description('Run a local self-review. Use `staged` to review only staged changes.')
|
|
104
|
+
.option('--base <ref>', 'Review local changes against a specific base ref')
|
|
105
|
+
.option('--models <models>', `Use only these review models (${SUPPORTED_CLI_REVIEW_MODELS.join(', ')})`)
|
|
106
|
+
.addOption(new Option('--json', 'Print machine-readable JSON instead of terminal-formatted markdown').hideHelp())
|
|
107
|
+
.action(async (source, options) => await review(parseReviewInput(source, options))));
|
|
108
|
+
program
|
|
109
|
+
.command('update')
|
|
110
|
+
.description('Check whether a newer Doistbot CLI version is available')
|
|
111
|
+
.action(async () => await updateCheck());
|
|
112
|
+
return program;
|
|
113
|
+
}
|
|
114
|
+
export async function runCliEntrypoint({ argv = process.argv, dependencies, exitProcess = exitCliProcess, writeErrorLine: writeError = writeErrorLine, } = {}) {
|
|
115
|
+
try {
|
|
116
|
+
await createProgram(dependencies).parseAsync(argv, { from: 'node' });
|
|
117
|
+
if (shouldForceExitAfterCommand(argv)) {
|
|
118
|
+
await exitProcess(getProcessExitCode());
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
writeError(`Error: ${error.message}`);
|
|
123
|
+
process.exitCode = 1;
|
|
124
|
+
if (shouldForceExitAfterCommand(argv)) {
|
|
125
|
+
await exitProcess(1);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export function shouldForceExitAfterCommand(argv) {
|
|
130
|
+
const command = argv[2];
|
|
131
|
+
const authSubcommand = argv[3];
|
|
132
|
+
return command === 'setup' || (command === 'auth' && authSubcommand !== 'status');
|
|
133
|
+
}
|
|
134
|
+
function flushStream(stream) {
|
|
135
|
+
if (!stream.writable) {
|
|
136
|
+
return Promise.resolve();
|
|
137
|
+
}
|
|
138
|
+
return new Promise((resolve) => {
|
|
139
|
+
stream.write('', () => resolve());
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async function exitCliProcess(code) {
|
|
143
|
+
await Promise.all([flushStream(process.stdout), flushStream(process.stderr)]);
|
|
144
|
+
process.exit(code);
|
|
145
|
+
}
|
|
146
|
+
function getProcessExitCode() {
|
|
147
|
+
return typeof process.exitCode === 'number' ? process.exitCode : 0;
|
|
148
|
+
}
|
|
149
|
+
export function isCliEntrypoint(argvPath, moduleUrl) {
|
|
150
|
+
if (!argvPath) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
const modulePath = fileURLToPath(moduleUrl);
|
|
154
|
+
const resolvedArgvPath = path.resolve(argvPath);
|
|
155
|
+
try {
|
|
156
|
+
return fs.realpathSync(resolvedArgvPath) === fs.realpathSync(modulePath);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return resolvedArgvPath === modulePath;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (isCliEntrypoint(process.argv[1], import.meta.url)) {
|
|
163
|
+
await runCliEntrypoint();
|
|
164
|
+
}
|
package/dist/input.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline/promises';
|
|
2
|
+
export async function promptInput(prompt, signal) {
|
|
3
|
+
const rl = createInterface({
|
|
4
|
+
input: process.stdin,
|
|
5
|
+
output: process.stdout,
|
|
6
|
+
});
|
|
7
|
+
try {
|
|
8
|
+
return await rl.question(prompt, { signal });
|
|
9
|
+
}
|
|
10
|
+
finally {
|
|
11
|
+
rl.close();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function promptHiddenInput(prompt) {
|
|
15
|
+
if (!process.stdin.isTTY) {
|
|
16
|
+
return promptInput(prompt);
|
|
17
|
+
}
|
|
18
|
+
return promptPassword(prompt);
|
|
19
|
+
}
|
|
20
|
+
export async function promptMultiSelect({ message, choices, }) {
|
|
21
|
+
const { default: checkbox } = await import('@inquirer/checkbox');
|
|
22
|
+
return (await checkbox({
|
|
23
|
+
message,
|
|
24
|
+
choices,
|
|
25
|
+
required: true,
|
|
26
|
+
theme: {
|
|
27
|
+
icon: {
|
|
28
|
+
checked: ' ●',
|
|
29
|
+
cursor: '›',
|
|
30
|
+
unchecked: ' ○',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
async function promptPassword(prompt) {
|
|
36
|
+
const { default: password } = await import('@inquirer/password');
|
|
37
|
+
return await password({
|
|
38
|
+
message: prompt.trim().replace(/:$/, ''),
|
|
39
|
+
});
|
|
40
|
+
}
|