@doist/doistbot-cli 1.0.0 → 1.0.2

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.
Files changed (39) hide show
  1. package/dist/actions/auth.js +135 -28
  2. package/dist/actions/doctor.js +12 -9
  3. package/dist/actions/review-usage.js +20 -0
  4. package/dist/actions/review.js +2 -1
  5. package/dist/actions/setup.js +2 -1
  6. package/dist/auth.js +30 -9
  7. package/dist/config.js +1 -1
  8. package/dist/env.js +1 -1
  9. package/dist/file-store.js +112 -0
  10. package/dist/index.js +76 -25
  11. package/dist/input.js +22 -48
  12. package/dist/secure-store.js +105 -16
  13. package/dist/terminal.js +4 -1
  14. package/dist/test-utils/secure-store-mocks.js +29 -0
  15. package/dist/update-cache-entrypoint.js +16 -0
  16. package/dist/update-check.js +235 -0
  17. package/package.json +10 -7
  18. package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +5 -2
  19. package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +34 -23
  20. package/sandbox/dist/core/pi.js +0 -4
  21. package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +1 -7
  22. package/sandbox/dist/tasks/issue-triage/fix-attempt.js +148 -100
  23. package/sandbox/dist/tasks/issue-triage/pr-creator.js +16 -9
  24. package/sandbox/dist/tasks/issue-triage/repo-config.js +28 -2
  25. package/sandbox/dist/tasks/issue-triage/triage.js +31 -30
  26. package/sandbox/dist/tasks/review/check-run.js +134 -14
  27. package/sandbox/dist/tasks/review/conversation-context.js +316 -0
  28. package/sandbox/dist/tasks/review/engines/dedupe.js +82 -63
  29. package/sandbox/dist/tasks/review/local-git.js +7 -0
  30. package/sandbox/dist/tasks/review/multi-focus-prompt.js +2 -1
  31. package/sandbox/dist/tasks/review/prompt.js +15 -2
  32. package/sandbox/dist/tasks/review/review-feedback.js +4 -0
  33. package/sandbox/dist/tasks/review/review.js +39 -1
  34. package/sandbox/dist/tasks/review/runner.js +5 -1
  35. package/sandbox/dist/tasks/review/severity.js +5 -0
  36. package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +5 -2
  37. package/sandbox/node_modules/doistbot-repo-config/dist/index.js +34 -23
  38. package/sandbox/review-prompt.md +8 -2
  39. package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +7 -2
package/dist/input.js CHANGED
@@ -12,55 +12,29 @@ export function promptInput(prompt) {
12
12
  });
13
13
  }
