byterover-cli 3.0.1 → 3.2.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.
Files changed (196) hide show
  1. package/.env.production +4 -0
  2. package/README.md +17 -0
  3. package/dist/agent/core/domain/tools/constants.d.ts +1 -0
  4. package/dist/agent/core/domain/tools/constants.js +1 -0
  5. package/dist/agent/core/interfaces/cipher-services.d.ts +8 -0
  6. package/dist/agent/core/interfaces/i-cipher-agent.d.ts +1 -0
  7. package/dist/agent/infra/agent/agent-error-codes.d.ts +0 -1
  8. package/dist/agent/infra/agent/agent-error-codes.js +0 -1
  9. package/dist/agent/infra/agent/agent-error.d.ts +0 -1
  10. package/dist/agent/infra/agent/agent-error.js +0 -1
  11. package/dist/agent/infra/agent/agent-schemas.d.ts +8 -0
  12. package/dist/agent/infra/agent/agent-schemas.js +1 -0
  13. package/dist/agent/infra/agent/agent-state-manager.d.ts +1 -3
  14. package/dist/agent/infra/agent/agent-state-manager.js +1 -3
  15. package/dist/agent/infra/agent/base-agent.d.ts +1 -1
  16. package/dist/agent/infra/agent/base-agent.js +1 -1
  17. package/dist/agent/infra/agent/cipher-agent.d.ts +15 -1
  18. package/dist/agent/infra/agent/cipher-agent.js +188 -3
  19. package/dist/agent/infra/agent/index.d.ts +1 -1
  20. package/dist/agent/infra/agent/index.js +1 -1
  21. package/dist/agent/infra/agent/service-initializer.d.ts +3 -3
  22. package/dist/agent/infra/agent/service-initializer.js +14 -8
  23. package/dist/agent/infra/agent/types.d.ts +0 -1
  24. package/dist/agent/infra/file-system/file-system-service.js +6 -5
  25. package/dist/agent/infra/folder-pack/folder-pack-service.d.ts +1 -0
  26. package/dist/agent/infra/folder-pack/folder-pack-service.js +29 -15
  27. package/dist/agent/infra/llm/providers/openai.js +12 -0
  28. package/dist/agent/infra/llm/stream-to-text.d.ts +7 -0
  29. package/dist/agent/infra/llm/stream-to-text.js +14 -0
  30. package/dist/agent/infra/map/abstract-generator.d.ts +22 -0
  31. package/dist/agent/infra/map/abstract-generator.js +67 -0
  32. package/dist/agent/infra/map/abstract-queue.d.ts +67 -0
  33. package/dist/agent/infra/map/abstract-queue.js +218 -0
  34. package/dist/agent/infra/memory/memory-deduplicator.d.ts +44 -0
  35. package/dist/agent/infra/memory/memory-deduplicator.js +88 -0
  36. package/dist/agent/infra/memory/memory-manager.d.ts +1 -0
  37. package/dist/agent/infra/memory/memory-manager.js +6 -5
  38. package/dist/agent/infra/sandbox/curate-service.d.ts +4 -2
  39. package/dist/agent/infra/sandbox/curate-service.js +20 -7
  40. package/dist/agent/infra/sandbox/local-sandbox.d.ts +5 -0
  41. package/dist/agent/infra/sandbox/local-sandbox.js +57 -1
  42. package/dist/agent/infra/sandbox/sandbox-service.js +1 -0
  43. package/dist/agent/infra/sandbox/tools-sdk.d.ts +13 -1
  44. package/dist/agent/infra/sandbox/tools-sdk.js +9 -1
  45. package/dist/agent/infra/session/session-compressor.d.ts +43 -0
  46. package/dist/agent/infra/session/session-compressor.js +296 -0
  47. package/dist/agent/infra/session/session-manager.d.ts +7 -0
  48. package/dist/agent/infra/session/session-manager.js +9 -0
  49. package/dist/agent/infra/tools/implementations/curate-tool.d.ts +3 -2
  50. package/dist/agent/infra/tools/implementations/curate-tool.js +54 -27
  51. package/dist/agent/infra/tools/implementations/expand-knowledge-tool.d.ts +3 -3
  52. package/dist/agent/infra/tools/implementations/expand-knowledge-tool.js +34 -7
  53. package/dist/agent/infra/tools/implementations/ingest-resource-tool.d.ts +17 -0
  54. package/dist/agent/infra/tools/implementations/ingest-resource-tool.js +224 -0
  55. package/dist/agent/infra/tools/implementations/memory-symbol-tree.d.ts +8 -0
  56. package/dist/agent/infra/tools/implementations/search-knowledge-service.d.ts +1 -1
  57. package/dist/agent/infra/tools/implementations/search-knowledge-service.js +392 -106
  58. package/dist/agent/infra/tools/implementations/search-knowledge-tool.js +2 -2
  59. package/dist/agent/infra/tools/implementations/write-file-tool.d.ts +2 -1
  60. package/dist/agent/infra/tools/implementations/write-file-tool.js +16 -2
  61. package/dist/agent/infra/tools/tool-provider.js +1 -0
  62. package/dist/agent/infra/tools/tool-registry.d.ts +3 -0
  63. package/dist/agent/infra/tools/tool-registry.js +16 -5
  64. package/dist/agent/infra/tools/write-guard.d.ts +11 -0
  65. package/dist/agent/infra/tools/write-guard.js +48 -0
  66. package/dist/agent/resources/prompts/system-prompt.yml +9 -0
  67. package/dist/agent/resources/tools/expand_knowledge.txt +4 -0
  68. package/dist/agent/resources/tools/search_knowledge.txt +11 -1
  69. package/dist/oclif/commands/curate/index.js +4 -3
  70. package/dist/oclif/commands/curate/view.js +2 -2
  71. package/dist/oclif/commands/main.js +13 -0
  72. package/dist/oclif/commands/query.js +4 -3
  73. package/dist/oclif/commands/source/add.d.ts +12 -0
  74. package/dist/oclif/commands/source/add.js +42 -0
  75. package/dist/oclif/commands/source/index.d.ts +6 -0
  76. package/dist/oclif/commands/source/index.js +8 -0
  77. package/dist/oclif/commands/source/list.d.ts +6 -0
  78. package/dist/oclif/commands/source/list.js +32 -0
  79. package/dist/oclif/commands/source/remove.d.ts +9 -0
  80. package/dist/oclif/commands/source/remove.js +33 -0
  81. package/dist/oclif/commands/status.d.ts +5 -1
  82. package/dist/oclif/commands/status.js +41 -6
  83. package/dist/oclif/commands/worktree/add.d.ts +12 -0
  84. package/dist/oclif/commands/worktree/add.js +44 -0
  85. package/dist/oclif/commands/worktree/index.d.ts +6 -0
  86. package/dist/oclif/commands/worktree/index.js +8 -0
  87. package/dist/oclif/commands/worktree/list.d.ts +6 -0
  88. package/dist/oclif/commands/worktree/list.js +28 -0
  89. package/dist/oclif/commands/worktree/remove.d.ts +9 -0
  90. package/dist/oclif/commands/worktree/remove.js +35 -0
  91. package/dist/oclif/hooks/init/validate-brv-config.js +4 -0
  92. package/dist/oclif/lib/daemon-client.d.ts +4 -2
  93. package/dist/oclif/lib/daemon-client.js +19 -3
  94. package/dist/server/constants.d.ts +8 -0
  95. package/dist/server/constants.js +10 -0
  96. package/dist/server/core/domain/client/client-info.d.ts +7 -0
  97. package/dist/server/core/domain/client/client-info.js +11 -0
  98. package/dist/server/core/domain/knowledge/memory-scoring.d.ts +3 -3
  99. package/dist/server/core/domain/knowledge/memory-scoring.js +5 -5
  100. package/dist/server/core/domain/knowledge/summary-types.d.ts +4 -0
  101. package/dist/server/core/domain/project/worktrees-schema.d.ts +29 -0
  102. package/dist/server/core/domain/project/worktrees-schema.js +17 -0
  103. package/dist/server/core/domain/source/source-operations.d.ts +31 -0
  104. package/dist/server/core/domain/source/source-operations.js +201 -0
  105. package/dist/server/core/domain/source/source-schema.d.ts +94 -0
  106. package/dist/server/core/domain/source/source-schema.js +121 -0
  107. package/dist/server/core/domain/transport/schemas.d.ts +18 -10
  108. package/dist/server/core/domain/transport/schemas.js +4 -0
  109. package/dist/server/core/domain/transport/task-info.d.ts +2 -0
  110. package/dist/server/core/interfaces/client/i-client-manager.d.ts +13 -0
  111. package/dist/server/core/interfaces/executor/i-curate-executor.d.ts +4 -0
  112. package/dist/server/core/interfaces/executor/i-folder-pack-executor.d.ts +7 -3
  113. package/dist/server/core/interfaces/executor/i-query-executor.d.ts +2 -0
  114. package/dist/server/infra/client/client-manager.d.ts +1 -0
  115. package/dist/server/infra/client/client-manager.js +16 -0
  116. package/dist/server/infra/context-tree/derived-artifact.js +5 -1
  117. package/dist/server/infra/context-tree/file-context-tree-manifest-service.d.ts +2 -1
  118. package/dist/server/infra/context-tree/file-context-tree-manifest-service.js +43 -7
  119. package/dist/server/infra/context-tree/file-context-tree-summary-service.js +20 -2
  120. package/dist/server/infra/daemon/agent-process.js +15 -5
  121. package/dist/server/infra/executor/curate-executor.js +6 -3
  122. package/dist/server/infra/executor/direct-search-responder.js +5 -1
  123. package/dist/server/infra/executor/folder-pack-executor.js +88 -7
  124. package/dist/server/infra/executor/query-executor.d.ts +23 -0
  125. package/dist/server/infra/executor/query-executor.js +125 -23
  126. package/dist/server/infra/mcp/mcp-mode-detector.d.ts +7 -5
  127. package/dist/server/infra/mcp/mcp-mode-detector.js +11 -18
  128. package/dist/server/infra/mcp/mcp-server.d.ts +1 -0
  129. package/dist/server/infra/mcp/mcp-server.js +11 -6
  130. package/dist/server/infra/mcp/tools/brv-curate-tool.d.ts +2 -1
  131. package/dist/server/infra/mcp/tools/brv-curate-tool.js +9 -16
  132. package/dist/server/infra/mcp/tools/brv-query-tool.d.ts +2 -1
  133. package/dist/server/infra/mcp/tools/brv-query-tool.js +9 -16
  134. package/dist/server/infra/mcp/tools/mcp-project-context.d.ts +11 -0
  135. package/dist/server/infra/mcp/tools/mcp-project-context.js +54 -0
  136. package/dist/server/infra/process/connection-coordinator.js +11 -0
  137. package/dist/server/infra/process/feature-handlers.js +4 -1
  138. package/dist/server/infra/process/task-router.d.ts +1 -0
  139. package/dist/server/infra/process/task-router.js +60 -5
  140. package/dist/server/infra/project/resolve-project.d.ts +106 -0
  141. package/dist/server/infra/project/resolve-project.js +473 -0
  142. package/dist/server/infra/transport/handlers/index.d.ts +4 -0
  143. package/dist/server/infra/transport/handlers/index.js +2 -0
  144. package/dist/server/infra/transport/handlers/source-handler.d.ts +12 -0
  145. package/dist/server/infra/transport/handlers/source-handler.js +37 -0
  146. package/dist/server/infra/transport/handlers/status-handler.js +65 -13
  147. package/dist/server/infra/transport/handlers/worktree-handler.d.ts +12 -0
  148. package/dist/server/infra/transport/handlers/worktree-handler.js +67 -0
  149. package/dist/server/infra/transport/transport-connector.d.ts +10 -4
  150. package/dist/server/infra/transport/transport-connector.js +2 -2
  151. package/dist/server/utils/curate-result-parser.d.ts +4 -4
  152. package/dist/server/utils/path-utils.d.ts +5 -0
  153. package/dist/server/utils/path-utils.js +11 -1
  154. package/dist/shared/transport/events/client-events.d.ts +3 -0
  155. package/dist/shared/transport/events/client-events.js +3 -0
  156. package/dist/shared/transport/events/index.d.ts +13 -0
  157. package/dist/shared/transport/events/index.js +9 -0
  158. package/dist/shared/transport/events/source-events.d.ts +30 -0
  159. package/dist/shared/transport/events/source-events.js +5 -0
  160. package/dist/shared/transport/events/status-events.d.ts +5 -0
  161. package/dist/shared/transport/events/task-events.d.ts +4 -1
  162. package/dist/shared/transport/events/worktree-events.d.ts +31 -0
  163. package/dist/shared/transport/events/worktree-events.js +5 -0
  164. package/dist/shared/transport/types/dto.d.ts +26 -0
  165. package/dist/tui/features/commands/definitions/index.js +6 -0
  166. package/dist/tui/features/commands/definitions/source-add.d.ts +2 -0
  167. package/dist/tui/features/commands/definitions/source-add.js +48 -0
  168. package/dist/tui/features/commands/definitions/source-list.d.ts +2 -0
  169. package/dist/tui/features/commands/definitions/source-list.js +47 -0
  170. package/dist/tui/features/commands/definitions/source-remove.d.ts +2 -0
  171. package/dist/tui/features/commands/definitions/source-remove.js +38 -0
  172. package/dist/tui/features/commands/definitions/source.d.ts +2 -0
  173. package/dist/tui/features/commands/definitions/source.js +8 -0
  174. package/dist/tui/features/commands/definitions/worktree-add.d.ts +2 -0
  175. package/dist/tui/features/commands/definitions/worktree-add.js +35 -0
  176. package/dist/tui/features/commands/definitions/worktree-list.d.ts +2 -0
  177. package/dist/tui/features/commands/definitions/worktree-list.js +36 -0
  178. package/dist/tui/features/commands/definitions/worktree-remove.d.ts +2 -0
  179. package/dist/tui/features/commands/definitions/worktree-remove.js +33 -0
  180. package/dist/tui/features/commands/definitions/worktree.d.ts +2 -0
  181. package/dist/tui/features/commands/definitions/worktree.js +8 -0
  182. package/dist/tui/features/curate/api/create-curate-task.js +3 -1
  183. package/dist/tui/features/query/api/create-query-task.js +3 -1
  184. package/dist/tui/features/source/api/source-api.d.ts +4 -0
  185. package/dist/tui/features/source/api/source-api.js +22 -0
  186. package/dist/tui/features/status/api/get-status.js +2 -1
  187. package/dist/tui/features/status/utils/format-status.js +23 -1
  188. package/dist/tui/features/transport/components/transport-initializer.js +36 -1
  189. package/dist/tui/features/worktree/api/worktree-api.d.ts +4 -0
  190. package/dist/tui/features/worktree/api/worktree-api.js +22 -0
  191. package/dist/tui/repl-startup.d.ts +2 -0
  192. package/dist/tui/repl-startup.js +5 -3
  193. package/dist/tui/stores/transport-store.d.ts +6 -0
  194. package/dist/tui/stores/transport-store.js +6 -0
  195. package/oclif.manifest.json +261 -1
  196. package/package.json +10 -4
