@doist/doistbot-cli 1.0.0 → 1.0.1
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 +135 -28
- package/dist/actions/doctor.js +12 -9
- package/dist/auth.js +30 -9
- package/dist/config.js +1 -1
- package/dist/env.js +1 -1
- package/dist/file-store.js +112 -0
- package/dist/index.js +76 -25
- package/dist/input.js +22 -48
- package/dist/secure-store.js +105 -16
- package/dist/terminal.js +4 -1
- 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 +8 -6
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +5 -2
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +34 -23
- package/sandbox/dist/core/pi.js +0 -4
- package/sandbox/dist/tasks/issue-triage/fix-attempt.js +136 -90
- package/sandbox/dist/tasks/issue-triage/pr-creator.js +16 -9
- package/sandbox/dist/tasks/issue-triage/repo-config.js +28 -2
- package/sandbox/dist/tasks/issue-triage/triage.js +30 -23
- package/sandbox/dist/tasks/review/check-run.js +134 -14
- package/sandbox/dist/tasks/review/conversation-context.js +316 -0
- package/sandbox/dist/tasks/review/engines/dedupe.js +82 -63
- package/sandbox/dist/tasks/review/local-git.js +7 -0
- package/sandbox/dist/tasks/review/multi-focus-prompt.js +2 -1
- package/sandbox/dist/tasks/review/prompt.js +15 -2
- package/sandbox/dist/tasks/review/review-feedback.js +4 -0
- package/sandbox/dist/tasks/review/review.js +39 -1
- package/sandbox/dist/tasks/review/runner.js +5 -1
- package/sandbox/dist/tasks/review/severity.js +5 -0
- package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +5 -2
- package/sandbox/node_modules/doistbot-repo-config/dist/index.js +34 -23
- package/sandbox/review-prompt.md +8 -2
- package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +7 -2
package/dist/actions/auth.js
CHANGED
|
@@ -1,50 +1,157 @@
|
|
|
1
1
|
import { deleteGeminiApiKey, getAuthStatus, loginOpenAICodex, logoutOpenAICodex, saveGeminiApiKey, } from '../auth.js';
|
|
2
|
-
import {
|
|
2
|
+
import { getEnvApiKeys } from '../env.js';
|
|
3
|
+
import { FILE_STORE_PATH } from '../file-store.js';
|
|
4
|
+
import { promptHiddenInput, promptMultiSelect } from '../input.js';
|
|
5
|
+
import { getSecureStoreLabel } from '../secure-store.js';
|
|
3
6
|
import { writeLine } from '../terminal.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
const AUTH_LOGOUT_PROVIDER_CONFIGS = [
|
|
8
|
+
{
|
|
9
|
+
clear: async ({ logoutOpenAICodex }) => {
|
|
10
|
+
await logoutOpenAICodex();
|
|
11
|
+
return '✓ OpenAI stored auth cleared.';
|
|
12
|
+
},
|
|
13
|
+
envWarning: (env) => getEnvApiKeys(env).openai
|
|
14
|
+
? 'OpenAI is still configured from an environment variable.'
|
|
15
|
+
: undefined,
|
|
16
|
+
name: 'OpenAI Codex',
|
|
17
|
+
value: 'openai',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
clear: async ({ deleteGeminiApiKey }) => {
|
|
21
|
+
const deleted = await deleteGeminiApiKey();
|
|
22
|
+
return deleted
|
|
23
|
+
? '✓ Gemini stored API key cleared.'
|
|
24
|
+
: 'Gemini had no stored API key to clear.';
|
|
25
|
+
},
|
|
26
|
+
envWarning: (env) => getEnvApiKeys(env).gemini
|
|
27
|
+
? 'Gemini is still configured from GEMINI_API_KEY.'
|
|
28
|
+
: undefined,
|
|
29
|
+
name: 'Gemini',
|
|
30
|
+
value: 'gemini',
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
const AUTH_LOGOUT_PROVIDERS = AUTH_LOGOUT_PROVIDER_CONFIGS.map(({ value }) => value);
|
|
34
|
+
export const AUTH_LOGOUT_PROVIDER_USAGE = `${AUTH_LOGOUT_PROVIDERS.join('|')}|all`;
|
|
35
|
+
export async function runAuthSetup(dependencies = {}) {
|
|
36
|
+
const deps = {
|
|
37
|
+
getAuthStatus,
|
|
38
|
+
isInteractive: process.stdin.isTTY === true,
|
|
39
|
+
loginOpenAICodex,
|
|
40
|
+
promptHiddenInput,
|
|
41
|
+
saveGeminiApiKey,
|
|
42
|
+
writeLine,
|
|
43
|
+
...dependencies,
|
|
44
|
+
};
|
|
45
|
+
const status = await deps.getAuthStatus();
|
|
46
|
+
deps.writeLine('1. OpenAI Codex');
|
|
47
|
+
deps.writeLine();
|
|
48
|
+
if (status.openai.configured) {
|
|
49
|
+
deps.writeLine(formatOpenAIConfiguredMessage(status.openai.source));
|
|
10
50
|
}
|
|
11
51
|
else {
|
|
12
|
-
if (!
|
|
52
|
+
if (!deps.isInteractive) {
|
|
13
53
|
throw new Error('Cannot configure OpenAI Codex auth in non-interactive mode.');
|
|
14
54
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
await loginOpenAICodex();
|
|
19
|
-
writeLine();
|
|
20
|
-
writeLine('✓ OpenAI configured.');
|
|
55
|
+
await deps.loginOpenAICodex();
|
|
56
|
+
deps.writeLine();
|
|
57
|
+
deps.writeLine('✓ OpenAI configured.');
|
|
21
58
|
}
|
|
22
|
-
writeLine();
|
|
23
|
-
writeLine('2. Gemini API key');
|
|
24
|
-
writeLine();
|
|
59
|
+
deps.writeLine();
|
|
60
|
+
deps.writeLine('2. Gemini API key');
|
|
61
|
+
deps.writeLine();
|
|
25
62
|
if (status.gemini.source === 'env') {
|
|
26
|
-
writeLine('✓ Gemini already configured from GEMINI_API_KEY.');
|
|
63
|
+
deps.writeLine('✓ Gemini already configured from GEMINI_API_KEY.');
|
|
27
64
|
return;
|
|
28
65
|
}
|
|
29
66
|
if (status.gemini.source === 'keychain') {
|
|
30
|
-
writeLine(
|
|
67
|
+
deps.writeLine(`✓ Gemini already configured in the ${getSecureStoreLabel()}.`);
|
|
31
68
|
return;
|
|
32
69
|
}
|
|
33
|
-
if (
|
|
70
|
+
if (status.gemini.source === 'file') {
|
|
71
|
+
deps.writeLine(`✓ Gemini already configured (${FILE_STORE_PATH}).`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (!deps.isInteractive) {
|
|
34
75
|
throw new Error('Cannot prompt for Gemini API key in non-interactive mode. Set GEMINI_API_KEY instead.');
|
|
35
76
|
}
|
|
36
|
-
writeLine('Doistbot requires a Gemini API key. Please create one at https://aistudio.google.com/app/api-keys and paste it here.');
|
|
37
|
-
await saveGeminiApiKey(await promptHiddenInput('Gemini API key: '));
|
|
38
|
-
writeLine();
|
|
39
|
-
|
|
77
|
+
deps.writeLine('Doistbot requires a Gemini API key. Please create one at https://aistudio.google.com/app/api-keys and paste it here.');
|
|
78
|
+
const backend = await deps.saveGeminiApiKey(await deps.promptHiddenInput('Gemini API key: '));
|
|
79
|
+
deps.writeLine();
|
|
80
|
+
if (backend === 'file') {
|
|
81
|
+
deps.writeLine(`Note: ${getSecureStoreLabel()} unavailable; key stored in ${FILE_STORE_PATH} with 0600 permissions.`);
|
|
82
|
+
}
|
|
83
|
+
deps.writeLine('✓ Gemini configured.');
|
|
40
84
|
}
|
|
41
85
|
export async function runAuthStatus() {
|
|
42
86
|
const status = await getAuthStatus();
|
|
43
87
|
writeLine(`OpenAI: ${status.openai.configured ? `configured (${status.openai.source ?? 'unknown'})` : 'not configured'}`);
|
|
44
88
|
writeLine(`Gemini: ${status.gemini.configured ? `configured (${status.gemini.source ?? 'unknown'})` : 'not configured'}`);
|
|
45
89
|
}
|
|
46
|
-
export async function runAuthLogout() {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
90
|
+
export async function runAuthLogout(target, dependencies = {}) {
|
|
91
|
+
const deps = {
|
|
92
|
+
deleteGeminiApiKey,
|
|
93
|
+
env: process.env,
|
|
94
|
+
isInteractive: process.stdin.isTTY === true,
|
|
95
|
+
logoutOpenAICodex,
|
|
96
|
+
promptProviderSelection: promptLogoutProviderSelection,
|
|
97
|
+
writeLine,
|
|
98
|
+
...dependencies,
|
|
99
|
+
};
|
|
100
|
+
const providers = await resolveLogoutProviders(target, deps);
|
|
101
|
+
const providerConfigs = providers.map(getAuthLogoutProviderConfig);
|
|
102
|
+
const clearMessages = await Promise.all(providerConfigs.map((config) => config.clear(deps)));
|
|
103
|
+
for (const message of clearMessages) {
|
|
104
|
+
deps.writeLine(message);
|
|
105
|
+
}
|
|
106
|
+
for (const config of providerConfigs) {
|
|
107
|
+
const warning = config.envWarning(deps.env);
|
|
108
|
+
if (warning) {
|
|
109
|
+
deps.writeLine(warning);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function resolveLogoutProviders(target, dependencies) {
|
|
114
|
+
if (target === undefined) {
|
|
115
|
+
if (!dependencies.isInteractive) {
|
|
116
|
+
throw new Error(`Provider required in non-interactive mode. Use \`doistbot auth logout ${AUTH_LOGOUT_PROVIDER_USAGE}\`.`);
|
|
117
|
+
}
|
|
118
|
+
return await dependencies.promptProviderSelection();
|
|
119
|
+
}
|
|
120
|
+
const normalized = target.toLowerCase();
|
|
121
|
+
if (normalized === 'all') {
|
|
122
|
+
return [...AUTH_LOGOUT_PROVIDERS];
|
|
123
|
+
}
|
|
124
|
+
if (isAuthLogoutProvider(normalized)) {
|
|
125
|
+
return [normalized];
|
|
126
|
+
}
|
|
127
|
+
throw new Error(`Usage: doistbot auth logout [${AUTH_LOGOUT_PROVIDER_USAGE}]`);
|
|
128
|
+
}
|
|
129
|
+
function isAuthLogoutProvider(value) {
|
|
130
|
+
return AUTH_LOGOUT_PROVIDERS.includes(value);
|
|
131
|
+
}
|
|
132
|
+
function getAuthLogoutProviderConfig(provider) {
|
|
133
|
+
const config = AUTH_LOGOUT_PROVIDER_CONFIGS.find(({ value }) => value === provider);
|
|
134
|
+
if (!config) {
|
|
135
|
+
throw new Error(`Unsupported auth logout provider: ${provider}`);
|
|
136
|
+
}
|
|
137
|
+
return config;
|
|
138
|
+
}
|
|
139
|
+
async function promptLogoutProviderSelection() {
|
|
140
|
+
return await promptMultiSelect({
|
|
141
|
+
message: 'Select providers to log out:',
|
|
142
|
+
choices: AUTH_LOGOUT_PROVIDER_CONFIGS.map(({ name, value }) => ({ name, value })),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function formatOpenAIConfiguredMessage(source) {
|
|
146
|
+
switch (source) {
|
|
147
|
+
case 'pi-auth':
|
|
148
|
+
return '✓ OpenAI already configured (pi-auth).';
|
|
149
|
+
case 'env':
|
|
150
|
+
return '✓ OpenAI already configured from the environment.';
|
|
151
|
+
case 'keychain':
|
|
152
|
+
return '✓ OpenAI already configured (keychain).';
|
|
153
|
+
case 'file':
|
|
154
|
+
case undefined:
|
|
155
|
+
return '✓ OpenAI already configured.';
|
|
156
|
+
}
|
|
50
157
|
}
|
package/dist/actions/doctor.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { getAuthStatus } from '../auth.js';
|
|
2
|
-
import {
|
|
2
|
+
import { getEnvApiKeys } from '../env.js';
|
|
3
3
|
import { checkGit, checkPiDependency, checkRepository, checkSandboxBuild } from '../runtime.js';
|
|
4
4
|
import { renderDiagnosticCheck } from '../terminal.js';
|
|
5
|
-
function getConfiguredProviders({
|
|
5
|
+
export function getConfiguredProviders({ status, }) {
|
|
6
6
|
return [
|
|
7
7
|
status.openai.configured
|
|
8
8
|
? `OpenAI (${status.openai.source === 'pi-auth'
|
|
@@ -10,15 +10,18 @@ function getConfiguredProviders({ apiKeys, status, }) {
|
|
|
10
10
|
: (status.openai.source ?? 'configured')})`
|
|
11
11
|
: undefined,
|
|
12
12
|
status.gemini.configured ? `Gemini (${status.gemini.source ?? 'configured'})` : undefined,
|
|
13
|
-
apiKeys.anthropic ? 'Anthropic (env)' : undefined,
|
|
14
13
|
].filter((provider) => provider !== undefined);
|
|
15
14
|
}
|
|
16
|
-
async function checkAuth() {
|
|
17
|
-
const
|
|
18
|
-
const configured = getConfiguredProviders({
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
export async function checkAuth({ env = process.env, getStatus = getAuthStatus, } = {}) {
|
|
16
|
+
const status = await getStatus({ env });
|
|
17
|
+
const configured = getConfiguredProviders({ status });
|
|
18
|
+
if (configured.length > 0) {
|
|
19
|
+
return { ok: true, message: `${configured.join(', ')} configured` };
|
|
20
|
+
}
|
|
21
|
+
if (getEnvApiKeys(env).anthropic) {
|
|
22
|
+
return { ok: true, message: 'provider auth configured' };
|
|
23
|
+
}
|
|
24
|
+
return { ok: false, message: 'run `doistbot auth`' };
|
|
22
25
|
}
|
|
23
26
|
export async function runDoctor() {
|
|
24
27
|
await renderDiagnosticCheck('git', checkGit);
|
package/dist/auth.js
CHANGED
|
@@ -2,8 +2,13 @@ import { spawn } from 'child_process';
|
|
|
2
2
|
import { homedir } from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { getEnvApiKeys } from './env.js';
|
|
5
|
+
import { createFileStore } from './file-store.js';
|
|
5
6
|
import { promptInput } from './input.js';
|
|
6
|
-
import {
|
|
7
|
+
import { createCompositeStore, createKeyringStore, SecureStoreUnavailableError, } from './secure-store.js';
|
|
8
|
+
const GEMINI_ACCOUNT_NAME = 'gemini-api-key';
|
|
9
|
+
export function createGeminiKeyStore() {
|
|
10
|
+
return createCompositeStore(createKeyringStore(GEMINI_ACCOUNT_NAME), createFileStore(GEMINI_ACCOUNT_NAME));
|
|
11
|
+
}
|
|
7
12
|
export const OPENAI_CODEX_PROVIDER_ID = 'openai-codex';
|
|
8
13
|
export const PI_AUTH_API_KEY = `pi-auth://${OPENAI_CODEX_PROVIDER_ID}`;
|
|
9
14
|
export async function createPiAuthStorage() {
|
|
@@ -31,25 +36,37 @@ export async function logoutOpenAICodex(storage) {
|
|
|
31
36
|
const resolvedStorage = storage ?? (await createPiAuthStorage());
|
|
32
37
|
resolvedStorage.logout(OPENAI_CODEX_PROVIDER_ID);
|
|
33
38
|
}
|
|
39
|
+
export async function readStoredGeminiApiKey(store = createGeminiKeyStore()) {
|
|
40
|
+
const result = await store.getSecret();
|
|
41
|
+
const trimmed = result.value?.trim();
|
|
42
|
+
return trimmed ? { value: trimmed, backend: result.backend } : { value: undefined };
|
|
43
|
+
}
|
|
34
44
|
export async function getStoredGeminiApiKey(store = createGeminiKeyStore()) {
|
|
35
|
-
|
|
36
|
-
return key || undefined;
|
|
45
|
+
return (await readStoredGeminiApiKey(store)).value;
|
|
37
46
|
}
|
|
38
47
|
export async function saveGeminiApiKey(apiKey, store = createGeminiKeyStore()) {
|
|
39
48
|
const normalized = apiKey.trim();
|
|
40
49
|
if (!normalized) {
|
|
41
50
|
throw new Error('Gemini API key cannot be empty');
|
|
42
51
|
}
|
|
43
|
-
|
|
52
|
+
try {
|
|
53
|
+
return await store.setSecret(normalized);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error instanceof SecureStoreUnavailableError) {
|
|
57
|
+
throw new SecureStoreUnavailableError(`${error.message} Set GEMINI_API_KEY in your environment instead.`);
|
|
58
|
+
}
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
44
61
|
}
|
|
45
62
|
export async function deleteGeminiApiKey(store = createGeminiKeyStore()) {
|
|
46
63
|
return await store.deleteSecret();
|
|
47
64
|
}
|
|
48
65
|
export async function getAuthStatus({ env = process.env, storage, geminiStore = createGeminiKeyStore(), } = {}) {
|
|
49
66
|
const envApiKeys = getEnvApiKeys(env);
|
|
50
|
-
const
|
|
51
|
-
? undefined
|
|
52
|
-
: await
|
|
67
|
+
const geminiStored = envApiKeys.gemini
|
|
68
|
+
? { value: undefined }
|
|
69
|
+
: await readStoredGeminiApiKey(geminiStore).catch(() => ({ value: undefined }));
|
|
53
70
|
const openAICodexAuthConfigured = await hasOpenAICodexAuth(storage);
|
|
54
71
|
return {
|
|
55
72
|
openai: openAICodexAuthConfigured
|
|
@@ -61,8 +78,12 @@ export async function getAuthStatus({ env = process.env, storage, geminiStore =
|
|
|
61
78
|
gemini: envApiKeys.gemini
|
|
62
79
|
? { configured: true, source: 'env' }
|
|
63
80
|
: {
|
|
64
|
-
configured:
|
|
65
|
-
source:
|
|
81
|
+
configured: geminiStored.value !== undefined,
|
|
82
|
+
source: geminiStored.value !== undefined
|
|
83
|
+
? geminiStored.backend === 'file'
|
|
84
|
+
? 'file'
|
|
85
|
+
: 'keychain'
|
|
86
|
+
: undefined,
|
|
66
87
|
},
|
|
67
88
|
};
|
|
68
89
|
}
|
package/dist/config.js
CHANGED
|
@@ -10,8 +10,8 @@ export async function getApiKeys(env = process.env, auth = {
|
|
|
10
10
|
: await auth.getStoredGeminiApiKey().catch(() => undefined);
|
|
11
11
|
const openAICodexAuthConfigured = await auth.hasOpenAICodexAuth();
|
|
12
12
|
return {
|
|
13
|
+
anthropic: envApiKeys.anthropic,
|
|
13
14
|
gemini: envApiKeys.gemini || storedGeminiApiKey,
|
|
14
15
|
openai: openAICodexAuthConfigured ? PI_AUTH_API_KEY : envApiKeys.openai,
|
|
15
|
-
anthropic: envApiKeys.anthropic,
|
|
16
16
|
};
|
|
17
17
|
}
|
package/dist/env.js
CHANGED
|
@@ -3,8 +3,8 @@ function getEnvValue(value) {
|
|
|
3
3
|
}
|
|
4
4
|
export function getEnvApiKeys(env = process.env) {
|
|
5
5
|
return {
|
|
6
|
+
anthropic: getEnvValue(env.ANTHROPIC_KEY) || getEnvValue(env.ANTHROPIC_API_KEY),
|
|
6
7
|
gemini: getEnvValue(env.GEMINI_API_KEY),
|
|
7
8
|
openai: getEnvValue(env.OPENAI_KEY) || getEnvValue(env.OPENAI_API_KEY),
|
|
8
|
-
anthropic: getEnvValue(env.ANTHROPIC_KEY) || getEnvValue(env.ANTHROPIC_API_KEY),
|
|
9
9
|
};
|
|
10
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 GEMINI_API_KEY.');
|
|
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
CHANGED
|
@@ -3,11 +3,12 @@ import fs from 'fs';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { Command } from 'commander';
|
|
6
|
-
import { runAuthLogout, runAuthSetup, runAuthStatus } from './actions/auth.js';
|
|
6
|
+
import { AUTH_LOGOUT_PROVIDER_USAGE, runAuthLogout, runAuthSetup, runAuthStatus, } from './actions/auth.js';
|
|
7
7
|
import { runDoctor } from './actions/doctor.js';
|
|
8
8
|
import { runReview } from './actions/review.js';
|
|
9
9
|
import { runSetup } from './actions/setup.js';
|
|
10
10
|
import { writeErrorLine } from './terminal.js';
|
|
11
|
+
import { notifyAndRefreshCliUpdateCache, notifyIfCliUpdateAvailable } from './update-check.js';
|
|
11
12
|
export { getSandboxRoot } from './runtime.js';
|
|
12
13
|
function getPackageVersion() {
|
|
13
14
|
const packageJsonPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../package.json');
|
|
@@ -19,44 +20,97 @@ function getPackageVersion() {
|
|
|
19
20
|
return '0.0.0';
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
|
-
|
|
23
|
+
const PACKAGE_VERSION = getPackageVersion();
|
|
24
|
+
function getUpdateCacheEntrypointPath() {
|
|
25
|
+
return fileURLToPath(new URL('./update-cache-entrypoint.js', import.meta.url));
|
|
26
|
+
}
|
|
27
|
+
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({
|
|
28
|
+
currentVersion: PACKAGE_VERSION,
|
|
29
|
+
updateCacheEntrypointPath: getUpdateCacheEntrypointPath(),
|
|
30
|
+
}), runUpdateCheck: updateCheck = async () => await notifyIfCliUpdateAvailable({ currentVersion: PACKAGE_VERSION }), } = {}) {
|
|
23
31
|
const program = new Command();
|
|
32
|
+
const automaticUpdateCommands = new Set();
|
|
33
|
+
function withAutomaticUpdateCheck(command) {
|
|
34
|
+
automaticUpdateCommands.add(command);
|
|
35
|
+
return command;
|
|
36
|
+
}
|
|
24
37
|
program
|
|
25
38
|
.name('doistbot')
|
|
26
39
|
.description('DoistBot local self-review CLI')
|
|
27
|
-
.version(
|
|
28
|
-
|
|
40
|
+
.version(PACKAGE_VERSION)
|
|
41
|
+
.hook('postAction', async (_thisCommand, actionCommand) => {
|
|
42
|
+
if (automaticUpdateCommands.has(actionCommand)) {
|
|
43
|
+
await automaticUpdateCheck();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
withAutomaticUpdateCheck(program
|
|
29
47
|
.command('doctor')
|
|
30
48
|
.description('Diagnose local DoistBot CLI setup')
|
|
31
|
-
.action(async () => await doctor());
|
|
32
|
-
program
|
|
49
|
+
.action(async () => await doctor()));
|
|
50
|
+
withAutomaticUpdateCheck(program
|
|
33
51
|
.command('setup')
|
|
34
52
|
.description('Check prerequisites and configure local provider auth')
|
|
35
|
-
.action(async () => await setup());
|
|
36
|
-
const authCommand = program
|
|
53
|
+
.action(async () => await setup()));
|
|
54
|
+
const authCommand = withAutomaticUpdateCheck(program
|
|
37
55
|
.command('auth')
|
|
38
56
|
.description('Authenticate DoistBot CLI with local review providers')
|
|
39
|
-
.action(async () => await authSetup());
|
|
40
|
-
authCommand
|
|
57
|
+
.action(async () => await authSetup()));
|
|
58
|
+
withAutomaticUpdateCheck(authCommand
|
|
41
59
|
.command('status')
|
|
42
60
|
.description('Show provider authentication status')
|
|
43
|
-
.action(async () => await authStatus());
|
|
44
|
-
authCommand
|
|
61
|
+
.action(async () => await authStatus()));
|
|
62
|
+
withAutomaticUpdateCheck(authCommand
|
|
45
63
|
.command('login')
|
|
46
64
|
.description('Configure provider authentication')
|
|
47
|
-
.action(async () => await authSetup());
|
|
48
|
-
authCommand
|
|
49
|
-
.command('logout')
|
|
50
|
-
.description(
|
|
51
|
-
.action(async () => await authLogout());
|
|
52
|
-
program
|
|
65
|
+
.action(async () => await authSetup()));
|
|
66
|
+
withAutomaticUpdateCheck(authCommand
|
|
67
|
+
.command('logout [provider]')
|
|
68
|
+
.description(`Clear stored DoistBot CLI auth for ${AUTH_LOGOUT_PROVIDER_USAGE}`)
|
|
69
|
+
.action(async (provider) => await authLogout(provider)));
|
|
70
|
+
withAutomaticUpdateCheck(program
|
|
53
71
|
.command('review [source]')
|
|
54
72
|
.description('Run a local self-review. Use `staged` to review only staged changes.')
|
|
55
|
-
.action(async (source) => await review(source));
|
|
73
|
+
.action(async (source) => await review(source)));
|
|
74
|
+
program
|
|
75
|
+
.command('update')
|
|
76
|
+
.description('Check whether a newer DoistBot CLI version is available')
|
|
77
|
+
.action(async () => await updateCheck());
|
|
56
78
|
return program;
|
|
57
79
|
}
|
|
58
|
-
async function
|
|
59
|
-
|
|
80
|
+
export async function runCliEntrypoint({ argv = process.argv, dependencies, exitProcess = exitCliProcess, writeErrorLine: writeError = writeErrorLine, } = {}) {
|
|
81
|
+
try {
|
|
82
|
+
await createProgram(dependencies).parseAsync(argv, { from: 'node' });
|
|
83
|
+
if (shouldForceExitAfterCommand(argv)) {
|
|
84
|
+
await exitProcess(getProcessExitCode());
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
writeError(`Error: ${error.message}`);
|
|
89
|
+
process.exitCode = 1;
|
|
90
|
+
if (shouldForceExitAfterCommand(argv)) {
|
|
91
|
+
await exitProcess(1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export function shouldForceExitAfterCommand(argv) {
|
|
96
|
+
const command = argv[2];
|
|
97
|
+
const authSubcommand = argv[3];
|
|
98
|
+
return command === 'setup' || (command === 'auth' && authSubcommand !== 'status');
|
|
99
|
+
}
|
|
100
|
+
function flushStream(stream) {
|
|
101
|
+
if (!stream.writable) {
|
|
102
|
+
return Promise.resolve();
|
|
103
|
+
}
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
stream.write('', () => resolve());
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
async function exitCliProcess(code) {
|
|
109
|
+
await Promise.all([flushStream(process.stdout), flushStream(process.stderr)]);
|
|
110
|
+
process.exit(code);
|
|
111
|
+
}
|
|
112
|
+
function getProcessExitCode() {
|
|
113
|
+
return typeof process.exitCode === 'number' ? process.exitCode : 0;
|
|
60
114
|
}
|
|
61
115
|
export function isCliEntrypoint(argvPath, moduleUrl) {
|
|
62
116
|
if (!argvPath) {
|
|
@@ -72,8 +126,5 @@ export function isCliEntrypoint(argvPath, moduleUrl) {
|
|
|
72
126
|
}
|
|
73
127
|
}
|
|
74
128
|
if (isCliEntrypoint(process.argv[1], import.meta.url)) {
|
|
75
|
-
await
|
|
76
|
-
writeErrorLine(`Error: ${error.message}`);
|
|
77
|
-
process.exitCode = 1;
|
|
78
|
-
});
|
|
129
|
+
await runCliEntrypoint();
|
|
79
130
|
}
|
package/dist/input.js
CHANGED
|
@@ -12,55 +12,29 @@ export function promptInput(prompt) {
|
|
|
12
12
|
});
|
|
13
13
|
}
|
|
14
14
|
export function promptHiddenInput(prompt) {
|
|
15
|
-
|
|
16
|
-
if (!process.stdin.isTTY || !input.setRawMode) {
|
|
15
|
+
if (!process.stdin.isTTY) {
|
|
17
16
|
return promptInput(prompt);
|
|
18
17
|
}
|
|
19
|
-
return
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
function onData(chunk) {
|
|
42
|
-
for (const character of chunk) {
|
|
43
|
-
if (character === '\u0003') {
|
|
44
|
-
cancel();
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
if (character === '\r' || character === '\n') {
|
|
48
|
-
finish();
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
if (character === '\u007F' || character === '\b') {
|
|
52
|
-
value = value.slice(0, -1);
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
if (character >= ' ') {
|
|
56
|
-
value += character;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
process.stdout.write(prompt);
|
|
61
|
-
input.setRawMode(true);
|
|
62
|
-
input.resume();
|
|
63
|
-
input.setEncoding('utf8');
|
|
64
|
-
input.on('data', onData);
|
|
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(/:$/, ''),
|
|
65
39
|
});
|
|
66
40
|
}
|