byterover-cli 3.1.0 → 3.3.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 +17 -0
- package/dist/agent/infra/agent/agent-schemas.d.ts +8 -0
- package/dist/agent/infra/agent/agent-schemas.js +1 -0
- package/dist/agent/infra/sandbox/curate-service.js +14 -0
- package/dist/agent/infra/sandbox/sandbox-service.js +1 -0
- package/dist/agent/infra/sandbox/tools-sdk.d.ts +10 -0
- package/dist/agent/infra/sandbox/tools-sdk.js +9 -1
- package/dist/agent/infra/tools/implementations/search-knowledge-service.js +226 -103
- package/dist/agent/infra/tools/implementations/write-file-tool.d.ts +2 -1
- package/dist/agent/infra/tools/implementations/write-file-tool.js +16 -2
- package/dist/agent/infra/tools/tool-registry.js +1 -1
- package/dist/agent/infra/tools/write-guard.d.ts +11 -0
- package/dist/agent/infra/tools/write-guard.js +48 -0
- package/dist/agent/resources/prompts/system-prompt.yml +9 -0
- package/dist/agent/resources/tools/expand_knowledge.txt +4 -0
- package/dist/agent/resources/tools/search_knowledge.txt +11 -1
- package/dist/oclif/commands/curate/index.d.ts +1 -0
- package/dist/oclif/commands/curate/index.js +19 -4
- package/dist/oclif/commands/curate/view.js +2 -2
- package/dist/oclif/commands/main.js +13 -0
- package/dist/oclif/commands/query.d.ts +1 -0
- package/dist/oclif/commands/query.js +19 -4
- package/dist/oclif/commands/search.d.ts +20 -0
- package/dist/oclif/commands/search.js +186 -0
- package/dist/oclif/commands/source/add.d.ts +12 -0
- package/dist/oclif/commands/source/add.js +42 -0
- package/dist/oclif/commands/source/index.d.ts +6 -0
- package/dist/oclif/commands/source/index.js +8 -0
- package/dist/oclif/commands/source/list.d.ts +6 -0
- package/dist/oclif/commands/source/list.js +32 -0
- package/dist/oclif/commands/source/remove.d.ts +9 -0
- package/dist/oclif/commands/source/remove.js +33 -0
- package/dist/oclif/commands/status.d.ts +5 -1
- package/dist/oclif/commands/status.js +45 -6
- package/dist/oclif/commands/worktree/add.d.ts +12 -0
- package/dist/oclif/commands/worktree/add.js +44 -0
- package/dist/oclif/commands/worktree/index.d.ts +6 -0
- package/dist/oclif/commands/worktree/index.js +8 -0
- package/dist/oclif/commands/worktree/list.d.ts +6 -0
- package/dist/oclif/commands/worktree/list.js +28 -0
- package/dist/oclif/commands/worktree/remove.d.ts +9 -0
- package/dist/oclif/commands/worktree/remove.js +35 -0
- package/dist/oclif/hooks/init/validate-brv-config.js +4 -0
- package/dist/oclif/lib/daemon-client.d.ts +4 -2
- package/dist/oclif/lib/daemon-client.js +19 -4
- package/dist/oclif/lib/search-format.d.ts +10 -0
- package/dist/oclif/lib/search-format.js +25 -0
- package/dist/oclif/lib/task-client.d.ts +6 -0
- package/dist/oclif/lib/task-client.js +10 -3
- package/dist/server/constants.d.ts +7 -1
- package/dist/server/constants.js +10 -0
- package/dist/server/core/domain/client/client-info.d.ts +7 -0
- package/dist/server/core/domain/client/client-info.js +11 -0
- package/dist/server/core/domain/errors/task-error.d.ts +2 -2
- package/dist/server/core/domain/errors/task-error.js +5 -4
- package/dist/server/core/domain/project/worktrees-schema.d.ts +29 -0
- package/dist/server/core/domain/project/worktrees-schema.js +17 -0
- package/dist/server/core/domain/source/source-operations.d.ts +31 -0
- package/dist/server/core/domain/source/source-operations.js +201 -0
- package/dist/server/core/domain/source/source-schema.d.ts +94 -0
- package/dist/server/core/domain/source/source-schema.js +121 -0
- package/dist/server/core/domain/transport/schemas.d.ts +18 -10
- package/dist/server/core/domain/transport/schemas.js +7 -3
- package/dist/server/core/domain/transport/task-info.d.ts +2 -0
- package/dist/server/core/interfaces/client/i-client-manager.d.ts +13 -0
- package/dist/server/core/interfaces/executor/i-curate-executor.d.ts +4 -0
- package/dist/server/core/interfaces/executor/i-folder-pack-executor.d.ts +7 -3
- package/dist/server/core/interfaces/executor/i-query-executor.d.ts +2 -0
- package/dist/server/core/interfaces/executor/i-search-executor.d.ts +34 -0
- package/dist/server/core/interfaces/executor/i-search-executor.js +1 -0
- package/dist/server/core/interfaces/executor/index.d.ts +1 -0
- package/dist/server/core/interfaces/executor/index.js +1 -0
- package/dist/server/infra/client/client-manager.d.ts +1 -0
- package/dist/server/infra/client/client-manager.js +16 -0
- package/dist/server/infra/daemon/agent-process.js +35 -12
- package/dist/server/infra/executor/curate-executor.js +4 -2
- package/dist/server/infra/executor/direct-search-responder.js +5 -1
- package/dist/server/infra/executor/folder-pack-executor.js +23 -12
- package/dist/server/infra/executor/query-executor.d.ts +23 -0
- package/dist/server/infra/executor/query-executor.js +115 -21
- package/dist/server/infra/executor/search-executor.d.ts +17 -0
- package/dist/server/infra/executor/search-executor.js +30 -0
- package/dist/server/infra/mcp/mcp-mode-detector.d.ts +7 -5
- package/dist/server/infra/mcp/mcp-mode-detector.js +11 -18
- package/dist/server/infra/mcp/mcp-server.d.ts +1 -0
- package/dist/server/infra/mcp/mcp-server.js +11 -6
- package/dist/server/infra/mcp/tools/brv-curate-tool.d.ts +2 -1
- package/dist/server/infra/mcp/tools/brv-curate-tool.js +9 -16
- package/dist/server/infra/mcp/tools/brv-query-tool.d.ts +2 -1
- package/dist/server/infra/mcp/tools/brv-query-tool.js +9 -16
- package/dist/server/infra/mcp/tools/mcp-project-context.d.ts +11 -0
- package/dist/server/infra/mcp/tools/mcp-project-context.js +54 -0
- package/dist/server/infra/process/connection-coordinator.js +11 -0
- package/dist/server/infra/process/feature-handlers.js +4 -1
- package/dist/server/infra/process/task-router.d.ts +1 -0
- package/dist/server/infra/process/task-router.js +60 -5
- package/dist/server/infra/project/resolve-project.d.ts +106 -0
- package/dist/server/infra/project/resolve-project.js +473 -0
- package/dist/server/infra/transport/handlers/index.d.ts +4 -0
- package/dist/server/infra/transport/handlers/index.js +2 -0
- package/dist/server/infra/transport/handlers/pull-handler.js +3 -3
- package/dist/server/infra/transport/handlers/push-handler.js +3 -3
- package/dist/server/infra/transport/handlers/source-handler.d.ts +12 -0
- package/dist/server/infra/transport/handlers/source-handler.js +37 -0
- package/dist/server/infra/transport/handlers/status-handler.js +76 -27
- package/dist/server/infra/transport/handlers/worktree-handler.d.ts +12 -0
- package/dist/server/infra/transport/handlers/worktree-handler.js +67 -0
- package/dist/server/infra/transport/transport-connector.d.ts +10 -4
- package/dist/server/infra/transport/transport-connector.js +2 -2
- package/dist/server/templates/skill/SKILL.md +25 -5
- package/dist/server/utils/path-utils.d.ts +5 -0
- package/dist/server/utils/path-utils.js +11 -1
- package/dist/shared/transport/events/client-events.d.ts +3 -0
- package/dist/shared/transport/events/client-events.js +3 -0
- package/dist/shared/transport/events/index.d.ts +13 -0
- package/dist/shared/transport/events/index.js +9 -0
- package/dist/shared/transport/events/source-events.d.ts +30 -0
- package/dist/shared/transport/events/source-events.js +5 -0
- package/dist/shared/transport/events/status-events.d.ts +5 -0
- package/dist/shared/transport/events/task-events.d.ts +4 -1
- package/dist/shared/transport/events/worktree-events.d.ts +31 -0
- package/dist/shared/transport/events/worktree-events.js +5 -0
- package/dist/shared/transport/search-content.d.ts +28 -0
- package/dist/shared/transport/search-content.js +38 -0
- package/dist/shared/transport/types/dto.d.ts +20 -1
- package/dist/tui/features/commands/definitions/index.js +6 -0
- package/dist/tui/features/commands/definitions/source-add.d.ts +2 -0
- package/dist/tui/features/commands/definitions/source-add.js +48 -0
- package/dist/tui/features/commands/definitions/source-list.d.ts +2 -0
- package/dist/tui/features/commands/definitions/source-list.js +47 -0
- package/dist/tui/features/commands/definitions/source-remove.d.ts +2 -0
- package/dist/tui/features/commands/definitions/source-remove.js +38 -0
- package/dist/tui/features/commands/definitions/source.d.ts +2 -0
- package/dist/tui/features/commands/definitions/source.js +8 -0
- package/dist/tui/features/commands/definitions/worktree-add.d.ts +2 -0
- package/dist/tui/features/commands/definitions/worktree-add.js +35 -0
- package/dist/tui/features/commands/definitions/worktree-list.d.ts +2 -0
- package/dist/tui/features/commands/definitions/worktree-list.js +36 -0
- package/dist/tui/features/commands/definitions/worktree-remove.d.ts +2 -0
- package/dist/tui/features/commands/definitions/worktree-remove.js +33 -0
- package/dist/tui/features/commands/definitions/worktree.d.ts +2 -0
- package/dist/tui/features/commands/definitions/worktree.js +8 -0
- package/dist/tui/features/curate/api/create-curate-task.js +3 -1
- package/dist/tui/features/query/api/create-query-task.js +3 -1
- package/dist/tui/features/source/api/source-api.d.ts +4 -0
- package/dist/tui/features/source/api/source-api.js +22 -0
- package/dist/tui/features/status/api/get-status.js +2 -1
- package/dist/tui/features/status/utils/format-status.js +28 -1
- package/dist/tui/features/transport/components/transport-initializer.js +36 -1
- package/dist/tui/features/worktree/api/worktree-api.d.ts +4 -0
- package/dist/tui/features/worktree/api/worktree-api.js +22 -0
- package/dist/tui/repl-startup.d.ts +2 -0
- package/dist/tui/repl-startup.js +5 -3
- package/dist/tui/stores/transport-store.d.ts +6 -0
- package/dist/tui/stores/transport-store.js +6 -0
- package/dist/tui/utils/error-messages.js +2 -2
- package/oclif.manifest.json +380 -36
- package/package.json +1 -1
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
3
|
+
import { BRV_DIR, PROJECT_CONFIG_FILE, WORKTREE_LINK_METADATA, WORKTREES_DIR } from '../../constants.js';
|
|
4
|
+
import { WorktreeLinkMetadataSchema, WorktreePointerSchema } from '../../core/domain/project/worktrees-schema.js';
|
|
5
|
+
import { resolvePath } from '../../utils/path-utils.js';
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Errors
|
|
8
|
+
// ============================================================================
|
|
9
|
+
/**
|
|
10
|
+
* Error thrown when a .brv pointer file points to a project root that no longer has .brv/.
|
|
11
|
+
*/
|
|
12
|
+
export class BrokenWorktreePointerError extends Error {
|
|
13
|
+
pointerDir;
|
|
14
|
+
targetProjectRoot;
|
|
15
|
+
constructor(pointerDir, targetProjectRoot) {
|
|
16
|
+
super(`Worktree pointer broken: "${targetProjectRoot}" no longer has ${BRV_DIR}/${PROJECT_CONFIG_FILE}. ` +
|
|
17
|
+
`Run 'brv worktree remove' to remove the pointer.`);
|
|
18
|
+
this.pointerDir = pointerDir;
|
|
19
|
+
this.targetProjectRoot = targetProjectRoot;
|
|
20
|
+
this.name = 'BrokenWorktreePointerError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Error thrown when a .brv file (pointer) exists but contains malformed/invalid content.
|
|
25
|
+
*/
|
|
26
|
+
export class MalformedWorktreePointerError extends Error {
|
|
27
|
+
pointerDir;
|
|
28
|
+
reason;
|
|
29
|
+
constructor(pointerDir, reason) {
|
|
30
|
+
super(`Worktree pointer in "${pointerDir}" is malformed: ${reason}. ` +
|
|
31
|
+
`Fix the .brv file or run 'brv worktree remove' to remove it.`);
|
|
32
|
+
this.pointerDir = pointerDir;
|
|
33
|
+
this.reason = reason;
|
|
34
|
+
this.name = 'MalformedWorktreePointerError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Core helpers
|
|
39
|
+
// ============================================================================
|
|
40
|
+
export function hasBrvConfig(dir) {
|
|
41
|
+
return existsSync(join(dir, BRV_DIR, PROJECT_CONFIG_FILE));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Checks if a directory is a git root (.git directory or .git file for worktrees/submodules).
|
|
45
|
+
*/
|
|
46
|
+
export function isGitRoot(dir) {
|
|
47
|
+
const gitPath = join(dir, '.git');
|
|
48
|
+
try {
|
|
49
|
+
const stat = lstatSync(gitPath);
|
|
50
|
+
return stat.isDirectory() || stat.isFile();
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Checks if .brv in the given directory is a FILE (pointer), not a directory.
|
|
58
|
+
*/
|
|
59
|
+
export function isWorktreePointer(dir) {
|
|
60
|
+
const brvPath = join(dir, BRV_DIR);
|
|
61
|
+
try {
|
|
62
|
+
return lstatSync(brvPath).isFile();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Reads and validates a .brv pointer file.
|
|
70
|
+
* @throws MalformedWorktreePointerError if the file has invalid content
|
|
71
|
+
*/
|
|
72
|
+
export function readWorktreePointer(dir) {
|
|
73
|
+
const brvPath = join(dir, BRV_DIR);
|
|
74
|
+
let raw;
|
|
75
|
+
try {
|
|
76
|
+
raw = readFileSync(brvPath, 'utf8');
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
80
|
+
throw new MalformedWorktreePointerError(dir, `cannot read .brv file: ${message}`);
|
|
81
|
+
}
|
|
82
|
+
let json;
|
|
83
|
+
try {
|
|
84
|
+
json = JSON.parse(raw);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
throw new MalformedWorktreePointerError(dir, 'invalid JSON');
|
|
88
|
+
}
|
|
89
|
+
const result = WorktreePointerSchema.safeParse(json);
|
|
90
|
+
if (!result.success) {
|
|
91
|
+
throw new MalformedWorktreePointerError(dir, 'missing or invalid "projectRoot" field');
|
|
92
|
+
}
|
|
93
|
+
return result.data.projectRoot;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Canonical project resolver — single source of truth for discovering
|
|
97
|
+
* which .brv/ project a given working directory belongs to.
|
|
98
|
+
*
|
|
99
|
+
* Git-style resolution: walks up from cwd to find the nearest `.brv`
|
|
100
|
+
* (file or directory), just like git walks up to find `.git`.
|
|
101
|
+
*
|
|
102
|
+
* 1. --project-root flag
|
|
103
|
+
* 2. Walk up from cwd looking for .brv:
|
|
104
|
+
* a. .brv is a directory with config.json → source: 'direct'
|
|
105
|
+
* b. .brv is a file (pointer) → follow to parent → source: 'linked'
|
|
106
|
+
* 3. null (no .brv found at cwd or any ancestor)
|
|
107
|
+
*
|
|
108
|
+
* The walk-up stops at the **first** .brv found — it does NOT skip past
|
|
109
|
+
* a .brv to find a "better" one higher up. This prevents accidental
|
|
110
|
+
* inheritance from stale .brv/ directories in ancestor directories.
|
|
111
|
+
*
|
|
112
|
+
* @throws BrokenWorktreePointerError if .brv file points to a missing project
|
|
113
|
+
* @throws MalformedWorktreePointerError if .brv file has invalid content
|
|
114
|
+
*/
|
|
115
|
+
export function resolveProject(options) {
|
|
116
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
117
|
+
// Step 1: Explicit --project-root flag — fail loudly if invalid
|
|
118
|
+
if (options?.projectRootFlag) {
|
|
119
|
+
const flagRoot = resolve(options.projectRootFlag);
|
|
120
|
+
if (!hasBrvConfig(flagRoot)) {
|
|
121
|
+
throw new Error(`--project-root "${flagRoot}" is not a ByteRover project (no ${BRV_DIR}/${PROJECT_CONFIG_FILE}).`);
|
|
122
|
+
}
|
|
123
|
+
const canonical = resolvePath(flagRoot);
|
|
124
|
+
return {
|
|
125
|
+
projectRoot: canonical,
|
|
126
|
+
source: 'flag',
|
|
127
|
+
worktreeRoot: canonical,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
let startDir;
|
|
131
|
+
try {
|
|
132
|
+
startDir = resolvePath(cwd);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
// Step 2: Walk up from cwd looking for .brv (file or directory)
|
|
138
|
+
let current = startDir;
|
|
139
|
+
const root = resolve('/');
|
|
140
|
+
while (current !== root) {
|
|
141
|
+
const result = resolveAtDir(current);
|
|
142
|
+
if (result !== undefined) {
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
const parent = dirname(current);
|
|
146
|
+
if (parent === current)
|
|
147
|
+
break;
|
|
148
|
+
current = parent;
|
|
149
|
+
}
|
|
150
|
+
// Also check root
|
|
151
|
+
if (current !== startDir) {
|
|
152
|
+
const result = resolveAtDir(current);
|
|
153
|
+
if (result !== undefined) {
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Step 3: No .brv found anywhere
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Check a single directory for .brv and resolve it.
|
|
162
|
+
* Returns ProjectResolution, null (invalid .brv), or undefined (no .brv here, keep walking).
|
|
163
|
+
*/
|
|
164
|
+
function resolveAtDir(dir) {
|
|
165
|
+
const brvPath = join(dir, BRV_DIR);
|
|
166
|
+
let stat;
|
|
167
|
+
try {
|
|
168
|
+
stat = lstatSync(brvPath);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// .brv doesn't exist here — keep walking
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
// .brv is a directory → real project
|
|
175
|
+
if (stat.isDirectory()) {
|
|
176
|
+
if (existsSync(join(brvPath, PROJECT_CONFIG_FILE))) {
|
|
177
|
+
return {
|
|
178
|
+
projectRoot: dir,
|
|
179
|
+
source: 'direct',
|
|
180
|
+
worktreeRoot: dir,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
// .brv/ exists but no config.json — stop here (don't walk past it)
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
// .brv is a file → pointer to parent project
|
|
187
|
+
if (stat.isFile()) {
|
|
188
|
+
const targetRoot = readWorktreePointer(dir);
|
|
189
|
+
let canonicalTarget;
|
|
190
|
+
try {
|
|
191
|
+
canonicalTarget = resolvePath(targetRoot);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
throw new BrokenWorktreePointerError(dir, targetRoot);
|
|
195
|
+
}
|
|
196
|
+
if (!hasBrvConfig(canonicalTarget)) {
|
|
197
|
+
throw new BrokenWorktreePointerError(dir, canonicalTarget);
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
projectRoot: canonicalTarget,
|
|
201
|
+
source: 'linked',
|
|
202
|
+
worktreeRoot: dir,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
// .brv is something else — stop here
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
// ============================================================================
|
|
209
|
+
// Worktree CRUD operations
|
|
210
|
+
// ============================================================================
|
|
211
|
+
/**
|
|
212
|
+
* Sanitize a path into a safe directory name for the worktrees registry.
|
|
213
|
+
* Replaces path separators and special chars with dashes, then appends
|
|
214
|
+
* a numeric suffix if the name already exists in .brv/worktrees/.
|
|
215
|
+
*/
|
|
216
|
+
function sanitizeWorktreeName(worktreePath, projectRoot) {
|
|
217
|
+
// Try to use relative path for readability, fall back to basename
|
|
218
|
+
const name = worktreePath.startsWith(projectRoot + sep) || worktreePath.startsWith(projectRoot + '/') ? worktreePath.slice(projectRoot.length + 1) : basename(worktreePath);
|
|
219
|
+
const baseName = name.replaceAll(/[/\\]/g, '-').replaceAll(/[^a-zA-Z0-9._-]/g, '-');
|
|
220
|
+
// Check for collisions in existing registry entries
|
|
221
|
+
const worktreesDir = join(projectRoot, BRV_DIR, WORKTREES_DIR);
|
|
222
|
+
if (!existsSync(worktreesDir)) {
|
|
223
|
+
return baseName;
|
|
224
|
+
}
|
|
225
|
+
const existing = new Set();
|
|
226
|
+
try {
|
|
227
|
+
for (const entry of readdirSync(worktreesDir)) {
|
|
228
|
+
existing.add(entry);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return baseName;
|
|
233
|
+
}
|
|
234
|
+
if (!existing.has(baseName)) {
|
|
235
|
+
return baseName;
|
|
236
|
+
}
|
|
237
|
+
let counter = 2;
|
|
238
|
+
while (existing.has(`${baseName}-${counter}`)) {
|
|
239
|
+
counter++;
|
|
240
|
+
}
|
|
241
|
+
return `${baseName}-${counter}`;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Register a worktree: writes .brv pointer file in the target directory
|
|
245
|
+
* and creates a registry entry in the parent's .brv/worktrees/.
|
|
246
|
+
*
|
|
247
|
+
* If the target already has a .brv/ directory (e.g., auto-init'd), it is
|
|
248
|
+
* backed up to .brv-backup/ and replaced with a pointer file.
|
|
249
|
+
*/
|
|
250
|
+
export function addWorktree(projectRoot, worktreePath, options) {
|
|
251
|
+
// Validate parent has .brv/config.json
|
|
252
|
+
if (!hasBrvConfig(projectRoot)) {
|
|
253
|
+
return { message: `"${projectRoot}" is not a ByteRover project (no .brv/config.json).`, success: false };
|
|
254
|
+
}
|
|
255
|
+
// Validate target directory exists
|
|
256
|
+
if (!existsSync(worktreePath)) {
|
|
257
|
+
return { message: `Target directory does not exist: ${worktreePath}`, success: false };
|
|
258
|
+
}
|
|
259
|
+
// Cannot add self
|
|
260
|
+
if (worktreePath === projectRoot) {
|
|
261
|
+
return { message: 'Cannot add the project root as its own worktree.', success: false };
|
|
262
|
+
}
|
|
263
|
+
const targetBrvPath = join(worktreePath, BRV_DIR);
|
|
264
|
+
let backedUp = false;
|
|
265
|
+
try {
|
|
266
|
+
const stat = lstatSync(targetBrvPath);
|
|
267
|
+
if (stat.isFile()) {
|
|
268
|
+
// Already a pointer — check if it points to the same parent
|
|
269
|
+
try {
|
|
270
|
+
const existingTarget = readWorktreePointer(worktreePath);
|
|
271
|
+
const canonicalExisting = resolvePath(existingTarget);
|
|
272
|
+
const canonicalProject = resolvePath(projectRoot);
|
|
273
|
+
if (canonicalExisting === canonicalProject) {
|
|
274
|
+
return { message: `Already registered as worktree of "${projectRoot}".`, success: true };
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
message: `"${worktreePath}" is already a worktree of "${canonicalExisting}". Remove it first with 'brv worktree remove'.`,
|
|
278
|
+
success: false,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
// Malformed pointer — overwrite below
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (stat.isDirectory()) {
|
|
286
|
+
// Existing .brv/ directory — back up and replace
|
|
287
|
+
if (!options?.force) {
|
|
288
|
+
return {
|
|
289
|
+
message: `"${worktreePath}" has its own .brv/ project. Use --force to replace it with a worktree pointer. ` +
|
|
290
|
+
'The existing .brv/ will be moved to .brv-backup/.',
|
|
291
|
+
success: false,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
const backupPath = join(worktreePath, '.brv-backup');
|
|
295
|
+
if (existsSync(backupPath)) {
|
|
296
|
+
rmSync(backupPath, { force: true, recursive: true });
|
|
297
|
+
}
|
|
298
|
+
renameSync(targetBrvPath, backupPath);
|
|
299
|
+
backedUp = true;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
// .brv doesn't exist — proceed
|
|
304
|
+
}
|
|
305
|
+
// Write .brv pointer file
|
|
306
|
+
const pointerContent = JSON.stringify({ projectRoot: resolvePath(projectRoot) }, null, 2) + '\n';
|
|
307
|
+
writeFileSync(targetBrvPath, pointerContent, 'utf8');
|
|
308
|
+
// Create registry entry in parent
|
|
309
|
+
const name = sanitizeWorktreeName(worktreePath, projectRoot);
|
|
310
|
+
const worktreeDir = join(projectRoot, BRV_DIR, WORKTREES_DIR, name);
|
|
311
|
+
mkdirSync(worktreeDir, { recursive: true });
|
|
312
|
+
const metadata = {
|
|
313
|
+
addedAt: new Date().toISOString(),
|
|
314
|
+
worktreePath: resolvePath(worktreePath),
|
|
315
|
+
};
|
|
316
|
+
writeFileSync(join(worktreeDir, WORKTREE_LINK_METADATA), JSON.stringify(metadata, null, 2) + '\n', 'utf8');
|
|
317
|
+
const msg = backedUp
|
|
318
|
+
? `Added worktree "${worktreePath}" (existing .brv/ backed up to .brv-backup/).`
|
|
319
|
+
: `Added worktree "${worktreePath}".`;
|
|
320
|
+
return { backedUp, message: msg, success: true };
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Remove a worktree: deletes the .brv pointer file and cleans up the registry entry.
|
|
324
|
+
* If a .brv-backup/ exists, restores it.
|
|
325
|
+
*/
|
|
326
|
+
export function removeWorktree(worktreePath) {
|
|
327
|
+
const brvPath = join(worktreePath, BRV_DIR);
|
|
328
|
+
// Verify .brv is a pointer file
|
|
329
|
+
try {
|
|
330
|
+
const stat = lstatSync(brvPath);
|
|
331
|
+
if (!stat.isFile()) {
|
|
332
|
+
return { message: `"${worktreePath}" is not a worktree (has .brv/ directory, not pointer file).`, success: false };
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
return { message: `No .brv found in "${worktreePath}".`, success: false };
|
|
337
|
+
}
|
|
338
|
+
// Read pointer to find parent
|
|
339
|
+
let projectRoot;
|
|
340
|
+
try {
|
|
341
|
+
projectRoot = readWorktreePointer(worktreePath);
|
|
342
|
+
projectRoot = resolvePath(projectRoot);
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
// Can't read pointer — just delete the file
|
|
346
|
+
unlinkSync(brvPath);
|
|
347
|
+
return { message: `Removed worktree pointer (parent project unknown).`, success: true };
|
|
348
|
+
}
|
|
349
|
+
// Delete pointer file
|
|
350
|
+
unlinkSync(brvPath);
|
|
351
|
+
// Restore backup if exists
|
|
352
|
+
const backupPath = join(worktreePath, '.brv-backup');
|
|
353
|
+
if (existsSync(backupPath)) {
|
|
354
|
+
renameSync(backupPath, brvPath);
|
|
355
|
+
}
|
|
356
|
+
// Clean up registry entry in parent
|
|
357
|
+
const worktreesDir = join(projectRoot, BRV_DIR, WORKTREES_DIR);
|
|
358
|
+
if (existsSync(worktreesDir)) {
|
|
359
|
+
try {
|
|
360
|
+
const entries = readdirSync(worktreesDir, { withFileTypes: true });
|
|
361
|
+
const canonicalWorktree = resolvePath(worktreePath);
|
|
362
|
+
for (const entry of entries) {
|
|
363
|
+
if (!entry.isDirectory())
|
|
364
|
+
continue;
|
|
365
|
+
const metaPath = join(worktreesDir, entry.name, WORKTREE_LINK_METADATA);
|
|
366
|
+
if (!existsSync(metaPath))
|
|
367
|
+
continue;
|
|
368
|
+
try {
|
|
369
|
+
const meta = JSON.parse(readFileSync(metaPath, 'utf8'));
|
|
370
|
+
const parsed = WorktreeLinkMetadataSchema.safeParse(meta);
|
|
371
|
+
if (parsed.success && resolvePath(parsed.data.worktreePath) === canonicalWorktree) {
|
|
372
|
+
rmSync(join(worktreesDir, entry.name), { force: true, recursive: true });
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
// Malformed metadata — skip
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
// Registry cleanup is best-effort
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return { message: `Removed worktree "${worktreePath}".`, success: true };
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* List all registered worktrees for a project by scanning .brv/worktrees/ entries.
|
|
389
|
+
*/
|
|
390
|
+
export function listWorktrees(projectRoot) {
|
|
391
|
+
const worktreesDir = join(projectRoot, BRV_DIR, WORKTREES_DIR);
|
|
392
|
+
if (!existsSync(worktreesDir)) {
|
|
393
|
+
return [];
|
|
394
|
+
}
|
|
395
|
+
const result = [];
|
|
396
|
+
try {
|
|
397
|
+
const entries = readdirSync(worktreesDir, { withFileTypes: true });
|
|
398
|
+
for (const entry of entries) {
|
|
399
|
+
if (!entry.isDirectory())
|
|
400
|
+
continue;
|
|
401
|
+
const metaPath = join(worktreesDir, entry.name, WORKTREE_LINK_METADATA);
|
|
402
|
+
if (!existsSync(metaPath))
|
|
403
|
+
continue;
|
|
404
|
+
try {
|
|
405
|
+
const meta = JSON.parse(readFileSync(metaPath, 'utf8'));
|
|
406
|
+
const parsed = WorktreeLinkMetadataSchema.safeParse(meta);
|
|
407
|
+
if (parsed.success) {
|
|
408
|
+
result.push({
|
|
409
|
+
name: entry.name,
|
|
410
|
+
worktreePath: parsed.data.worktreePath,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
// Skip malformed entries
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
// Directory unreadable
|
|
421
|
+
}
|
|
422
|
+
return result;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Walk up from startDir looking for the nearest directory with .brv/ DIRECTORY
|
|
426
|
+
* (not a .brv file). Used only by `brv worktree add` auto-detect mode.
|
|
427
|
+
* NOT used by the resolver.
|
|
428
|
+
*/
|
|
429
|
+
export function findParentProject(startDir) {
|
|
430
|
+
let current;
|
|
431
|
+
try {
|
|
432
|
+
current = resolvePath(startDir);
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
return undefined;
|
|
436
|
+
}
|
|
437
|
+
// Skip cwd itself — we're looking for a PARENT
|
|
438
|
+
current = dirname(current);
|
|
439
|
+
const root = resolve('/');
|
|
440
|
+
while (current !== root) {
|
|
441
|
+
const brvPath = join(current, BRV_DIR);
|
|
442
|
+
try {
|
|
443
|
+
const stat = lstatSync(brvPath);
|
|
444
|
+
if (stat.isDirectory() && existsSync(join(brvPath, PROJECT_CONFIG_FILE))) {
|
|
445
|
+
return current;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
// .brv doesn't exist here
|
|
450
|
+
}
|
|
451
|
+
if (isGitRoot(current)) {
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
const parent = dirname(current);
|
|
455
|
+
if (parent === current)
|
|
456
|
+
break;
|
|
457
|
+
current = parent;
|
|
458
|
+
}
|
|
459
|
+
// Check the boundary directory itself
|
|
460
|
+
if (current !== startDir) {
|
|
461
|
+
const brvPath = join(current, BRV_DIR);
|
|
462
|
+
try {
|
|
463
|
+
const stat = lstatSync(brvPath);
|
|
464
|
+
if (stat.isDirectory() && existsSync(join(brvPath, PROJECT_CONFIG_FILE))) {
|
|
465
|
+
return current;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
// Not found
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return undefined;
|
|
473
|
+
}
|
|
@@ -24,9 +24,13 @@ export { ResetHandler } from './reset-handler.js';
|
|
|
24
24
|
export type { ResetHandlerDeps } from './reset-handler.js';
|
|
25
25
|
export { ReviewHandler } from './review-handler.js';
|
|
26
26
|
export type { ReviewHandlerDeps } from './review-handler.js';
|
|
27
|
+
export { SourceHandler } from './source-handler.js';
|
|
28
|
+
export type { SourceHandlerDeps } from './source-handler.js';
|
|
27
29
|
export { SpaceHandler } from './space-handler.js';
|
|
28
30
|
export type { SpaceHandlerDeps } from './space-handler.js';
|
|
29
31
|
export { StatusHandler } from './status-handler.js';
|
|
30
32
|
export type { StatusHandlerDeps } from './status-handler.js';
|
|
31
33
|
export { VcHandler } from './vc-handler.js';
|
|
32
34
|
export type { IVcHandlerDeps } from './vc-handler.js';
|
|
35
|
+
export { WorktreeHandler } from './worktree-handler.js';
|
|
36
|
+
export type { WorktreeHandlerDeps } from './worktree-handler.js';
|
|
@@ -11,6 +11,8 @@ export { PullHandler } from './pull-handler.js';
|
|
|
11
11
|
export { PushHandler } from './push-handler.js';
|
|
12
12
|
export { ResetHandler } from './reset-handler.js';
|
|
13
13
|
export { ReviewHandler } from './review-handler.js';
|
|
14
|
+
export { SourceHandler } from './source-handler.js';
|
|
14
15
|
export { SpaceHandler } from './space-handler.js';
|
|
15
16
|
export { StatusHandler } from './status-handler.js';
|
|
16
17
|
export { VcHandler } from './vc-handler.js';
|
|
18
|
+
export { WorktreeHandler } from './worktree-handler.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PullEvents, } from '../../../../shared/transport/events/pull-events.js';
|
|
2
|
-
import { LocalChangesExistError, NotAuthenticatedError, ProjectNotInitError,
|
|
2
|
+
import { LegacySyncUnavailableError, LocalChangesExistError, NotAuthenticatedError, ProjectNotInitError, } from '../../../core/domain/errors/task-error.js';
|
|
3
3
|
import { guardAgainstGitVc, hasAnyChanges, resolveRequiredProjectPath, } from './handler-types.js';
|
|
4
4
|
/**
|
|
5
5
|
* Handles pull:* events.
|
|
@@ -42,7 +42,7 @@ export class PullHandler {
|
|
|
42
42
|
throw new ProjectNotInitError();
|
|
43
43
|
}
|
|
44
44
|
if (!config.teamId || !config.spaceId) {
|
|
45
|
-
throw new
|
|
45
|
+
throw new LegacySyncUnavailableError();
|
|
46
46
|
}
|
|
47
47
|
// Check for local changes that would be overwritten
|
|
48
48
|
const changes = await this.contextTreeSnapshotService.getChanges(projectPath);
|
|
@@ -79,7 +79,7 @@ export class PullHandler {
|
|
|
79
79
|
throw new ProjectNotInitError();
|
|
80
80
|
}
|
|
81
81
|
if (!config.teamId || !config.spaceId) {
|
|
82
|
-
throw new
|
|
82
|
+
throw new LegacySyncUnavailableError();
|
|
83
83
|
}
|
|
84
84
|
const changes = await this.contextTreeSnapshotService.getChanges(projectPath);
|
|
85
85
|
const hasLocalChanges = hasAnyChanges(changes);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join, relative } from 'node:path';
|
|
2
2
|
import { PushEvents, } from '../../../../shared/transport/events/push-events.js';
|
|
3
|
-
import { NotAuthenticatedError, ProjectNotInitError,
|
|
3
|
+
import { LegacySyncUnavailableError, NotAuthenticatedError, ProjectNotInitError, } from '../../../core/domain/errors/task-error.js';
|
|
4
4
|
import { mapToPushContexts } from '../../cogit/context-tree-to-push-context-mapper.js';
|
|
5
5
|
import { guardAgainstGitVc, resolveRequiredProjectPath, } from './handler-types.js';
|
|
6
6
|
/** Path prefix of the context tree relative to the project root. */
|
|
@@ -88,7 +88,7 @@ export class PushHandler {
|
|
|
88
88
|
throw new ProjectNotInitError();
|
|
89
89
|
}
|
|
90
90
|
if (!config.teamId || !config.spaceId) {
|
|
91
|
-
throw new
|
|
91
|
+
throw new LegacySyncUnavailableError();
|
|
92
92
|
}
|
|
93
93
|
this.broadcastToProject(projectPath, PushEvents.PROGRESS, { message: 'Reading context files...', step: 'reading' });
|
|
94
94
|
const changes = await this.contextTreeSnapshotService.getChanges(projectPath);
|
|
@@ -163,7 +163,7 @@ export class PushHandler {
|
|
|
163
163
|
throw new ProjectNotInitError();
|
|
164
164
|
}
|
|
165
165
|
if (!config.teamId || !config.spaceId) {
|
|
166
|
-
throw new
|
|
166
|
+
throw new LegacySyncUnavailableError();
|
|
167
167
|
}
|
|
168
168
|
const hasSnapshot = await this.contextTreeSnapshotService.hasSnapshot(projectPath);
|
|
169
169
|
if (!hasSnapshot) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ITransportServer } from '../../../core/interfaces/transport/i-transport-server.js';
|
|
2
|
+
import { type ProjectPathResolver } from './handler-types.js';
|
|
3
|
+
export interface SourceHandlerDeps {
|
|
4
|
+
resolveProjectPath: ProjectPathResolver;
|
|
5
|
+
transport: ITransportServer;
|
|
6
|
+
}
|
|
7
|
+
export declare class SourceHandler {
|
|
8
|
+
private readonly resolveProjectPath;
|
|
9
|
+
private readonly transport;
|
|
10
|
+
constructor(deps: SourceHandlerDeps);
|
|
11
|
+
setup(): void;
|
|
12
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { SourceEvents, } from '../../../../shared/transport/events/source-events.js';
|
|
2
|
+
import { addSource, listSourceStatuses, removeSource } from '../../../core/domain/source/source-operations.js';
|
|
3
|
+
import { resolveRequiredProjectPath } from './handler-types.js';
|
|
4
|
+
export class SourceHandler {
|
|
5
|
+
resolveProjectPath;
|
|
6
|
+
transport;
|
|
7
|
+
constructor(deps) {
|
|
8
|
+
this.resolveProjectPath = deps.resolveProjectPath;
|
|
9
|
+
this.transport = deps.transport;
|
|
10
|
+
}
|
|
11
|
+
setup() {
|
|
12
|
+
this.transport.onRequest(SourceEvents.ADD, async (data, clientId) => {
|
|
13
|
+
const projectPath = resolveRequiredProjectPath(this.resolveProjectPath, clientId);
|
|
14
|
+
const result = addSource(projectPath, data.targetPath, data.alias);
|
|
15
|
+
return {
|
|
16
|
+
message: result.message,
|
|
17
|
+
success: result.success,
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
this.transport.onRequest(SourceEvents.REMOVE, async (data, clientId) => {
|
|
21
|
+
const projectPath = resolveRequiredProjectPath(this.resolveProjectPath, clientId);
|
|
22
|
+
const result = removeSource(projectPath, data.aliasOrPath);
|
|
23
|
+
return {
|
|
24
|
+
message: result.message,
|
|
25
|
+
success: result.success,
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
this.transport.onRequest(SourceEvents.LIST, async (_data, clientId) => {
|
|
29
|
+
const projectPath = resolveRequiredProjectPath(this.resolveProjectPath, clientId);
|
|
30
|
+
const result = listSourceStatuses(projectPath);
|
|
31
|
+
return {
|
|
32
|
+
error: result.error,
|
|
33
|
+
statuses: result.statuses,
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|