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.
Files changed (53) hide show
  1. package/README.md +8 -34
  2. package/dist/agent/infra/llm/providers/google.js +1 -1
  3. package/dist/oclif/commands/login.d.ts +15 -2
  4. package/dist/oclif/commands/login.js +106 -29
  5. package/dist/oclif/commands/providers/list.js +3 -0
  6. package/dist/oclif/commands/vc/diff.d.ts +12 -0
  7. package/dist/oclif/commands/vc/diff.js +40 -0
  8. package/dist/oclif/commands/vc/remote/remove.d.ts +9 -0
  9. package/dist/oclif/commands/vc/remote/remove.js +23 -0
  10. package/dist/server/core/domain/entities/brv-config.d.ts +4 -0
  11. package/dist/server/core/domain/entities/brv-config.js +12 -0
  12. package/dist/server/core/domain/entities/provider-registry.js +3 -3
  13. package/dist/server/core/interfaces/services/i-git-service.d.ts +55 -4
  14. package/dist/server/infra/context-tree/summary-frontmatter.js +2 -2
  15. package/dist/server/infra/dream/operations/consolidate.js +5 -4
  16. package/dist/server/infra/dream/operations/synthesize.js +1 -1
  17. package/dist/server/infra/git/isomorphic-git-service.d.ts +24 -1
  18. package/dist/server/infra/git/isomorphic-git-service.js +207 -7
  19. package/dist/server/infra/transport/handlers/config-handler.js +1 -0
  20. package/dist/server/infra/transport/handlers/locations-handler.d.ts +1 -0
  21. package/dist/server/infra/transport/handlers/locations-handler.js +25 -1
  22. package/dist/server/infra/transport/handlers/reveal-command.d.ts +9 -0
  23. package/dist/server/infra/transport/handlers/reveal-command.js +7 -0
  24. package/dist/server/infra/transport/handlers/vc-handler.d.ts +11 -0
  25. package/dist/server/infra/transport/handlers/vc-handler.js +143 -9
  26. package/dist/server/infra/webui/webui-middleware.js +10 -4
  27. package/dist/shared/transport/events/config-events.d.ts +1 -0
  28. package/dist/shared/transport/events/index.d.ts +1 -0
  29. package/dist/shared/transport/events/locations-events.d.ts +7 -0
  30. package/dist/shared/transport/events/locations-events.js +1 -0
  31. package/dist/shared/transport/events/vc-events.d.ts +56 -5
  32. package/dist/shared/transport/events/vc-events.js +7 -0
  33. package/dist/tui/features/commands/definitions/vc-diff.d.ts +2 -0
  34. package/dist/tui/features/commands/definitions/vc-diff.js +23 -0
  35. package/dist/tui/features/commands/definitions/vc-remote.js +16 -7
  36. package/dist/tui/features/commands/definitions/vc.js +2 -0
  37. package/dist/tui/features/vc/diff/api/execute-vc-diff.d.ts +8 -0
  38. package/dist/tui/features/vc/diff/api/execute-vc-diff.js +13 -0
  39. package/dist/tui/features/vc/diff/components/vc-diff-flow.d.ts +8 -0
  40. package/dist/tui/features/vc/diff/components/vc-diff-flow.js +31 -0
  41. package/dist/tui/features/vc/diff/utils/format-diff.d.ts +2 -0
  42. package/dist/tui/features/vc/diff/utils/format-diff.js +83 -0
  43. package/dist/tui/features/vc/diff/utils/parse-mode.d.ts +2 -0
  44. package/dist/tui/features/vc/diff/utils/parse-mode.js +16 -0
  45. package/dist/tui/features/vc/remote/components/vc-remote-flow.js +23 -8
  46. package/dist/webui/assets/index-CvcqpMYn.css +1 -0
  47. package/dist/webui/assets/index-thSZZahh.js +130 -0
  48. package/dist/webui/index.html +3 -3
  49. package/dist/webui/sw.js +1 -1
  50. package/oclif.manifest.json +1009 -933
  51. package/package.json +3 -1
  52. package/dist/webui/assets/index-Cti7S_1o.js +0 -130
  53. package/dist/webui/assets/index-Dpw6osIL.css +0 -1
