@hunsu/bridge 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -361,6 +361,7 @@ export type StudioServerOptions = {
361
361
  roadmapRegistryPath?: string;
362
362
  runtimeConfig?: BridgeRuntimeConfig;
363
363
  security?: StudioServerSecurityOptions;
364
+ directoryPicker?: NativeDirectoryPicker;
364
365
  };
365
366
  export type StudioCommandResult = {
366
367
  acceptedEvents: DomainEvent[];
@@ -455,6 +456,29 @@ export type FilesystemGrantRequest = {
455
456
  export type FilesystemGrantResult = {
456
457
  capability: FilesystemBrowseCapability;
457
458
  };
459
+ export type NativeDirectoryPickerOptions = {
460
+ cwd: string;
461
+ };
462
+ export type NativeDirectoryPickerResult = {
463
+ status: "selected";
464
+ path: string;
465
+ } | {
466
+ status: "cancelled";
467
+ } | {
468
+ status: "unavailable";
469
+ reason: string;
470
+ };
471
+ export type NativeDirectoryPicker = (options: NativeDirectoryPickerOptions) => Promise<NativeDirectoryPickerResult>;
472
+ export type FilesystemDirectoryPickResult = {
473
+ status: "selected";
474
+ capability: FilesystemBrowseCapability;
475
+ entry: FilesystemBrowseEntry;
476
+ } | {
477
+ status: "cancelled";
478
+ } | {
479
+ status: "unavailable";
480
+ reason: string;
481
+ };
458
482
  export type MoveFileNode = {
459
483
  kind: "directory";
460
484
  name: string;
@@ -914,6 +938,11 @@ export declare function createFilesystemBrowseGrant(request: FilesystemGrantRequ
914
938
  export declare function tryCreateFilesystemBrowseGrant(request: FilesystemGrantRequest, state: StudioServerState, options?: {
915
939
  cwd?: string;
916
940
  }): Result<FilesystemGrantResult, FilesystemBrowseError>;
941
+ export declare function pickFilesystemDirectory(state: StudioServerState, options?: {
942
+ cwd?: string;
943
+ picker?: NativeDirectoryPicker;
944
+ }): Promise<FilesystemDirectoryPickResult>;
945
+ export declare function openNativeDirectoryPicker(_options: NativeDirectoryPickerOptions): Promise<NativeDirectoryPickerResult>;
917
946
  export declare function createRouteWorktree(cwd: string, routeId: string, createdAt?: string, requestedBaseRef?: string, requestedWorktreeRoot?: string): WorktreeRef;
918
947
  export declare function createExecuteWorktree(cwd: string, executeId: string, createdAt?: string, requestedBaseRef?: string, requestedWorktreeRoot?: string): WorktreeRef;
919
948
  export declare function readMoveDiff(board: BoardProjection, moveId: string, cwd?: string): MoveDiff;
package/dist/index.js CHANGED
@@ -167,7 +167,7 @@ function createStudioServerSecurity(options, runtimeConfig) {
167
167
  allowNoOrigin: options?.allowNoOrigin ?? true
168
168
  };
169
169
  }
170
- function evaluateStudioRequestSecurity(request, url, security) {
170
+ function evaluateStudioRequestSecurity(request, url, security, options = {}) {
171
171
  const origin = requestHeader(request, "origin");
172
172
  const corsHeaders = corsHeadersForOrigin(origin, security);
173
173
  if (origin && corsHeaders === undefined) {
@@ -187,7 +187,7 @@ function evaluateStudioRequestSecurity(request, url, security) {
187
187
  };
188
188
  }
189
189
  const headers = corsHeaders ?? baseCorsHeaders();
190
- if (security.authToken && !isPublicBridgeRequest(url) && !hasValidBridgeApiToken(request, url, security.authToken)) {
190
+ if (!options.skipAuth && security.authToken && !isPublicBridgeRequest(url) && !hasValidBridgeApiToken(request, url, security.authToken)) {
191
191
  return {
192
192
  allowed: false,
193
193
  status: 401,
@@ -662,11 +662,13 @@ export function createStudioServer(options = {}) {
662
662
  const runner = options.runner ?? createConfiguredRunner(runtimeConfig);
663
663
  const actionRunner = options.actionRunner;
664
664
  const security = createStudioServerSecurity(options.security, runtimeConfig);
665
+ const directoryPicker = options.directoryPicker ?? openNativeDirectoryPicker;
666
+ let activeDirectoryPick = false;
665
667
  return createServer(async (request, response) => {
666
668
  try {
667
669
  const url = new URL(request.url ?? "/", "http://localhost");
668
670
  const pathname = url.pathname;
669
- const requestSecurity = evaluateStudioRequestSecurity(request, url, security);
671
+ const requestSecurity = evaluateStudioRequestSecurity(request, url, security, { skipAuth: request.method === "OPTIONS" });
670
672
  responseSecurityHeaders.set(response, requestSecurity.corsHeaders);
671
673
  if (request.method === "OPTIONS") {
672
674
  sendJson(response, requestSecurity.allowed ? 204 : requestSecurity.status ?? 403, requestSecurity.allowed ? {} : { error: requestSecurity.error ?? "Origin is not allowed." });
@@ -699,6 +701,20 @@ export function createStudioServer(options = {}) {
699
701
  }));
700
702
  return;
701
703
  }
704
+ if (request.method === "POST" && pathname === "/api/filesystem/pick-directory") {
705
+ if (activeDirectoryPick) {
706
+ sendJson(response, 409, { status: "unavailable", reason: "A folder picker is already open." });
707
+ return;
708
+ }
709
+ activeDirectoryPick = true;
710
+ try {
711
+ sendJson(response, 200, await pickFilesystemDirectory(state, { cwd: repositoryPath, picker: directoryPicker }));
712
+ }
713
+ finally {
714
+ activeDirectoryPick = false;
715
+ }
716
+ return;
717
+ }
702
718
  if (request.method === "POST" && pathname === "/api/filesystem/grants") {
703
719
  const body = await readJson(request);
704
720
  sendJson(response, 201, createFilesystemBrowseGrant(body, state, { cwd: repositoryPath }));
@@ -2994,6 +3010,73 @@ export function tryCreateFilesystemBrowseGrant(request, state, options = {}) {
2994
3010
  state.filesystemCapabilities[token] = capability;
2995
3011
  return ok({ capability });
2996
3012
  }
3013
+ export async function pickFilesystemDirectory(state, options = {}) {
3014
+ const cwd = options.cwd ?? process.cwd();
3015
+ const picker = options.picker ?? openNativeDirectoryPicker;
3016
+ const picked = await picker({ cwd });
3017
+ if (picked.status !== "selected") {
3018
+ return picked;
3019
+ }
3020
+ const grant = createFilesystemBrowseGrant({ path: picked.path }, state, { cwd });
3021
+ return {
3022
+ status: "selected",
3023
+ capability: grant.capability,
3024
+ entry: filesystemBrowseEntryForPath(grant.capability.path, grant.capability.rootId)
3025
+ };
3026
+ }
3027
+ export async function openNativeDirectoryPicker(_options) {
3028
+ if (process.platform === "darwin") {
3029
+ return runDirectoryPickerCommand("osascript", [
3030
+ "-e",
3031
+ "POSIX path of (choose folder with prompt \"Choose a project folder for Hunsu Studio\")"
3032
+ ], {
3033
+ cancelPatterns: [/User canceled/i, /-128/]
3034
+ });
3035
+ }
3036
+ if (process.platform === "win32") {
3037
+ return runDirectoryPickerCommand("powershell.exe", [
3038
+ "-NoProfile",
3039
+ "-STA",
3040
+ "-Command",
3041
+ [
3042
+ "Add-Type -AssemblyName System.Windows.Forms;",
3043
+ "$dialog = New-Object System.Windows.Forms.FolderBrowserDialog;",
3044
+ "$dialog.Description = 'Choose a project folder for Hunsu Studio';",
3045
+ "$dialog.ShowNewFolderButton = $true;",
3046
+ "if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {",
3047
+ " [Console]::Out.WriteLine($dialog.SelectedPath);",
3048
+ " exit 0;",
3049
+ "}",
3050
+ "exit 2;"
3051
+ ].join(" ")
3052
+ ], {
3053
+ cancelCodes: [2]
3054
+ });
3055
+ }
3056
+ const zenity = await runDirectoryPickerCommand("zenity", [
3057
+ "--file-selection",
3058
+ "--directory",
3059
+ "--title=Choose a project folder for Hunsu Studio"
3060
+ ], {
3061
+ cancelCodes: [1],
3062
+ unavailablePatterns: [/cannot open display/i, /no display/i]
3063
+ });
3064
+ if (zenity.status !== "unavailable") {
3065
+ return zenity;
3066
+ }
3067
+ const kdialog = await runDirectoryPickerCommand("kdialog", [
3068
+ "--title",
3069
+ "Choose a project folder for Hunsu Studio",
3070
+ "--getexistingdirectory"
3071
+ ], {
3072
+ cancelCodes: [1],
3073
+ unavailablePatterns: [/cannot connect/i, /no display/i]
3074
+ });
3075
+ if (kdialog.status !== "unavailable") {
3076
+ return kdialog;
3077
+ }
3078
+ return { status: "unavailable", reason: "No supported native folder picker is available in this environment." };
3079
+ }
2997
3080
  function resolveRoadmapOpenRequestPath(request, state) {
2998
3081
  if (!request.browseToken) {
2999
3082
  return request;
@@ -3031,6 +3114,39 @@ function browseRootLabel(path) {
3031
3114
  }
3032
3115
  return basename(path) || path;
3033
3116
  }
3117
+ async function runDirectoryPickerCommand(command, args, options = {}) {
3118
+ try {
3119
+ const { stdout } = await execFileAsync(command, args);
3120
+ const selectedPath = stdout.trim();
3121
+ return selectedPath ? { status: "selected", path: selectedPath } : { status: "cancelled" };
3122
+ }
3123
+ catch (error) {
3124
+ const pickerError = error;
3125
+ const output = `${pickerError.stdout ?? ""}\n${pickerError.stderr ?? ""}\n${pickerError.message}`;
3126
+ if (pickerError.code === "ENOENT" || options.unavailablePatterns?.some(pattern => pattern.test(output))) {
3127
+ return { status: "unavailable", reason: `${command} is not available.` };
3128
+ }
3129
+ if (typeof pickerError.code === "number" && options.cancelCodes?.includes(pickerError.code)) {
3130
+ return { status: "cancelled" };
3131
+ }
3132
+ if (options.cancelPatterns?.some(pattern => pattern.test(output))) {
3133
+ return { status: "cancelled" };
3134
+ }
3135
+ return { status: "unavailable", reason: output.trim() || `${command} failed to open a folder picker.` };
3136
+ }
3137
+ }
3138
+ function filesystemBrowseEntryForPath(path, rootId) {
3139
+ const entryPath = String(path);
3140
+ return {
3141
+ kind: "directory",
3142
+ name: basename(entryPath) || entryPath,
3143
+ path: entryPath,
3144
+ rootId,
3145
+ type: "directory",
3146
+ isGitRepository: existsSync(join(entryPath, ".git")),
3147
+ isRoadmap: hasBrowseVisibleHunsuRuntimeFiles(entryPath)
3148
+ };
3149
+ }
3034
3150
  function hashText(value) {
3035
3151
  return hashHex(createHash("sha256").update(value));
3036
3152
  }
@@ -3082,7 +3198,12 @@ function canonicalizeGrantTargetPath(path, roots) {
3082
3198
  }
3083
3199
  }
3084
3200
  function isAllowedBrowsePath(path, roots) {
3085
- return roots.some(root => path === root || path.startsWith(`${root}/`));
3201
+ const normalizedPath = normalize(path);
3202
+ return roots.some(root => {
3203
+ const normalizedRoot = normalize(root);
3204
+ const childPath = relative(normalizedRoot, normalizedPath);
3205
+ return childPath === "" || (!childPath.startsWith("..") && !isAbsolute(childPath));
3206
+ });
3086
3207
  }
3087
3208
  function shouldHideBrowseEntry(name) {
3088
3209
  return name === "node_modules"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hunsu/bridge",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "description": "Hunsu Bridge localhost runtime and Studio API server.",
6
6
  "license": "Apache-2.0",
@@ -29,8 +29,8 @@
29
29
  "dependencies": {
30
30
  "@hunsu/codex-runner": "0.1.1",
31
31
  "@hunsu/core": "0.1.1",
32
- "@hunsu/protocol": "0.1.1",
33
32
  "@hunsu/protocol-registry": "0.1.1",
33
+ "@hunsu/protocol": "0.1.1",
34
34
  "@hunsu/config": "0.1.1"
35
35
  },
36
36
  "scripts": {
package/src/index.ts CHANGED
@@ -582,6 +582,7 @@ export type StudioServerOptions = {
582
582
  roadmapRegistryPath?: string;
583
583
  runtimeConfig?: BridgeRuntimeConfig;
584
584
  security?: StudioServerSecurityOptions;
585
+ directoryPicker?: NativeDirectoryPicker;
585
586
  };
586
587
 
587
588
  export type StudioCommandResult = {
@@ -687,6 +688,22 @@ export type FilesystemGrantResult = {
687
688
  capability: FilesystemBrowseCapability;
688
689
  };
689
690
 
691
+ export type NativeDirectoryPickerOptions = {
692
+ cwd: string;
693
+ };
694
+
695
+ export type NativeDirectoryPickerResult =
696
+ | { status: "selected"; path: string }
697
+ | { status: "cancelled" }
698
+ | { status: "unavailable"; reason: string };
699
+
700
+ export type NativeDirectoryPicker = (options: NativeDirectoryPickerOptions) => Promise<NativeDirectoryPickerResult>;
701
+
702
+ export type FilesystemDirectoryPickResult =
703
+ | { status: "selected"; capability: FilesystemBrowseCapability; entry: FilesystemBrowseEntry }
704
+ | { status: "cancelled" }
705
+ | { status: "unavailable"; reason: string };
706
+
690
707
  export type MoveFileNode =
691
708
  | { kind: "directory"; name: string; path: string }
692
709
  | { kind: "textFile"; name: string; path: string; size: number }
@@ -1170,7 +1187,7 @@ function createStudioServerSecurity(options: StudioServerSecurityOptions | undef
1170
1187
  };
1171
1188
  }
1172
1189
 
1173
- function evaluateStudioRequestSecurity(request: IncomingMessage, url: URL, security: StudioServerSecurity): StudioRequestSecurity {
1190
+ function evaluateStudioRequestSecurity(request: IncomingMessage, url: URL, security: StudioServerSecurity, options: { skipAuth?: boolean } = {}): StudioRequestSecurity {
1174
1191
  const origin = requestHeader(request, "origin");
1175
1192
  const corsHeaders = corsHeadersForOrigin(origin, security);
1176
1193
  if (origin && corsHeaders === undefined) {
@@ -1190,7 +1207,7 @@ function evaluateStudioRequestSecurity(request: IncomingMessage, url: URL, secur
1190
1207
  };
1191
1208
  }
1192
1209
  const headers = corsHeaders ?? baseCorsHeaders();
1193
- if (security.authToken && !isPublicBridgeRequest(url) && !hasValidBridgeApiToken(request, url, security.authToken)) {
1210
+ if (!options.skipAuth && security.authToken && !isPublicBridgeRequest(url) && !hasValidBridgeApiToken(request, url, security.authToken)) {
1194
1211
  return {
1195
1212
  allowed: false,
1196
1213
  status: 401,
@@ -1695,12 +1712,14 @@ export function createStudioServer(options: StudioServerOptions = {}) {
1695
1712
  const runner = options.runner ?? createConfiguredRunner(runtimeConfig);
1696
1713
  const actionRunner = options.actionRunner;
1697
1714
  const security = createStudioServerSecurity(options.security, runtimeConfig);
1715
+ const directoryPicker = options.directoryPicker ?? openNativeDirectoryPicker;
1716
+ let activeDirectoryPick = false;
1698
1717
 
1699
1718
  return createServer(async (request, response) => {
1700
1719
  try {
1701
1720
  const url = new URL(request.url ?? "/", "http://localhost");
1702
1721
  const pathname = url.pathname;
1703
- const requestSecurity = evaluateStudioRequestSecurity(request, url, security);
1722
+ const requestSecurity = evaluateStudioRequestSecurity(request, url, security, { skipAuth: request.method === "OPTIONS" });
1704
1723
  responseSecurityHeaders.set(response, requestSecurity.corsHeaders);
1705
1724
  if (request.method === "OPTIONS") {
1706
1725
  sendJson(response, requestSecurity.allowed ? 204 : requestSecurity.status ?? 403, requestSecurity.allowed ? {} : { error: requestSecurity.error ?? "Origin is not allowed." });
@@ -1739,6 +1758,20 @@ export function createStudioServer(options: StudioServerOptions = {}) {
1739
1758
  return;
1740
1759
  }
1741
1760
 
1761
+ if (request.method === "POST" && pathname === "/api/filesystem/pick-directory") {
1762
+ if (activeDirectoryPick) {
1763
+ sendJson(response, 409, { status: "unavailable", reason: "A folder picker is already open." });
1764
+ return;
1765
+ }
1766
+ activeDirectoryPick = true;
1767
+ try {
1768
+ sendJson(response, 200, await pickFilesystemDirectory(state, { cwd: repositoryPath, picker: directoryPicker }));
1769
+ } finally {
1770
+ activeDirectoryPick = false;
1771
+ }
1772
+ return;
1773
+ }
1774
+
1742
1775
  if (request.method === "POST" && pathname === "/api/filesystem/grants") {
1743
1776
  const body = await readJson<FilesystemGrantRequest>(request);
1744
1777
  sendJson(response, 201, createFilesystemBrowseGrant(body, state, { cwd: repositoryPath }));
@@ -4427,6 +4460,82 @@ export function tryCreateFilesystemBrowseGrant(
4427
4460
  return ok({ capability });
4428
4461
  }
4429
4462
 
4463
+ export async function pickFilesystemDirectory(
4464
+ state: StudioServerState,
4465
+ options: { cwd?: string; picker?: NativeDirectoryPicker } = {}
4466
+ ): Promise<FilesystemDirectoryPickResult> {
4467
+ const cwd = options.cwd ?? process.cwd();
4468
+ const picker = options.picker ?? openNativeDirectoryPicker;
4469
+ const picked = await picker({ cwd });
4470
+ if (picked.status !== "selected") {
4471
+ return picked;
4472
+ }
4473
+ const grant = createFilesystemBrowseGrant({ path: picked.path }, state, { cwd });
4474
+ return {
4475
+ status: "selected",
4476
+ capability: grant.capability,
4477
+ entry: filesystemBrowseEntryForPath(grant.capability.path, grant.capability.rootId)
4478
+ };
4479
+ }
4480
+
4481
+ export async function openNativeDirectoryPicker(_options: NativeDirectoryPickerOptions): Promise<NativeDirectoryPickerResult> {
4482
+ if (process.platform === "darwin") {
4483
+ return runDirectoryPickerCommand("osascript", [
4484
+ "-e",
4485
+ "POSIX path of (choose folder with prompt \"Choose a project folder for Hunsu Studio\")"
4486
+ ], {
4487
+ cancelPatterns: [/User canceled/i, /-128/]
4488
+ });
4489
+ }
4490
+
4491
+ if (process.platform === "win32") {
4492
+ return runDirectoryPickerCommand("powershell.exe", [
4493
+ "-NoProfile",
4494
+ "-STA",
4495
+ "-Command",
4496
+ [
4497
+ "Add-Type -AssemblyName System.Windows.Forms;",
4498
+ "$dialog = New-Object System.Windows.Forms.FolderBrowserDialog;",
4499
+ "$dialog.Description = 'Choose a project folder for Hunsu Studio';",
4500
+ "$dialog.ShowNewFolderButton = $true;",
4501
+ "if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {",
4502
+ " [Console]::Out.WriteLine($dialog.SelectedPath);",
4503
+ " exit 0;",
4504
+ "}",
4505
+ "exit 2;"
4506
+ ].join(" ")
4507
+ ], {
4508
+ cancelCodes: [2]
4509
+ });
4510
+ }
4511
+
4512
+ const zenity = await runDirectoryPickerCommand("zenity", [
4513
+ "--file-selection",
4514
+ "--directory",
4515
+ "--title=Choose a project folder for Hunsu Studio"
4516
+ ], {
4517
+ cancelCodes: [1],
4518
+ unavailablePatterns: [/cannot open display/i, /no display/i]
4519
+ });
4520
+ if (zenity.status !== "unavailable") {
4521
+ return zenity;
4522
+ }
4523
+
4524
+ const kdialog = await runDirectoryPickerCommand("kdialog", [
4525
+ "--title",
4526
+ "Choose a project folder for Hunsu Studio",
4527
+ "--getexistingdirectory"
4528
+ ], {
4529
+ cancelCodes: [1],
4530
+ unavailablePatterns: [/cannot connect/i, /no display/i]
4531
+ });
4532
+ if (kdialog.status !== "unavailable") {
4533
+ return kdialog;
4534
+ }
4535
+
4536
+ return { status: "unavailable", reason: "No supported native folder picker is available in this environment." };
4537
+ }
4538
+
4430
4539
  function resolveRoadmapOpenRequestPath<T extends RoadmapOpenRequest>(request: T, state: StudioServerState): T {
4431
4540
  if (!request.browseToken) {
4432
4541
  return request;
@@ -4469,6 +4578,44 @@ function browseRootLabel(path: string): string {
4469
4578
  return basename(path) || path;
4470
4579
  }
4471
4580
 
4581
+ async function runDirectoryPickerCommand(
4582
+ command: string,
4583
+ args: string[],
4584
+ options: { cancelCodes?: number[]; cancelPatterns?: RegExp[]; unavailablePatterns?: RegExp[] } = {}
4585
+ ): Promise<NativeDirectoryPickerResult> {
4586
+ try {
4587
+ const { stdout } = await execFileAsync(command, args);
4588
+ const selectedPath = stdout.trim();
4589
+ return selectedPath ? { status: "selected", path: selectedPath } : { status: "cancelled" };
4590
+ } catch (error) {
4591
+ const pickerError = error as Error & { code?: string | number; stderr?: string; stdout?: string };
4592
+ const output = `${pickerError.stdout ?? ""}\n${pickerError.stderr ?? ""}\n${pickerError.message}`;
4593
+ if (pickerError.code === "ENOENT" || options.unavailablePatterns?.some(pattern => pattern.test(output))) {
4594
+ return { status: "unavailable", reason: `${command} is not available.` };
4595
+ }
4596
+ if (typeof pickerError.code === "number" && options.cancelCodes?.includes(pickerError.code)) {
4597
+ return { status: "cancelled" };
4598
+ }
4599
+ if (options.cancelPatterns?.some(pattern => pattern.test(output))) {
4600
+ return { status: "cancelled" };
4601
+ }
4602
+ return { status: "unavailable", reason: output.trim() || `${command} failed to open a folder picker.` };
4603
+ }
4604
+ }
4605
+
4606
+ function filesystemBrowseEntryForPath(path: CanonicalLocalPath, rootId: BrowseRootId): FilesystemBrowseEntry {
4607
+ const entryPath = String(path);
4608
+ return {
4609
+ kind: "directory",
4610
+ name: basename(entryPath) || entryPath,
4611
+ path: entryPath,
4612
+ rootId,
4613
+ type: "directory",
4614
+ isGitRepository: existsSync(join(entryPath, ".git")),
4615
+ isRoadmap: hasBrowseVisibleHunsuRuntimeFiles(entryPath)
4616
+ };
4617
+ }
4618
+
4472
4619
  function hashText(value: string): string {
4473
4620
  return hashHex(createHash("sha256").update(value));
4474
4621
  }
@@ -4523,7 +4670,12 @@ function canonicalizeGrantTargetPath(path: string, roots: string[]): string | un
4523
4670
  }
4524
4671
 
4525
4672
  function isAllowedBrowsePath(path: string, roots: string[]): boolean {
4526
- return roots.some(root => path === root || path.startsWith(`${root}/`));
4673
+ const normalizedPath = normalize(path);
4674
+ return roots.some(root => {
4675
+ const normalizedRoot = normalize(root);
4676
+ const childPath = relative(normalizedRoot, normalizedPath);
4677
+ return childPath === "" || (!childPath.startsWith("..") && !isAbsolute(childPath));
4678
+ });
4527
4679
  }
4528
4680
 
4529
4681
  function shouldHideBrowseEntry(name: string): boolean {