@nextclaw/server 0.15.5 → 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.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, parseThinkingLevel, probeFeishu, readAgentAvatarContent, removeAgentProfile, resolveEffectiveAgentProfiles, saveConfig, toExtensionConfigView, updateAgentProfile } from "@nextclaw/core";
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 stripProviderIdPrefix(providerId, model) {
2498
+ function rewriteProviderRoutePrefix(providerId, targetPrefix, model) {
2499
2499
  const prefix = `${providerId}/`;
2500
2500
  if (!model.startsWith(prefix)) return model;
2501
- return model.slice(prefix.length).trim() || model;
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 spec ? requestedModel.replace(`${providerId}/`, `${spec.name}/`) : stripProviderIdPrefix(providerId, requestedModel);
2505
+ if (requestedModel) return rewriteProviderRoutePrefix(providerId, spec?.name ?? null, requestedModel);
2505
2506
  const providerModels = normalizeModelList(provider.models ?? []).map((modelId) => {
2506
- const providerModel = stripProviderIdPrefix(providerId, modelId);
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 spec ? defaultModel.replace(`${providerId}/`, `${spec.name}/`) : stripProviderIdPrefix(providerId, defaultModel);
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: {
@@ -3812,6 +3752,23 @@ var SessionSkillsViewBuilder = class {
3812
3752
  //#endregion
3813
3753
  //#region src/features/sessions/controllers/sessions.controller.ts
3814
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
+ }
3815
3772
  function readPositiveInt(value) {
3816
3773
  if (typeof value !== "string") return;
3817
3774
  const parsed = Number.parseInt(value, 10);
@@ -3845,49 +3802,6 @@ function normalizeSessionActivityPreview(session) {
3845
3802
  }
3846
3803
  };
3847
3804
  }
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
3805
  var NcpSessionRoutesController = class {
3892
3806
  sessionSkillsViewBuilder;
3893
3807
  constructor(options) {
@@ -3943,14 +3857,17 @@ var NcpSessionRoutesController = class {
3943
3857
  const hasProjectRootOverride = Object.prototype.hasOwnProperty.call(query, "projectRoot");
3944
3858
  const metadata = readSessionMetadata((await sessionManager.getSession(sessionId))?.metadata);
3945
3859
  if (hasProjectRootOverride) try {
3946
- const projectRoot = await normalizeSessionProjectRoot(query.projectRoot);
3860
+ const projectRoot = await this.options.kernel.projectManager.resolveExistingProjectRoot(query.projectRoot);
3947
3861
  if (projectRoot) metadata.project_root = projectRoot;
3948
3862
  else {
3949
3863
  delete metadata.project_root;
3950
3864
  delete metadata.projectRoot;
3951
3865
  }
3952
3866
  } catch (error) {
3953
- if (isSessionProjectRootValidationError(error)) return c.json(err(error.code, error.message), 400);
3867
+ if (isProjectError(error)) {
3868
+ const mapped = sessionProjectError(error);
3869
+ return c.json(err(mapped.code, mapped.message), 400);
3870
+ }
3954
3871
  throw error;
3955
3872
  }
3956
3873
  return c.json(ok(this.sessionSkillsViewBuilder.build({
@@ -3965,19 +3882,17 @@ var NcpSessionRoutesController = class {
3965
3882
  if (!body.ok || !body.data || typeof body.data !== "object") return c.json(err("INVALID_BODY", "invalid json body"), 400);
3966
3883
  const patch = body.data;
3967
3884
  if (patch.clearHistory) return c.json(err("UNSUPPORTED_PATCH", "clearHistory is not supported for ncp sessions"), 400);
3968
- let patched;
3885
+ let updated;
3969
3886
  try {
3970
- const projectRootPatch = Object.prototype.hasOwnProperty.call(patch, "projectRoot") ? await normalizeSessionProjectRoot(patch.projectRoot) : void 0;
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 }));
3887
+ updated = await sessionManager.patchSessionSettings(sessionId, patch, { createIfMissing: true });
3974
3888
  } catch (error) {
3975
- if (error instanceof Error && error.message === "PREFERRED_THINKING_INVALID") return c.json(err("PREFERRED_THINKING_INVALID", "preferredThinking must be a supported thinking level"), 400);
3976
- if (isSessionProjectRootValidationError(error)) return c.json(err(error.code, error.message), 400);
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
+ }
3977
3894
  throw error;
3978
3895
  }
3979
- if (!patched) return c.json(err("NOT_FOUND", `ncp session not found: ${sessionId}`), 404);
3980
- const updated = await sessionManager.getSession(sessionId);
3981
3896
  if (!updated) return c.json(err("NOT_FOUND", `ncp session not found: ${sessionId}`), 404);
3982
3897
  return c.json(ok(this.withRuntimeStatus(updated)));
3983
3898
  };
@@ -4452,6 +4367,31 @@ var PreferencesRoutesController = class {
4452
4367
  };
4453
4368
  };
4454
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
4455
4395
  //#region src/features/service-apps/controllers/service-apps.controller.ts
4456
4396
  const PANEL_BRIDGE_SESSION_HEADER = "x-nextclaw-panel-bridge-session";
4457
4397
  function statusForServiceAppError(code) {
@@ -4652,6 +4592,100 @@ function createNcpSessionEventStreamResponse(eventBus, payload, signal) {
4652
4592
  } });
4653
4593
  }
4654
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
4655
4689
  //#region src/features/server-path/utils/server-path-browse.utils.ts
4656
4690
  var ServerPathBrowseError = class extends Error {
4657
4691
  constructor(code, message) {
@@ -4660,15 +4694,16 @@ var ServerPathBrowseError = class extends Error {
4660
4694
  this.name = "ServerPathBrowseError";
4661
4695
  }
4662
4696
  };
4663
- function normalizeBrowsePath(options) {
4664
- const { basePath, path } = options;
4665
- const rawPath = typeof path === "string" ? path.trim() : "";
4666
- const normalizedBasePath = typeof basePath === "string" ? basePath.trim() : "";
4667
- if (!rawPath) return resolve(expandHome(normalizedBasePath || homedir()));
4668
- const expandedPath = expandHome(rawPath);
4669
- if (expandedPath.startsWith("/")) return resolve(expandedPath);
4670
- if (!normalizedBasePath) throw new ServerPathBrowseError("SERVER_PATH_BASE_REQUIRED", "relative server path requires a base path");
4671
- return resolve(expandHome(normalizedBasePath), expandedPath);
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
+ }
4672
4707
  }
4673
4708
  function readRootLabel(rootPath) {
4674
4709
  return rootPath.replace(/[\\/]+$/, "") || rootPath;
@@ -4714,7 +4749,7 @@ function sortEntryViews(left, right) {
4714
4749
  return left.name.localeCompare(right.name);
4715
4750
  }
4716
4751
  async function browseServerPath(options = {}) {
4717
- const currentPath = normalizeBrowsePath(options);
4752
+ const currentPath = resolveBrowsePath(options);
4718
4753
  let currentPathStats;
4719
4754
  try {
4720
4755
  currentPathStats = await stat(currentPath);
@@ -4734,37 +4769,59 @@ async function browseServerPath(options = {}) {
4734
4769
  includeFiles: options.includeFiles ?? false
4735
4770
  })))).filter((entry) => entry !== null).sort(sortEntryViews);
4736
4771
  const parentPath = dirname(currentPath);
4772
+ const homePath = resolve(homedir());
4737
4773
  return {
4738
4774
  currentPath,
4739
4775
  parentPath: parentPath === currentPath ? null : parentPath,
4740
- homePath: resolve(homedir()),
4776
+ homePath,
4741
4777
  breadcrumbs: buildBreadcrumbs(currentPath),
4742
- entries
4778
+ entries,
4779
+ locations: await resolveServerPathLocations(homePath)
4743
4780
  };
4744
4781
  }
4745
4782
  function isServerPathBrowseError(error) {
4746
4783
  return error instanceof ServerPathBrowseError;
4747
4784
  }
4748
4785
  //#endregion
4749
- //#region src/features/server-path/utils/server-path-resolution.utils.ts
4750
- var ServerPathResolutionError = class extends Error {
4751
- constructor(message) {
4786
+ //#region src/features/server-path/utils/server-path-directory.utils.ts
4787
+ var ServerPathDirectoryCreateError = class extends Error {
4788
+ constructor(code, message) {
4752
4789
  super(message);
4753
- this.name = "ServerPathResolutionError";
4790
+ this.code = code;
4791
+ this.name = "ServerPathDirectoryCreateError";
4754
4792
  }
4755
4793
  };
4756
- function resolveServerPath(params) {
4757
- const { path, basePath: rawBasePath, defaultToHome } = params;
4758
- const rawPath = typeof path === "string" ? path.trim() : "";
4759
- const basePath = typeof rawBasePath === "string" ? rawBasePath.trim() : "";
4760
- if (!rawPath) {
4761
- if (defaultToHome) return resolve(expandHome(basePath || homedir()));
4762
- throw new ServerPathResolutionError("server path is required");
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;
4809
+ }
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;
4763
4821
  }
4764
- const expandedPath = expandHome(rawPath);
4765
- if (expandedPath.startsWith("/") || /^[a-z]:[\\/]/i.test(expandedPath)) return resolve(expandedPath);
4766
- if (!basePath) throw new ServerPathResolutionError("relative server path requires a base path");
4767
- return resolve(expandHome(basePath), expandedPath);
4822
+ }
4823
+ function isServerPathDirectoryCreateError(error) {
4824
+ return error instanceof ServerPathDirectoryCreateError;
4768
4825
  }
4769
4826
  //#endregion
4770
4827
  //#region src/features/server-path/utils/server-path-read.utils.ts
@@ -5055,6 +5112,215 @@ function isServerPathContentError(error) {
5055
5112
  return error instanceof ServerPathContentError;
5056
5113
  }
5057
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
5058
5324
  //#region src/features/server-path/controllers/server-path.controller.ts
5059
5325
  function readIncludeFilesFlag(value) {
5060
5326
  return value === "1" || value === "true";
@@ -5070,6 +5336,7 @@ function statusForServerPathContentError(code) {
5070
5336
  return code === "SERVER_PATH_NOT_FOUND" ? 404 : 400;
5071
5337
  }
5072
5338
  var ServerPathRoutesController = class {
5339
+ searchService = new ServerPathSearchService();
5073
5340
  browse = async (c) => {
5074
5341
  try {
5075
5342
  const payload = await browseServerPath({
@@ -5083,6 +5350,32 @@ var ServerPathRoutesController = class {
5083
5350
  throw error;
5084
5351
  }
5085
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
+ };
5086
5379
  read = async (c) => {
5087
5380
  try {
5088
5381
  const payload = await readServerPath({
@@ -5131,6 +5424,7 @@ function createUiRouteControllers(options, authService, marketplaceBaseUrls) {
5131
5424
  ncpAsset: new NcpAssetRoutesController(options),
5132
5425
  panelApps: new PanelAppsRoutesController(kernel.panelAppManager, { panelAppClientSdkScript }),
5133
5426
  preferences: new PreferencesRoutesController(kernel.preferenceManager),
5427
+ projects: new ProjectsRoutesController(kernel.projectManager),
5134
5428
  serviceApps: new ServiceAppsRoutesController({
5135
5429
  panelAppManager: kernel.panelAppManager,
5136
5430
  serviceAppManager: kernel.serviceAppManager
@@ -5218,7 +5512,7 @@ var UiRouteRegistry = class {
5218
5512
  ]]);
5219
5513
  };
5220
5514
  mountResourceRoutes = () => {
5221
- const { ncpSession, panelApps, preferences, serviceApps, serverPath } = this.controllers;
5515
+ const { ncpSession, panelApps, preferences, projects, serviceApps, serverPath } = this.controllers;
5222
5516
  this.mountRoutes([
5223
5517
  [
5224
5518
  "get",
@@ -5325,6 +5619,16 @@ var UiRouteRegistry = class {
5325
5619
  "/api/preferences/:key",
5326
5620
  preferences.delete
5327
5621
  ],
5622
+ [
5623
+ "get",
5624
+ "/api/projects",
5625
+ projects.list
5626
+ ],
5627
+ [
5628
+ "post",
5629
+ "/api/projects",
5630
+ projects.create
5631
+ ],
5328
5632
  [
5329
5633
  "delete",
5330
5634
  "/api/panel-apps/:id",
@@ -5415,6 +5719,16 @@ var UiRouteRegistry = class {
5415
5719
  "/api/server-paths/browse",
5416
5720
  serverPath.browse
5417
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
+ ],
5418
5732
  [
5419
5733
  "get",
5420
5734
  "/api/server-paths/read",