@@ -1,4 +1,4 @@
1
- import type { AbortMergeGitParams, AddGitParams, AddRemoteGitParams, AheadBehind, BaseGitParams, BlobContents, CheckoutGitParams, CloneGitParams, CommitGitParams, CreateBranchGitParams, DeleteBranchGitParams, FetchGitParams, GetAheadBehindParams, GetBlobContentParams, GetBlobContentsParams, GetRemoteUrlGitParams, GetTrackingBranchParams, GitBranch, GitCommit, GitConflict, GitRemote, GitStatus, IGitService, InitGitParams, ListBranchesGitParams, LogGitParams, MergeGitParams, MergeResult, PullGitParams, PullResult, PushGitParams, PushResult, RemoveRemoteGitParams, ResetGitParams, ResetResult, SetTrackingBranchParams, TrackingBranch } from '../../core/interfaces/services/i-git-service.js';
1
+ import type { AbortMergeGitParams, AddGitParams, AddRemoteGitParams, AheadBehind, BaseGitParams, BlobContents, ChangedFile, CheckoutGitParams, CloneGitParams, CommitGitParams, CreateBranchGitParams, DeleteBranchGitParams, FetchGitParams, GetAheadBehindParams, GetBlobContentParams, GetBlobContentsParams, GetRemoteUrlGitParams, GetTrackingBranchParams, GitBranch, GitCommit, GitConflict, GitRemote, GitStatus, IGitService, InitGitParams, ListBranchesGitParams, ListChangedFilesParams, LogGitParams, MergeGitParams, MergeResult, PullGitParams, PullResult, PushGitParams, PushResult, RemoveRemoteGitParams, ResetGitParams, ResetResult, SetTrackingBranchParams, TextBlob, TrackingBranch } from '../../core/interfaces/services/i-git-service.js';
2
2
  import type { IAuthStateStore } from '../../core/interfaces/state/i-auth-state-store.js';