@@ -124,6 +124,7 @@ export declare class TaskRouter {
124
124
  */
125
125
  private notifyHooksError;
126
126
  private registerLlmEvent;
127
+ private resolveTaskContext;
127
128
  /**
128
129
  * Generic handler for routing LLM events from Agent to clients.
129
130
  * Checks both active and recently completed tasks (within grace period).
@@ -16,8 +16,10 @@
16
16
  */
17
17
  import { AgentNotAvailableError, serializeTaskError } from '../../core/domain/errors/task-error.js';
18
18
  import { LlmEventNames, TransportLlmEventList, TransportTaskEventNames } from '../../core/domain/transport/schemas.js';
19
+ import { isDescendantOf } from '../../utils/path-utils.js';
19
20
  import { transportLog } from '../../utils/process-logger.js';
20
21
  import { isValidTaskType } from '../../utils/type-guards.js';
22
+ import { resolveProject } from '../project/resolve-project.js';
21
23
  import { broadcastToProjectRoom } from './broadcast-utils.js';
22
24
  /**
23
25
  * Grace period (in ms) to keep completed tasks in memory for late-arriving events.
@@ -245,11 +247,27 @@ export class TaskRouter {
245
247
  broadcastToProjectRoom(this.projectRegistry, this.projectRouter, projectPath, TransportTaskEventNames.ERROR, { error, taskId }, clientId);
246
248
  return { taskId };
247
249
  }
248
- // ── Resolve projectPath & store task synchronously ────────────────────────
249
- // Resolve projectPath: explicit field > client's registered projectPath > clientCwd.
250
- // Client's registered projectPath is the walked-up project root (where .brv/ lives),
251
- // while clientCwd is the raw working directory (may be a subdirectory).
252
- const projectPath = data.projectPath ?? this.resolveClientProjectPath?.(clientId) ?? data.clientCwd;
250
+ // ── Resolve projectPath & worktreeRoot, store task synchronously ─────────
251
+ let projectPath;
252
+ let worktreeRoot;
253
+ try {
254
+ const taskContext = this.resolveTaskContext(data, clientId);
255
+ if (taskContext.error) {
256
+ const error = serializeTaskError(new Error(taskContext.error));
257
+ this.transport.sendTo(clientId, TransportTaskEventNames.ERROR, { error, taskId });
258
+ broadcastToProjectRoom(this.projectRegistry, this.projectRouter, taskContext.projectPath, TransportTaskEventNames.ERROR, { error, taskId }, clientId);
259
+ return { taskId };
260
+ }
261
+ projectPath = taskContext.projectPath;
262
+ worktreeRoot = taskContext.worktreeRoot;
263
+ }
264
+ catch (error_) {
265
+ const error = serializeTaskError(error_ instanceof Error ? error_ : new Error(String(error_)));
266
+ const fallbackProjectPath = data.projectPath ?? this.resolveClientProjectPath?.(clientId) ?? data.clientCwd;
267
+ this.transport.sendTo(clientId, TransportTaskEventNames.ERROR, { error, taskId });
268
+ broadcastToProjectRoom(this.projectRegistry, this.projectRouter, fallbackProjectPath, TransportTaskEventNames.ERROR, { error, taskId }, clientId);
269
+ return { taskId };
270
+ }
253
271
  transportLog(`Task accepted: ${taskId} (type=${data.type}, client=${clientId})`);
254
272
  this.tasks.set(taskId, {
255
273
  clientId,
@@ -261,6 +279,7 @@ export class TaskRouter {
261
279
  ...(projectPath ? { projectPath } : {}),
262
280
  taskId,
263
281
  type: data.type,
282
+ ...(worktreeRoot ? { worktreeRoot } : {}),
264
283
  });
265
284
  // ── Send task:created synchronously (before any await) ────────────────────
266
285
  const createdPayload = {
@@ -294,6 +313,7 @@ export class TaskRouter {
294
313
  ...(projectPath ? { projectPath } : {}),
295
314
  taskId,
296
315
  type: data.type,
316
+ ...(worktreeRoot ? { worktreeRoot } : {}),
297
317
  };
298
318
  // eslint-disable-next-line no-void
299
319
  void this.agentPool
@@ -467,6 +487,41 @@ export class TaskRouter {
467
487
  this.routeLlmEvent(eventName, data);
468
488
  });
469
489
  }
490
+ resolveTaskContext(data, clientId) {
491
+ // When both projectPath and worktreeRoot are explicitly provided,
492
+ // skip the resolver entirely — a broken link under clientCwd must not
493
+ // reject an otherwise valid explicit payload.
494
+ if (data.projectPath && data.worktreeRoot) {
495
+ if (!isDescendantOf(data.worktreeRoot, data.projectPath)) {
496
+ return {
497
+ error: `worktreeRoot "${data.worktreeRoot}" must be equal to or within projectPath "${data.projectPath}".`,
498
+ projectPath: data.projectPath,
499
+ };
500
+ }
501
+ return { projectPath: data.projectPath, worktreeRoot: data.worktreeRoot };
502
+ }
503
+ // Resolve from clientCwd (fresh, workspace-link-aware) when needed.
504
+ let resolvedProjectPath;
505
+ let resolvedWorkspaceRoot;
506
+ if (data.clientCwd) {
507
+ const resolution = resolveProject({ cwd: data.clientCwd });
508
+ resolvedProjectPath = resolution?.projectRoot;
509
+ resolvedWorkspaceRoot = resolution?.worktreeRoot;
510
+ }
511
+ // Fallback order: explicit > fresh cwd resolution > stale registration > raw clientCwd.
512
+ // Fresh resolution is preferred over registered path because the registered path
513
+ // may be stale (e.g. in-flight reassociation after worktree add/remove).
514
+ const registeredProjectPath = this.resolveClientProjectPath?.(clientId);
515
+ const projectPath = data.projectPath ?? resolvedProjectPath ?? registeredProjectPath ?? data.clientCwd;
516
+ const worktreeRoot = data.worktreeRoot ?? resolvedWorkspaceRoot ?? projectPath;
517
+ if (projectPath && worktreeRoot && !isDescendantOf(worktreeRoot, projectPath)) {
518
+ return {
519
+ error: `worktreeRoot "${worktreeRoot}" must be equal to or within projectPath "${projectPath}".`,
520
+ projectPath,
521
+ };
522
+ }
523
+ return { projectPath, worktreeRoot };
524
+ }
470
525
  /**
471
526
  * Generic handler for routing LLM events from Agent to clients.
472
527
  * Checks both active and recently completed tasks (within grace period).
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Result of canonical project resolution.
3
+ */
4
+ export interface ProjectResolution {
5
+ /** Directory containing .brv/config.json */
6
+ projectRoot: string;
7
+ /** How the project root was discovered */
8
+ source: 'direct' | 'flag' | 'linked';
9
+ /** Worktree root (equals projectRoot when direct, equals cwd when linked) */
10
+ worktreeRoot: string;
11
+ }
12
+ /**
13
+ * Error thrown when a .brv pointer file points to a project root that no longer has .brv/.
14
+ */
15
+ export declare class BrokenWorktreePointerError extends Error {
16
+ readonly pointerDir: string;
17
+ readonly targetProjectRoot: string;
18
+ constructor(pointerDir: string, targetProjectRoot: string);
19
+ }
20
+ /**
21
+ * Error thrown when a .brv file (pointer) exists but contains malformed/invalid content.
22
+ */
23
+ export declare class MalformedWorktreePointerError extends Error {
24
+ readonly pointerDir: string;
25
+ readonly reason: string;
26
+ constructor(pointerDir: string, reason: string);
27
+ }
28
+ export declare function hasBrvConfig(dir: string): boolean;
29
+ /**
30
+ * Checks if a directory is a git root (.git directory or .git file for worktrees/submodules).
31
+ */
32
+ export declare function isGitRoot(dir: string): boolean;
33
+ /**
34
+ * Checks if .brv in the given directory is a FILE (pointer), not a directory.
35
+ */
36
+ export declare function isWorktreePointer(dir: string): boolean;
37
+ /**
38
+ * Reads and validates a .brv pointer file.
39
+ * @throws MalformedWorktreePointerError if the file has invalid content
40
+ */
41
+ export declare function readWorktreePointer(dir: string): string;
42
+ export interface ResolveProjectOptions {
43
+ /** Override cwd (defaults to process.cwd()) */
44
+ cwd?: string;
45
+ /** Explicit --project-root flag value */
46
+ projectRootFlag?: string;
47
+ }
48
+ /**
49
+ * Canonical project resolver — single source of truth for discovering
50
+ * which .brv/ project a given working directory belongs to.
51
+ *
52
+ * Git-style resolution: walks up from cwd to find the nearest `.brv`
53
+ * (file or directory), just like git walks up to find `.git`.
54
+ *
55
+ * 1. --project-root flag
56
+ * 2. Walk up from cwd looking for .brv:
57
+ * a. .brv is a directory with config.json → source: 'direct'
58
+ * b. .brv is a file (pointer) → follow to parent → source: 'linked'
59
+ * 3. null (no .brv found at cwd or any ancestor)
60
+ *
61
+ * The walk-up stops at the **first** .brv found — it does NOT skip past
62
+ * a .brv to find a "better" one higher up. This prevents accidental
63
+ * inheritance from stale .brv/ directories in ancestor directories.
64
+ *
65
+ * @throws BrokenWorktreePointerError if .brv file points to a missing project
66
+ * @throws MalformedWorktreePointerError if .brv file has invalid content
67
+ */
68
+ export declare function resolveProject(options?: ResolveProjectOptions): null | ProjectResolution;
69
+ export interface AddWorktreeResult {
70
+ backedUp?: boolean;
71
+ message: string;
72
+ success: boolean;
73
+ }
74
+ /**
75
+ * Register a worktree: writes .brv pointer file in the target directory
76
+ * and creates a registry entry in the parent's .brv/worktrees/.
77
+ *
78
+ * If the target already has a .brv/ directory (e.g., auto-init'd), it is
79
+ * backed up to .brv-backup/ and replaced with a pointer file.
80
+ */
81
+ export declare function addWorktree(projectRoot: string, worktreePath: string, options?: {
82
+ force?: boolean;
83
+ }): AddWorktreeResult;
84
+ export interface RemoveWorktreeResult {
85
+ message: string;
86
+ success: boolean;
87
+ }
88
+ /**
89
+ * Remove a worktree: deletes the .brv pointer file and cleans up the registry entry.
90
+ * If a .brv-backup/ exists, restores it.
91
+ */
92
+ export declare function removeWorktree(worktreePath: string): RemoveWorktreeResult;
93
+ export interface WorktreeInfo {
94
+ name: string;
95
+ worktreePath: string;
96
+ }
97
+ /**
98
+ * List all registered worktrees for a project by scanning .brv/worktrees/ entries.
99
+ */
100
+ export declare function listWorktrees(projectRoot: string): WorktreeInfo[];
101
+ /**
102
+ * Walk up from startDir looking for the nearest directory with .brv/ DIRECTORY
103
+ * (not a .brv file). Used only by `brv worktree add` auto-detect mode.
104
+ * NOT used by the resolver.
105
+ */
106
+ export declare function findParentProject(startDir: string): string | undefined;
@@ -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
+ }