@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.
- package/dist/actions/auth.js +135 -28
- package/dist/actions/doctor.js +12 -9
- package/dist/actions/review-usage.js +20 -0
- package/dist/actions/review.js +2 -1
- package/dist/actions/setup.js +2 -1
- 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 +10 -7
- 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-fix-retry/fix-retry.js +1 -7
- package/sandbox/dist/tasks/issue-triage/fix-attempt.js +148 -100
- 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 +31 -30
- 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/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
|
}
|
package/dist/secure-store.js
CHANGED
|
@@ -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
|
|
11
|
-
|
|
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
|
|
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
|
-
|
|
30
|
+
if (cachedUnavailable) {
|
|
31
|
+
throw cachedUnavailable;
|
|
32
|
+
}
|
|
33
|
+
const entry = await getEntry(account, rememberUnavailable);
|
|
17
34
|
try {
|
|
18
|
-
|
|
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
|
|
39
|
+
throw rememberUnavailable(error);
|
|
22
40
|
}
|
|
23
41
|
},
|
|
24
42
|
async setSecret(secret) {
|
|
25
|
-
|
|
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
|
|
52
|
+
throw rememberUnavailable(error);
|
|
31
53
|
}
|
|
32
54
|
},
|
|
33
55
|
async deleteSecret() {
|
|
34
|
-
|
|
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
|
|
64
|
+
throw rememberUnavailable(error);
|
|
40
65
|
}
|
|
41
66
|
},
|
|
42
67
|
};
|
|
43
68
|
}
|
|
44
|
-
|
|
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
|
|
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
|
-
|
|
58
|
-
|
|
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:
|
|
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.
|
|
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
|
-
"@
|
|
43
|
-
"@
|
|
44
|
-
"@
|
|
45
|
-
"
|
|
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.
|
|
53
|
+
"yocto-spinner": "1.2.0"
|
|
51
54
|
},
|
|
52
55
|
"devDependencies": {
|
|
53
|
-
"vitest": "4.1.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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',
|