@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 +29 -0
- package/dist/index.js +125 -4
- package/package.json +2 -2
- package/src/index.ts +156 -4
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
|
-
|
|
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,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
|
-
|
|
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 {
|