byterover-cli 3.8.2 → 3.9.0
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/README.md +8 -34
- package/dist/agent/infra/llm/providers/google.js +1 -1
- package/dist/oclif/commands/login.d.ts +15 -2
- package/dist/oclif/commands/login.js +106 -29
- package/dist/oclif/commands/providers/list.js +3 -0
- package/dist/oclif/commands/vc/diff.d.ts +12 -0
- package/dist/oclif/commands/vc/diff.js +40 -0
- package/dist/oclif/commands/vc/remote/remove.d.ts +9 -0
- package/dist/oclif/commands/vc/remote/remove.js +23 -0
- package/dist/server/core/domain/entities/brv-config.d.ts +4 -0
- package/dist/server/core/domain/entities/brv-config.js +12 -0
- package/dist/server/core/domain/entities/provider-registry.js +3 -3
- package/dist/server/core/interfaces/services/i-git-service.d.ts +55 -4
- package/dist/server/infra/context-tree/summary-frontmatter.js +2 -2
- package/dist/server/infra/dream/operations/consolidate.js +5 -4
- package/dist/server/infra/dream/operations/synthesize.js +1 -1
- package/dist/server/infra/git/isomorphic-git-service.d.ts +24 -1
- package/dist/server/infra/git/isomorphic-git-service.js +207 -7
- package/dist/server/infra/transport/handlers/config-handler.js +1 -0
- package/dist/server/infra/transport/handlers/locations-handler.d.ts +1 -0
- package/dist/server/infra/transport/handlers/locations-handler.js +25 -1
- package/dist/server/infra/transport/handlers/reveal-command.d.ts +9 -0
- package/dist/server/infra/transport/handlers/reveal-command.js +7 -0
- package/dist/server/infra/transport/handlers/vc-handler.d.ts +11 -0
- package/dist/server/infra/transport/handlers/vc-handler.js +143 -9
- package/dist/server/infra/webui/webui-middleware.js +10 -4
- package/dist/shared/transport/events/config-events.d.ts +1 -0
- package/dist/shared/transport/events/index.d.ts +1 -0
- package/dist/shared/transport/events/locations-events.d.ts +7 -0
- package/dist/shared/transport/events/locations-events.js +1 -0
- package/dist/shared/transport/events/vc-events.d.ts +56 -5
- package/dist/shared/transport/events/vc-events.js +7 -0
- package/dist/tui/features/commands/definitions/vc-diff.d.ts +2 -0
- package/dist/tui/features/commands/definitions/vc-diff.js +23 -0
- package/dist/tui/features/commands/definitions/vc-remote.js +16 -7
- package/dist/tui/features/commands/definitions/vc.js +2 -0
- package/dist/tui/features/vc/diff/api/execute-vc-diff.d.ts +8 -0
- package/dist/tui/features/vc/diff/api/execute-vc-diff.js +13 -0
- package/dist/tui/features/vc/diff/components/vc-diff-flow.d.ts +8 -0
- package/dist/tui/features/vc/diff/components/vc-diff-flow.js +31 -0
- package/dist/tui/features/vc/diff/utils/format-diff.d.ts +2 -0
- package/dist/tui/features/vc/diff/utils/format-diff.js +83 -0
- package/dist/tui/features/vc/diff/utils/parse-mode.d.ts +2 -0
- package/dist/tui/features/vc/diff/utils/parse-mode.js +16 -0
- package/dist/tui/features/vc/remote/components/vc-remote-flow.js +23 -8
- package/dist/webui/assets/index-CvcqpMYn.css +1 -0
- package/dist/webui/assets/index-thSZZahh.js +130 -0
- package/dist/webui/index.html +3 -3
- package/dist/webui/sw.js +1 -1
- package/oclif.manifest.json +1009 -933
- package/package.json +3 -1
- package/dist/webui/assets/index-Cti7S_1o.js +0 -130
- package/dist/webui/assets/index-Dpw6osIL.css +0 -1
package/README.md
CHANGED
|
@@ -30,6 +30,7 @@ Or download our self-hosted PDF version of the paper [here](https://byterover.de
|
|
|
30
30
|
|
|
31
31
|
**Key Features:**
|
|
32
32
|
|
|
33
|
+
- 🌐 Web dashboard for curating and querying context (`brv webui`)
|
|
33
34
|
- 🖥️ Interactive TUI with REPL interface (React/Ink)
|
|
34
35
|
- 🧠 Context tree and knowledge storage management
|
|
35
36
|
- 🔀 Git-like version control for the context tree (branch, commit, merge, push/pull)
|
|
@@ -101,35 +102,6 @@ The REPL auto-configures on first run - no setup needed. Type `/` to discover al
|
|
|
101
102
|
/query How is authentication implemented?
|
|
102
103
|
```
|
|
103
104
|
|
|
104
|
-
## Web UI Development
|
|
105
|
-
|
|
106
|
-
The web UI supports a local-first development flow for the shared component library.
|
|
107
|
-
|
|
108
|
-
`npm run dev:ui` uses the git submodule at `packages/byterover-packages/ui` so edits to shared UI components hot-reload immediately in Vite.
|
|
109
|
-
|
|
110
|
-
```bash
|
|
111
|
-
# Clone with submodules, or initialize them after clone
|
|
112
|
-
git clone --recurse-submodules <repo-url>
|
|
113
|
-
# or
|
|
114
|
-
git submodule update --init --recursive
|
|
115
|
-
|
|
116
|
-
# Install dependencies
|
|
117
|
-
npm ci
|
|
118
|
-
|
|
119
|
-
# Start or restart the daemon
|
|
120
|
-
./bin/dev.js restart
|
|
121
|
-
|
|
122
|
-
# Start the web UI in local development mode
|
|
123
|
-
npm run dev:ui
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
Notes:
|
|
127
|
-
|
|
128
|
-
- Edit shared components in `packages/byterover-packages/ui/src`.
|
|
129
|
-
- `npm run dev:ui` uses the submodule source.
|
|
130
|
-
- `npm run build:ui` uses the installed package path.
|
|
131
|
-
- If `/api/ui/config` or transport bootstrap fails, restart the Vite dev server after restarting the daemon.
|
|
132
|
-
|
|
133
105
|
## ByteRover Cloud
|
|
134
106
|
|
|
135
107
|
ByteRover Cloud is a hosted platform for teams to sync, share, and manage context knowledge across projects and machines.
|
|
@@ -139,12 +111,12 @@ Everything works locally by default - Cloud adds collaboration and persistence w
|
|
|
139
111
|
<a href="https://app.byterover.dev"><img src="https://img.shields.io/badge/Try%20ByteRover%20Cloud-Free-blue?style=for-the-badge" alt="Try ByteRover Cloud" /></a>
|
|
140
112
|
</p>
|
|
141
113
|
|
|
142
|
-
Sign in
|
|
143
|
-
an [API key](https://app.byterover.dev/settings/keys) (`brv login`) to get started.
|
|
114
|
+
Sign in from the dashboard, or run `brv login` with an [API key](https://app.byterover.dev/settings/keys).
|
|
144
115
|
|
|
145
116
|
- 🔄 **Team context sync** — push and pull shared knowledge across teammates
|
|
146
117
|
- 📂 **Shared spaces** — organize context across multiple projects and teams
|
|
147
118
|
- 💻 **Multi-machine access** — sync your context tree across devices with cloud backup
|
|
119
|
+
- 💻 **Multi-machine access** — sync your context tree across devices
|
|
148
120
|
- 🧠 **Built-in hosted LLM** — start immediately with limited free usage
|
|
149
121
|
- 👥 **Team management** — manage members, spaces, and permissions via the web app
|
|
150
122
|
- 📊 **Usage analytics** — track seat allocation and monthly credit consumption
|
|
@@ -153,10 +125,13 @@ an [API key](https://app.byterover.dev/settings/keys) (`brv login`) to get start
|
|
|
153
125
|
<details>
|
|
154
126
|
<summary><h2>CLI Usage</h2></summary>
|
|
155
127
|
|
|
128
|
+
Most users only need `brv webui`. The commands below are for advanced users and automation. Run `brv --help` for the full, up-to-date reference.
|
|
129
|
+
|
|
156
130
|
### Core Workflow
|
|
157
131
|
|
|
158
132
|
```bash
|
|
159
133
|
brv # Start interactive REPL
|
|
134
|
+
brv webui # Open the ByteRover dashboard (primary UI)
|
|
160
135
|
brv status # Show project and daemon status
|
|
161
136
|
brv curate # Add context to knowledge storage
|
|
162
137
|
brv curate view # View curate history
|
|
@@ -245,7 +220,7 @@ Run `brv --help` for the full command reference.
|
|
|
245
220
|
<details>
|
|
246
221
|
<summary><h2>Supported LLM Providers</h2></summary>
|
|
247
222
|
|
|
248
|
-
ByteRover CLI supports 18 LLM providers out of the box.
|
|
223
|
+
ByteRover CLI supports 18 LLM providers out of the box. Connect and switch providers from the dashboard, or use `brv providers connect` / `brv providers switch`.
|
|
249
224
|
|
|
250
225
|
| Provider | Description |
|
|
251
226
|
|----------|-------------|
|
|
@@ -310,13 +285,12 @@ We welcome contributions! See our [Contributing Guide](CONTRIBUTING.md) for deve
|
|
|
310
285
|
ByteRover CLI is built and maintained by the [ByteRover team](https://byterover.dev/).
|
|
311
286
|
|
|
312
287
|
- Join our [Discord](https://discord.com/invite/UMRrpNjh5W) to share projects, ask questions, or just say hi
|
|
313
|
-
- [Report issues](https://github.com/campfirein/byterover-cli/issues)
|
|
288
|
+
- [Report issues](https://github.com/campfirein/byterover-cli/issues) on GitHub
|
|
314
289
|
- If you enjoy ByteRover CLI, please give us a star on GitHub — it helps a lot!
|
|
315
290
|
- Follow [@kevinnguyendn](https://x.com/kevinnguyendn) on X
|
|
316
291
|
|
|
317
292
|
## Contributors
|
|
318
293
|
|
|
319
|
-
<!-- TODO: ENG-1575 -->
|
|
320
294
|
[](https://github.com/campfirein/byterover-cli/graphs/contributors)
|
|
321
295
|
|
|
322
296
|
## Star History
|
|
@@ -15,7 +15,7 @@ export const googleProvider = {
|
|
|
15
15
|
model: provider(config.model),
|
|
16
16
|
});
|
|
17
17
|
},
|
|
18
|
-
defaultModel: 'gemini-
|
|
18
|
+
defaultModel: 'gemini-3-flash-preview',
|
|
19
19
|
description: 'Gemini models by Google',
|
|
20
20
|
envVars: ['GOOGLE_API_KEY', 'GEMINI_API_KEY'],
|
|
21
21
|
id: 'google',
|
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
-
import { type AuthLoginWithApiKeyResponse } from '../../shared/transport/events/auth-events.js';
|
|
2
|
+
import { type AuthLoginCompletedEvent, type AuthLoginWithApiKeyResponse } from '../../shared/transport/events/auth-events.js';
|
|
3
3
|
import { type DaemonClientOptions } from '../lib/daemon-client.js';
|
|
4
|
+
export interface LoginOAuthOptions extends DaemonClientOptions {
|
|
5
|
+
/** Max time to wait for LOGIN_COMPLETED after the browser opens. */
|
|
6
|
+
oauthTimeoutMs?: number;
|
|
7
|
+
/** Invoked with the auth URL once the daemon has started the flow. */
|
|
8
|
+
onAuthUrl?: (authUrl: string) => void;
|
|
9
|
+
}
|
|
4
10
|
export default class Login extends Command {
|
|
5
11
|
static description: string;
|
|
6
12
|
static examples: string[];
|
|
7
13
|
static flags: {
|
|
8
|
-
'api-key': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
15
|
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
16
|
};
|
|
17
|
+
/** Gates the OAuth flow. DISPLAY/WAYLAND_DISPLAY deliberately not checked — unset on macOS/Windows, would false-positive. */
|
|
18
|
+
protected canOpenBrowser(): boolean;
|
|
11
19
|
protected loginWithApiKey(apiKey: string, options?: DaemonClientOptions): Promise<AuthLoginWithApiKeyResponse>;
|
|
20
|
+
protected loginWithOAuth(options?: LoginOAuthOptions): Promise<AuthLoginCompletedEvent>;
|
|
12
21
|
run(): Promise<void>;
|
|
22
|
+
private emitError;
|
|
23
|
+
private emitSuccess;
|
|
24
|
+
private runApiKey;
|
|
25
|
+
private runOAuth;
|
|
13
26
|
}
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
import { Command, Flags } from '@oclif/core';
|
|
2
|
-
import { AuthEvents } from '../../shared/transport/events/auth-events.js';
|
|
2
|
+
import { AuthEvents, } from '../../shared/transport/events/auth-events.js';
|
|
3
3
|
import { formatConnectionError, withDaemonRetry } from '../lib/daemon-client.js';
|
|
4
4
|
import { writeJsonResponse } from '../lib/json-response.js';
|
|
5
|
+
const DEFAULT_OAUTH_TIMEOUT_MS = 5 * 60 * 1000;
|
|
5
6
|
export default class Login extends Command {
|
|
6
7
|
static description = 'Authenticate with ByteRover for cloud sync features (optional for local usage)';
|
|
7
8
|
static examples = [
|
|
9
|
+
'# Browser OAuth (default)',
|
|
10
|
+
'<%= config.bin %> <%= command.id %>',
|
|
11
|
+
'',
|
|
12
|
+
'# API key (for CI / headless environments)',
|
|
8
13
|
'<%= config.bin %> <%= command.id %> --api-key <key>',
|
|
9
14
|
'',
|
|
10
15
|
'# JSON output (for automation)',
|
|
11
|
-
'<%= config.bin %> <%= command.id %> --
|
|
16
|
+
'<%= config.bin %> <%= command.id %> --format json',
|
|
12
17
|
];
|
|
13
18
|
static flags = {
|
|
14
19
|
'api-key': Flags.string({
|
|
15
20
|
char: 'k',
|
|
16
|
-
description: 'API key for
|
|
17
|
-
required: true,
|
|
21
|
+
description: 'API key for headless/CI login (get yours at https://app.byterover.dev/settings/keys). Omit to use the browser OAuth flow.',
|
|
18
22
|
}),
|
|
19
23
|
format: Flags.string({
|
|
20
24
|
default: 'text',
|
|
@@ -22,44 +26,117 @@ export default class Login extends Command {
|
|
|
22
26
|
options: ['text', 'json'],
|
|
23
27
|
}),
|
|
24
28
|
};
|
|
29
|
+
/** Gates the OAuth flow. DISPLAY/WAYLAND_DISPLAY deliberately not checked — unset on macOS/Windows, would false-positive. */
|
|
30
|
+
canOpenBrowser() {
|
|
31
|
+
// Either stream not a TTY means piped/scripted/CI — no interactive user to complete OAuth.
|
|
32
|
+
if (process.stdout.isTTY !== true || process.stdin.isTTY !== true)
|
|
33
|
+
return false;
|
|
34
|
+
// SSH has a TTY but can't reach the user's local browser.
|
|
35
|
+
if (process.env.SSH_CONNECTION || process.env.SSH_CLIENT || process.env.SSH_TTY)
|
|
36
|
+
return false;
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
25
39
|
async loginWithApiKey(apiKey, options) {
|
|
26
40
|
return withDaemonRetry(async (client) => client.requestWithAck(AuthEvents.LOGIN_WITH_API_KEY, { apiKey }), options);
|
|
27
41
|
}
|
|
42
|
+
async loginWithOAuth(options) {
|
|
43
|
+
const timeoutMs = options?.oauthTimeoutMs ?? DEFAULT_OAUTH_TIMEOUT_MS;
|
|
44
|
+
return withDaemonRetry(async (client) => {
|
|
45
|
+
// Subscribe *before* initiating, so a fast callback cannot race past us.
|
|
46
|
+
let unsubscribe;
|
|
47
|
+
let timer;
|
|
48
|
+
const completion = new Promise((resolve, reject) => {
|
|
49
|
+
timer = setTimeout(() => {
|
|
50
|
+
unsubscribe?.();
|
|
51
|
+
timer = undefined;
|
|
52
|
+
reject(new Error(`Login timed out after ${Math.round(timeoutMs / 1000)}s`));
|
|
53
|
+
}, timeoutMs);
|
|
54
|
+
unsubscribe = client.on(AuthEvents.LOGIN_COMPLETED, (data) => {
|
|
55
|
+
if (timer) {
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
timer = undefined;
|
|
58
|
+
}
|
|
59
|
+
unsubscribe?.();
|
|
60
|
+
resolve(data);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
try {
|
|
64
|
+
const startResponse = await client.requestWithAck(AuthEvents.START_LOGIN);
|
|
65
|
+
options?.onAuthUrl?.(startResponse.authUrl);
|
|
66
|
+
return await completion;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (timer) {
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
timer = undefined;
|
|
72
|
+
}
|
|
73
|
+
unsubscribe?.();
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}, options);
|
|
77
|
+
}
|
|
28
78
|
async run() {
|
|
29
79
|
const { flags } = await this.parse(Login);
|
|
30
80
|
const apiKey = flags['api-key'];
|
|
31
|
-
const format =
|
|
81
|
+
const format = flags.format === 'json' ? 'json' : 'text';
|
|
82
|
+
if (!apiKey && !this.canOpenBrowser()) {
|
|
83
|
+
this.emitError(format, 'Cannot open a local browser here (non-interactive shell or SSH session). Use --api-key for headless login (get yours at https://app.byterover.dev/settings/keys).');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
32
86
|
try {
|
|
33
|
-
|
|
34
|
-
this.log('Logging in...');
|
|
35
|
-
}
|
|
36
|
-
const response = await this.loginWithApiKey(apiKey);
|
|
37
|
-
if (response.success) {
|
|
38
|
-
if (format === 'json') {
|
|
39
|
-
writeJsonResponse({ command: 'login', data: { userEmail: response.userEmail }, success: true });
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
this.log(`Logged in as ${response.userEmail}`);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
const errorMessage = response.error ?? 'Authentication failed';
|
|
47
|
-
if (format === 'json') {
|
|
48
|
-
writeJsonResponse({ command: 'login', data: { error: errorMessage }, success: false });
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
this.log(errorMessage);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
87
|
+
await (apiKey ? this.runApiKey(apiKey, format) : this.runOAuth(format));
|
|
54
88
|
}
|
|
55
89
|
catch (error) {
|
|
56
|
-
const
|
|
90
|
+
const message = formatConnectionError(error);
|
|
57
91
|
if (format === 'json') {
|
|
58
|
-
|
|
92
|
+
this.emitError(format, message);
|
|
59
93
|
}
|
|
60
94
|
else {
|
|
61
|
-
this.log(
|
|
95
|
+
this.log(message);
|
|
62
96
|
}
|
|
63
97
|
}
|
|
64
98
|
}
|
|
99
|
+
emitError(format, message) {
|
|
100
|
+
if (format === 'json') {
|
|
101
|
+
writeJsonResponse({ command: 'login', data: { error: message }, success: false });
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
this.log(message);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
emitSuccess(format, userEmail) {
|
|
108
|
+
if (format === 'json') {
|
|
109
|
+
writeJsonResponse({ command: 'login', data: { userEmail }, success: true });
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
this.log(userEmail ? `Logged in as ${userEmail}` : 'Logged in successfully');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async runApiKey(apiKey, format) {
|
|
116
|
+
if (format === 'text') {
|
|
117
|
+
this.log('Logging in...');
|
|
118
|
+
}
|
|
119
|
+
const response = await this.loginWithApiKey(apiKey);
|
|
120
|
+
if (response.success) {
|
|
121
|
+
this.emitSuccess(format, response.userEmail);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
this.emitError(format, response.error ?? 'Authentication failed');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async runOAuth(format) {
|
|
128
|
+
const onAuthUrl = (authUrl) => {
|
|
129
|
+
if (format === 'text') {
|
|
130
|
+
this.log('Opening browser for authentication...');
|
|
131
|
+
this.log(`If the browser did not open, visit: ${authUrl}`);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
const result = await this.loginWithOAuth({ onAuthUrl });
|
|
135
|
+
if (result.success) {
|
|
136
|
+
this.emitSuccess(format, result.user?.email);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
this.emitError(format, result.error ?? 'Authentication failed');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
65
142
|
}
|
|
@@ -29,6 +29,9 @@ export default class ProviderList extends Command {
|
|
|
29
29
|
const status = p.isCurrent ? chalk.green('(current)') : p.isConnected ? chalk.yellow('(connected)') : '';
|
|
30
30
|
const authBadge = p.authMethod === 'oauth' ? chalk.cyan('[OAuth]') : p.authMethod === 'api-key' ? chalk.dim('[API Key]') : '';
|
|
31
31
|
this.log(` ${p.name} [${p.id}] ${status} ${authBadge}`.trimEnd());
|
|
32
|
+
if (p.description) {
|
|
33
|
+
this.log(` ${chalk.dim(p.description)}`);
|
|
34
|
+
}
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
catch (error) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class VcDiff extends Command {
|
|
3
|
+
static args: {
|
|
4
|
+
ref: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
staged: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
};
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
+
import { VcEvents, } from '../../../shared/transport/events/vc-events.js';
|
|
3
|
+
import { formatDiff } from '../../../tui/features/vc/diff/utils/format-diff.js';
|
|
4
|
+
import { parseMode } from '../../../tui/features/vc/diff/utils/parse-mode.js';
|
|
5
|
+
import { formatConnectionError, withDaemonRetry } from '../../lib/daemon-client.js';
|
|
6
|
+
export default class VcDiff extends Command {
|
|
7
|
+
static args = {
|
|
8
|
+
ref: Args.string({ description: 'commit, branch, or <ref1>..<ref2> range' }),
|
|
9
|
+
};
|
|
10
|
+
static description = 'Show changes between commits, the index, or the working tree';
|
|
11
|
+
static examples = [
|
|
12
|
+
'<%= config.bin %> <%= command.id %>',
|
|
13
|
+
'<%= config.bin %> <%= command.id %> --staged',
|
|
14
|
+
'<%= config.bin %> <%= command.id %> HEAD~1',
|
|
15
|
+
'<%= config.bin %> <%= command.id %> main..feature/x',
|
|
16
|
+
'<%= config.bin %> <%= command.id %> main',
|
|
17
|
+
];
|
|
18
|
+
static flags = {
|
|
19
|
+
staged: Flags.boolean({ description: 'Show staged changes (HEAD vs index)' }),
|
|
20
|
+
};
|
|
21
|
+
async run() {
|
|
22
|
+
const { args, flags } = await this.parse(VcDiff);
|
|
23
|
+
let request;
|
|
24
|
+
try {
|
|
25
|
+
request = { mode: parseMode(args.ref, flags.staged) };
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
this.error(error instanceof Error ? error.message : String(error));
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const response = await withDaemonRetry((client) => client.requestWithAck(VcEvents.DIFFS, request));
|
|
32
|
+
const text = formatDiff(response);
|
|
33
|
+
if (text.length > 0)
|
|
34
|
+
process.stdout.write(text);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
this.error(formatConnectionError(error));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class VcRemoteRemove extends Command {
|
|
3
|
+
static args: {
|
|
4
|
+
name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
run(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Args, Command } from '@oclif/core';
|
|
2
|
+
import { VcEvents } from '../../../../shared/transport/events/vc-events.js';
|
|
3
|
+
import { formatConnectionError, withDaemonRetry } from '../../../lib/daemon-client.js';
|
|
4
|
+
export default class VcRemoteRemove extends Command {
|
|
5
|
+
static args = {
|
|
6
|
+
name: Args.string({ description: 'Remote name', required: true }),
|
|
7
|
+
};
|
|
8
|
+
static description = 'Remove a named remote';
|
|
9
|
+
static examples = [`<%= config.bin %> <%= command.id %> origin`];
|
|
10
|
+
async run() {
|
|
11
|
+
const { args } = await this.parse(VcRemoteRemove);
|
|
12
|
+
if (args.name !== 'origin') {
|
|
13
|
+
this.error(`Only 'origin' remote is currently supported.`);
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
await withDaemonRetry(async (client) => client.requestWithAck(VcEvents.REMOTE, { subcommand: 'remove' }));
|
|
17
|
+
this.log(`Remote 'origin' removed.`);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
this.error(formatConnectionError(error));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -85,6 +85,10 @@ export declare class BrvConfig {
|
|
|
85
85
|
* Serializes the config to JSON format
|
|
86
86
|
*/
|
|
87
87
|
toJson(): Record<string, unknown>;
|
|
88
|
+
/**
|
|
89
|
+
* Creates a new BrvConfig with space fields cleared, preserving all other fields.
|
|
90
|
+
*/
|
|
91
|
+
withoutSpace(): BrvConfig;
|
|
88
92
|
/**
|
|
89
93
|
* Creates a new BrvConfig with space fields replaced, preserving all other fields.
|
|
90
94
|
*/
|
|
@@ -170,6 +170,18 @@ export class BrvConfig {
|
|
|
170
170
|
version: this.version,
|
|
171
171
|
};
|
|
172
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Creates a new BrvConfig with space fields cleared, preserving all other fields.
|
|
175
|
+
*/
|
|
176
|
+
withoutSpace() {
|
|
177
|
+
return new BrvConfig({
|
|
178
|
+
...this,
|
|
179
|
+
spaceId: undefined,
|
|
180
|
+
spaceName: undefined,
|
|
181
|
+
teamId: undefined,
|
|
182
|
+
teamName: undefined,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
173
185
|
/**
|
|
174
186
|
* Creates a new BrvConfig with space fields replaced, preserving all other fields.
|
|
175
187
|
*/
|
|
@@ -26,7 +26,7 @@ export const PROVIDER_REGISTRY = {
|
|
|
26
26
|
byterover: {
|
|
27
27
|
baseUrl: '',
|
|
28
28
|
category: 'popular',
|
|
29
|
-
description: 'Built-in LLM,
|
|
29
|
+
description: 'Built-in LLM, ByteRover account required. Limited free usage.',
|
|
30
30
|
headers: {},
|
|
31
31
|
id: 'byterover',
|
|
32
32
|
modelsEndpoint: '',
|
|
@@ -89,7 +89,7 @@ export const PROVIDER_REGISTRY = {
|
|
|
89
89
|
apiKeyUrl: 'https://aistudio.google.com/apikey',
|
|
90
90
|
baseUrl: '',
|
|
91
91
|
category: 'popular',
|
|
92
|
-
defaultModel: 'gemini-
|
|
92
|
+
defaultModel: 'gemini-3-flash-preview',
|
|
93
93
|
description: 'Gemini models by Google',
|
|
94
94
|
envVars: ['GOOGLE_API_KEY', 'GEMINI_API_KEY'],
|
|
95
95
|
headers: {},
|
|
@@ -187,7 +187,7 @@ export const PROVIDER_REGISTRY = {
|
|
|
187
187
|
baseUrl: '',
|
|
188
188
|
category: 'other',
|
|
189
189
|
defaultModel: 'llama3',
|
|
190
|
-
description: '
|
|
190
|
+
description: 'OpenAI-compatible endpoint (Ollama, LM Studio, etc.)',
|
|
191
191
|
envVars: ['OPENAI_COMPATIBLE_API_KEY'],
|
|
192
192
|
headers: {},
|
|
193
193
|
id: 'openai-compatible',
|
|
@@ -166,10 +166,12 @@ export type CloneGitParams = BaseGitParams & {
|
|
|
166
166
|
};
|
|
167
167
|
/**
|
|
168
168
|
* Source of the blob content.
|
|
169
|
-
* - `'HEAD'` → blob at the HEAD commit
|
|
170
169
|
* - `'STAGE'` → blob in the git index (staging area)
|
|
170
|
+
* - `{commitish: string}` → blob at the resolved commit (branch name, tag, SHA, or `'HEAD'`)
|
|
171
171
|
*/
|
|
172
|
-
export type GitBlobRef = '
|
|
172
|
+
export type GitBlobRef = 'STAGE' | {
|
|
173
|
+
commitish: string;
|
|
174
|
+
};
|
|
173
175
|
export type GetBlobContentParams = BaseGitParams & {
|
|
174
176
|
path: string;
|
|
175
177
|
ref: GitBlobRef;
|
|
@@ -180,6 +182,30 @@ export type GetBlobContentsParams = BaseGitParams & {
|
|
|
180
182
|
};
|
|
181
183
|
/** Map of path → blob content (utf8). Missing entries indicate the blob is absent at that ref. */
|
|
182
184
|
export type BlobContents = Record<string, string | undefined>;
|
|
185
|
+
/**
|
|
186
|
+
* Source of the side being diffed. Beyond `GitBlobRef`, also supports `'WORKDIR'`
|
|
187
|
+
* (the working tree, used for unstaged and ref-vs-worktree comparisons).
|
|
188
|
+
*/
|
|
189
|
+
export type GitDiffSide = 'STAGE' | 'WORKDIR' | {
|
|
190
|
+
commitish: string;
|
|
191
|
+
};
|
|
192
|
+
export type ListChangedFilesParams = BaseGitParams & {
|
|
193
|
+
from: GitDiffSide;
|
|
194
|
+
to: GitDiffSide;
|
|
195
|
+
};
|
|
196
|
+
export type ChangedFile = {
|
|
197
|
+
path: string;
|
|
198
|
+
status: 'added' | 'deleted' | 'modified';
|
|
199
|
+
};
|
|
200
|
+
/** Content + short oid pair returned by {@link IGitService.getTextBlob}. */
|
|
201
|
+
export type TextBlob = {
|
|
202
|
+
/** True when the blob contains a NUL byte; `content` is then empty. */
|
|
203
|
+
binary?: boolean;
|
|
204
|
+
/** UTF-8 decoded blob content (empty string when binary). */
|
|
205
|
+
content: string;
|
|
206
|
+
/** 7-char short oid. */
|
|
207
|
+
oid: string;
|
|
208
|
+
};
|
|
183
209
|
export interface IGitService {
|
|
184
210
|
abortMerge(params: AbortMergeGitParams): Promise<void>;
|
|
185
211
|
add(params: AddGitParams): Promise<void>;
|
|
@@ -194,11 +220,11 @@ export interface IGitService {
|
|
|
194
220
|
getAheadBehind(params: GetAheadBehindParams): Promise<AheadBehind>;
|
|
195
221
|
/**
|
|
196
222
|
* Reads the content of a file blob at a given git ref.
|
|
197
|
-
* - `ref: 'HEAD'` → resolves HEAD commit, then reads the blob at `path`
|
|
198
223
|
* - `ref: 'STAGE'` → reads the blob staged in the index at `path`
|
|
224
|
+
* - `ref: {commitish}` → resolves the commit-ish ref (branch name, tag, SHA, or `'HEAD'`), then reads the blob at `path`
|
|
199
225
|
*
|
|
200
226
|
* Returns `undefined` when no blob exists at that ref (e.g. file not yet committed,
|
|
201
|
-
* or file not yet staged), or when
|
|
227
|
+
* or file not yet staged), or when the ref has no commits.
|
|
202
228
|
*/
|
|
203
229
|
getBlobContent(params: GetBlobContentParams): Promise<string | undefined>;
|
|
204
230
|
/**
|
|
@@ -227,8 +253,21 @@ export interface IGitService {
|
|
|
227
253
|
*/
|
|
228
254
|
getFilesWithConflictMarkers(params: BaseGitParams): Promise<string[]>;
|
|
229
255
|
getRemoteUrl(params: GetRemoteUrlGitParams): Promise<string | undefined>;
|
|
256
|
+
/**
|
|
257
|
+
* Reads a UTF-8 text blob together with its short oid in a single pass.
|
|
258
|
+
* Returns `undefined` when the blob is absent or detected as binary (contains a NUL byte).
|
|
259
|
+
* Used by diff producers to avoid the double-read pattern of calling `getBlobContent`
|
|
260
|
+
* and `git.hashBlob` separately.
|
|
261
|
+
*/
|
|
262
|
+
getTextBlob(params: GetBlobContentParams): Promise<TextBlob | undefined>;
|
|
230
263
|
/** Returns the upstream tracking branch config, or `undefined` if not configured. */
|
|
231
264
|
getTrackingBranch(params: GetTrackingBranchParams): Promise<TrackingBranch | undefined>;
|
|
265
|
+
/**
|
|
266
|
+
* Returns the 7-character short oid that git would assign to the given content,
|
|
267
|
+
* computed via `git.hashBlob`. Used to render the working-tree side of a
|
|
268
|
+
* `git diff`-style `index <oid>..<oid>` header (the working tree has no stored oid).
|
|
269
|
+
*/
|
|
270
|
+
hashBlob(content: Buffer): Promise<string>;
|
|
232
271
|
init(params: InitGitParams): Promise<void>;
|
|
233
272
|
/** Returns true if `ancestor` commit is reachable from `commit`. */
|
|
234
273
|
isAncestor(params: BaseGitParams & {
|
|
@@ -241,6 +280,18 @@ export interface IGitService {
|
|
|
241
280
|
isInitialized(params: BaseGitParams): Promise<boolean>;
|
|
242
281
|
/** Lists local branches. When `remote` is specified, also includes remote-tracking branches. */
|
|
243
282
|
listBranches(params: ListBranchesGitParams): Promise<GitBranch[]>;
|
|
283
|
+
/**
|
|
284
|
+
* Returns the set of files that differ between two diff sides, with their change status.
|
|
285
|
+
*
|
|
286
|
+
* Status mirrors `git diff` semantics:
|
|
287
|
+
* - `'added'` → present on `to` side, absent on `from` side
|
|
288
|
+
* - `'deleted'` → present on `from` side, absent on `to` side
|
|
289
|
+
* - `'modified'` → present on both, differs
|
|
290
|
+
*
|
|
291
|
+
* Untracked files (absent from both HEAD and STAGE) are excluded from the
|
|
292
|
+
* unstaged case (`from='STAGE', to='WORKDIR'`) to match `git diff` no-args behavior.
|
|
293
|
+
*/
|
|
294
|
+
listChangedFiles(params: ListChangedFilesParams): Promise<ChangedFile[]>;
|
|
244
295
|
listRemotes(params: BaseGitParams): Promise<GitRemote[]>;
|
|
245
296
|
log(params: LogGitParams): Promise<GitCommit[]>;
|
|
246
297
|
merge(params: MergeGitParams): Promise<MergeResult>;
|
|
@@ -44,7 +44,7 @@ export function generateSummaryContent(frontmatter, body) {
|
|
|
44
44
|
token_count: frontmatter.token_count,
|
|
45
45
|
type: 'summary',
|
|
46
46
|
};
|
|
47
|
-
const yamlContent = yamlDump(fm, { flowLevel: 1, lineWidth: -1, sortKeys:
|
|
47
|
+
const yamlContent = yamlDump(fm, { flowLevel: 1, lineWidth: -1, sortKeys: false }).trimEnd();
|
|
48
48
|
return `---\n${yamlContent}\n---\n${body}`;
|
|
49
49
|
}
|
|
50
50
|
// ---------------------------------------------------------------------------
|
|
@@ -79,7 +79,7 @@ export function generateArchiveStubContent(frontmatter, ghostCue) {
|
|
|
79
79
|
points_to: frontmatter.points_to,
|
|
80
80
|
type: 'archive_stub',
|
|
81
81
|
};
|
|
82
|
-
const yamlContent = yamlDump(fm, { flowLevel: 1, lineWidth: -1, sortKeys:
|
|
82
|
+
const yamlContent = yamlDump(fm, { flowLevel: 1, lineWidth: -1, sortKeys: false }).trimEnd();
|
|
83
83
|
return `---\n${yamlContent}\n---\n${ghostCue}`;
|
|
84
84
|
}
|
|
85
85
|
// ---------------------------------------------------------------------------
|
|
@@ -224,8 +224,9 @@ function addFrontmatterFields(content, fields) {
|
|
|
224
224
|
try {
|
|
225
225
|
const parsed = yamlLoad(yamlBlock);
|
|
226
226
|
if (parsed && typeof parsed === 'object') {
|
|
227
|
+
// Spread preserves existing key order; new fields are appended at end.
|
|
227
228
|
const merged = { ...parsed, ...fields };
|
|
228
|
-
const newYaml = yamlDump(merged, { flowLevel: 2, lineWidth: -1, sortKeys:
|
|
229
|
+
const newYaml = yamlDump(merged, { flowLevel: 2, lineWidth: -1, sortKeys: false }).trimEnd();
|
|
229
230
|
return `---\n${newYaml}\n---\n${body}`;
|
|
230
231
|
}
|
|
231
232
|
}
|
|
@@ -235,7 +236,7 @@ function addFrontmatterFields(content, fields) {
|
|
|
235
236
|
}
|
|
236
237
|
}
|
|
237
238
|
// No valid frontmatter — prepend
|
|
238
|
-
const yaml = yamlDump(fields, { flowLevel: 2, lineWidth: -1, sortKeys:
|
|
239
|
+
const yaml = yamlDump(fields, { flowLevel: 2, lineWidth: -1, sortKeys: false }).trimEnd();
|
|
239
240
|
return `---\n${yaml}\n---\n${content}`;
|
|
240
241
|
}
|
|
241
242
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
@@ -492,7 +493,7 @@ async function addRelatedLinks(filePath, relatedPaths) {
|
|
|
492
493
|
if (parsed && typeof parsed === 'object') {
|
|
493
494
|
const existing = Array.isArray(parsed.related) ? parsed.related : [];
|
|
494
495
|
parsed.related = [...new Set([...existing, ...relatedPaths])];
|
|
495
|
-
const newYaml = yamlDump(parsed, { flowLevel: 1, lineWidth: -1, sortKeys:
|
|
496
|
+
const newYaml = yamlDump(parsed, { flowLevel: 1, lineWidth: -1, sortKeys: false }).trimEnd();
|
|
496
497
|
await atomicWrite(filePath, `---\n${newYaml}\n---\n${body}`);
|
|
497
498
|
return;
|
|
498
499
|
}
|
|
@@ -503,7 +504,7 @@ async function addRelatedLinks(filePath, relatedPaths) {
|
|
|
503
504
|
}
|
|
504
505
|
}
|
|
505
506
|
// No existing frontmatter — add one with related field
|
|
506
|
-
const yaml = yamlDump({ related: relatedPaths }, { flowLevel: 1, lineWidth: -1, sortKeys:
|
|
507
|
+
const yaml = yamlDump({ related: relatedPaths }, { flowLevel: 1, lineWidth: -1, sortKeys: false }).trimEnd();
|
|
507
508
|
await atomicWrite(filePath, `---\n${yaml}\n---\n${content}`);
|
|
508
509
|
}
|
|
509
510
|
async function determineNeedsReview(actionType, files, opts) {
|
|
@@ -208,7 +208,7 @@ async function writeSynthesisFile(candidate, contextTreeDir, runtimeSignalStore,
|
|
|
208
208
|
type: 'synthesis',
|
|
209
209
|
};
|
|
210
210
|
/* eslint-enable camelcase */
|
|
211
|
-
const yaml = yamlDump(frontmatter, { lineWidth: -1, sortKeys:
|
|
211
|
+
const yaml = yamlDump(frontmatter, { lineWidth: -1, sortKeys: false }).trimEnd();
|
|
212
212
|
const body = [
|
|
213
213
|
`# ${candidate.title}`,
|
|
214
214
|
'',
|