@hunsu/bridge 0.1.2 → 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 +29 -0
- package/dist/index.js +122 -1
- package/package.json +3 -3
- package/src/index.ts +153 -1
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
|
@@ -662,6 +662,8 @@ 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");
|
|
@@ -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
|
-
|
|
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.
|
|
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,9 +29,9 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@hunsu/codex-runner": "0.1.1",
|
|
31
31
|
"@hunsu/core": "0.1.1",
|
|
32
|
-
"@hunsu/
|
|
32
|
+
"@hunsu/protocol-registry": "0.1.1",
|
|
33
33
|
"@hunsu/protocol": "0.1.1",
|
|
34
|
-
"@hunsu/
|
|
34
|
+
"@hunsu/config": "0.1.1"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"dev": "node --watch src/index.ts",
|
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 }
|
|
@@ -1695,6 +1712,8 @@ 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 {
|
|
@@ -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
|
-
|
|
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 {
|