@nextclaw/server 0.15.5 → 0.15.7
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 +43 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +493 -174
- 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: {
|
|
@@ -3047,6 +2987,7 @@ const MARKETPLACE_ZH_COPY_BY_SLUG = {
|
|
|
3047
2987
|
const MARKETPLACE_FETCH_TIMEOUT_MS = 12e3;
|
|
3048
2988
|
const DOMESTIC_MARKETPLACE_FETCH_TIMEOUT_MS = 2e3;
|
|
3049
2989
|
const DOMESTIC_MARKETPLACE_RETRY_ATTEMPTS = 2;
|
|
2990
|
+
const DOMESTIC_MARKETPLACE_MAX_SNAPSHOT_AGE_MS = 1200 * 1e3;
|
|
3050
2991
|
function resolveMarketplaceBaseUrls(options) {
|
|
3051
2992
|
const configured = options.marketplace?.apiBaseUrl?.trim();
|
|
3052
2993
|
if (configured) return [configured.replace(/\/$/, "")];
|
|
@@ -3069,6 +3010,11 @@ function getMarketplaceFetchOptions(baseUrl) {
|
|
|
3069
3010
|
function shouldFallbackMarketplaceResult(status) {
|
|
3070
3011
|
return status === 408 || status === 429 || status >= 500;
|
|
3071
3012
|
}
|
|
3013
|
+
function isStaleDomesticMarketplaceSnapshot(baseUrl, cachedAt, now = Date.now()) {
|
|
3014
|
+
if (baseUrl.replace(/\/$/, "") !== "https://api.nextclaw.net" || cachedAt === null) return false;
|
|
3015
|
+
const cachedAtMs = Date.parse(cachedAt);
|
|
3016
|
+
return !Number.isFinite(cachedAtMs) || now - cachedAtMs > DOMESTIC_MARKETPLACE_MAX_SNAPSHOT_AGE_MS;
|
|
3017
|
+
}
|
|
3072
3018
|
//#endregion
|
|
3073
3019
|
//#region src/features/marketplace/utils/marketplace-network-retry.utils.ts
|
|
3074
3020
|
const MARKETPLACE_NETWORK_RETRY_ATTEMPTS = 5;
|
|
@@ -3146,6 +3092,11 @@ async function fetchMarketplaceDataFromBase(params) {
|
|
|
3146
3092
|
message: error instanceof Error ? error.message : String(error)
|
|
3147
3093
|
};
|
|
3148
3094
|
}
|
|
3095
|
+
if (isStaleDomesticMarketplaceSnapshot(baseUrl, response.headers.get("x-nextclaw-mirror-cached-at"))) return {
|
|
3096
|
+
ok: false,
|
|
3097
|
+
status: 503,
|
|
3098
|
+
message: "domestic marketplace mirror snapshot is stale"
|
|
3099
|
+
};
|
|
3149
3100
|
let payload = null;
|
|
3150
3101
|
try {
|
|
3151
3102
|
payload = await response.json();
|
|
@@ -3514,15 +3465,6 @@ function collectSkillMarketplaceInstalledView(options) {
|
|
|
3514
3465
|
records: installed.records
|
|
3515
3466
|
};
|
|
3516
3467
|
}
|
|
3517
|
-
function collectKnownSkillNames(options) {
|
|
3518
|
-
const loader = createSkillsLoader(getWorkspacePathFromConfig(loadConfigOrDefault(options.configPath)));
|
|
3519
|
-
return new Set((loader?.listSkills(false) ?? []).map((skill) => skill.name));
|
|
3520
|
-
}
|
|
3521
|
-
function isSupportedMarketplaceSkillItem(item, knownSkillNames) {
|
|
3522
|
-
if (item.type !== "skill") return false;
|
|
3523
|
-
if (item.install.kind === "marketplace") return true;
|
|
3524
|
-
return item.install.kind === "builtin" && knownSkillNames.has(item.install.spec);
|
|
3525
|
-
}
|
|
3526
3468
|
function findUnsupportedSkillInstallKind(items) {
|
|
3527
3469
|
for (const item of items) {
|
|
3528
3470
|
if (item.type !== "skill") continue;
|
|
@@ -3605,12 +3547,11 @@ var SkillMarketplaceController = class {
|
|
|
3605
3547
|
status: 502,
|
|
3606
3548
|
message: `unsupported skill install kind from marketplace api: ${unsupportedKind}`
|
|
3607
3549
|
};
|
|
3608
|
-
const knownSkillNames = collectKnownSkillNames(this.options);
|
|
3609
3550
|
return {
|
|
3610
3551
|
ok: true,
|
|
3611
3552
|
data: {
|
|
3612
3553
|
...data,
|
|
3613
|
-
items: normalizedItems
|
|
3554
|
+
items: normalizedItems
|
|
3614
3555
|
}
|
|
3615
3556
|
};
|
|
3616
3557
|
};
|
|
@@ -3654,11 +3595,9 @@ var SkillMarketplaceController = class {
|
|
|
3654
3595
|
path: `/api/v1/skills/items/${slug}`
|
|
3655
3596
|
});
|
|
3656
3597
|
if (!result.ok) return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
|
|
3657
|
-
const knownSkillNames = collectKnownSkillNames(this.options);
|
|
3658
3598
|
const sanitized = normalizeMarketplaceItemForUi(sanitizeMarketplaceItemView(result.data));
|
|
3659
3599
|
const unsupportedKind = findUnsupportedSkillInstallKind([sanitized]);
|
|
3660
3600
|
if (unsupportedKind) return c.json(err("MARKETPLACE_CONTRACT_MISMATCH", `unsupported skill install kind from marketplace api: ${unsupportedKind}`), 502);
|
|
3661
|
-
if (!isSupportedMarketplaceSkillItem(sanitized, knownSkillNames)) return c.json(err("NOT_FOUND", "marketplace item not supported by nextclaw"), 404);
|
|
3662
3601
|
return c.json(ok(sanitized));
|
|
3663
3602
|
};
|
|
3664
3603
|
getItemContent = async (c) => {
|
|
@@ -3668,11 +3607,8 @@ var SkillMarketplaceController = class {
|
|
|
3668
3607
|
path: `/api/v1/skills/items/${slug}`
|
|
3669
3608
|
});
|
|
3670
3609
|
if (!result.ok) return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
|
|
3671
|
-
const
|
|
3672
|
-
const sanitized = normalizeMarketplaceItemForUi(sanitizeMarketplaceItemView(result.data));
|
|
3673
|
-
const unsupportedKind = findUnsupportedSkillInstallKind([sanitized]);
|
|
3610
|
+
const unsupportedKind = findUnsupportedSkillInstallKind([normalizeMarketplaceItemForUi(sanitizeMarketplaceItemView(result.data))]);
|
|
3674
3611
|
if (unsupportedKind) return c.json(err("MARKETPLACE_CONTRACT_MISMATCH", `unsupported skill install kind from marketplace api: ${unsupportedKind}`), 502);
|
|
3675
|
-
if (!isSupportedMarketplaceSkillItem(sanitized, knownSkillNames)) return c.json(err("NOT_FOUND", "marketplace item not supported by nextclaw"), 404);
|
|
3676
3612
|
const contentResult = await fetchMarketplaceData({
|
|
3677
3613
|
baseUrls: this.marketplaceBaseUrls,
|
|
3678
3614
|
path: `/api/v1/skills/items/${slug}/content`
|
|
@@ -3725,12 +3661,13 @@ var SkillMarketplaceController = class {
|
|
|
3725
3661
|
}
|
|
3726
3662
|
});
|
|
3727
3663
|
if (!result.ok) return c.json(err("MARKETPLACE_UNAVAILABLE", result.message), result.status);
|
|
3728
|
-
const
|
|
3729
|
-
const
|
|
3664
|
+
const items = sanitizeMarketplaceListItems(result.data.items).map((item) => normalizeMarketplaceItemForUi(item));
|
|
3665
|
+
const unsupportedKind = findUnsupportedSkillInstallKind(items);
|
|
3666
|
+
if (unsupportedKind) return c.json(err("MARKETPLACE_CONTRACT_MISMATCH", `unsupported skill install kind from marketplace api: ${unsupportedKind}`), 502);
|
|
3730
3667
|
return c.json(ok({
|
|
3731
3668
|
...result.data,
|
|
3732
|
-
total:
|
|
3733
|
-
items
|
|
3669
|
+
total: items.length,
|
|
3670
|
+
items
|
|
3734
3671
|
}));
|
|
3735
3672
|
};
|
|
3736
3673
|
};
|
|
@@ -3812,6 +3749,23 @@ var SessionSkillsViewBuilder = class {
|
|
|
3812
3749
|
//#endregion
|
|
3813
3750
|
//#region src/features/sessions/controllers/sessions.controller.ts
|
|
3814
3751
|
const INTERRUPTED_SESSION_STATUS_TEXT = "Run interrupted: no completion or error event was recorded. Please send the message again.";
|
|
3752
|
+
function sessionProjectError(error) {
|
|
3753
|
+
switch (error.code) {
|
|
3754
|
+
case "PROJECT_PATH_INVALID_TYPE": return {
|
|
3755
|
+
code: "PROJECT_ROOT_INVALID_TYPE",
|
|
3756
|
+
message: "projectRoot must be a string or null"
|
|
3757
|
+
};
|
|
3758
|
+
case "PROJECT_PATH_NOT_FOUND": return {
|
|
3759
|
+
code: "PROJECT_ROOT_NOT_FOUND",
|
|
3760
|
+
message: "projectRoot directory does not exist"
|
|
3761
|
+
};
|
|
3762
|
+
case "PROJECT_PATH_NOT_DIRECTORY": return {
|
|
3763
|
+
code: "PROJECT_ROOT_NOT_DIRECTORY",
|
|
3764
|
+
message: "projectRoot must point to a directory"
|
|
3765
|
+
};
|
|
3766
|
+
default: return error;
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3815
3769
|
function readPositiveInt(value) {
|
|
3816
3770
|
if (typeof value !== "string") return;
|
|
3817
3771
|
const parsed = Number.parseInt(value, 10);
|
|
@@ -3845,49 +3799,6 @@ function normalizeSessionActivityPreview(session) {
|
|
|
3845
3799
|
}
|
|
3846
3800
|
};
|
|
3847
3801
|
}
|
|
3848
|
-
function applySessionTypePatch(metadata, patch) {
|
|
3849
|
-
if (!Object.prototype.hasOwnProperty.call(patch, "sessionType")) return metadata;
|
|
3850
|
-
const sessionType = typeof patch.sessionType === "string" ? patch.sessionType.trim() : "";
|
|
3851
|
-
if (sessionType) {
|
|
3852
|
-
const { sessionType: _removed, ...nextMetadata } = metadata;
|
|
3853
|
-
return {
|
|
3854
|
-
...nextMetadata,
|
|
3855
|
-
session_type: sessionType,
|
|
3856
|
-
runtime: sessionType
|
|
3857
|
-
};
|
|
3858
|
-
}
|
|
3859
|
-
const { runtime: _runtime, session_type: _sessionType, sessionType: _camelSessionType, ...nextMetadata } = metadata;
|
|
3860
|
-
return nextMetadata;
|
|
3861
|
-
}
|
|
3862
|
-
function applyUiReadAtPatch(metadata, patch) {
|
|
3863
|
-
if (!Object.prototype.hasOwnProperty.call(patch, "uiReadAt")) return metadata;
|
|
3864
|
-
const uiReadAt = typeof patch.uiReadAt === "string" ? patch.uiReadAt.trim() : "";
|
|
3865
|
-
if (uiReadAt) return {
|
|
3866
|
-
...metadata,
|
|
3867
|
-
ui_last_read_at: uiReadAt
|
|
3868
|
-
};
|
|
3869
|
-
const { ui_last_read_at: _removed, ...nextMetadata } = metadata;
|
|
3870
|
-
return nextMetadata;
|
|
3871
|
-
}
|
|
3872
|
-
function applyProjectRootPatch(metadata, projectRoot) {
|
|
3873
|
-
if (projectRoot === void 0) return metadata;
|
|
3874
|
-
if (projectRoot) {
|
|
3875
|
-
const { projectRoot: _removed, ...nextMetadata } = metadata;
|
|
3876
|
-
return {
|
|
3877
|
-
...nextMetadata,
|
|
3878
|
-
project_root: projectRoot
|
|
3879
|
-
};
|
|
3880
|
-
}
|
|
3881
|
-
const { project_root: _projectRoot, projectRoot: _camelProjectRoot, ...nextMetadata } = metadata;
|
|
3882
|
-
return nextMetadata;
|
|
3883
|
-
}
|
|
3884
|
-
function buildPatchedSessionMetadata(metadata, patch, projectRootPatch) {
|
|
3885
|
-
return applyProjectRootPatch(applyUiReadAtPatch(applySessionTypePatch(applySessionPreferencePatch({
|
|
3886
|
-
metadata: structuredClone(metadata),
|
|
3887
|
-
patch,
|
|
3888
|
-
createInvalidThinkingError: () => /* @__PURE__ */ new Error("PREFERRED_THINKING_INVALID")
|
|
3889
|
-
}), patch), patch), projectRootPatch);
|
|
3890
|
-
}
|
|
3891
3802
|
var NcpSessionRoutesController = class {
|
|
3892
3803
|
sessionSkillsViewBuilder;
|
|
3893
3804
|
constructor(options) {
|
|
@@ -3943,14 +3854,17 @@ var NcpSessionRoutesController = class {
|
|
|
3943
3854
|
const hasProjectRootOverride = Object.prototype.hasOwnProperty.call(query, "projectRoot");
|
|
3944
3855
|
const metadata = readSessionMetadata((await sessionManager.getSession(sessionId))?.metadata);
|
|
3945
3856
|
if (hasProjectRootOverride) try {
|
|
3946
|
-
const projectRoot = await
|
|
3857
|
+
const projectRoot = await this.options.kernel.projectManager.resolveExistingProjectRoot(query.projectRoot);
|
|
3947
3858
|
if (projectRoot) metadata.project_root = projectRoot;
|
|
3948
3859
|
else {
|
|
3949
3860
|
delete metadata.project_root;
|
|
3950
3861
|
delete metadata.projectRoot;
|
|
3951
3862
|
}
|
|
3952
3863
|
} catch (error) {
|
|
3953
|
-
if (
|
|
3864
|
+
if (isProjectError(error)) {
|
|
3865
|
+
const mapped = sessionProjectError(error);
|
|
3866
|
+
return c.json(err(mapped.code, mapped.message), 400);
|
|
3867
|
+
}
|
|
3954
3868
|
throw error;
|
|
3955
3869
|
}
|
|
3956
3870
|
return c.json(ok(this.sessionSkillsViewBuilder.build({
|
|
@@ -3965,19 +3879,17 @@ var NcpSessionRoutesController = class {
|
|
|
3965
3879
|
if (!body.ok || !body.data || typeof body.data !== "object") return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
3966
3880
|
const patch = body.data;
|
|
3967
3881
|
if (patch.clearHistory) return c.json(err("UNSUPPORTED_PATCH", "clearHistory is not supported for ncp sessions"), 400);
|
|
3968
|
-
let
|
|
3882
|
+
let updated;
|
|
3969
3883
|
try {
|
|
3970
|
-
|
|
3971
|
-
const existing = await sessionManager.getSessionRecord(sessionId);
|
|
3972
|
-
const metadata = buildPatchedSessionMetadata(readSessionMetadata(existing?.metadata), patch, projectRootPatch);
|
|
3973
|
-
patched = existing ? await sessionManager.setSessionMetadata(sessionId, metadata) : Boolean(await sessionManager.updateSession(sessionId, { metadata }));
|
|
3884
|
+
updated = await sessionManager.patchSessionSettings(sessionId, patch, { createIfMissing: true });
|
|
3974
3885
|
} catch (error) {
|
|
3975
|
-
if (error
|
|
3976
|
-
if (
|
|
3886
|
+
if (isSessionSettingsError(error)) return c.json(err(error.code, error.message), 400);
|
|
3887
|
+
if (isProjectError(error)) {
|
|
3888
|
+
const mapped = sessionProjectError(error);
|
|
3889
|
+
return c.json(err(mapped.code, mapped.message), 400);
|
|
3890
|
+
}
|
|
3977
3891
|
throw error;
|
|
3978
3892
|
}
|
|
3979
|
-
if (!patched) return c.json(err("NOT_FOUND", `ncp session not found: ${sessionId}`), 404);
|
|
3980
|
-
const updated = await sessionManager.getSession(sessionId);
|
|
3981
3893
|
if (!updated) return c.json(err("NOT_FOUND", `ncp session not found: ${sessionId}`), 404);
|
|
3982
3894
|
return c.json(ok(this.withRuntimeStatus(updated)));
|
|
3983
3895
|
};
|
|
@@ -4452,6 +4364,31 @@ var PreferencesRoutesController = class {
|
|
|
4452
4364
|
};
|
|
4453
4365
|
};
|
|
4454
4366
|
//#endregion
|
|
4367
|
+
//#region src/features/projects/controllers/projects.controller.ts
|
|
4368
|
+
var ProjectsRoutesController = class {
|
|
4369
|
+
constructor(projectManager) {
|
|
4370
|
+
this.projectManager = projectManager;
|
|
4371
|
+
}
|
|
4372
|
+
list = async (c) => {
|
|
4373
|
+
const projects = await this.projectManager.listProjects();
|
|
4374
|
+
return c.json(ok({
|
|
4375
|
+
projects,
|
|
4376
|
+
templates: this.projectManager.listTemplates(),
|
|
4377
|
+
total: projects.length
|
|
4378
|
+
}));
|
|
4379
|
+
};
|
|
4380
|
+
create = async (c) => {
|
|
4381
|
+
const body = await readJson(c.req.raw);
|
|
4382
|
+
if (!body.ok || !isRecord$2(body.data) || typeof body.data.name !== "string") return c.json(err("INVALID_PROJECT", "project name is required"), 400);
|
|
4383
|
+
try {
|
|
4384
|
+
return c.json(ok(await this.projectManager.createProject(body.data)), 201);
|
|
4385
|
+
} catch (error) {
|
|
4386
|
+
if (isProjectError(error)) return c.json(err(error.code, error.message), 400);
|
|
4387
|
+
throw error;
|
|
4388
|
+
}
|
|
4389
|
+
};
|
|
4390
|
+
};
|
|
4391
|
+
//#endregion
|
|
4455
4392
|
//#region src/features/service-apps/controllers/service-apps.controller.ts
|
|
4456
4393
|
const PANEL_BRIDGE_SESSION_HEADER = "x-nextclaw-panel-bridge-session";
|
|
4457
4394
|
function statusForServiceAppError(code) {
|
|
@@ -4652,6 +4589,100 @@ function createNcpSessionEventStreamResponse(eventBus, payload, signal) {
|
|
|
4652
4589
|
} });
|
|
4653
4590
|
}
|
|
4654
4591
|
//#endregion
|
|
4592
|
+
//#region src/features/server-path/utils/server-path-locations.utils.ts
|
|
4593
|
+
const xdgLocationKinds = {
|
|
4594
|
+
XDG_DESKTOP_DIR: "desktop",
|
|
4595
|
+
XDG_DOCUMENTS_DIR: "documents",
|
|
4596
|
+
XDG_DOWNLOAD_DIR: "downloads"
|
|
4597
|
+
};
|
|
4598
|
+
function readAbsoluteEnvironmentPath(name) {
|
|
4599
|
+
const value = process.env[name]?.trim();
|
|
4600
|
+
return value && isAbsolute(value) ? resolve(value) : null;
|
|
4601
|
+
}
|
|
4602
|
+
function parseXdgLocationPath(value, homePath) {
|
|
4603
|
+
const expanded = value.trim().replace(/^(['"])(.*)\1$/, "$2").replace(/^\$(?:HOME|\{HOME\})(?=\/|$)/, homePath);
|
|
4604
|
+
const resolvedPath = isAbsolute(expanded) ? resolve(expanded) : null;
|
|
4605
|
+
return resolvedPath && resolvedPath !== homePath ? resolvedPath : null;
|
|
4606
|
+
}
|
|
4607
|
+
async function readXdgLocationPaths(homePath) {
|
|
4608
|
+
const configRoot = readAbsoluteEnvironmentPath("XDG_CONFIG_HOME") ?? join(homePath, ".config");
|
|
4609
|
+
let content;
|
|
4610
|
+
try {
|
|
4611
|
+
content = await readFile(join(configRoot, "user-dirs.dirs"), "utf8");
|
|
4612
|
+
} catch {
|
|
4613
|
+
return {};
|
|
4614
|
+
}
|
|
4615
|
+
const locations = {};
|
|
4616
|
+
for (const line of content.split(/\r?\n/)) {
|
|
4617
|
+
const separatorIndex = line.indexOf("=");
|
|
4618
|
+
if (separatorIndex < 1) continue;
|
|
4619
|
+
const kind = xdgLocationKinds[line.slice(0, separatorIndex).trim()];
|
|
4620
|
+
if (!kind) continue;
|
|
4621
|
+
const path = parseXdgLocationPath(line.slice(separatorIndex + 1), homePath);
|
|
4622
|
+
if (path) locations[kind] = path;
|
|
4623
|
+
}
|
|
4624
|
+
return locations;
|
|
4625
|
+
}
|
|
4626
|
+
async function resolveFirstDirectory(kind, candidatePaths) {
|
|
4627
|
+
const uniquePaths = [...new Set(candidatePaths.filter((path) => Boolean(path)))];
|
|
4628
|
+
for (const path of uniquePaths) try {
|
|
4629
|
+
if ((await stat(path)).isDirectory()) return {
|
|
4630
|
+
kind,
|
|
4631
|
+
path
|
|
4632
|
+
};
|
|
4633
|
+
} catch {}
|
|
4634
|
+
return null;
|
|
4635
|
+
}
|
|
4636
|
+
async function resolveServerPathLocations(homePath) {
|
|
4637
|
+
const serverPlatform = platform();
|
|
4638
|
+
const xdgLocations = serverPlatform === "linux" ? await readXdgLocationPaths(homePath) : {};
|
|
4639
|
+
const oneDriveRoots = serverPlatform === "win32" ? [
|
|
4640
|
+
"OneDrive",
|
|
4641
|
+
"OneDriveConsumer",
|
|
4642
|
+
"OneDriveCommercial"
|
|
4643
|
+
].map(readAbsoluteEnvironmentPath).filter((path) => path !== null) : [];
|
|
4644
|
+
const homeLocations = await Promise.all([
|
|
4645
|
+
resolveFirstDirectory("desktop", [
|
|
4646
|
+
xdgLocations.desktop,
|
|
4647
|
+
...oneDriveRoots.map((path) => join(path, "Desktop")),
|
|
4648
|
+
join(homePath, "Desktop")
|
|
4649
|
+
]),
|
|
4650
|
+
resolveFirstDirectory("documents", [
|
|
4651
|
+
xdgLocations.documents,
|
|
4652
|
+
...oneDriveRoots.map((path) => join(path, "Documents")),
|
|
4653
|
+
join(homePath, "Documents")
|
|
4654
|
+
]),
|
|
4655
|
+
resolveFirstDirectory("downloads", [xdgLocations.downloads, join(homePath, "Downloads")])
|
|
4656
|
+
]);
|
|
4657
|
+
const macLocations = serverPlatform === "darwin" ? await Promise.all([
|
|
4658
|
+
resolveFirstDirectory("icloud-drive", [join(homePath, "Library", "Mobile Documents", "com~apple~CloudDocs")]),
|
|
4659
|
+
resolveFirstDirectory("applications", ["/Applications"]),
|
|
4660
|
+
resolveFirstDirectory("volumes", ["/Volumes"])
|
|
4661
|
+
]) : [];
|
|
4662
|
+
return [...homeLocations, ...macLocations].filter((location) => location !== null);
|
|
4663
|
+
}
|
|
4664
|
+
//#endregion
|
|
4665
|
+
//#region src/features/server-path/utils/server-path-resolution.utils.ts
|
|
4666
|
+
var ServerPathResolutionError = class extends Error {
|
|
4667
|
+
constructor(message) {
|
|
4668
|
+
super(message);
|
|
4669
|
+
this.name = "ServerPathResolutionError";
|
|
4670
|
+
}
|
|
4671
|
+
};
|
|
4672
|
+
function resolveServerPath(params) {
|
|
4673
|
+
const { path, basePath: rawBasePath, defaultToHome } = params;
|
|
4674
|
+
const rawPath = typeof path === "string" ? path.trim() : "";
|
|
4675
|
+
const basePath = typeof rawBasePath === "string" ? rawBasePath.trim() : "";
|
|
4676
|
+
if (!rawPath) {
|
|
4677
|
+
if (defaultToHome) return resolve(expandHome(basePath || homedir()));
|
|
4678
|
+
throw new ServerPathResolutionError("server path is required");
|
|
4679
|
+
}
|
|
4680
|
+
const expandedPath = expandHome(rawPath);
|
|
4681
|
+
if (isAbsolute(expandedPath)) return resolve(expandedPath);
|
|
4682
|
+
if (!basePath) throw new ServerPathResolutionError("relative server path requires a base path");
|
|
4683
|
+
return resolve(expandHome(basePath), expandedPath);
|
|
4684
|
+
}
|
|
4685
|
+
//#endregion
|
|
4655
4686
|
//#region src/features/server-path/utils/server-path-browse.utils.ts
|
|
4656
4687
|
var ServerPathBrowseError = class extends Error {
|
|
4657
4688
|
constructor(code, message) {
|
|
@@ -4660,15 +4691,16 @@ var ServerPathBrowseError = class extends Error {
|
|
|
4660
4691
|
this.name = "ServerPathBrowseError";
|
|
4661
4692
|
}
|
|
4662
4693
|
};
|
|
4663
|
-
function
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4694
|
+
function resolveBrowsePath(options) {
|
|
4695
|
+
try {
|
|
4696
|
+
return resolveServerPath({
|
|
4697
|
+
...options,
|
|
4698
|
+
defaultToHome: true
|
|
4699
|
+
});
|
|
4700
|
+
} catch (error) {
|
|
4701
|
+
if (error instanceof ServerPathResolutionError) throw new ServerPathBrowseError("SERVER_PATH_BASE_REQUIRED", error.message);
|
|
4702
|
+
throw error;
|
|
4703
|
+
}
|
|
4672
4704
|
}
|
|
4673
4705
|
function readRootLabel(rootPath) {
|
|
4674
4706
|
return rootPath.replace(/[\\/]+$/, "") || rootPath;
|
|
@@ -4714,7 +4746,7 @@ function sortEntryViews(left, right) {
|
|
|
4714
4746
|
return left.name.localeCompare(right.name);
|
|
4715
4747
|
}
|
|
4716
4748
|
async function browseServerPath(options = {}) {
|
|
4717
|
-
const currentPath =
|
|
4749
|
+
const currentPath = resolveBrowsePath(options);
|
|
4718
4750
|
let currentPathStats;
|
|
4719
4751
|
try {
|
|
4720
4752
|
currentPathStats = await stat(currentPath);
|
|
@@ -4734,37 +4766,59 @@ async function browseServerPath(options = {}) {
|
|
|
4734
4766
|
includeFiles: options.includeFiles ?? false
|
|
4735
4767
|
})))).filter((entry) => entry !== null).sort(sortEntryViews);
|
|
4736
4768
|
const parentPath = dirname(currentPath);
|
|
4769
|
+
const homePath = resolve(homedir());
|
|
4737
4770
|
return {
|
|
4738
4771
|
currentPath,
|
|
4739
4772
|
parentPath: parentPath === currentPath ? null : parentPath,
|
|
4740
|
-
homePath
|
|
4773
|
+
homePath,
|
|
4741
4774
|
breadcrumbs: buildBreadcrumbs(currentPath),
|
|
4742
|
-
entries
|
|
4775
|
+
entries,
|
|
4776
|
+
locations: await resolveServerPathLocations(homePath)
|
|
4743
4777
|
};
|
|
4744
4778
|
}
|
|
4745
4779
|
function isServerPathBrowseError(error) {
|
|
4746
4780
|
return error instanceof ServerPathBrowseError;
|
|
4747
4781
|
}
|
|
4748
4782
|
//#endregion
|
|
4749
|
-
//#region src/features/server-path/utils/server-path-
|
|
4750
|
-
var
|
|
4751
|
-
constructor(message) {
|
|
4783
|
+
//#region src/features/server-path/utils/server-path-directory.utils.ts
|
|
4784
|
+
var ServerPathDirectoryCreateError = class extends Error {
|
|
4785
|
+
constructor(code, message) {
|
|
4752
4786
|
super(message);
|
|
4753
|
-
this.
|
|
4787
|
+
this.code = code;
|
|
4788
|
+
this.name = "ServerPathDirectoryCreateError";
|
|
4754
4789
|
}
|
|
4755
4790
|
};
|
|
4756
|
-
function
|
|
4757
|
-
const
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4791
|
+
function normalizeDirectoryName(value) {
|
|
4792
|
+
const name = typeof value === "string" ? value.trim() : "";
|
|
4793
|
+
if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\\")) throw new ServerPathDirectoryCreateError("SERVER_PATH_DIRECTORY_NAME_INVALID", "directory name is invalid");
|
|
4794
|
+
return name;
|
|
4795
|
+
}
|
|
4796
|
+
function readFileErrorCode(error) {
|
|
4797
|
+
return typeof error === "object" && error !== null && "code" in error ? String(error.code ?? "") : void 0;
|
|
4798
|
+
}
|
|
4799
|
+
async function createServerPathDirectory(input) {
|
|
4800
|
+
let parentPath;
|
|
4801
|
+
try {
|
|
4802
|
+
parentPath = resolveServerPath({ path: typeof input.parentPath === "string" ? input.parentPath : null });
|
|
4803
|
+
} catch (error) {
|
|
4804
|
+
if (error instanceof ServerPathResolutionError) throw new ServerPathDirectoryCreateError("SERVER_PATH_PARENT_INVALID", error.message);
|
|
4805
|
+
throw error;
|
|
4763
4806
|
}
|
|
4764
|
-
const
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4807
|
+
const directoryPath = join(parentPath, normalizeDirectoryName(input.name));
|
|
4808
|
+
try {
|
|
4809
|
+
await mkdir(directoryPath);
|
|
4810
|
+
return { path: directoryPath };
|
|
4811
|
+
} catch (error) {
|
|
4812
|
+
const code = readFileErrorCode(error);
|
|
4813
|
+
if (code === "EEXIST") throw new ServerPathDirectoryCreateError("SERVER_PATH_DIRECTORY_EXISTS", "directory already exists");
|
|
4814
|
+
if (code === "ENOENT") throw new ServerPathDirectoryCreateError("SERVER_PATH_PARENT_NOT_FOUND", "parent directory does not exist");
|
|
4815
|
+
if (code === "ENOTDIR") throw new ServerPathDirectoryCreateError("SERVER_PATH_PARENT_NOT_DIRECTORY", "parent path must point to a directory");
|
|
4816
|
+
if (code === "EACCES" || code === "EPERM" || code === "EROFS") throw new ServerPathDirectoryCreateError("SERVER_PATH_PARENT_NOT_WRITABLE", "parent directory is not writable");
|
|
4817
|
+
throw error;
|
|
4818
|
+
}
|
|
4819
|
+
}
|
|
4820
|
+
function isServerPathDirectoryCreateError(error) {
|
|
4821
|
+
return error instanceof ServerPathDirectoryCreateError;
|
|
4768
4822
|
}
|
|
4769
4823
|
//#endregion
|
|
4770
4824
|
//#region src/features/server-path/utils/server-path-read.utils.ts
|
|
@@ -5055,6 +5109,215 @@ function isServerPathContentError(error) {
|
|
|
5055
5109
|
return error instanceof ServerPathContentError;
|
|
5056
5110
|
}
|
|
5057
5111
|
//#endregion
|
|
5112
|
+
//#region src/features/server-path/utils/server-path-search.utils.ts
|
|
5113
|
+
function isServerPathInside(basePath, candidatePath) {
|
|
5114
|
+
const relativePath = relative(basePath, candidatePath);
|
|
5115
|
+
return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
|
|
5116
|
+
}
|
|
5117
|
+
function normalizeServerPathRelativePath(value) {
|
|
5118
|
+
return value.split(sep).join("/");
|
|
5119
|
+
}
|
|
5120
|
+
function resolveServerPathSearchScore(entry, query) {
|
|
5121
|
+
if (!query) return 1;
|
|
5122
|
+
const normalizedQuery = query.toLocaleLowerCase();
|
|
5123
|
+
const normalizedName = entry.name.toLocaleLowerCase();
|
|
5124
|
+
const normalizedPath = entry.relativePath.toLocaleLowerCase();
|
|
5125
|
+
if (!normalizedQuery.split(/\s+/).filter(Boolean).every((term) => normalizedPath.includes(term))) return 0;
|
|
5126
|
+
if (normalizedName === normalizedQuery) return 1e3;
|
|
5127
|
+
if (normalizedName.startsWith(normalizedQuery)) return 850;
|
|
5128
|
+
if (normalizedName.includes(normalizedQuery)) return 700;
|
|
5129
|
+
if (normalizedPath.startsWith(normalizedQuery)) return 600;
|
|
5130
|
+
if (normalizedPath.split("/").some((segment) => segment.startsWith(normalizedQuery))) return 550;
|
|
5131
|
+
return 400 - Math.min(normalizedPath.length, 300);
|
|
5132
|
+
}
|
|
5133
|
+
//#endregion
|
|
5134
|
+
//#region src/features/server-path/services/server-path-search.service.ts
|
|
5135
|
+
const DEFAULT_RESULT_LIMIT = 50;
|
|
5136
|
+
const MAX_RESULT_LIMIT = 100;
|
|
5137
|
+
const MAX_SCANNED_ENTRIES = 2e4;
|
|
5138
|
+
const MAX_SEARCH_DEPTH = 24;
|
|
5139
|
+
const IGNORED_DIRECTORY_NAMES = new Set([
|
|
5140
|
+
".git",
|
|
5141
|
+
".cache",
|
|
5142
|
+
".next",
|
|
5143
|
+
".nuxt",
|
|
5144
|
+
".output",
|
|
5145
|
+
".parcel-cache",
|
|
5146
|
+
".turbo",
|
|
5147
|
+
".vite",
|
|
5148
|
+
"build",
|
|
5149
|
+
"coverage",
|
|
5150
|
+
"dist",
|
|
5151
|
+
"node_modules",
|
|
5152
|
+
"out",
|
|
5153
|
+
"target",
|
|
5154
|
+
"ui-dist",
|
|
5155
|
+
"vendor"
|
|
5156
|
+
]);
|
|
5157
|
+
var ServerPathSearchError = class extends Error {
|
|
5158
|
+
constructor(code, message) {
|
|
5159
|
+
super(message);
|
|
5160
|
+
this.code = code;
|
|
5161
|
+
this.name = "ServerPathSearchError";
|
|
5162
|
+
}
|
|
5163
|
+
};
|
|
5164
|
+
var ServerPathSearchService = class {
|
|
5165
|
+
search = async (options) => {
|
|
5166
|
+
const basePath = await this.resolveBasePath(options.basePath);
|
|
5167
|
+
const query = options.query?.trim() ?? "";
|
|
5168
|
+
const limit = Math.min(Math.max(options.limit ?? DEFAULT_RESULT_LIMIT, 1), MAX_RESULT_LIMIT);
|
|
5169
|
+
const { candidates, scanLimitReached } = await this.scanCandidates({
|
|
5170
|
+
basePath,
|
|
5171
|
+
query
|
|
5172
|
+
});
|
|
5173
|
+
candidates.sort((left, right) => {
|
|
5174
|
+
if (right.score !== left.score) return right.score - left.score;
|
|
5175
|
+
if (left.entry.kind !== right.entry.kind) return left.entry.kind === "directory" ? -1 : 1;
|
|
5176
|
+
return left.entry.relativePath.localeCompare(right.entry.relativePath, void 0, {
|
|
5177
|
+
numeric: true,
|
|
5178
|
+
sensitivity: "base"
|
|
5179
|
+
});
|
|
5180
|
+
});
|
|
5181
|
+
return {
|
|
5182
|
+
basePath,
|
|
5183
|
+
query,
|
|
5184
|
+
entries: candidates.slice(0, limit).map(({ entry }) => entry),
|
|
5185
|
+
truncated: scanLimitReached || candidates.length > limit
|
|
5186
|
+
};
|
|
5187
|
+
};
|
|
5188
|
+
scanCandidates = async (params) => {
|
|
5189
|
+
const { basePath, query } = params;
|
|
5190
|
+
const queue = [{
|
|
5191
|
+
absolutePath: basePath,
|
|
5192
|
+
depth: 0
|
|
5193
|
+
}];
|
|
5194
|
+
const candidates = [];
|
|
5195
|
+
let scannedEntries = 0;
|
|
5196
|
+
let scanLimitReached = false;
|
|
5197
|
+
while (queue.length > 0 && !scanLimitReached) {
|
|
5198
|
+
const current = queue.shift();
|
|
5199
|
+
if (!current) break;
|
|
5200
|
+
const scan = await this.scanDirectory({
|
|
5201
|
+
basePath,
|
|
5202
|
+
directory: current,
|
|
5203
|
+
query,
|
|
5204
|
+
remainingEntries: MAX_SCANNED_ENTRIES - scannedEntries
|
|
5205
|
+
});
|
|
5206
|
+
scannedEntries += scan.scannedEntries;
|
|
5207
|
+
candidates.push(...scan.candidates);
|
|
5208
|
+
queue.push(...scan.directories);
|
|
5209
|
+
scanLimitReached = scan.limitReached;
|
|
5210
|
+
if (!query) break;
|
|
5211
|
+
}
|
|
5212
|
+
return {
|
|
5213
|
+
candidates,
|
|
5214
|
+
scanLimitReached
|
|
5215
|
+
};
|
|
5216
|
+
};
|
|
5217
|
+
scanDirectory = async (params) => {
|
|
5218
|
+
const { basePath, directory, query, remainingEntries } = params;
|
|
5219
|
+
const entries = await this.readDirectory({
|
|
5220
|
+
path: directory.absolutePath,
|
|
5221
|
+
required: directory.depth === 0
|
|
5222
|
+
});
|
|
5223
|
+
const candidates = [];
|
|
5224
|
+
const directories = [];
|
|
5225
|
+
let scannedEntries = 0;
|
|
5226
|
+
for (const dirent of entries) {
|
|
5227
|
+
if (scannedEntries >= remainingEntries) return {
|
|
5228
|
+
candidates,
|
|
5229
|
+
directories,
|
|
5230
|
+
limitReached: true,
|
|
5231
|
+
scannedEntries
|
|
5232
|
+
};
|
|
5233
|
+
scannedEntries += 1;
|
|
5234
|
+
const result = await this.resolveEntry({
|
|
5235
|
+
basePath,
|
|
5236
|
+
directory,
|
|
5237
|
+
dirent
|
|
5238
|
+
});
|
|
5239
|
+
if (!result) continue;
|
|
5240
|
+
const score = resolveServerPathSearchScore(result.entry, query);
|
|
5241
|
+
if (score > 0) candidates.push({
|
|
5242
|
+
entry: result.entry,
|
|
5243
|
+
score
|
|
5244
|
+
});
|
|
5245
|
+
if (query && result.canTraverse && directory.depth < MAX_SEARCH_DEPTH && !IGNORED_DIRECTORY_NAMES.has(dirent.name)) directories.push({
|
|
5246
|
+
absolutePath: result.entry.path,
|
|
5247
|
+
depth: directory.depth + 1
|
|
5248
|
+
});
|
|
5249
|
+
}
|
|
5250
|
+
return {
|
|
5251
|
+
candidates,
|
|
5252
|
+
directories,
|
|
5253
|
+
limitReached: false,
|
|
5254
|
+
scannedEntries
|
|
5255
|
+
};
|
|
5256
|
+
};
|
|
5257
|
+
resolveBasePath = async (value) => {
|
|
5258
|
+
let resolvedPath;
|
|
5259
|
+
try {
|
|
5260
|
+
resolvedPath = resolveServerPath({ path: value });
|
|
5261
|
+
} catch (error) {
|
|
5262
|
+
if (error instanceof ServerPathResolutionError) throw new ServerPathSearchError("SERVER_PATH_BASE_REQUIRED", error.message);
|
|
5263
|
+
throw error;
|
|
5264
|
+
}
|
|
5265
|
+
let stats;
|
|
5266
|
+
try {
|
|
5267
|
+
stats = await stat(resolvedPath);
|
|
5268
|
+
} catch {
|
|
5269
|
+
throw new ServerPathSearchError("SERVER_PATH_NOT_FOUND", "server path does not exist");
|
|
5270
|
+
}
|
|
5271
|
+
if (!stats.isDirectory()) throw new ServerPathSearchError("SERVER_PATH_NOT_DIRECTORY", "server path must point to a directory");
|
|
5272
|
+
return await realpath(resolvedPath);
|
|
5273
|
+
};
|
|
5274
|
+
readDirectory = async (params) => {
|
|
5275
|
+
try {
|
|
5276
|
+
return (await readdir(params.path, { withFileTypes: true })).sort((left, right) => left.name.localeCompare(right.name, void 0, {
|
|
5277
|
+
numeric: true,
|
|
5278
|
+
sensitivity: "base"
|
|
5279
|
+
}));
|
|
5280
|
+
} catch {
|
|
5281
|
+
if (!params.required) return [];
|
|
5282
|
+
throw new ServerPathSearchError("SERVER_PATH_NOT_READABLE", "server path is not readable");
|
|
5283
|
+
}
|
|
5284
|
+
};
|
|
5285
|
+
resolveEntry = async (params) => {
|
|
5286
|
+
const { basePath, directory, dirent } = params;
|
|
5287
|
+
const entryPath = resolve(directory.absolutePath, dirent.name);
|
|
5288
|
+
let kind = dirent.isDirectory() ? "directory" : dirent.isFile() ? "file" : null;
|
|
5289
|
+
let canTraverse = kind === "directory";
|
|
5290
|
+
if (dirent.isSymbolicLink()) {
|
|
5291
|
+
let targetPath;
|
|
5292
|
+
try {
|
|
5293
|
+
targetPath = await realpath(entryPath);
|
|
5294
|
+
} catch {
|
|
5295
|
+
return null;
|
|
5296
|
+
}
|
|
5297
|
+
if (!isServerPathInside(basePath, targetPath)) return null;
|
|
5298
|
+
const targetStats = await stat(targetPath).catch(() => null);
|
|
5299
|
+
kind = targetStats?.isDirectory() ? "directory" : targetStats?.isFile() ? "file" : null;
|
|
5300
|
+
canTraverse = false;
|
|
5301
|
+
}
|
|
5302
|
+
if (!kind) return null;
|
|
5303
|
+
const relativePath = normalizeServerPathRelativePath(relative(basePath, entryPath));
|
|
5304
|
+
return {
|
|
5305
|
+
entry: {
|
|
5306
|
+
name: basename(entryPath),
|
|
5307
|
+
path: entryPath,
|
|
5308
|
+
relativePath,
|
|
5309
|
+
parentRelativePath: normalizeServerPathRelativePath(dirname(relativePath)) === "." ? "" : normalizeServerPathRelativePath(dirname(relativePath)),
|
|
5310
|
+
kind,
|
|
5311
|
+
hidden: dirent.name.startsWith(".")
|
|
5312
|
+
},
|
|
5313
|
+
canTraverse
|
|
5314
|
+
};
|
|
5315
|
+
};
|
|
5316
|
+
};
|
|
5317
|
+
function isServerPathSearchError(error) {
|
|
5318
|
+
return error instanceof ServerPathSearchError;
|
|
5319
|
+
}
|
|
5320
|
+
//#endregion
|
|
5058
5321
|
//#region src/features/server-path/controllers/server-path.controller.ts
|
|
5059
5322
|
function readIncludeFilesFlag(value) {
|
|
5060
5323
|
return value === "1" || value === "true";
|
|
@@ -5070,6 +5333,7 @@ function statusForServerPathContentError(code) {
|
|
|
5070
5333
|
return code === "SERVER_PATH_NOT_FOUND" ? 404 : 400;
|
|
5071
5334
|
}
|
|
5072
5335
|
var ServerPathRoutesController = class {
|
|
5336
|
+
searchService = new ServerPathSearchService();
|
|
5073
5337
|
browse = async (c) => {
|
|
5074
5338
|
try {
|
|
5075
5339
|
const payload = await browseServerPath({
|
|
@@ -5083,6 +5347,32 @@ var ServerPathRoutesController = class {
|
|
|
5083
5347
|
throw error;
|
|
5084
5348
|
}
|
|
5085
5349
|
};
|
|
5350
|
+
search = async (c) => {
|
|
5351
|
+
try {
|
|
5352
|
+
const payload = await this.searchService.search({
|
|
5353
|
+
basePath: c.req.query("basePath"),
|
|
5354
|
+
query: c.req.query("query"),
|
|
5355
|
+
limit: readPositiveInteger(c.req.query("limit"))
|
|
5356
|
+
});
|
|
5357
|
+
return c.json(ok(payload));
|
|
5358
|
+
} catch (error) {
|
|
5359
|
+
if (isServerPathSearchError(error)) return c.json(err(error.code, error.message), 400);
|
|
5360
|
+
throw error;
|
|
5361
|
+
}
|
|
5362
|
+
};
|
|
5363
|
+
createDirectory = async (c) => {
|
|
5364
|
+
const body = await readJson(c.req.raw);
|
|
5365
|
+
if (!body.ok || !isRecord$2(body.data)) return c.json(err("INVALID_SERVER_PATH_DIRECTORY", "directory input is required"), 400);
|
|
5366
|
+
try {
|
|
5367
|
+
return c.json(ok(await createServerPathDirectory({
|
|
5368
|
+
parentPath: body.data.parentPath,
|
|
5369
|
+
name: body.data.name
|
|
5370
|
+
})), 201);
|
|
5371
|
+
} catch (error) {
|
|
5372
|
+
if (isServerPathDirectoryCreateError(error)) return c.json(err(error.code, error.message), 400);
|
|
5373
|
+
throw error;
|
|
5374
|
+
}
|
|
5375
|
+
};
|
|
5086
5376
|
read = async (c) => {
|
|
5087
5377
|
try {
|
|
5088
5378
|
const payload = await readServerPath({
|
|
@@ -5131,6 +5421,7 @@ function createUiRouteControllers(options, authService, marketplaceBaseUrls) {
|
|
|
5131
5421
|
ncpAsset: new NcpAssetRoutesController(options),
|
|
5132
5422
|
panelApps: new PanelAppsRoutesController(kernel.panelAppManager, { panelAppClientSdkScript }),
|
|
5133
5423
|
preferences: new PreferencesRoutesController(kernel.preferenceManager),
|
|
5424
|
+
projects: new ProjectsRoutesController(kernel.projectManager),
|
|
5134
5425
|
serviceApps: new ServiceAppsRoutesController({
|
|
5135
5426
|
panelAppManager: kernel.panelAppManager,
|
|
5136
5427
|
serviceAppManager: kernel.serviceAppManager
|
|
@@ -5218,7 +5509,7 @@ var UiRouteRegistry = class {
|
|
|
5218
5509
|
]]);
|
|
5219
5510
|
};
|
|
5220
5511
|
mountResourceRoutes = () => {
|
|
5221
|
-
const { ncpSession, panelApps, preferences, serviceApps, serverPath } = this.controllers;
|
|
5512
|
+
const { ncpSession, panelApps, preferences, projects, serviceApps, serverPath } = this.controllers;
|
|
5222
5513
|
this.mountRoutes([
|
|
5223
5514
|
[
|
|
5224
5515
|
"get",
|
|
@@ -5325,6 +5616,16 @@ var UiRouteRegistry = class {
|
|
|
5325
5616
|
"/api/preferences/:key",
|
|
5326
5617
|
preferences.delete
|
|
5327
5618
|
],
|
|
5619
|
+
[
|
|
5620
|
+
"get",
|
|
5621
|
+
"/api/projects",
|
|
5622
|
+
projects.list
|
|
5623
|
+
],
|
|
5624
|
+
[
|
|
5625
|
+
"post",
|
|
5626
|
+
"/api/projects",
|
|
5627
|
+
projects.create
|
|
5628
|
+
],
|
|
5328
5629
|
[
|
|
5329
5630
|
"delete",
|
|
5330
5631
|
"/api/panel-apps/:id",
|
|
@@ -5415,6 +5716,16 @@ var UiRouteRegistry = class {
|
|
|
5415
5716
|
"/api/server-paths/browse",
|
|
5416
5717
|
serverPath.browse
|
|
5417
5718
|
],
|
|
5719
|
+
[
|
|
5720
|
+
"get",
|
|
5721
|
+
"/api/server-paths/search",
|
|
5722
|
+
serverPath.search
|
|
5723
|
+
],
|
|
5724
|
+
[
|
|
5725
|
+
"post",
|
|
5726
|
+
"/api/server-paths/directory",
|
|
5727
|
+
serverPath.createDirectory
|
|
5728
|
+
],
|
|
5418
5729
|
[
|
|
5419
5730
|
"get",
|
|
5420
5731
|
"/api/server-paths/read",
|
|
@@ -5941,6 +6252,14 @@ async function startUiServer(gateway) {
|
|
|
5941
6252
|
const resolvedAllowOrigin = allowOrigin ?? (isPanelAppRuntimeCors ? NULL_ORIGIN : null);
|
|
5942
6253
|
if (resolvedAllowOrigin) for (const [name, value] of createCorsHeaders(resolvedAllowOrigin, allowHeaders, c.res.headers.get("Vary"))) c.res.headers.set(name, value);
|
|
5943
6254
|
});
|
|
6255
|
+
const uiInjectionPath = join(dirname(gateway.configPath), "ui-inject.js");
|
|
6256
|
+
app.get("/api/ui-inject.js", async (c) => c.body(await readFile(uiInjectionPath, "utf-8").catch((error) => {
|
|
6257
|
+
if (error.code === "ENOENT") return "";
|
|
6258
|
+
throw error;
|
|
6259
|
+
}), 200, {
|
|
6260
|
+
"content-type": "application/javascript; charset=utf-8",
|
|
6261
|
+
"cache-control": "no-store"
|
|
6262
|
+
}));
|
|
5944
6263
|
const eventStreamAuth = new EventStreamAuthService({
|
|
5945
6264
|
uiAuth: authService,
|
|
5946
6265
|
extensionAuth: gateway.extensions,
|