14
14
  export function promptHiddenInput(prompt) {
15
- const input = process.stdin;
16
- if (!process.stdin.isTTY || !input.setRawMode) {
15
+ if (!process.stdin.isTTY) {
17
16
  return promptInput(prompt);
18
17
  }
19
- return new Promise((resolve, reject) => {
20
- let value = '';
21
- const previousRawMode = input.isRaw === true;
22
- const previousEncoding = input.readableEncoding;
23
- function cleanup() {
24
- input.off('data', onData);
25
- input.setRawMode?.(previousRawMode);
26
- if (previousEncoding) {
27
- input.setEncoding(previousEncoding);
28
- }
29
- input.pause();
30
- }
31
- function finish() {
32
- cleanup();
33
- process.stdout.write('\n');
34
- resolve(value);
35
- }
36
- function cancel() {
37
- cleanup();
38
- process.stdout.write('\n');
39
- reject(new Error('Input cancelled'));
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
  }
@@ -1,59 +1,148 @@
1
1
  const SERVICE_NAME = 'doistbot-cli';
2
- const GEMINI_ACCOUNT_NAME = 'gemini-api-key';
3
- export const SECURE_STORE_DESCRIPTION = 'system credential manager';
4
2
  export class SecureStoreUnavailableError extends Error {
5
3
  constructor(message = 'System credential storage is unavailable') {
6
4
  super(message);
7
5
  this.name = 'SecureStoreUnavailableError';
8
6
  }
9
7
  }
10
- export function createGeminiKeyStore() {
11
- return createSecureStore(GEMINI_ACCOUNT_NAME);
8
+ export function getSecureStoreLabel(platform = process.platform) {
9
+ switch (platform) {
10
+ case 'darwin':
11
+ return 'macOS Keychain';
12
+ case 'win32':
13
+ return 'Windows Credential Manager';
14
+ default:
15
+ return 'system keyring';
16
+ }
17
+ }
18
+ export function describeError(error) {
19
+ return error instanceof Error ? error.message : String(error);
12
20
  }
13
- function createSecureStore(account) {
21
+ export function createKeyringStore(account) {
22
+ let cachedUnavailable;
23
+ function rememberUnavailable(error) {
24
+ const wrapped = toUnavailableError(error);
25
+ cachedUnavailable = wrapped;
26
+ return wrapped;
27
+ }
14
28
  return {
15
29
  async getSecret() {
16
- const entry = await getEntry(account);
30
+ if (cachedUnavailable) {
31
+ throw cachedUnavailable;
32
+ }
33
+ const entry = await getEntry(account, rememberUnavailable);
17
34
  try {
18
- return (await entry.getPassword()) ?? null;
35
+ const raw = (await entry.getPassword()) ?? null;
36
+ return raw === null ? { value: null } : { value: raw, backend: 'keyring' };
19
37
  }
20
38
  catch (error) {
21
- throw toUnavailableError(error);
39
+ throw rememberUnavailable(error);
22
40
  }
23
41
  },
24
42
  async setSecret(secret) {
25
- const entry = await getEntry(account);
43
+ if (cachedUnavailable) {
44
+ throw cachedUnavailable;
45
+ }
46
+ const entry = await getEntry(account, rememberUnavailable);
26
47
  try {
27
48
  await entry.setPassword(secret);
49
+ return 'keyring';
28
50
  }
29
51
  catch (error) {
30
- throw toUnavailableError(error);
52
+ throw rememberUnavailable(error);
31
53
  }
32
54
  },
33
55
  async deleteSecret() {
34
- const entry = await getEntry(account);
56
+ if (cachedUnavailable) {
57
+ throw cachedUnavailable;
58
+ }
59
+ const entry = await getEntry(account, rememberUnavailable);
35
60
  try {
36
61
  return await entry.deleteCredential();
37
62
  }
38
63
  catch (error) {
39
- throw toUnavailableError(error);
64
+ throw rememberUnavailable(error);
40
65
  }
41
66
  },
42
67
  };
43
68
  }
44
- async function getEntry(account) {
69
+ export function createCompositeStore(keyring, file) {
70
+ return {
71
+ async getSecret() {
72
+ try {
73
+ const fromKeyring = await keyring.getSecret();
74
+ if (fromKeyring.value !== null) {
75
+ return fromKeyring;
76
+ }
77
+ }
78
+ catch (error) {
79
+ if (!(error instanceof SecureStoreUnavailableError)) {
80
+ throw error;
81
+ }
82
+ }
83
+ return await file.getSecret();
84
+ },
85
+ async setSecret(secret) {
86
+ try {
87
+ const backend = await keyring.setSecret(secret);
88
+ await file.deleteSecret().catch(() => undefined);
89
+ return backend;
90
+ }
91
+ catch (keyringError) {
92
+ if (!(keyringError instanceof SecureStoreUnavailableError)) {
93
+ throw keyringError;
94
+ }
95
+ try {
96
+ return await file.setSecret(secret);
97
+ }
98
+ catch (fileError) {
99
+ throw combineFailure(keyringError, fileError);
100
+ }
101
+ }
102
+ },
103
+ async deleteSecret() {
104
+ let deleted = false;
105
+ let keyringFailure;
106
+ try {
107
+ deleted = (await keyring.deleteSecret()) || deleted;
108
+ }
109
+ catch (error) {
110
+ if (!(error instanceof SecureStoreUnavailableError)) {
111
+ throw error;
112
+ }
113
+ keyringFailure = error;
114
+ }
115
+ try {
116
+ deleted = (await file.deleteSecret()) || deleted;
117
+ }
118
+ catch (fileError) {
119
+ if (keyringFailure) {
120
+ throw combineFailure(keyringFailure, fileError);
121
+ }
122
+ throw fileError;
123
+ }
124
+ if (keyringFailure) {
125
+ throw new SecureStoreUnavailableError(`Failed to delete from system keyring (${keyringFailure.message}). File store deletion succeeded but the keyring entry may still exist.`);
126
+ }
127
+ return deleted;
128
+ },
129
+ };
130
+ }
131
+ async function getEntry(account, rememberUnavailable) {
45
132
  try {
46
133
  const { AsyncEntry } = await import('@napi-rs/keyring');
47
134
  return new AsyncEntry(SERVICE_NAME, account);
48
135
  }
49
136
  catch (error) {
50
- throw toUnavailableError(error);
137
+ throw rememberUnavailable(error);
51
138
  }
52
139
  }
53
140
  function toUnavailableError(error) {
54
141
  if (error instanceof SecureStoreUnavailableError) {
55
142
  return error;
56
143
  }
57
- const message = error instanceof Error ? error.message : 'System credential storage is unavailable';
58
- return new SecureStoreUnavailableError(message);
144
+ return new SecureStoreUnavailableError(describeError(error));
145
+ }
146
+ function combineFailure(keyringError, fileError) {
147
+ return new SecureStoreUnavailableError(`System keyring unavailable (${keyringError.message}) and file fallback failed (${describeError(fileError)}).`);
59
148
  }
package/dist/terminal.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import yoctoSpinner from 'yocto-spinner';
3
+ export function isInteractiveOutputAllowed({ env = process.env, stream = process.stderr, } = {}) {
4
+ return env.CI !== 'true' && env.CI !== '1' && stream.isTTY === true;
5
+ }
3
6
  export function writeLine(message = '') {
4
7
  process.stdout.write(`${message}\n`);
5
8
  }
@@ -12,7 +15,7 @@ function getDefaultTerminalConfig() {
12
15
  process.env.TERM !== 'dumb' &&
13
16
  process.stdout.isTTY === true &&
14
17
  chalk.level > 0,
15
- isInteractive: process.stderr.isTTY === true && process.env.CI !== 'true',
18
+ isInteractive: isInteractiveOutputAllowed(),
16
19
  };
17
20
  }
18
21
  function colorize(value, style, terminalConfig) {
@@ -0,0 +1,29 @@
1
+ import { vi } from 'vitest';
2
+ import { SecureStoreUnavailableError, } from '../secure-store.js';
3
+ export function createMemoryStore({ initial = null, backend = 'keyring', } = {}) {
4
+ let secret = initial;
5
+ return {
6
+ getSecret: vi.fn(async () => secret === null ? { value: null } : { value: secret, backend }),
7
+ setSecret: vi.fn(async (value) => {
8
+ secret = value;
9
+ return backend;
10
+ }),
11
+ deleteSecret: vi.fn(async () => {
12
+ if (secret === null) {
13
+ return false;
14
+ }
15
+ secret = null;
16
+ return true;
17
+ }),
18
+ };
19
+ }
20
+ export function createUnavailableStore(message = 'keyring offline') {
21
+ async function fail() {
22
+ throw new SecureStoreUnavailableError(message);
23
+ }
24
+ return {
25
+ getSecret: vi.fn(fail),
26
+ setSecret: vi.fn(fail),
27
+ deleteSecret: vi.fn(fail),
28
+ };
29
+ }
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
3
+ import { refreshCliUpdateCache } from './update-check.js';
4
+ const { values } = parseArgs({
5
+ options: {
6
+ 'cache-path': { type: 'string' },
7
+ 'current-version': { type: 'string' },
8
+ },
9
+ });
10
+ if (!values['current-version']) {
11
+ throw new Error('Missing --current-version');
12
+ }
13
+ await refreshCliUpdateCache({
14
+ cachePath: values['cache-path'],
15
+ currentVersion: values['current-version'],
16
+ });
@@ -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 = '@doist/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,10 +1,11 @@
1
1
  {
2
2
  "name": "@doist/doistbot-cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Local self-review CLI for DoistBot",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
+ "db": "dist/index.js",
8
9
  "doistbot": "dist/index.js"
9
10
  },
10
11
  "keywords": [
@@ -39,17 +40,19 @@
39
40
  "test": "vitest run src scripts --config ../../config/vitest.base.ts"
40
41
  },
41
42
  "dependencies": {
42
- "@mariozechner/pi-ai": "0.72.0",
43
- "@mariozechner/pi-coding-agent": "0.72.0",
44
- "@napi-rs/keyring": "1.2.0",
45
- "ajv": "8.18.0",
43
+ "@inquirer/checkbox": "5.1.5",
44
+ "@inquirer/password": "5.0.13",
45
+ "@mariozechner/pi-ai": "0.73.1",
46
+ "@mariozechner/pi-coding-agent": "0.73.1",
47
+ "@napi-rs/keyring": "1.3.0",
48
+ "ajv": "8.20.0",
46
49
  "chalk": "4.1.2",
47
50
  "commander": "14.0.3",
48
51
  "js-yaml": "4.1.1",
49
52
  "octokit": "5.0.5",
50
- "yocto-spinner": "1.1.0"
53
+ "yocto-spinner": "1.2.0"
51
54
  },
52
55
  "devDependencies": {
53
- "vitest": "4.1.5"
56
+ "vitest": "4.1.6"
54
57
  }
55
58
  }
@@ -41,8 +41,11 @@ type FetchFileParams = {
41
41
  logger: Logger;
42
42
  };
43
43
  /**
44
- * Fetches the repository-specific configuration from .github/doistbot.yml
45
- * Returns null if no config file exists.
44
+ * Fetches the repository-specific configuration from .github/doistbot.yml.
45
+ * Returns null when the file is genuinely absent (404) or the path is not a file.
46
+ * Throws on transport / authorization errors so callers can distinguish "no
47
+ * config" from "could not load config" — important when an opt-out flag would
48
+ * otherwise be silently ignored.
46
49
  */
47
50
  export declare function getRepoConfig({ octokit, owner, repo, ref, logger, }: Omit<FetchFileParams, 'path'>): Promise<RepoConfig | null>;
48
51
  export declare function getLocalRepoConfig({ workspaceDir, logger, }: {
@@ -16,25 +16,17 @@ function resolveWorkspacePath(workspaceDir, repoPath) {
16
16
  return resolved;
17
17
  }
18
18
  async function fetchFileContent({ octokit, owner, repo, path, ref, logger, }) {
19
+ const params = {
20
+ owner,
21
+ repo,
22
+ path,
23
+ };
24
+ if (ref) {
25
+ params.ref = ref;
26
+ }
27
+ let response;
19
28
  try {
20
- const params = {
21
- owner,
22
- repo,
23
- path,
24
- };
25
- if (ref) {
26
- params.ref = ref;
27
- }
28
- const { data } = await octokit.rest.repos.getContent(params);
29
- if (!('type' in data) || data.type !== 'file' || !('content' in data) || !data.content) {
30
- logger.warn('Path exists but is not a file', {
31
- component: 'repo-config',
32
- repository: `${owner}/${repo}`,
33
- path,
34
- });
35
- return null;
36
- }
37
- return Buffer.from(data.content, 'base64').toString('utf8');
29
+ response = await octokit.rest.repos.getContent(params);
38
30
  }
39
31
  catch (error) {
40
32
  if (error instanceof Error && 'status' in error && error.status === 404) {
@@ -45,18 +37,25 @@ async function fetchFileContent({ octokit, owner, repo, path, ref, logger, }) {
45
37
  });
46
38
  return null;
47
39
  }
48
- logger.warn('Error fetching file', {
40
+ throw error;
41
+ }
42
+ const { data } = response;
43
+ if (!('type' in data) || data.type !== 'file' || !('content' in data) || !data.content) {
44
+ logger.warn('Path exists but is not a file', {
49
45
  component: 'repo-config',
50
46
  repository: `${owner}/${repo}`,
51
47
  path,
52
- error: error instanceof Error ? error.message : String(error),
53
48
  });
54
49
  return null;
55
50
  }
51
+ return Buffer.from(data.content, 'base64').toString('utf8');
56
52
  }
57
53
  /**
58
- * Fetches the repository-specific configuration from .github/doistbot.yml
59
- * Returns null if no config file exists.
54
+ * Fetches the repository-specific configuration from .github/doistbot.yml.
55
+ * Returns null when the file is genuinely absent (404) or the path is not a file.
56
+ * Throws on transport / authorization errors so callers can distinguish "no
57
+ * config" from "could not load config" — important when an opt-out flag would
58
+ * otherwise be silently ignored.
60
59
  */
61
60
  export async function getRepoConfig({ octokit, owner, repo, ref, logger, }) {
62
61
  const content = await fetchFileContent({
@@ -105,7 +104,19 @@ export function getLocalRepoConfig({ workspaceDir, logger, }) {
105
104
  * Returns null if the file doesn't exist.
106
105
  */
107
106
  export async function getCustomPrompt({ octokit, owner, repo, path, ref, logger, }) {
108
- const content = await fetchFileContent({ octokit, owner, repo, path, ref, logger });
107
+ let content;
108
+ try {
109
+ content = await fetchFileContent({ octokit, owner, repo, path, ref, logger });
110
+ }
111
+ catch (error) {
112
+ logger.warn('Error fetching custom prompt', {
113
+ component: 'repo-config',
114
+ repository: `${owner}/${repo}`,
115
+ path,
116
+ error: error instanceof Error ? error.message : String(error),
117
+ });
118
+ return null;
119
+ }
109
120
  if (content !== null) {
110
121
  logger.info('Loaded custom prompt', {
111
122
  component: 'repo-config',