@nextclaw/server 0.15.4 → 0.15.6
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 +46 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +538 -161
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import { compress } from "hono/compress";
|
|
3
3
|
import { serve } from "@hono/node-server";
|
|
4
|
-
import { AccessManager, PanelAppError, isPanelAppError, isPreferenceError, isServiceAppError } from "@nextclaw/kernel";
|
|
4
|
+
import { AccessManager, PanelAppError, isPanelAppError, isPreferenceError, isProjectError, isServiceAppError, isSessionSettingsError } from "@nextclaw/kernel";
|
|
5
5
|
import { WebSocket, WebSocketServer } from "ws";
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { open, readFile, readdir, realpath, stat } from "node:fs/promises";
|
|
8
|
-
import { basename, dirname, extname, isAbsolute, join, parse, resolve } from "node:path";
|
|
7
|
+
import { mkdir, open, readFile, readdir, realpath, stat } from "node:fs/promises";
|
|
8
|
+
import { basename, dirname, extname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
|
|
9
9
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
10
10
|
import * as NextclawCore from "@nextclaw/core";
|
|
11
|
-
import { ConfigSchema, DEFAULT_WORKSPACE_PATH, buildConfigSchema, createAgentProfile, expandHome, findEffectiveAgentProfile, getDataDir, getPackageVersion, getProviderName, hasSecretRef, isSensitiveConfigPath, loadConfig, mergeExtensionConfigView, normalizeModelThinkingCapability, normalizeProviderModelConfig,
|
|
12
|
-
import { homedir } from "node:os";
|
|
11
|
+
import { ConfigSchema, DEFAULT_WORKSPACE_PATH, buildConfigSchema, createAgentProfile, expandHome, findEffectiveAgentProfile, getDataDir, getPackageVersion, getProviderName, hasSecretRef, isSensitiveConfigPath, loadConfig, mergeExtensionConfigView, normalizeModelThinkingCapability, normalizeProviderModelConfig, probeFeishu, readAgentAvatarContent, removeAgentProfile, resolveEffectiveAgentProfiles, saveConfig, toExtensionConfigView, updateAgentProfile } from "@nextclaw/core";
|
|
12
|
+
import { homedir, platform } from "node:os";
|
|
13
13
|
import { findBuiltinProviderByName, listBuiltinProviders } from "@nextclaw/runtime";
|
|
14
14
|
import { McpInstalledViewService } from "@nextclaw/mcp";
|
|
15
15
|
import { serveStatic } from "hono/serve-static";
|
|
@@ -2495,22 +2495,23 @@ function buildScopedProviderModel(providerName, model, spec) {
|
|
|
2495
2495
|
if (!prefix) return trimmed;
|
|
2496
2496
|
return `${prefix}/${trimmed}`;
|
|
2497
2497
|
}
|
|
2498
|
-
function
|
|
2498
|
+
function rewriteProviderRoutePrefix(providerId, targetPrefix, model) {
|
|
2499
2499
|
const prefix = `${providerId}/`;
|
|
2500
2500
|
if (!model.startsWith(prefix)) return model;
|
|
2501
|
-
|
|
2501
|
+
const stripped = model.slice(prefix.length).trim();
|
|
2502
|
+
return stripped ? targetPrefix ? `${targetPrefix}/${stripped}` : stripped : model;
|
|
2502
2503
|
}
|
|
2503
2504
|
function resolveTestModel(config, providerId, requestedModel, provider, spec) {
|
|
2504
|
-
if (requestedModel) return
|
|
2505
|
+
if (requestedModel) return rewriteProviderRoutePrefix(providerId, spec?.name ?? null, requestedModel);
|
|
2505
2506
|
const providerModels = normalizeModelList(provider.models ?? []).map((modelId) => {
|
|
2506
|
-
const providerModel =
|
|
2507
|
+
const providerModel = rewriteProviderRoutePrefix(providerId, null, modelId);
|
|
2507
2508
|
return spec ? buildScopedProviderModel(spec.name, providerModel, spec) : providerModel;
|
|
2508
2509
|
}).filter((modelId) => modelId.length > 0);
|
|
2509
2510
|
if (providerModels.length > 0) return providerModels[0];
|
|
2510
2511
|
const defaultModel = normalizeOptionalString(config.agents.defaults.model);
|
|
2511
2512
|
if (defaultModel) {
|
|
2512
2513
|
const routedProvider = getProviderName(config, defaultModel);
|
|
2513
|
-
if (!routedProvider || routedProvider === providerId) return
|
|
2514
|
+
if (!routedProvider || routedProvider === providerId) return rewriteProviderRoutePrefix(providerId, spec?.name ?? null, defaultModel);
|
|
2514
2515
|
}
|
|
2515
2516
|
if (!spec) return null;
|
|
2516
2517
|
return normalizeModelList(spec?.defaultModels ?? [])[0] ?? null ?? defaultModel ?? null;
|
|
@@ -2930,67 +2931,6 @@ var NcpAssetRoutesController = class {
|
|
|
2930
2931
|
} });
|
|
2931
2932
|
};
|
|
2932
2933
|
};
|
|
2933
|
-
//#endregion
|
|
2934
|
-
//#region src/features/sessions/utils/session-preference-patch.utils.ts
|
|
2935
|
-
function applySessionPreferencePatch(params) {
|
|
2936
|
-
const nextMetadata = params.metadata;
|
|
2937
|
-
const { patch } = params;
|
|
2938
|
-
if (Object.prototype.hasOwnProperty.call(patch, "label")) {
|
|
2939
|
-
const label = typeof patch.label === "string" ? patch.label.trim() : "";
|
|
2940
|
-
if (label) nextMetadata.label = label;
|
|
2941
|
-
else delete nextMetadata.label;
|
|
2942
|
-
}
|
|
2943
|
-
if (Object.prototype.hasOwnProperty.call(patch, "preferredModel")) {
|
|
2944
|
-
const preferredModel = typeof patch.preferredModel === "string" ? patch.preferredModel.trim() : "";
|
|
2945
|
-
if (preferredModel) {
|
|
2946
|
-
nextMetadata.preferred_model = preferredModel;
|
|
2947
|
-
nextMetadata.model = preferredModel;
|
|
2948
|
-
} else {
|
|
2949
|
-
delete nextMetadata.preferred_model;
|
|
2950
|
-
delete nextMetadata.model;
|
|
2951
|
-
}
|
|
2952
|
-
}
|
|
2953
|
-
if (Object.prototype.hasOwnProperty.call(patch, "preferredThinking")) {
|
|
2954
|
-
const preferredThinking = typeof patch.preferredThinking === "string" ? patch.preferredThinking.trim() : "";
|
|
2955
|
-
if (!preferredThinking) delete nextMetadata.preferred_thinking;
|
|
2956
|
-
else {
|
|
2957
|
-
const normalizedThinking = parseThinkingLevel(preferredThinking);
|
|
2958
|
-
if (!normalizedThinking) throw params.createInvalidThinkingError("preferredThinking must be a supported thinking level");
|
|
2959
|
-
nextMetadata.preferred_thinking = normalizedThinking;
|
|
2960
|
-
}
|
|
2961
|
-
}
|
|
2962
|
-
return nextMetadata;
|
|
2963
|
-
}
|
|
2964
|
-
//#endregion
|
|
2965
|
-
//#region src/features/sessions/utils/session-project-root.utils.ts
|
|
2966
|
-
var SessionProjectRootValidationError = class extends Error {
|
|
2967
|
-
constructor(code, message) {
|
|
2968
|
-
super(message);
|
|
2969
|
-
this.code = code;
|
|
2970
|
-
this.name = "SessionProjectRootValidationError";
|
|
2971
|
-
}
|
|
2972
|
-
};
|
|
2973
|
-
function resolveCandidateProjectRoot(value) {
|
|
2974
|
-
return resolve(expandHome(value));
|
|
2975
|
-
}
|
|
2976
|
-
async function normalizeSessionProjectRoot(value) {
|
|
2977
|
-
if (value == null) return null;
|
|
2978
|
-
if (typeof value !== "string") throw new SessionProjectRootValidationError("PROJECT_ROOT_INVALID_TYPE", "projectRoot must be a string or null");
|
|
2979
|
-
const trimmed = value.trim();
|
|
2980
|
-
if (!trimmed) return null;
|
|
2981
|
-
const candidate = resolveCandidateProjectRoot(trimmed);
|
|
2982
|
-
let canonicalPath;
|
|
2983
|
-
try {
|
|
2984
|
-
canonicalPath = await realpath(candidate);
|
|
2985
|
-
} catch {
|
|
2986
|
-
throw new SessionProjectRootValidationError("PROJECT_ROOT_NOT_FOUND", "projectRoot directory does not exist");
|
|
2987
|
-
}
|
|
2988
|
-
if (!(await stat(canonicalPath)).isDirectory()) throw new SessionProjectRootValidationError("PROJECT_ROOT_NOT_DIRECTORY", "projectRoot must point to a directory");
|
|
2989
|
-
return canonicalPath;
|
|
2990
|
-
}
|
|
2991
|
-
function isSessionProjectRootValidationError(error) {
|
|
2992
|
-
return error instanceof SessionProjectRootValidationError;
|
|
2993
|
-
}
|
|
2994
2934
|
const DEFAULT_MARKETPLACE_READ_API_BASES = ["https://api.nextclaw.net", "https://marketplace-api.nextclaw.io"];
|
|
2995
2935
|
const MARKETPLACE_ZH_COPY_BY_SLUG = {
|
|
2996
2936
|
weather: {
|
|
@@ -3759,7 +3699,8 @@ function mountMarketplaceRoutes(app, controllers) {
|
|
|
3759
3699
|
const SCOPE_SORT_ORDER = {
|
|
3760
3700
|
project: 0,
|
|
3761
3701
|
workspace: 1,
|
|
3762
|
-
|
|
3702
|
+
global: 2,
|
|
3703
|
+
builtin: 3
|
|
3763
3704
|
};
|
|
3764
3705
|
var SessionSkillsViewBuilder = class {
|
|
3765
3706
|
constructor(options) {
|
|
@@ -3773,7 +3714,8 @@ var SessionSkillsViewBuilder = class {
|
|
|
3773
3714
|
});
|
|
3774
3715
|
const skillsLoader = new NextclawCore.SkillsLoader({
|
|
3775
3716
|
workspace: projectContext.hostWorkspace,
|
|
3776
|
-
projectRoot: projectContext.projectRoot
|
|
3717
|
+
projectRoot: projectContext.projectRoot,
|
|
3718
|
+
includeGlobal: true
|
|
3777
3719
|
});
|
|
3778
3720
|
const availableRefs = new Set(skillsLoader.listSkills(true).map((skill) => skill.ref));
|
|
3779
3721
|
const records = skillsLoader.listSkills(false).map((skill) => this.buildRecord(skill, skillsLoader, availableRefs)).sort((left, right) => this.compareRecords(left, right));
|
|
@@ -3810,6 +3752,23 @@ var SessionSkillsViewBuilder = class {
|
|
|
3810
3752
|
//#endregion
|
|
3811
3753
|
//#region src/features/sessions/controllers/sessions.controller.ts
|
|
3812
3754
|
const INTERRUPTED_SESSION_STATUS_TEXT = "Run interrupted: no completion or error event was recorded. Please send the message again.";
|
|
3755
|
+
function sessionProjectError(error) {
|
|
3756
|
+
switch (error.code) {
|
|
3757
|
+
case "PROJECT_PATH_INVALID_TYPE": return {
|
|
3758
|
+
code: "PROJECT_ROOT_INVALID_TYPE",
|
|
3759
|
+
message: "projectRoot must be a string or null"
|
|
3760
|
+
};
|
|
3761
|
+
case "PROJECT_PATH_NOT_FOUND": return {
|
|
3762
|
+
code: "PROJECT_ROOT_NOT_FOUND",
|
|
3763
|
+
message: "projectRoot directory does not exist"
|
|
3764
|
+
};
|
|
3765
|
+
case "PROJECT_PATH_NOT_DIRECTORY": return {
|
|
3766
|
+
code: "PROJECT_ROOT_NOT_DIRECTORY",
|
|
3767
|
+
message: "projectRoot must point to a directory"
|
|
3768
|
+
};
|
|
3769
|
+
default: return error;
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3813
3772
|
function readPositiveInt(value) {
|
|
3814
3773
|
if (typeof value !== "string") return;
|
|
3815
3774
|
const parsed = Number.parseInt(value, 10);
|
|
@@ -3843,49 +3802,6 @@ function normalizeSessionActivityPreview(session) {
|
|
|
3843
3802
|
}
|
|
3844
3803
|
};
|
|
3845
3804
|
}
|
|
3846
|
-
function applySessionTypePatch(metadata, patch) {
|
|
3847
|
-
if (!Object.prototype.hasOwnProperty.call(patch, "sessionType")) return metadata;
|
|
3848
|
-
const sessionType = typeof patch.sessionType === "string" ? patch.sessionType.trim() : "";
|
|
3849
|
-
if (sessionType) {
|
|
3850
|
-
const { sessionType: _removed, ...nextMetadata } = metadata;
|
|
3851
|
-
return {
|
|
3852
|
-
...nextMetadata,
|
|
3853
|
-
session_type: sessionType,
|
|
3854
|
-
runtime: sessionType
|
|
3855
|
-
};
|
|
3856
|
-
}
|
|
3857
|
-
const { runtime: _runtime, session_type: _sessionType, sessionType: _camelSessionType, ...nextMetadata } = metadata;
|
|
3858
|
-
return nextMetadata;
|
|
3859
|
-
}
|
|
3860
|
-
function applyUiReadAtPatch(metadata, patch) {
|
|
3861
|
-
if (!Object.prototype.hasOwnProperty.call(patch, "uiReadAt")) return metadata;
|
|
3862
|
-
const uiReadAt = typeof patch.uiReadAt === "string" ? patch.uiReadAt.trim() : "";
|
|
3863
|
-
if (uiReadAt) return {
|
|
3864
|
-
...metadata,
|
|
3865
|
-
ui_last_read_at: uiReadAt
|
|
3866
|
-
};
|
|
3867
|
-
const { ui_last_read_at: _removed, ...nextMetadata } = metadata;
|
|
3868
|
-
return nextMetadata;
|
|
3869
|
-
}
|
|
3870
|
-
function applyProjectRootPatch(metadata, projectRoot) {
|
|
3871
|
-
if (projectRoot === void 0) return metadata;
|
|
3872
|
-
if (projectRoot) {
|
|
3873
|
-
const { projectRoot: _removed, ...nextMetadata } = metadata;
|
|
3874
|
-
return {
|
|
3875
|
-
...nextMetadata,
|
|
3876
|
-
project_root: projectRoot
|
|
3877
|
-
};
|
|
3878
|
-
}
|
|
3879
|
-
const { project_root: _projectRoot, projectRoot: _camelProjectRoot, ...nextMetadata } = metadata;
|
|
3880
|
-
return nextMetadata;
|
|
3881
|
-
}
|
|
3882
|
-
function buildPatchedSessionMetadata(metadata, patch, projectRootPatch) {
|
|
3883
|
-
return applyProjectRootPatch(applyUiReadAtPatch(applySessionTypePatch(applySessionPreferencePatch({
|
|
3884
|
-
metadata: structuredClone(metadata),
|
|
3885
|
-
patch,
|
|
3886
|
-
createInvalidThinkingError: () => /* @__PURE__ */ new Error("PREFERRED_THINKING_INVALID")
|
|
3887
|
-
}), patch), patch), projectRootPatch);
|
|
3888
|
-
}
|
|
3889
3805
|
var NcpSessionRoutesController = class {
|
|
3890
3806
|
sessionSkillsViewBuilder;
|
|
3891
3807
|
constructor(options) {
|
|
@@ -3941,14 +3857,17 @@ var NcpSessionRoutesController = class {
|
|
|
3941
3857
|
const hasProjectRootOverride = Object.prototype.hasOwnProperty.call(query, "projectRoot");
|
|
3942
3858
|
const metadata = readSessionMetadata((await sessionManager.getSession(sessionId))?.metadata);
|
|
3943
3859
|
if (hasProjectRootOverride) try {
|
|
3944
|
-
const projectRoot = await
|
|
3860
|
+
const projectRoot = await this.options.kernel.projectManager.resolveExistingProjectRoot(query.projectRoot);
|
|
3945
3861
|
if (projectRoot) metadata.project_root = projectRoot;
|
|
3946
3862
|
else {
|
|
3947
3863
|
delete metadata.project_root;
|
|
3948
3864
|
delete metadata.projectRoot;
|
|
3949
3865
|
}
|
|
3950
3866
|
} catch (error) {
|
|
3951
|
-
if (
|
|
3867
|
+
if (isProjectError(error)) {
|
|
3868
|
+
const mapped = sessionProjectError(error);
|
|
3869
|
+
return c.json(err(mapped.code, mapped.message), 400);
|
|
3870
|
+
}
|
|
3952
3871
|
throw error;
|
|
3953
3872
|
}
|
|
3954
3873
|
return c.json(ok(this.sessionSkillsViewBuilder.build({
|
|
@@ -3963,19 +3882,17 @@ var NcpSessionRoutesController = class {
|
|
|
3963
3882
|
if (!body.ok || !body.data || typeof body.data !== "object") return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3964
3883
|
const patch = body.data;
|
|
3965
3884
|
if (patch.clearHistory) return c.json(err("UNSUPPORTED_PATCH", "clearHistory is not supported for ncp sessions"), 400);
|
|
3966
|
-
let
|
|
3885
|
+
let updated;
|
|
3967
3886
|
try {
|
|
3968
|
-
|
|
3969
|
-
const existing = await sessionManager.getSessionRecord(sessionId);
|
|
3970
|
-
const metadata = buildPatchedSessionMetadata(readSessionMetadata(existing?.metadata), patch, projectRootPatch);
|
|
3971
|
-
patched = existing ? await sessionManager.setSessionMetadata(sessionId, metadata) : Boolean(await sessionManager.updateSession(sessionId, { metadata }));
|
|
3887
|
+
updated = await sessionManager.patchSessionSettings(sessionId, patch, { createIfMissing: true });
|
|
3972
3888
|
} catch (error) {
|
|
3973
|
-
if (error
|
|
3974
|
-
if (
|
|
3889
|
+
if (isSessionSettingsError(error)) return c.json(err(error.code, error.message), 400);
|
|
3890
|
+
if (isProjectError(error)) {
|
|
3891
|
+
const mapped = sessionProjectError(error);
|
|
3892
|
+
return c.json(err(mapped.code, mapped.message), 400);
|
|
3893
|
+
}
|
|
3975
3894
|
throw error;
|
|
3976
3895
|
}
|
|
3977
|
-
if (!patched) return c.json(err("NOT_FOUND", `ncp session not found: ${sessionId}`), 404);
|
|
3978
|
-
const updated = await sessionManager.getSession(sessionId);
|
|
3979
3896
|
if (!updated) return c.json(err("NOT_FOUND", `ncp session not found: ${sessionId}`), 404);
|
|
3980
3897
|
return c.json(ok(this.withRuntimeStatus(updated)));
|
|
3981
3898
|
};
|
|
@@ -4450,6 +4367,31 @@ var PreferencesRoutesController = class {
|
|
|
4450
4367
|
};
|
|
4451
4368
|
};
|
|
4452
4369
|
//#endregion
|
|
4370
|
+
//#region src/features/projects/controllers/projects.controller.ts
|
|
4371
|
+
var ProjectsRoutesController = class {
|
|
4372
|
+
constructor(projectManager) {
|
|
4373
|
+
this.projectManager = projectManager;
|
|
4374
|
+
}
|
|
4375
|
+
list = async (c) => {
|
|
4376
|
+
const projects = await this.projectManager.listProjects();
|
|
4377
|
+
return c.json(ok({
|
|
4378
|
+
projects,
|
|
4379
|
+
templates: this.projectManager.listTemplates(),
|
|
4380
|
+
total: projects.length
|
|
4381
|
+
}));
|
|
4382
|
+
};
|
|
4383
|
+
create = async (c) => {
|
|
4384
|
+
const body = await readJson(c.req.raw);
|
|
4385
|
+
if (!body.ok || !isRecord$2(body.data) || typeof body.data.name !== "string") return c.json(err("INVALID_PROJECT", "project name is required"), 400);
|
|
4386
|
+
try {
|
|
4387
|
+
return c.json(ok(await this.projectManager.createProject(body.data)), 201);
|
|
4388
|
+
} catch (error) {
|
|
4389
|
+
if (isProjectError(error)) return c.json(err(error.code, error.message), 400);
|
|
4390
|
+
throw error;
|
|
4391
|
+
}
|
|
4392
|
+
};
|
|
4393
|
+
};
|
|
4394
|
+
//#endregion
|
|
4453
4395
|
//#region src/features/service-apps/controllers/service-apps.controller.ts
|
|
4454
4396
|
const PANEL_BRIDGE_SESSION_HEADER = "x-nextclaw-panel-bridge-session";
|
|
4455
4397
|
function statusForServiceAppError(code) {
|
|
@@ -4650,6 +4592,100 @@ function createNcpSessionEventStreamResponse(eventBus, payload, signal) {
|
|
|
4650
4592
|
} });
|
|
4651
4593
|
}
|
|
4652
4594
|
//#endregion
|
|
4595
|
+
//#region src/features/server-path/utils/server-path-locations.utils.ts
|
|
4596
|
+
const xdgLocationKinds = {
|
|
4597
|
+
XDG_DESKTOP_DIR: "desktop",
|
|
4598
|
+
XDG_DOCUMENTS_DIR: "documents",
|
|
4599
|
+
XDG_DOWNLOAD_DIR: "downloads"
|
|
4600
|
+
};
|
|
4601
|
+
function readAbsoluteEnvironmentPath(name) {
|
|
4602
|
+
const value = process.env[name]?.trim();
|
|
4603
|
+
return value && isAbsolute(value) ? resolve(value) : null;
|
|
4604
|
+
}
|
|
4605
|
+
function parseXdgLocationPath(value, homePath) {
|
|
4606
|
+
const expanded = value.trim().replace(/^(['"])(.*)\1$/, "$2").replace(/^\$(?:HOME|\{HOME\})(?=\/|$)/, homePath);
|
|
4607
|
+
const resolvedPath = isAbsolute(expanded) ? resolve(expanded) : null;
|
|
4608
|
+
return resolvedPath && resolvedPath !== homePath ? resolvedPath : null;
|
|
4609
|
+
}
|
|
4610
|
+
async function readXdgLocationPaths(homePath) {
|
|
4611
|
+
const configRoot = readAbsoluteEnvironmentPath("XDG_CONFIG_HOME") ?? join(homePath, ".config");
|
|
4612
|
+
let content;
|
|
4613
|
+
try {
|
|
4614
|
+
content = await readFile(join(configRoot, "user-dirs.dirs"), "utf8");
|
|
4615
|
+
} catch {
|
|
4616
|
+
return {};
|
|
4617
|
+
}
|
|
4618
|
+
const locations = {};
|
|
4619
|
+
for (const line of content.split(/\r?\n/)) {
|
|
4620
|
+
const separatorIndex = line.indexOf("=");
|
|
4621
|
+
if (separatorIndex < 1) continue;
|
|
4622
|
+
const kind = xdgLocationKinds[line.slice(0, separatorIndex).trim()];
|
|
4623
|
+
if (!kind) continue;
|
|
4624
|
+
const path = parseXdgLocationPath(line.slice(separatorIndex + 1), homePath);
|
|
4625
|
+
if (path) locations[kind] = path;
|
|
4626
|
+
}
|
|
4627
|
+
return locations;
|
|
4628
|
+
}
|
|
4629
|
+
async function resolveFirstDirectory(kind, candidatePaths) {
|
|
4630
|
+
const uniquePaths = [...new Set(candidatePaths.filter((path) => Boolean(path)))];
|
|
4631
|
+
for (const path of uniquePaths) try {
|
|
4632
|
+
if ((await stat(path)).isDirectory()) return {
|
|
4633
|
+
kind,
|
|
4634
|
+
path
|
|
4635
|
+
};
|
|
4636
|
+
} catch {}
|
|
4637
|
+
return null;
|
|
4638
|
+
}
|
|
4639
|
+
async function resolveServerPathLocations(homePath) {
|
|
4640
|
+
const serverPlatform = platform();
|
|
4641
|
+
const xdgLocations = serverPlatform === "linux" ? await readXdgLocationPaths(homePath) : {};
|
|
4642
|
+
const oneDriveRoots = serverPlatform === "win32" ? [
|
|
4643
|
+
"OneDrive",
|
|
4644
|
+
"OneDriveConsumer",
|
|
4645
|
+
"OneDriveCommercial"
|
|
4646
|
+
].map(readAbsoluteEnvironmentPath).filter((path) => path !== null) : [];
|
|
4647
|
+
const homeLocations = await Promise.all([
|
|
4648
|
+
resolveFirstDirectory("desktop", [
|
|
4649
|
+
xdgLocations.desktop,
|
|
4650
|
+
...oneDriveRoots.map((path) => join(path, "Desktop")),
|
|
4651
|
+
join(homePath, "Desktop")
|
|
4652
|
+
]),
|
|
4653
|
+
resolveFirstDirectory("documents", [
|
|
4654
|
+
xdgLocations.documents,
|
|
4655
|
+
...oneDriveRoots.map((path) => join(path, "Documents")),
|
|
4656
|
+
join(homePath, "Documents")
|
|
4657
|
+
]),
|
|
4658
|
+
resolveFirstDirectory("downloads", [xdgLocations.downloads, join(homePath, "Downloads")])
|
|
4659
|
+
]);
|
|
4660
|
+
const macLocations = serverPlatform === "darwin" ? await Promise.all([
|
|
4661
|
+
resolveFirstDirectory("icloud-drive", [join(homePath, "Library", "Mobile Documents", "com~apple~CloudDocs")]),
|
|
4662
|
+
resolveFirstDirectory("applications", ["/Applications"]),
|
|
4663
|
+
resolveFirstDirectory("volumes", ["/Volumes"])
|
|
4664
|
+
]) : [];
|
|
4665
|
+
return [...homeLocations, ...macLocations].filter((location) => location !== null);
|
|
4666
|
+
}
|
|
4667
|
+
//#endregion
|
|
4668
|
+
//#region src/features/server-path/utils/server-path-resolution.utils.ts
|
|
4669
|
+
var ServerPathResolutionError = class extends Error {
|
|
4670
|
+
constructor(message) {
|
|
4671
|
+
super(message);
|
|
4672
|
+
this.name = "ServerPathResolutionError";
|
|
4673
|
+
}
|
|
4674
|
+
};
|
|
4675
|
+
function resolveServerPath(params) {
|
|
4676
|
+
const { path, basePath: rawBasePath, defaultToHome } = params;
|
|
4677
|
+
const rawPath = typeof path === "string" ? path.trim() : "";
|
|
4678
|
+
const basePath = typeof rawBasePath === "string" ? rawBasePath.trim() : "";
|
|
4679
|
+
if (!rawPath) {
|
|
4680
|
+
if (defaultToHome) return resolve(expandHome(basePath || homedir()));
|
|
4681
|
+
throw new ServerPathResolutionError("server path is required");
|
|
4682
|
+
}
|
|
4683
|
+
const expandedPath = expandHome(rawPath);
|
|
4684
|
+
if (isAbsolute(expandedPath)) return resolve(expandedPath);
|
|
4685
|
+
if (!basePath) throw new ServerPathResolutionError("relative server path requires a base path");
|
|
4686
|
+
return resolve(expandHome(basePath), expandedPath);
|
|
4687
|
+
}
|
|
4688
|
+
//#endregion
|
|
4653
4689
|
//#region src/features/server-path/utils/server-path-browse.utils.ts
|
|
4654
4690
|
var ServerPathBrowseError = class extends Error {
|
|
4655
4691
|
constructor(code, message) {
|
|
@@ -4658,15 +4694,16 @@ var ServerPathBrowseError = class extends Error {
|
|
|
4658
4694
|
this.name = "ServerPathBrowseError";
|
|
4659
4695
|
}
|
|
4660
4696
|
};
|
|
4661
|
-
function
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4697
|
+
function resolveBrowsePath(options) {
|
|
4698
|
+
try {
|
|
4699
|
+
return resolveServerPath({
|
|
4700
|
+
...options,
|
|
4701
|
+
defaultToHome: true
|
|
4702
|
+
});
|
|
4703
|
+
} catch (error) {
|
|
4704
|
+
if (error instanceof ServerPathResolutionError) throw new ServerPathBrowseError("SERVER_PATH_BASE_REQUIRED", error.message);
|
|
4705
|
+
throw error;
|
|
4706
|
+
}
|
|
4670
4707
|
}
|
|
4671
4708
|
function readRootLabel(rootPath) {
|
|
4672
4709
|
return rootPath.replace(/[\\/]+$/, "") || rootPath;
|
|
@@ -4712,7 +4749,7 @@ function sortEntryViews(left, right) {
|
|
|
4712
4749
|
return left.name.localeCompare(right.name);
|
|
4713
4750
|
}
|
|
4714
4751
|
async function browseServerPath(options = {}) {
|
|
4715
|
-
const currentPath =
|
|
4752
|
+
const currentPath = resolveBrowsePath(options);
|
|
4716
4753
|
let currentPathStats;
|
|
4717
4754
|
try {
|
|
4718
4755
|
currentPathStats = await stat(currentPath);
|
|
@@ -4732,41 +4769,65 @@ async function browseServerPath(options = {}) {
|
|
|
4732
4769
|
includeFiles: options.includeFiles ?? false
|
|
4733
4770
|
})))).filter((entry) => entry !== null).sort(sortEntryViews);
|
|
4734
4771
|
const parentPath = dirname(currentPath);
|
|
4772
|
+
const homePath = resolve(homedir());
|
|
4735
4773
|
return {
|
|
4736
4774
|
currentPath,
|
|
4737
4775
|
parentPath: parentPath === currentPath ? null : parentPath,
|
|
4738
|
-
homePath
|
|
4776
|
+
homePath,
|
|
4739
4777
|
breadcrumbs: buildBreadcrumbs(currentPath),
|
|
4740
|
-
entries
|
|
4778
|
+
entries,
|
|
4779
|
+
locations: await resolveServerPathLocations(homePath)
|
|
4741
4780
|
};
|
|
4742
4781
|
}
|
|
4743
4782
|
function isServerPathBrowseError(error) {
|
|
4744
4783
|
return error instanceof ServerPathBrowseError;
|
|
4745
4784
|
}
|
|
4746
4785
|
//#endregion
|
|
4747
|
-
//#region src/features/server-path/utils/server-path-
|
|
4748
|
-
var
|
|
4749
|
-
constructor(message) {
|
|
4786
|
+
//#region src/features/server-path/utils/server-path-directory.utils.ts
|
|
4787
|
+
var ServerPathDirectoryCreateError = class extends Error {
|
|
4788
|
+
constructor(code, message) {
|
|
4750
4789
|
super(message);
|
|
4751
|
-
this.
|
|
4790
|
+
this.code = code;
|
|
4791
|
+
this.name = "ServerPathDirectoryCreateError";
|
|
4752
4792
|
}
|
|
4753
4793
|
};
|
|
4754
|
-
function
|
|
4755
|
-
const
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4794
|
+
function normalizeDirectoryName(value) {
|
|
4795
|
+
const name = typeof value === "string" ? value.trim() : "";
|
|
4796
|
+
if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\\")) throw new ServerPathDirectoryCreateError("SERVER_PATH_DIRECTORY_NAME_INVALID", "directory name is invalid");
|
|
4797
|
+
return name;
|
|
4798
|
+
}
|
|
4799
|
+
function readFileErrorCode(error) {
|
|
4800
|
+
return typeof error === "object" && error !== null && "code" in error ? String(error.code ?? "") : void 0;
|
|
4801
|
+
}
|
|
4802
|
+
async function createServerPathDirectory(input) {
|
|
4803
|
+
let parentPath;
|
|
4804
|
+
try {
|
|
4805
|
+
parentPath = resolveServerPath({ path: typeof input.parentPath === "string" ? input.parentPath : null });
|
|
4806
|
+
} catch (error) {
|
|
4807
|
+
if (error instanceof ServerPathResolutionError) throw new ServerPathDirectoryCreateError("SERVER_PATH_PARENT_INVALID", error.message);
|
|
4808
|
+
throw error;
|
|
4761
4809
|
}
|
|
4762
|
-
const
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4810
|
+
const directoryPath = join(parentPath, normalizeDirectoryName(input.name));
|
|
4811
|
+
try {
|
|
4812
|
+
await mkdir(directoryPath);
|
|
4813
|
+
return { path: directoryPath };
|
|
4814
|
+
} catch (error) {
|
|
4815
|
+
const code = readFileErrorCode(error);
|
|
4816
|
+
if (code === "EEXIST") throw new ServerPathDirectoryCreateError("SERVER_PATH_DIRECTORY_EXISTS", "directory already exists");
|
|
4817
|
+
if (code === "ENOENT") throw new ServerPathDirectoryCreateError("SERVER_PATH_PARENT_NOT_FOUND", "parent directory does not exist");
|
|
4818
|
+
if (code === "ENOTDIR") throw new ServerPathDirectoryCreateError("SERVER_PATH_PARENT_NOT_DIRECTORY", "parent path must point to a directory");
|
|
4819
|
+
if (code === "EACCES" || code === "EPERM" || code === "EROFS") throw new ServerPathDirectoryCreateError("SERVER_PATH_PARENT_NOT_WRITABLE", "parent directory is not writable");
|
|
4820
|
+
throw error;
|
|
4821
|
+
}
|
|
4822
|
+
}
|
|
4823
|
+
function isServerPathDirectoryCreateError(error) {
|
|
4824
|
+
return error instanceof ServerPathDirectoryCreateError;
|
|
4766
4825
|
}
|
|
4767
4826
|
//#endregion
|
|
4768
4827
|
//#region src/features/server-path/utils/server-path-read.utils.ts
|
|
4769
4828
|
const DEFAULT_PREVIEW_MAX_BYTES = 2e5;
|
|
4829
|
+
const PREVIEW_CONTEXT_LINES = 20;
|
|
4830
|
+
const PREVIEW_SCAN_CHUNK_BYTES = 64e3;
|
|
4770
4831
|
const MARKDOWN_EXTENSIONS = new Set([
|
|
4771
4832
|
".md",
|
|
4772
4833
|
".mdx",
|
|
@@ -4829,16 +4890,66 @@ function readLanguageHint(path) {
|
|
|
4829
4890
|
if (MARKDOWN_EXTENSIONS.has(extension)) return "markdown";
|
|
4830
4891
|
return extension.slice(1) || null;
|
|
4831
4892
|
}
|
|
4893
|
+
async function findPreviewWindowLocation(params) {
|
|
4894
|
+
const { fileHandle, maxBytes, sizeBytes, targetLine } = params;
|
|
4895
|
+
const lineStarts = [{
|
|
4896
|
+
line: 1,
|
|
4897
|
+
offset: 0
|
|
4898
|
+
}];
|
|
4899
|
+
const chunk = Buffer.alloc(Math.min(PREVIEW_SCAN_CHUNK_BYTES, sizeBytes));
|
|
4900
|
+
let currentLine = 1;
|
|
4901
|
+
let fileOffset = 0;
|
|
4902
|
+
while (fileOffset < sizeBytes) {
|
|
4903
|
+
const bytesToRead = Math.min(chunk.length, sizeBytes - fileOffset);
|
|
4904
|
+
const { bytesRead } = await fileHandle.read(chunk, 0, bytesToRead, fileOffset);
|
|
4905
|
+
if (bytesRead === 0) break;
|
|
4906
|
+
for (let index = 0; index < bytesRead; index += 1) {
|
|
4907
|
+
if (chunk[index] !== 10) continue;
|
|
4908
|
+
currentLine += 1;
|
|
4909
|
+
const targetOffset = fileOffset + index + 1;
|
|
4910
|
+
lineStarts.push({
|
|
4911
|
+
line: currentLine,
|
|
4912
|
+
offset: targetOffset
|
|
4913
|
+
});
|
|
4914
|
+
if (lineStarts.length > PREVIEW_CONTEXT_LINES + 1) lineStarts.shift();
|
|
4915
|
+
if (currentLine === targetLine) {
|
|
4916
|
+
const contextStart = lineStarts[0];
|
|
4917
|
+
return targetOffset - contextStart.offset < maxBytes ? {
|
|
4918
|
+
offset: contextStart.offset,
|
|
4919
|
+
startLine: contextStart.line
|
|
4920
|
+
} : {
|
|
4921
|
+
offset: targetOffset,
|
|
4922
|
+
startLine: targetLine
|
|
4923
|
+
};
|
|
4924
|
+
}
|
|
4925
|
+
}
|
|
4926
|
+
fileOffset += bytesRead;
|
|
4927
|
+
}
|
|
4928
|
+
return null;
|
|
4929
|
+
}
|
|
4832
4930
|
async function readFilePreviewBytes(params) {
|
|
4833
|
-
const { maxBytes, resolvedPath, sizeBytes } = params;
|
|
4834
|
-
const bytesToRead = Math.min(sizeBytes, maxBytes);
|
|
4931
|
+
const { maxBytes, resolvedPath, sizeBytes, targetLine } = params;
|
|
4835
4932
|
const fileHandle = await open(resolvedPath, "r");
|
|
4836
4933
|
try {
|
|
4934
|
+
const { offset, startLine } = (targetLine === 1 ? {
|
|
4935
|
+
offset: 0,
|
|
4936
|
+
startLine: 1
|
|
4937
|
+
} : targetLine ? await findPreviewWindowLocation({
|
|
4938
|
+
fileHandle,
|
|
4939
|
+
maxBytes,
|
|
4940
|
+
sizeBytes,
|
|
4941
|
+
targetLine
|
|
4942
|
+
}) : null) ?? {
|
|
4943
|
+
offset: 0,
|
|
4944
|
+
startLine: 1
|
|
4945
|
+
};
|
|
4946
|
+
const bytesToRead = Math.min(sizeBytes - offset, maxBytes);
|
|
4837
4947
|
const buffer = Buffer.alloc(bytesToRead);
|
|
4838
|
-
const { bytesRead } = await fileHandle.read(buffer, 0, bytesToRead,
|
|
4948
|
+
const { bytesRead } = await fileHandle.read(buffer, 0, bytesToRead, offset);
|
|
4839
4949
|
return {
|
|
4840
4950
|
buffer: buffer.subarray(0, bytesRead),
|
|
4841
|
-
|
|
4951
|
+
startLine,
|
|
4952
|
+
truncated: offset > 0 || offset + bytesRead < sizeBytes
|
|
4842
4953
|
};
|
|
4843
4954
|
} finally {
|
|
4844
4955
|
await fileHandle.close();
|
|
@@ -4865,10 +4976,12 @@ async function readServerPath(options = {}) {
|
|
|
4865
4976
|
if (!resolvedStats.isFile()) throw new ServerPathReadError("SERVER_PATH_NOT_FILE", "server path must point to a file");
|
|
4866
4977
|
let previewBytes;
|
|
4867
4978
|
try {
|
|
4979
|
+
const targetLine = Number.isSafeInteger(options.line) && (options.line ?? 0) > 0 ? options.line ?? null : null;
|
|
4868
4980
|
previewBytes = await readFilePreviewBytes({
|
|
4869
4981
|
resolvedPath,
|
|
4870
4982
|
sizeBytes: resolvedStats.size,
|
|
4871
|
-
maxBytes: options.maxBytes ?? DEFAULT_PREVIEW_MAX_BYTES
|
|
4983
|
+
maxBytes: options.maxBytes ?? DEFAULT_PREVIEW_MAX_BYTES,
|
|
4984
|
+
targetLine
|
|
4872
4985
|
});
|
|
4873
4986
|
} catch {
|
|
4874
4987
|
throw new ServerPathReadError("SERVER_PATH_NOT_READABLE", "server path is not readable");
|
|
@@ -4880,6 +4993,7 @@ async function readServerPath(options = {}) {
|
|
|
4880
4993
|
resolvedPath,
|
|
4881
4994
|
kind: "binary",
|
|
4882
4995
|
sizeBytes: resolvedStats.size,
|
|
4996
|
+
startLine: 1,
|
|
4883
4997
|
truncated: previewBytes.truncated,
|
|
4884
4998
|
languageHint: readLanguageHint(resolvedPath)
|
|
4885
4999
|
};
|
|
@@ -4888,6 +5002,7 @@ async function readServerPath(options = {}) {
|
|
|
4888
5002
|
resolvedPath,
|
|
4889
5003
|
kind: resolvedKind,
|
|
4890
5004
|
sizeBytes: resolvedStats.size,
|
|
5005
|
+
startLine: previewBytes.startLine,
|
|
4891
5006
|
truncated: previewBytes.truncated,
|
|
4892
5007
|
text: previewBytes.buffer.toString("utf8"),
|
|
4893
5008
|
languageHint: readLanguageHint(resolvedPath)
|
|
@@ -4997,10 +5112,223 @@ function isServerPathContentError(error) {
|
|
|
4997
5112
|
return error instanceof ServerPathContentError;
|
|
4998
5113
|
}
|
|
4999
5114
|
//#endregion
|
|
5115
|
+
//#region src/features/server-path/utils/server-path-search.utils.ts
|
|
5116
|
+
function isServerPathInside(basePath, candidatePath) {
|
|
5117
|
+
const relativePath = relative(basePath, candidatePath);
|
|
5118
|
+
return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
|
|
5119
|
+
}
|
|
5120
|
+
function normalizeServerPathRelativePath(value) {
|
|
5121
|
+
return value.split(sep).join("/");
|
|
5122
|
+
}
|
|
5123
|
+
function resolveServerPathSearchScore(entry, query) {
|
|
5124
|
+
if (!query) return 1;
|
|
5125
|
+
const normalizedQuery = query.toLocaleLowerCase();
|
|
5126
|
+
const normalizedName = entry.name.toLocaleLowerCase();
|
|
5127
|
+
const normalizedPath = entry.relativePath.toLocaleLowerCase();
|
|
5128
|
+
if (!normalizedQuery.split(/\s+/).filter(Boolean).every((term) => normalizedPath.includes(term))) return 0;
|
|
5129
|
+
if (normalizedName === normalizedQuery) return 1e3;
|
|
5130
|
+
if (normalizedName.startsWith(normalizedQuery)) return 850;
|
|
5131
|
+
if (normalizedName.includes(normalizedQuery)) return 700;
|
|
5132
|
+
if (normalizedPath.startsWith(normalizedQuery)) return 600;
|
|
5133
|
+
if (normalizedPath.split("/").some((segment) => segment.startsWith(normalizedQuery))) return 550;
|
|
5134
|
+
return 400 - Math.min(normalizedPath.length, 300);
|
|
5135
|
+
}
|
|
5136
|
+
//#endregion
|
|
5137
|
+
//#region src/features/server-path/services/server-path-search.service.ts
|
|
5138
|
+
const DEFAULT_RESULT_LIMIT = 50;
|
|
5139
|
+
const MAX_RESULT_LIMIT = 100;
|
|
5140
|
+
const MAX_SCANNED_ENTRIES = 2e4;
|
|
5141
|
+
const MAX_SEARCH_DEPTH = 24;
|
|
5142
|
+
const IGNORED_DIRECTORY_NAMES = new Set([
|
|
5143
|
+
".git",
|
|
5144
|
+
".cache",
|
|
5145
|
+
".next",
|
|
5146
|
+
".nuxt",
|
|
5147
|
+
".output",
|
|
5148
|
+
".parcel-cache",
|
|
5149
|
+
".turbo",
|
|
5150
|
+
".vite",
|
|
5151
|
+
"build",
|
|
5152
|
+
"coverage",
|
|
5153
|
+
"dist",
|
|
5154
|
+
"node_modules",
|
|
5155
|
+
"out",
|
|
5156
|
+
"target",
|
|
5157
|
+
"ui-dist",
|
|
5158
|
+
"vendor"
|
|
5159
|
+
]);
|
|
5160
|
+
var ServerPathSearchError = class extends Error {
|
|
5161
|
+
constructor(code, message) {
|
|
5162
|
+
super(message);
|
|
5163
|
+
this.code = code;
|
|
5164
|
+
this.name = "ServerPathSearchError";
|
|
5165
|
+
}
|
|
5166
|
+
};
|
|
5167
|
+
var ServerPathSearchService = class {
|
|
5168
|
+
search = async (options) => {
|
|
5169
|
+
const basePath = await this.resolveBasePath(options.basePath);
|
|
5170
|
+
const query = options.query?.trim() ?? "";
|
|
5171
|
+
const limit = Math.min(Math.max(options.limit ?? DEFAULT_RESULT_LIMIT, 1), MAX_RESULT_LIMIT);
|
|
5172
|
+
const { candidates, scanLimitReached } = await this.scanCandidates({
|
|
5173
|
+
basePath,
|
|
5174
|
+
query
|
|
5175
|
+
});
|
|
5176
|
+
candidates.sort((left, right) => {
|
|
5177
|
+
if (right.score !== left.score) return right.score - left.score;
|
|
5178
|
+
if (left.entry.kind !== right.entry.kind) return left.entry.kind === "directory" ? -1 : 1;
|
|
5179
|
+
return left.entry.relativePath.localeCompare(right.entry.relativePath, void 0, {
|
|
5180
|
+
numeric: true,
|
|
5181
|
+
sensitivity: "base"
|
|
5182
|
+
});
|
|
5183
|
+
});
|
|
5184
|
+
return {
|
|
5185
|
+
basePath,
|
|
5186
|
+
query,
|
|
5187
|
+
entries: candidates.slice(0, limit).map(({ entry }) => entry),
|
|
5188
|
+
truncated: scanLimitReached || candidates.length > limit
|
|
5189
|
+
};
|
|
5190
|
+
};
|
|
5191
|
+
scanCandidates = async (params) => {
|
|
5192
|
+
const { basePath, query } = params;
|
|
5193
|
+
const queue = [{
|
|
5194
|
+
absolutePath: basePath,
|
|
5195
|
+
depth: 0
|
|
5196
|
+
}];
|
|
5197
|
+
const candidates = [];
|
|
5198
|
+
let scannedEntries = 0;
|
|
5199
|
+
let scanLimitReached = false;
|
|
5200
|
+
while (queue.length > 0 && !scanLimitReached) {
|
|
5201
|
+
const current = queue.shift();
|
|
5202
|
+
if (!current) break;
|
|
5203
|
+
const scan = await this.scanDirectory({
|
|
5204
|
+
basePath,
|
|
5205
|
+
directory: current,
|
|
5206
|
+
query,
|
|
5207
|
+
remainingEntries: MAX_SCANNED_ENTRIES - scannedEntries
|
|
5208
|
+
});
|
|
5209
|
+
scannedEntries += scan.scannedEntries;
|
|
5210
|
+
candidates.push(...scan.candidates);
|
|
5211
|
+
queue.push(...scan.directories);
|
|
5212
|
+
scanLimitReached = scan.limitReached;
|
|
5213
|
+
if (!query) break;
|
|
5214
|
+
}
|
|
5215
|
+
return {
|
|
5216
|
+
candidates,
|
|
5217
|
+
scanLimitReached
|
|
5218
|
+
};
|
|
5219
|
+
};
|
|
5220
|
+
scanDirectory = async (params) => {
|
|
5221
|
+
const { basePath, directory, query, remainingEntries } = params;
|
|
5222
|
+
const entries = await this.readDirectory({
|
|
5223
|
+
path: directory.absolutePath,
|
|
5224
|
+
required: directory.depth === 0
|
|
5225
|
+
});
|
|
5226
|
+
const candidates = [];
|
|
5227
|
+
const directories = [];
|
|
5228
|
+
let scannedEntries = 0;
|
|
5229
|
+
for (const dirent of entries) {
|
|
5230
|
+
if (scannedEntries >= remainingEntries) return {
|
|
5231
|
+
candidates,
|
|
5232
|
+
directories,
|
|
5233
|
+
limitReached: true,
|
|
5234
|
+
scannedEntries
|
|
5235
|
+
};
|
|
5236
|
+
scannedEntries += 1;
|
|
5237
|
+
const result = await this.resolveEntry({
|
|
5238
|
+
basePath,
|
|
5239
|
+
directory,
|
|
5240
|
+
dirent
|
|
5241
|
+
});
|
|
5242
|
+
if (!result) continue;
|
|
5243
|
+
const score = resolveServerPathSearchScore(result.entry, query);
|
|
5244
|
+
if (score > 0) candidates.push({
|
|
5245
|
+
entry: result.entry,
|
|
5246
|
+
score
|
|
5247
|
+
});
|
|
5248
|
+
if (query && result.canTraverse && directory.depth < MAX_SEARCH_DEPTH && !IGNORED_DIRECTORY_NAMES.has(dirent.name)) directories.push({
|
|
5249
|
+
absolutePath: result.entry.path,
|
|
5250
|
+
depth: directory.depth + 1
|
|
5251
|
+
});
|
|
5252
|
+
}
|
|
5253
|
+
return {
|
|
5254
|
+
candidates,
|
|
5255
|
+
directories,
|
|
5256
|
+
limitReached: false,
|
|
5257
|
+
scannedEntries
|
|
5258
|
+
};
|
|
5259
|
+
};
|
|
5260
|
+
resolveBasePath = async (value) => {
|
|
5261
|
+
let resolvedPath;
|
|
5262
|
+
try {
|
|
5263
|
+
resolvedPath = resolveServerPath({ path: value });
|
|
5264
|
+
} catch (error) {
|
|
5265
|
+
if (error instanceof ServerPathResolutionError) throw new ServerPathSearchError("SERVER_PATH_BASE_REQUIRED", error.message);
|
|
5266
|
+
throw error;
|
|
5267
|
+
}
|
|
5268
|
+
let stats;
|
|
5269
|
+
try {
|
|
5270
|
+
stats = await stat(resolvedPath);
|
|
5271
|
+
} catch {
|
|
5272
|
+
throw new ServerPathSearchError("SERVER_PATH_NOT_FOUND", "server path does not exist");
|
|
5273
|
+
}
|
|
5274
|
+
if (!stats.isDirectory()) throw new ServerPathSearchError("SERVER_PATH_NOT_DIRECTORY", "server path must point to a directory");
|
|
5275
|
+
return await realpath(resolvedPath);
|
|
5276
|
+
};
|
|
5277
|
+
readDirectory = async (params) => {
|
|
5278
|
+
try {
|
|
5279
|
+
return (await readdir(params.path, { withFileTypes: true })).sort((left, right) => left.name.localeCompare(right.name, void 0, {
|
|
5280
|
+
numeric: true,
|
|
5281
|
+
sensitivity: "base"
|
|
5282
|
+
}));
|
|
5283
|
+
} catch {
|
|
5284
|
+
if (!params.required) return [];
|
|
5285
|
+
throw new ServerPathSearchError("SERVER_PATH_NOT_READABLE", "server path is not readable");
|
|
5286
|
+
}
|
|
5287
|
+
};
|
|
5288
|
+
resolveEntry = async (params) => {
|
|
5289
|
+
const { basePath, directory, dirent } = params;
|
|
5290
|
+
const entryPath = resolve(directory.absolutePath, dirent.name);
|
|
5291
|
+
let kind = dirent.isDirectory() ? "directory" : dirent.isFile() ? "file" : null;
|
|
5292
|
+
let canTraverse = kind === "directory";
|
|
5293
|
+
if (dirent.isSymbolicLink()) {
|
|
5294
|
+
let targetPath;
|
|
5295
|
+
try {
|
|
5296
|
+
targetPath = await realpath(entryPath);
|
|
5297
|
+
} catch {
|
|
5298
|
+
return null;
|
|
5299
|
+
}
|
|
5300
|
+
if (!isServerPathInside(basePath, targetPath)) return null;
|
|
5301
|
+
const targetStats = await stat(targetPath).catch(() => null);
|
|
5302
|
+
kind = targetStats?.isDirectory() ? "directory" : targetStats?.isFile() ? "file" : null;
|
|
5303
|
+
canTraverse = false;
|
|
5304
|
+
}
|
|
5305
|
+
if (!kind) return null;
|
|
5306
|
+
const relativePath = normalizeServerPathRelativePath(relative(basePath, entryPath));
|
|
5307
|
+
return {
|
|
5308
|
+
entry: {
|
|
5309
|
+
name: basename(entryPath),
|
|
5310
|
+
path: entryPath,
|
|
5311
|
+
relativePath,
|
|
5312
|
+
parentRelativePath: normalizeServerPathRelativePath(dirname(relativePath)) === "." ? "" : normalizeServerPathRelativePath(dirname(relativePath)),
|
|
5313
|
+
kind,
|
|
5314
|
+
hidden: dirent.name.startsWith(".")
|
|
5315
|
+
},
|
|
5316
|
+
canTraverse
|
|
5317
|
+
};
|
|
5318
|
+
};
|
|
5319
|
+
};
|
|
5320
|
+
function isServerPathSearchError(error) {
|
|
5321
|
+
return error instanceof ServerPathSearchError;
|
|
5322
|
+
}
|
|
5323
|
+
//#endregion
|
|
5000
5324
|
//#region src/features/server-path/controllers/server-path.controller.ts
|
|
5001
5325
|
function readIncludeFilesFlag(value) {
|
|
5002
5326
|
return value === "1" || value === "true";
|
|
5003
5327
|
}
|
|
5328
|
+
function readPositiveInteger(value) {
|
|
5329
|
+
const parsed = Number(value);
|
|
5330
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : void 0;
|
|
5331
|
+
}
|
|
5004
5332
|
function encodeContentDispositionFileName(fileName) {
|
|
5005
5333
|
return encodeURIComponent(fileName).replace(/\*/g, "%2A");
|
|
5006
5334
|
}
|
|
@@ -5008,6 +5336,7 @@ function statusForServerPathContentError(code) {
|
|
|
5008
5336
|
return code === "SERVER_PATH_NOT_FOUND" ? 404 : 400;
|
|
5009
5337
|
}
|
|
5010
5338
|
var ServerPathRoutesController = class {
|
|
5339
|
+
searchService = new ServerPathSearchService();
|
|
5011
5340
|
browse = async (c) => {
|
|
5012
5341
|
try {
|
|
5013
5342
|
const payload = await browseServerPath({
|
|
@@ -5021,11 +5350,38 @@ var ServerPathRoutesController = class {
|
|
|
5021
5350
|
throw error;
|
|
5022
5351
|
}
|
|
5023
5352
|
};
|
|
5353
|
+
search = async (c) => {
|
|
5354
|
+
try {
|
|
5355
|
+
const payload = await this.searchService.search({
|
|
5356
|
+
basePath: c.req.query("basePath"),
|
|
5357
|
+
query: c.req.query("query"),
|
|
5358
|
+
limit: readPositiveInteger(c.req.query("limit"))
|
|
5359
|
+
});
|
|
5360
|
+
return c.json(ok(payload));
|
|
5361
|
+
} catch (error) {
|
|
5362
|
+
if (isServerPathSearchError(error)) return c.json(err(error.code, error.message), 400);
|
|
5363
|
+
throw error;
|
|
5364
|
+
}
|
|
5365
|
+
};
|
|
5366
|
+
createDirectory = async (c) => {
|
|
5367
|
+
const body = await readJson(c.req.raw);
|
|
5368
|
+
if (!body.ok || !isRecord$2(body.data)) return c.json(err("INVALID_SERVER_PATH_DIRECTORY", "directory input is required"), 400);
|
|
5369
|
+
try {
|
|
5370
|
+
return c.json(ok(await createServerPathDirectory({
|
|
5371
|
+
parentPath: body.data.parentPath,
|
|
5372
|
+
name: body.data.name
|
|
5373
|
+
})), 201);
|
|
5374
|
+
} catch (error) {
|
|
5375
|
+
if (isServerPathDirectoryCreateError(error)) return c.json(err(error.code, error.message), 400);
|
|
5376
|
+
throw error;
|
|
5377
|
+
}
|
|
5378
|
+
};
|
|
5024
5379
|
read = async (c) => {
|
|
5025
5380
|
try {
|
|
5026
5381
|
const payload = await readServerPath({
|
|
5027
5382
|
path: c.req.query("path"),
|
|
5028
|
-
basePath: c.req.query("basePath")
|
|
5383
|
+
basePath: c.req.query("basePath"),
|
|
5384
|
+
line: readPositiveInteger(c.req.query("line"))
|
|
5029
5385
|
});
|
|
5030
5386
|
return c.json(ok(payload));
|
|
5031
5387
|
} catch (error) {
|
|
@@ -5068,6 +5424,7 @@ function createUiRouteControllers(options, authService, marketplaceBaseUrls) {
|
|
|
5068
5424
|
ncpAsset: new NcpAssetRoutesController(options),
|
|
5069
5425
|
panelApps: new PanelAppsRoutesController(kernel.panelAppManager, { panelAppClientSdkScript }),
|
|
5070
5426
|
preferences: new PreferencesRoutesController(kernel.preferenceManager),
|
|
5427
|
+
projects: new ProjectsRoutesController(kernel.projectManager),
|
|
5071
5428
|
serviceApps: new ServiceAppsRoutesController({
|
|
5072
5429
|
panelAppManager: kernel.panelAppManager,
|
|
5073
5430
|
serviceAppManager: kernel.serviceAppManager
|
|
@@ -5155,7 +5512,7 @@ var UiRouteRegistry = class {
|
|
|
5155
5512
|
]]);
|
|
5156
5513
|
};
|
|
5157
5514
|
mountResourceRoutes = () => {
|
|
5158
|
-
const { ncpSession, panelApps, preferences, serviceApps, serverPath } = this.controllers;
|
|
5515
|
+
const { ncpSession, panelApps, preferences, projects, serviceApps, serverPath } = this.controllers;
|
|
5159
5516
|
this.mountRoutes([
|
|
5160
5517
|
[
|
|
5161
5518
|
"get",
|
|
@@ -5262,6 +5619,16 @@ var UiRouteRegistry = class {
|
|
|
5262
5619
|
"/api/preferences/:key",
|
|
5263
5620
|
preferences.delete
|
|
5264
5621
|
],
|
|
5622
|
+
[
|
|
5623
|
+
"get",
|
|
5624
|
+
"/api/projects",
|
|
5625
|
+
projects.list
|
|
5626
|
+
],
|
|
5627
|
+
[
|
|
5628
|
+
"post",
|
|
5629
|
+
"/api/projects",
|
|
5630
|
+
projects.create
|
|
5631
|
+
],
|
|
5265
5632
|
[
|
|
5266
5633
|
"delete",
|
|
5267
5634
|
"/api/panel-apps/:id",
|
|
@@ -5352,6 +5719,16 @@ var UiRouteRegistry = class {
|
|
|
5352
5719
|
"/api/server-paths/browse",
|
|
5353
5720
|
serverPath.browse
|
|
5354
5721
|
],
|
|
5722
|
+
[
|
|
5723
|
+
"get",
|
|
5724
|
+
"/api/server-paths/search",
|
|
5725
|
+
serverPath.search
|
|
5726
|
+
],
|
|
5727
|
+
[
|
|
5728
|
+
"post",
|
|
5729
|
+
"/api/server-paths/directory",
|
|
5730
|
+
serverPath.createDirectory
|
|
5731
|
+
],
|
|
5355
5732
|
[
|
|
5356
5733
|
"get",
|
|
5357
5734
|
"/api/server-paths/read",
|