3
3
  export declare class IsomorphicGitService implements IGitService {
4
4
  private readonly authStateStore;
@@ -21,7 +21,9 @@ export declare class IsomorphicGitService implements IGitService {
21
21
  getCurrentBranch(params: BaseGitParams): Promise<string | undefined>;
22
22
  getFilesWithConflictMarkers(params: BaseGitParams): Promise<string[]>;
23
23
  getRemoteUrl(params: GetRemoteUrlGitParams): Promise<string | undefined>;
24
+ getTextBlob(params: GetBlobContentParams): Promise<TextBlob | undefined>;
24
25
  getTrackingBranch(params: GetTrackingBranchParams): Promise<TrackingBranch | undefined>;
26
+ hashBlob(content: Buffer): Promise<string>;
25
27
  init(params: InitGitParams): Promise<void>;
26
28
  isAncestor(params: BaseGitParams & {
27
29
  ancestor: string;
@@ -30,6 +32,7 @@ export declare class IsomorphicGitService implements IGitService {
30
32
  isEmptyRepository(params: BaseGitParams): Promise<boolean>;
31
33
  isInitialized(params: BaseGitParams): Promise<boolean>;
32
34
  listBranches(params: ListBranchesGitParams): Promise<GitBranch[]>;
35
+ listChangedFiles(params: ListChangedFilesParams): Promise<ChangedFile[]>;
33
36
  listRemotes(params: BaseGitParams): Promise<GitRemote[]>;
34
37
  log(params: LogGitParams): Promise<GitCommit[]>;
35
38
  merge(params: MergeGitParams): Promise<MergeResult>;
@@ -40,7 +43,11 @@ export declare class IsomorphicGitService implements IGitService {
40
43
  setTrackingBranch(params: SetTrackingBranchParams): Promise<void>;
41
44
  status(params: BaseGitParams): Promise<GitStatus>;
42
45
  private buildBasicAuthHeaders;
46
+ private classifyRefVsWorkdir;
47
+ private classifyStagedRow;
48
+ private classifyUnstagedRow;
43
49
  private conflictsFromError;
50
+ private describeSide;
44
51
  private getAuthor;
45
52
  private getOnAuth;
46
53
  private getOnAuthFailure;
@@ -51,6 +58,10 @@ export declare class IsomorphicGitService implements IGitService {
51
58
  * to match native git behavior.
52
59
  */
53
60
  private guardStagedConflicts;
61
+ private hashWorkdirFile;
62
+ private isCommitishSide;
63
+ private listChangedBetweenCommits;
64
+ private listChangedFromMatrix;
54
65
  /**
55
66
  * Manual merge for unrelated histories (no common ancestor).
56
67
  * isomorphic-git throws MergeNotSupportedError because it can't handle
@@ -59,6 +70,12 @@ export declare class IsomorphicGitService implements IGitService {
59
70
  private mergeUnrelatedHistories;
60
71
  private parseMatrix;
61
72
  private readBlobOid;
73
+ /**
74
+ * Reads the raw blob bytes + full oid at the given ref in a single pass.
75
+ * Returns `undefined` when the blob is absent (path missing, ref unresolved, etc.).
76
+ * Used by {@link getTextBlob}; callers downstream decide binary vs text.
77
+ */
78
+ private readRawBlob;
62
79
  private requireDirectory;
63
80
  private requireToken;
64
81
  /**
@@ -71,6 +88,12 @@ export declare class IsomorphicGitService implements IGitService {
71
88
  * Falls back to git.resolveRef for plain refs.
72
89
  */
73
90
  private resolveRefExpression;
91
+ /**
92
+ * Resolve a single ref (branch name, tag, full SHA, or short SHA).
93
+ * Falls back to `git.expandOid` for short SHAs since `git.resolveRef`
94
+ * only accepts full OIDs and symbolic refs.
95
+ */
96
+ private resolveSingleRef;
74
97
  /**
75
98
  * Fixes conflict markers written by isomorphic-git to match native git:
76
99
  * 1. Replaces `<<<<<<< <branchName>` with `<<<<<<< HEAD`
@@ -246,13 +246,13 @@ export class IsomorphicGitService {
246
246
  const result = Object.fromEntries(paths.map((p) => [p, undefined]));
247
247
  if (paths.length === 0)
248
248
  return result;
249
- if (ref === 'HEAD') {
250
- const headOid = await git.resolveRef({ dir, fs, ref: 'HEAD' }).catch(() => null);
251
- if (!headOid)
249
+ if (ref !== 'STAGE') {
250
+ const commitOid = await this.resolveRefExpression(dir, ref.commitish).catch(() => null);
251
+ if (!commitOid)
252
252
  return result;
253
253
  await Promise.all(paths.map(async (path) => {
254
254
  try {
255
- const { blob } = await git.readBlob({ dir, filepath: path, fs, oid: headOid });
255
+ const { blob } = await git.readBlob({ dir, filepath: path, fs, oid: commitOid });
256
256
  result[path] = Buffer.from(blob).toString('utf8');
257
257
  }
258
258
  catch {
@@ -361,6 +361,16 @@ export class IsomorphicGitService {
361
361
  const result = await git.getConfig({ dir, fs, path: `remote.${params.remote}.url` });
362
362
  return result === undefined || result === null ? undefined : String(result);
363
363
  }
364
+ async getTextBlob(params) {
365
+ const dir = this.requireDirectory(params);
366
+ const { path, ref } = params;
367
+ const raw = await this.readRawBlob(dir, path, ref);
368
+ if (!raw)
369
+ return undefined;
370
+ if (raw.bytes.includes(0))
371
+ return { binary: true, content: '', oid: raw.oid.slice(0, 7) };
372
+ return { content: Buffer.from(raw.bytes).toString('utf8'), oid: raw.oid.slice(0, 7) };
373
+ }
364
374
  async getTrackingBranch(params) {
365
375
  const dir = this.requireDirectory(params);
366
376
  const remote = await git.getConfig({ dir, fs, path: `branch.${params.branch}.remote` });
@@ -374,6 +384,10 @@ export class IsomorphicGitService {
374
384
  const remoteBranch = mergeStr.startsWith('refs/heads/') ? mergeStr.slice('refs/heads/'.length) : mergeStr;
375
385
  return { remote: String(remote), remoteBranch };
376
386
  }
387
+ async hashBlob(content) {
388
+ const { oid } = await git.hashBlob({ object: content });
389
+ return oid.slice(0, 7);
390
+ }
377
391
  async init(params) {
378
392
  const dir = this.requireDirectory(params);
379
393
  await git.init({ defaultBranch: params.defaultBranch ?? 'main', dir, fs });
@@ -431,6 +445,15 @@ export class IsomorphicGitService {
431
445
  }
432
446
  return result;
433
447
  }
448
+ async listChangedFiles(params) {
449
+ const dir = this.requireDirectory(params);
450
+ const { from, to } = params;
451
+ // Commit-vs-commit: walk both trees, compare oids
452
+ if (from !== 'STAGE' && from !== 'WORKDIR' && to !== 'STAGE' && to !== 'WORKDIR') {
453
+ return this.listChangedBetweenCommits(dir, from.commitish, to.commitish);
454
+ }
455
+ return this.listChangedFromMatrix(dir, from, to);
456
+ }
434
457
  async listRemotes(params) {
435
458
  const dir = this.requireDirectory(params);
436
459
  const remotes = await git.listRemotes({ dir, fs });
@@ -737,6 +760,56 @@ export class IsomorphicGitService {
737
760
  const credentials = Buffer.from(`${userId}:${sessionKey}`).toString('base64');
738
761
  return { Authorization: `Basic ${credentials}` };
739
762
  }
763
+ async classifyRefVsWorkdir(dir, fromOid, refSet, matrix) {
764
+ // "Tracked in workdir" = present on disk (workdir != 0) AND tracked (HEAD or stage has it).
765
+ // This excludes untracked files (matches `git diff <commit>` which does not show untracked).
766
+ const trackedOnDisk = new Set();
767
+ for (const [filepath, head, workdir, stage] of matrix) {
768
+ if (workdir !== 0 && (head !== 0 || stage !== 0))
769
+ trackedOnDisk.add(String(filepath));
770
+ }
771
+ const candidates = new Set([...refSet, ...trackedOnDisk]);
772
+ const results = await Promise.all([...candidates].map(async (path) => {
773
+ const inRef = refSet.has(path);
774
+ const inWork = trackedOnDisk.has(path);
775
+ if (inRef && !inWork)
776
+ return { path, status: 'deleted' };
777
+ if (!inRef && inWork)
778
+ return { path, status: 'added' };
779
+ if (inRef && inWork) {
780
+ const [fromBlobOid, workOid] = await Promise.all([
781
+ this.readBlobOid(dir, fromOid, path),
782
+ this.hashWorkdirFile(dir, path),
783
+ ]);
784
+ if (fromBlobOid && workOid && fromBlobOid !== workOid)
785
+ return { path, status: 'modified' };
786
+ }
787
+ return undefined;
788
+ }));
789
+ return results.filter((r) => r !== undefined).sort((a, b) => a.path.localeCompare(b.path));
790
+ }
791
+ classifyStagedRow(row) {
792
+ const [filepath, head, , stage] = row;
793
+ const path = String(filepath);
794
+ if (head === 0 && (stage === 2 || stage === 3))
795
+ return { path, status: 'added' };
796
+ if (head === 1 && stage === 0)
797
+ return { path, status: 'deleted' };
798
+ if (head === 1 && (stage === 2 || stage === 3))
799
+ return { path, status: 'modified' };
800
+ return undefined;
801
+ }
802
+ classifyUnstagedRow(row) {
803
+ const [filepath, head, workdir, stage] = row;
804
+ const path = String(filepath);
805
+ if (head === 0 && stage === 0)
806
+ return undefined; // skip untracked
807
+ if (workdir === 0 && (stage === 1 || stage === 2 || stage === 3))
808
+ return { path, status: 'deleted' };
809
+ if (workdir === 2 && (stage === 1 || stage === 3))
810
+ return { path, status: 'modified' };
811
+ return undefined;
812
+ }
740
813
  conflictsFromError(error) {
741
814
  if (!IsomorphicGitService.isConflictError(error))
742
815
  return [];
@@ -756,6 +829,13 @@ export class IsomorphicGitService {
756
829
  }))
757
830
  .sort((a, b) => a.path.localeCompare(b.path));
758
831
  }
832
+ describeSide(side) {
833
+ if (side === 'STAGE')
834
+ return 'STAGE';
835
+ if (side === 'WORKDIR')
836
+ return 'WORKDIR';
837
+ return `commitish(${side.commitish})`;
838
+ }
759
839
  getAuthor() {
760
840
  const token = this.authStateStore.getToken();
761
841
  if (!token)
@@ -812,6 +892,69 @@ export class IsomorphicGitService {
812
892
  '\nPlease commit your changes or stash them before you switch branches.');
813
893
  }
814
894
  }
895
+ async hashWorkdirFile(dir, path) {
896
+ try {
897
+ const buf = await fs.promises.readFile(join(dir, path));
898
+ const { oid } = await git.hashBlob({ object: buf });
899
+ return oid;
900
+ }
901
+ catch {
902
+ return null;
903
+ }
904
+ }
905
+ isCommitishSide(side) {
906
+ return side !== 'STAGE' && side !== 'WORKDIR';
907
+ }
908
+ async listChangedBetweenCommits(dir, fromRef, toRef) {
909
+ const [fromOid, toOid] = await Promise.all([
910
+ this.resolveRefExpression(dir, fromRef),
911
+ this.resolveRefExpression(dir, toRef),
912
+ ]);
913
+ const changes = [];
914
+ await git.walk({
915
+ dir,
916
+ fs,
917
+ async map(filepath, [a, b]) {
918
+ if (filepath === '.')
919
+ return;
920
+ const [aType, bType] = await Promise.all([a?.type(), b?.type()]);
921
+ if (aType === 'tree' || bType === 'tree')
922
+ return;
923
+ if (!a && b && bType === 'blob') {
924
+ changes.push({ path: filepath, status: 'added' });
925
+ return;
926
+ }
927
+ if (a && !b && aType === 'blob') {
928
+ changes.push({ path: filepath, status: 'deleted' });
929
+ return;
930
+ }
931
+ if (a && b && aType === 'blob' && bType === 'blob') {
932
+ const [aOid, bOid] = await Promise.all([a.oid(), b.oid()]);
933
+ if (aOid !== bOid)
934
+ changes.push({ path: filepath, status: 'modified' });
935
+ }
936
+ },
937
+ // eslint-disable-next-line new-cap
938
+ trees: [git.TREE({ ref: fromOid }), git.TREE({ ref: toOid })],
939
+ });
940
+ return changes;
941
+ }
942
+ async listChangedFromMatrix(dir, from, to) {
943
+ const matrix = await git.statusMatrix({ dir, fs });
944
+ if (from === 'STAGE' && to === 'WORKDIR') {
945
+ return matrix.map((row) => this.classifyUnstagedRow(row)).filter((c) => c !== undefined);
946
+ }
947
+ if (this.isCommitishSide(from) && to === 'STAGE') {
948
+ return matrix.map((row) => this.classifyStagedRow(row)).filter((c) => c !== undefined);
949
+ }
950
+ if (this.isCommitishSide(from) && to === 'WORKDIR') {
951
+ const fromOid = await this.resolveRefExpression(dir, from.commitish);
952
+ const tracked = await git.listFiles({ dir, fs, ref: fromOid });
953
+ const trackedSet = new Set(tracked);
954
+ return this.classifyRefVsWorkdir(dir, fromOid, trackedSet, matrix);
955
+ }
956
+ throw new GitError(`unsupported diff side combination: from=${this.describeSide(from)} to=${this.describeSide(to)}`);
957
+ }
815
958
  /**
816
959
  * Manual merge for unrelated histories (no common ancestor).
817
960
  * isomorphic-git throws MergeNotSupportedError because it can't handle
@@ -940,6 +1083,46 @@ export class IsomorphicGitService {
940
1083
  return null;
941
1084
  }
942
1085
  }
1086
+ /**
1087
+ * Reads the raw blob bytes + full oid at the given ref in a single pass.
1088
+ * Returns `undefined` when the blob is absent (path missing, ref unresolved, etc.).
1089
+ * Used by {@link getTextBlob}; callers downstream decide binary vs text.
1090
+ */
1091
+ async readRawBlob(dir, path, ref) {
1092
+ if (ref === 'STAGE') {
1093
+ let found;
1094
+ await git
1095
+ .walk({
1096
+ dir,
1097
+ fs,
1098
+ async map(filepath, [entry]) {
1099
+ if (filepath !== path || !entry)
1100
+ return;
1101
+ if ((await entry.type()) !== 'blob')
1102
+ return;
1103
+ const oid = await entry.oid();
1104
+ const { blob } = await git.readBlob({ dir, fs, oid });
1105
+ found = { bytes: blob, oid };
1106
+ },
1107
+ // eslint-disable-next-line new-cap
1108
+ trees: [git.STAGE()],
1109
+ })
1110
+ .catch(() => {
1111
+ // leave found undefined
1112
+ });
1113
+ return found;
1114
+ }
1115
+ const commitOid = await this.resolveRefExpression(dir, ref.commitish).catch(() => null);
1116
+ if (!commitOid)
1117
+ return undefined;
1118
+ try {
1119
+ const { blob, oid } = await git.readBlob({ dir, filepath: path, fs, oid: commitOid });
1120
+ return { bytes: blob, oid };
1121
+ }
1122
+ catch {
1123
+ return undefined;
1124
+ }
1125
+ }
943
1126
  requireDirectory(params) {
944
1127
  // Guard against empty string — undefined/null are caught by TypeScript at compile time
945
1128
  if (!params.directory)
@@ -1006,14 +1189,14 @@ export class IsomorphicGitService {
1006
1189
  async resolveRefExpression(dir, ref) {
1007
1190
  const tildeMatch = /^(.+)~(\d+)$/.exec(ref);
1008
1191
  if (!tildeMatch) {
1009
- return git.resolveRef({ dir, fs, ref });
1192
+ return this.resolveSingleRef(dir, ref);
1010
1193
  }
1011
1194
  const baseRef = tildeMatch[1];
1012
1195
  const count = Number.parseInt(tildeMatch[2], 10);
1013
1196
  if (count === 0) {
1014
- return git.resolveRef({ dir, fs, ref: baseRef });
1197
+ return this.resolveSingleRef(dir, baseRef);
1015
1198
  }
1016
- let oid = await git.resolveRef({ dir, fs, ref: baseRef });
1199
+ let oid = await this.resolveSingleRef(dir, baseRef);
1017
1200
  for (let i = 0; i < count; i++) {
1018
1201
  // eslint-disable-next-line no-await-in-loop
1019
1202
  const commit = await git.readCommit({ dir, fs, oid });
@@ -1024,6 +1207,23 @@ export class IsomorphicGitService {
1024
1207
  }
1025
1208
  return oid;
1026
1209
  }
1210
+ /**
1211
+ * Resolve a single ref (branch name, tag, full SHA, or short SHA).
1212
+ * Falls back to `git.expandOid` for short SHAs since `git.resolveRef`
1213
+ * only accepts full OIDs and symbolic refs.
1214
+ */
1215
+ async resolveSingleRef(dir, ref) {
1216
+ try {
1217
+ return await git.resolveRef({ dir, fs, ref });
1218
+ }
1219
+ catch (error) {
1220
+ // Short SHA: 4-39 hex chars. Try expandOid which disambiguates against the object DB.
1221
+ if (/^[\da-f]{4,39}$/i.test(ref)) {
1222
+ return git.expandOid({ dir, fs, oid: ref });
1223
+ }
1224
+ throw error;
1225
+ }
1226
+ }
1027
1227
  /**
1028
1228
  * Fixes conflict markers written by isomorphic-git to match native git:
1029
1229
  * 1. Replaces `<<<<<<< <branchName>` with `<<<<<<< HEAD`
@@ -13,6 +13,7 @@ export class ConfigHandler {
13
13
  this.transport.onRequest(ConfigEvents.GET_ENVIRONMENT, () => {
14
14
  const config = getCurrentConfig();
15
15
  return {
16
+ gitRemoteBaseUrl: config.gitRemoteBaseUrl,
16
17
  iamBaseUrl: config.iamBaseUrl,
17
18
  isDevelopment: isDevelopment(),
18
19
  webAppUrl: config.webAppUrl,
@@ -24,4 +24,5 @@ export declare class LocationsHandler {
24
24
  constructor(deps: LocationsHandlerDeps);
25
25
  setup(): void;
26
26
  private buildLocations;
27
+ private handleReveal;
27
28
  }
@@ -1,6 +1,8 @@
1
+ import { spawn } from 'node:child_process';
1
2
  import { join } from 'node:path';
2
- import { LocationsEvents } from '../../../../shared/transport/events/locations-events.js';
3
+ import { LocationsEvents, } from '../../../../shared/transport/events/locations-events.js';
3
4
  import { BRV_DIR, CONTEXT_TREE_DIR } from '../../../constants.js';
5
+ import { resolveRevealCommand } from './reveal-command.js';
4
6
  /**
5
7
  * Handles locations:get event.
6
8
  * Returns all registered project locations with context tree status.
@@ -31,6 +33,7 @@ export class LocationsHandler {
31
33
  return { locations: [] };
32
34
  }
33
35
  });
36
+ this.transport.onRequest(LocationsEvents.REVEAL, async (data) => this.handleReveal(data));
34
37
  }
35
38
  async buildLocations(currentProjectPath) {
36
39
  const all = this.projectRegistry.getAll();
@@ -75,4 +78,25 @@ export class LocationsHandler {
75
78
  return (all.get(b.projectPath)?.registeredAt ?? 0) - (all.get(a.projectPath)?.registeredAt ?? 0);
76
79
  });
77
80
  }
81
+ async handleReveal(data) {
82
+ const { projectPath } = data;
83
+ if (!projectPath)
84
+ throw new Error('projectPath is required');
85
+ // Only allow revealing paths that are registered projects — the client
86
+ // controls this argument, so we must not trust it blindly.
87
+ const registered = this.projectRegistry.getAll();
88
+ if (!registered.has(projectPath)) {
89
+ throw new Error('Project is not registered.');
90
+ }
91
+ const exists = await this.pathExists(projectPath).catch(() => false);
92
+ if (!exists)
93
+ throw new Error('Project folder no longer exists.');
94
+ const { args, command } = resolveRevealCommand(process.platform, projectPath);
95
+ const child = spawn(command, args, { detached: true, stdio: 'ignore', windowsHide: true });
96
+ child.on('error', () => {
97
+ /* best-effort — nothing to report back once the ack resolved */
98
+ });
99
+ child.unref();
100
+ return { projectPath };
101
+ }
78
102
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Resolves the OS-native command for revealing a path in the system file manager.
3
+ * Extracted so the branching can be unit-tested without spawning real processes.
4
+ */
5
+ export type RevealCommand = {
6
+ args: string[];
7
+ command: string;
8
+ };
9
+ export declare function resolveRevealCommand(platformName: NodeJS.Platform, targetPath: string): RevealCommand;
@@ -0,0 +1,7 @@
1
+ export function resolveRevealCommand(platformName, targetPath) {
2
+ if (platformName === 'darwin')
3
+ return { args: [targetPath], command: 'open' };
4
+ if (platformName === 'win32')
5
+ return { args: [targetPath], command: 'explorer' };
6
+ return { args: [targetPath], command: 'xdg-open' };
7
+ }
@@ -40,6 +40,11 @@ export declare class VcHandler {
40
40
  constructor(deps: IVcHandlerDeps);
41
41
  setup(): void;
42
42
  private buildAuthorHint;
43
+ /**
44
+ * Builds a single diff entry for a changed file, or `undefined` when either required side
45
+ * is absent/binary (the caller filters these out to keep binaries out of diff output).
46
+ */
47
+ private buildDiffFile;
43
48
  private buildNoRemoteMessage;
44
49
  private computeDiff;
45
50
  /**
@@ -77,6 +82,12 @@ export declare class VcHandler {
77
82
  private handleRemote;
78
83
  private handleReset;
79
84
  private handleStatus;
85
+ /**
86
+ * Reads a diff side's content + short oid in a single pass. Returns `undefined`
87
+ * only when the blob is absent on disk / at the ref. Binary content (NUL byte)
88
+ * is marked via `binary: true` so the caller can emit `Binary files ... differ`.
89
+ */
90
+ private readSideEntry;
80
91
  /**
81
92
  * Resolve clone request data into a clean cogit URL + team/space info.
82
93
  * Accepts either a URL or explicit teamName/spaceName.