@yejiming/dsh-data-agent 0.0.9 → 0.0.10

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.
@@ -1,5 +1,7 @@
1
1
  import "./defaults-DP4RyRh1.js";
2
- import { resolve } from "node:path";
2
+ import { readdir } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { posix, resolve, win32 } from "node:path";
3
5
  import z from "schemastery";
4
6
  import { credentialRef } from "@deepseek-ai/dsh-credentials";
5
7
  //#region src/sql.ts
@@ -595,7 +597,8 @@ function quoteStringLiteral(value) {
595
597
  /** Loader schema for one client override (all fields optional at input). */
596
598
  const clientConfigSchema = z.object({
597
599
  command: z.string(),
598
- args: z.array(z.string())
600
+ args: z.array(z.string()),
601
+ searchPaths: z.array(z.string())
599
602
  });
600
603
  /** Loader schema for the whole `clients` config object (any type key). */
601
604
  const clientsSchema = z.dict(clientConfigSchema).default({});
@@ -891,6 +894,291 @@ function parseColumns(type, stdout) {
891
894
  return columns;
892
895
  }
893
896
  //#endregion
897
+ //#region src/client-discovery.ts
898
+ /**
899
+ * Cross-platform database CLI discovery.
900
+ *
901
+ * The subprocess provider remains the authority for executable validation.
902
+ * This module only builds a bounded, platform-aware PATH fallback when the
903
+ * provider cannot resolve the configured/default bare command from its
904
+ * current execution environment. No shell, registry, or recursive scan is
905
+ * involved, and the exact discovery environment is returned for spawn.
906
+ * @module @yejiming/dsh-data-agent/client-discovery
907
+ */
908
+ /** Maximum child names consumed from one known version/formula directory. */
909
+ const MAX_DYNAMIC_ENTRIES = 64;
910
+ /** Production host facts. */
911
+ const DEFAULT_SYSTEM = {
912
+ platform: process.platform,
913
+ env: process.env,
914
+ homeDir: homedir(),
915
+ cwd: process.cwd(),
916
+ async readDirectory(directory) {
917
+ return await readdir(directory);
918
+ }
919
+ };
920
+ const HOME_ENV_BY_TYPE = {
921
+ mysql: ["MYSQL_HOME"],
922
+ postgres: ["PGHOME", "PGROOT"],
923
+ sqlite: ["SQLITE_HOME"],
924
+ oracle: ["ORACLE_HOME"],
925
+ hive: ["HIVE_HOME"],
926
+ impala: ["IMPALA_HOME"]
927
+ };
928
+ function pathApi(platform) {
929
+ return platform === "win32" ? win32 : posix;
930
+ }
931
+ function environmentValue(env, name, platform) {
932
+ if (platform !== "win32") return env[name];
933
+ const key = Object.keys(env).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
934
+ return key === void 0 ? void 0 : env[key];
935
+ }
936
+ function expandHome(directory, system, paths) {
937
+ const trimmed = directory.trim();
938
+ if (trimmed === "~") return system.homeDir;
939
+ if (trimmed.startsWith("~/") || trimmed.startsWith("~\\")) return paths.join(system.homeDir, trimmed.slice(2));
940
+ return paths.isAbsolute(trimmed) ? paths.normalize(trimmed) : paths.resolve(system.cwd, trimmed);
941
+ }
942
+ function normalizeDirectories(directories, system, paths) {
943
+ const result = [];
944
+ const seen = /* @__PURE__ */ new Set();
945
+ for (const raw of directories) {
946
+ if (raw.trim() === "") continue;
947
+ const directory = expandHome(raw, system, paths);
948
+ const key = system.platform === "win32" ? directory.toLowerCase() : directory;
949
+ if (seen.has(key)) continue;
950
+ seen.add(key);
951
+ result.push(directory);
952
+ }
953
+ return result;
954
+ }
955
+ function clientHomeDirectories(type, system, paths) {
956
+ const result = [];
957
+ for (const name of HOME_ENV_BY_TYPE[type]) {
958
+ const value = environmentValue(system.env, name, system.platform)?.trim();
959
+ if (value === void 0 || value === "") continue;
960
+ result.push(paths.join(value, "bin"), value);
961
+ }
962
+ return result;
963
+ }
964
+ function macFixedDirectories(type) {
965
+ return [
966
+ "/opt/homebrew/bin",
967
+ ...{
968
+ mysql: [
969
+ "/opt/homebrew/opt/mysql-client/bin",
970
+ "/opt/homebrew/opt/mysql/bin",
971
+ "/usr/local/opt/mysql-client/bin",
972
+ "/usr/local/opt/mysql/bin",
973
+ "/usr/local/mysql/bin"
974
+ ],
975
+ postgres: [
976
+ "/opt/homebrew/opt/libpq/bin",
977
+ "/usr/local/opt/libpq/bin",
978
+ "/Applications/Postgres.app/Contents/Versions/latest/bin"
979
+ ],
980
+ sqlite: ["/opt/homebrew/opt/sqlite/bin", "/usr/local/opt/sqlite/bin"],
981
+ oracle: [],
982
+ hive: ["/opt/homebrew/opt/hive/bin", "/usr/local/opt/hive/bin"],
983
+ impala: ["/opt/homebrew/opt/impala/bin", "/usr/local/opt/impala/bin"]
984
+ }[type],
985
+ "/usr/local/bin",
986
+ "/opt/local/bin",
987
+ "/usr/bin"
988
+ ];
989
+ }
990
+ function linuxFixedDirectories(system, paths) {
991
+ return [
992
+ paths.join(system.homeDir, ".local", "bin"),
993
+ "/home/linuxbrew/.linuxbrew/bin",
994
+ paths.join(system.homeDir, ".linuxbrew", "bin"),
995
+ "/usr/local/bin",
996
+ "/usr/bin",
997
+ "/snap/bin",
998
+ paths.join(system.homeDir, ".nix-profile", "bin"),
999
+ "/nix/var/nix/profiles/default/bin"
1000
+ ];
1001
+ }
1002
+ function windowsFixedDirectories(type, system, paths) {
1003
+ const localAppData = environmentValue(system.env, "LOCALAPPDATA", system.platform);
1004
+ const userProfile = environmentValue(system.env, "USERPROFILE", system.platform) ?? system.homeDir;
1005
+ const chocolatey = environmentValue(system.env, "ChocolateyInstall", system.platform);
1006
+ const programData = environmentValue(system.env, "ProgramData", system.platform) ?? "C:\\ProgramData";
1007
+ const programFiles = environmentValue(system.env, "ProgramFiles", system.platform) ?? "C:\\Program Files";
1008
+ const typeSpecific = {
1009
+ mysql: [],
1010
+ postgres: [],
1011
+ sqlite: [paths.join("C:\\", "sqlite"), paths.join(programFiles, "SQLite")],
1012
+ oracle: [],
1013
+ hive: [],
1014
+ impala: []
1015
+ };
1016
+ return [
1017
+ ...localAppData === void 0 ? [] : [paths.join(localAppData, "Microsoft", "WinGet", "Links")],
1018
+ paths.join(userProfile, "scoop", "shims"),
1019
+ ...chocolatey === void 0 ? [] : [paths.join(chocolatey, "bin")],
1020
+ paths.join(programData, "chocolatey", "bin"),
1021
+ ...typeSpecific[type]
1022
+ ];
1023
+ }
1024
+ function formulaPattern(type) {
1025
+ switch (type) {
1026
+ case "mysql": return /^(?:mysql|mysql-client)(?:@.+)?$/i;
1027
+ case "postgres": return /^(?:postgresql(?:@.+)?|libpq)$/i;
1028
+ case "sqlite": return /^sqlite(?:@.+)?$/i;
1029
+ case "oracle": return /^(?:oracle|instantclient)(?:@.+)?$/i;
1030
+ case "hive": return /^hive(?:@.+)?$/i;
1031
+ case "impala": return /^impala(?:@.+)?$/i;
1032
+ }
1033
+ }
1034
+ function dynamicDirectories(type, system, paths) {
1035
+ const result = [];
1036
+ if (system.platform === "darwin") {
1037
+ const pattern = formulaPattern(type);
1038
+ result.push({
1039
+ root: "/opt/homebrew/opt",
1040
+ accepts: (name) => pattern.test(name),
1041
+ suffix: ["bin"]
1042
+ }, {
1043
+ root: "/usr/local/opt",
1044
+ accepts: (name) => pattern.test(name),
1045
+ suffix: ["bin"]
1046
+ });
1047
+ if (type === "postgres") result.push({
1048
+ root: "/Library/PostgreSQL",
1049
+ accepts: () => true,
1050
+ suffix: ["bin"]
1051
+ }, {
1052
+ root: "/Applications/Postgres.app/Contents/Versions",
1053
+ accepts: (name) => name !== "latest",
1054
+ suffix: ["bin"]
1055
+ });
1056
+ if (type === "oracle") result.push({
1057
+ root: "/opt/oracle",
1058
+ accepts: (name) => /^instantclient/i.test(name),
1059
+ suffix: []
1060
+ });
1061
+ } else if (system.platform === "linux") {
1062
+ const pattern = formulaPattern(type);
1063
+ result.push({
1064
+ root: "/home/linuxbrew/.linuxbrew/opt",
1065
+ accepts: (name) => pattern.test(name),
1066
+ suffix: ["bin"]
1067
+ }, {
1068
+ root: paths.join(system.homeDir, ".linuxbrew", "opt"),
1069
+ accepts: (name) => pattern.test(name),
1070
+ suffix: ["bin"]
1071
+ });
1072
+ } else if (system.platform === "win32") {
1073
+ const roots = [environmentValue(system.env, "ProgramFiles", system.platform) ?? "C:\\Program Files", environmentValue(system.env, "ProgramFiles(x86)", system.platform) ?? "C:\\Program Files (x86)"];
1074
+ for (const root of roots) if (type === "mysql") result.push({
1075
+ root: paths.join(root, "MySQL"),
1076
+ accepts: () => true,
1077
+ suffix: ["bin"]
1078
+ }, {
1079
+ root,
1080
+ accepts: (name) => /^MariaDB/i.test(name),
1081
+ suffix: ["bin"]
1082
+ });
1083
+ else if (type === "postgres") result.push({
1084
+ root: paths.join(root, "PostgreSQL"),
1085
+ accepts: () => true,
1086
+ suffix: ["bin"]
1087
+ });
1088
+ else if (type === "oracle") result.push({
1089
+ root: paths.join(root, "Oracle"),
1090
+ accepts: () => true,
1091
+ suffix: ["bin"]
1092
+ });
1093
+ }
1094
+ return result;
1095
+ }
1096
+ async function expandDynamicDirectories(descriptors, system, paths, signal) {
1097
+ return (await Promise.all(descriptors.map(async (descriptor) => {
1098
+ signal.throwIfAborted();
1099
+ let names;
1100
+ try {
1101
+ names = await system.readDirectory(descriptor.root);
1102
+ } catch {
1103
+ return [];
1104
+ }
1105
+ signal.throwIfAborted();
1106
+ return names.filter((name) => descriptor.accepts(name)).sort((left, right) => right.localeCompare(left, void 0, {
1107
+ numeric: true,
1108
+ sensitivity: "base"
1109
+ })).slice(0, MAX_DYNAMIC_ENTRIES).map((name) => paths.join(descriptor.root, name, ...descriptor.suffix));
1110
+ }))).flat();
1111
+ }
1112
+ /** Build ordered fallback directories without recursively scanning the host. */
1113
+ async function buildClientSearchDirectories(type, config, signal, system = DEFAULT_SYSTEM) {
1114
+ const paths = pathApi(system.platform);
1115
+ const configured = config?.searchPaths ?? [];
1116
+ const homes = clientHomeDirectories(type, system, paths);
1117
+ const fixed = system.platform === "win32" ? windowsFixedDirectories(type, system, paths) : system.platform === "darwin" ? macFixedDirectories(type) : linuxFixedDirectories(system, paths);
1118
+ const dynamic = await expandDynamicDirectories(dynamicDirectories(type, system, paths), system, paths, signal);
1119
+ signal.throwIfAborted();
1120
+ return normalizeDirectories([
1121
+ ...configured,
1122
+ ...homes,
1123
+ ...fixed,
1124
+ ...dynamic
1125
+ ], system, paths);
1126
+ }
1127
+ function hasPathSeparator(command) {
1128
+ return command.includes("/") || command.includes("\\");
1129
+ }
1130
+ function withSearchPath(explicitEnv, directories, system) {
1131
+ const pathName = system.platform === "win32" ? Object.keys(system.env).find((name) => name.toLowerCase() === "path") ?? "Path" : "PATH";
1132
+ const explicitPathName = Object.keys(explicitEnv).find((name) => system.platform === "win32" ? name.toLowerCase() === "path" : name === "PATH");
1133
+ const parentPath = explicitPathName === void 0 ? environmentValue(system.env, "PATH", system.platform) : explicitEnv[explicitPathName];
1134
+ const separator = system.platform === "win32" ? ";" : ":";
1135
+ const prefix = directories.join(separator);
1136
+ const combined = parentPath === void 0 || parentPath === "" ? prefix : `${prefix}${separator}${parentPath}`;
1137
+ const result = { ...explicitEnv };
1138
+ if (explicitPathName !== void 0 && explicitPathName !== pathName) delete result[explicitPathName];
1139
+ result[pathName] = combined;
1140
+ return result;
1141
+ }
1142
+ function errorText(error) {
1143
+ return error instanceof Error ? error.message : String(error);
1144
+ }
1145
+ function checkedDirectoriesText(directories) {
1146
+ const visible = directories.slice(0, 16);
1147
+ const suffix = directories.length > visible.length ? `,另有${directories.length - visible.length}个目录` : "";
1148
+ return visible.length === 0 ? "无补充目录" : `${visible.join("、")}${suffix}`;
1149
+ }
1150
+ /**
1151
+ * Resolve one configured/default client. Current PATH (or an explicit path)
1152
+ * always wins. Only a missing bare command activates bounded PATH discovery.
1153
+ */
1154
+ async function resolveClientExecutable(options) {
1155
+ const system = options.system ?? DEFAULT_SYSTEM;
1156
+ let initialError;
1157
+ try {
1158
+ return {
1159
+ executable: await options.resolveExecutable(options.command, options.env, options.signal),
1160
+ env: options.env,
1161
+ searchedDirectories: []
1162
+ };
1163
+ } catch (error) {
1164
+ options.signal.throwIfAborted();
1165
+ initialError = error;
1166
+ }
1167
+ if (pathApi(system.platform).isAbsolute(options.command) || hasPathSeparator(options.command)) throw new Error(`无法解析数据库客户端 "${options.command}"(类型 ${options.type}:${errorText(initialError)});该显式路径不会回退到默认命令,请检查 clients.${options.type}.command`);
1168
+ const directories = await buildClientSearchDirectories(options.type, options.config, options.signal, system);
1169
+ const discoveryEnv = withSearchPath(options.env, directories, system);
1170
+ try {
1171
+ return {
1172
+ executable: await options.resolveExecutable(options.command, discoveryEnv, options.signal),
1173
+ env: discoveryEnv,
1174
+ searchedDirectories: directories
1175
+ };
1176
+ } catch (fallbackError) {
1177
+ options.signal.throwIfAborted();
1178
+ throw new Error(`无法解析数据库客户端 "${options.command}"(类型 ${options.type};当前PATH:${errorText(initialError)};补充PATH:${errorText(fallbackError)})。已检查:${checkedDirectoriesText(directories)};请确认客户端已安装,或配置 clients.${options.type}.command / clients.${options.type}.searchPaths`);
1179
+ }
1180
+ }
1181
+ //#endregion
894
1182
  //#region src/query.ts
895
1183
  /** Read one collected stream from offset 0. */
896
1184
  function readCaptured(reader) {
@@ -934,15 +1222,16 @@ async function runClientQuery(ctx, connection, sql, options, externalSignal, int
934
1222
  if (externalSignal.aborted) controller.abort(externalSignal.reason);
935
1223
  else externalSignal.addEventListener("abort", onExternalAbort, { once: true });
936
1224
  try {
937
- let executable;
938
- try {
939
- executable = await ctx.subprocess.resolveExecutable(template.command, template.env, controller.signal);
940
- } catch (error) {
941
- controller.signal.throwIfAborted();
942
- throw new Error(`无法解析数据库客户端 "${template.command}"(${error instanceof Error ? error.message : String(error)});请确认客户端已安装,或在 data-agent 插件配置的 clients 中覆盖命令名/路径`);
943
- }
1225
+ const resolution = await resolveClientExecutable({
1226
+ type: connection.type,
1227
+ command: template.command,
1228
+ config: options.clients[connection.type],
1229
+ env: template.env,
1230
+ signal: controller.signal,
1231
+ resolveExecutable: ctx.subprocess.resolveExecutable.bind(ctx.subprocess)
1232
+ });
944
1233
  const handle = ctx.subprocess.spawn({
945
- argv: [executable, ...template.args],
1234
+ argv: [resolution.executable, ...template.args],
946
1235
  cwd: process.cwd(),
947
1236
  stdio: {
948
1237
  stdin: { data: `${template.stdinPrefix}${sql}\n` },
@@ -951,7 +1240,7 @@ async function runClientQuery(ctx, connection, sql, options, externalSignal, int
951
1240
  },
952
1241
  graceMs: options.graceMs ?? 5e3,
953
1242
  signal: controller.signal,
954
- env: template.env
1243
+ env: resolution.env
955
1244
  });
956
1245
  let outcome;
957
1246
  try {
package/lib/index.js CHANGED
@@ -1,6 +1,9 @@
1
- import { o as clientsSchema, t as createConnectionService } from "./connections-DeauhaZi.js";
1
+ import { o as clientsSchema, t as createConnectionService } from "./connections-WmjuUrDj.js";
2
2
  import { a as DEFAULT_PRESET_ID, i as DEFAULT_MAX_RESULT_CHARS, o as DEFAULT_QUERY_TIMEOUT_MS, r as DEFAULT_MAX_QUERY_CHARS, t as DEFAULT_CONNECT_TIMEOUT_MS } from "./defaults-DP4RyRh1.js";
3
- import { access, cp, mkdir, readFile } from "node:fs/promises";
3
+ import { r as apply$1 } from "./command-LFgLb6el.js";
4
+ import { n as apply$2 } from "./tool-Dka6RyEp.js";
5
+ import { createHash } from "node:crypto";
6
+ import { access, cp, mkdir, readFile, writeFile } from "node:fs/promises";
4
7
  import { homedir } from "node:os";
5
8
  import { join, resolve } from "node:path";
6
9
  import { fileURLToPath } from "node:url";
@@ -109,24 +112,32 @@ function createDomainConnectionPersistence(domain) {
109
112
  //#endregion
110
113
  //#region src/index.ts
111
114
  /**
112
- * Data Agent server half for the dsh web GUI. The host row provides the
115
+ * Data Agent profile entry. The host row provides the
113
116
  * `dataAgentConnections` service (shared non-secret profile/binding storage;
114
117
  * temporary passwords stay process-local), seeds config connections (`connections`, `'*'` =
115
- * wildcard default), and installs the `data-agent` agent preset into
116
- * `$DSH_HOME/.agent-presets/` (idempotent, never overwrites a user-edited
117
- * directory).
118
+ * wildcard default), installs the `data-agent` agent preset into
119
+ * `$DSH_HOME/.agent-presets/`, and preloads the preset-scoped database tools
120
+ * and command through this profile bundle entry.
118
121
  *
119
122
  * The HTTP routes live in the separate `./routes` entry
120
123
  * (`@yejiming/dsh-data-agent/routes`, cordis row `data-agent-routes`) so
121
- * this row keeps working in headless profiles without a webserver; the
122
- * database tools themselves live in the `./tool` entry and are mounted only
123
- * by the data-agent preset.
124
+ * this row keeps working in headless profiles without a webserver. The
125
+ * database implementations still have public `./tool` and `./command`
126
+ * exports, but the shipped preset does not dynamically import those package
127
+ * subpaths. Loading them here keeps Desktop on the same profile-startup path
128
+ * as other UI bundles and avoids Electron ASAR package-resolution drift.
124
129
  * @module @yejiming/dsh-data-agent
125
130
  */
126
131
  /** Cordis plugin name (diagnostics only). */
127
132
  const name = "data-agent";
128
- /** Services required before the store can serve. */
129
- const inject = ["subprocess", "credentials"];
133
+ /** Services required before the profile entry can mount its preset layer. */
134
+ const inject = [
135
+ "agentPresets",
136
+ "commands",
137
+ "credentials",
138
+ "subprocess",
139
+ "tools"
140
+ ];
130
141
  /** Loader schema with deployment defaults (no library defaults). */
131
142
  const Config = z.object({
132
143
  presetId: z.string().default(DEFAULT_PRESET_ID),
@@ -135,6 +146,7 @@ const Config = z.object({
135
146
  introspectMaxTables: z.number().step(1).min(1).default(500),
136
147
  queryTimeoutMs: z.number().step(1).min(1e3).default(DEFAULT_QUERY_TIMEOUT_MS),
137
148
  maxResultChars: z.number().step(1).min(1024).default(DEFAULT_MAX_RESULT_CHARS),
149
+ maxRows: z.number().step(1).min(1).default(100),
138
150
  maxQueryChars: z.number().step(1).min(1024).default(DEFAULT_MAX_QUERY_CHARS),
139
151
  readonly: z.boolean().default(false),
140
152
  persistConnections: z.boolean().default(true),
@@ -168,26 +180,57 @@ function resolveDshHome(env = process.env) {
168
180
  }
169
181
  /**
170
182
  * Install the packaged `preset/data-agent/` directory into
171
- * `$DSH_HOME/.agent-presets/<presetId>/`. Idempotent: an existing target
172
- * directory is left untouched (user edits survive); `installPreset: false`
173
- * never calls this. Best-effort a failure logs a warning with manual
174
- * install instructions instead of failing the boot.
183
+ * `$DSH_HOME/.agent-presets/<presetId>/`. Idempotent: an existing target is
184
+ * normally left untouched. The exact package-owned 0.0.9 composition is
185
+ * migrated once because its two dynamic package rows are incompatible with
186
+ * DSH Desktop's unpacked-ASAR loader; user-edited compositions are never
187
+ * overwritten. `installPreset: false` never calls this. Best-effort — a
188
+ * failure logs a warning with manual install instructions instead of failing
189
+ * the boot.
175
190
  */
176
191
  async function installPreset(ctx, presetId) {
177
192
  const targetDir = join(resolveDshHome(), ".agent-presets", presetId);
193
+ const sourceDir = fileURLToPath(new URL("../preset/data-agent/", import.meta.url));
178
194
  try {
179
195
  await access(targetDir);
180
- ctx.logger.info("data-agent: preset \"%s\" already present at %s, skipping install", presetId, targetDir);
181
- await diagnoseExistingPreset(ctx, targetDir);
182
- return;
196
+ return await synchronizeExistingPreset(ctx, targetDir, sourceDir, presetId);
183
197
  } catch {}
184
- const sourceDir = fileURLToPath(new URL("../preset/data-agent/", import.meta.url));
185
198
  try {
186
199
  await mkdir(targetDir, { recursive: true });
187
200
  await cp(sourceDir, targetDir, { recursive: true });
188
201
  ctx.logger.info("data-agent: installed preset \"%s\" to %s", presetId, targetDir);
202
+ return true;
189
203
  } catch (error) {
190
204
  ctx.logger.warn("data-agent: failed to install preset \"%s\" to %s (%s); copy preset/data-agent/ manually to enable the 数据模式 preset", presetId, targetDir, error instanceof Error ? error.message : String(error));
205
+ return false;
206
+ }
207
+ }
208
+ /** SHA-256 of the unmodified 0.0.9 composition that imported /tool and /command dynamically. */
209
+ const LEGACY_PRESET_0_0_9_SHA256 = "bae875a90d638ea78715030246b0f8a9f1a2c3359ca61febb6ceb59d0fcd930a";
210
+ /** Public for regression tests of the non-destructive preset migration gate. */
211
+ function isLegacyManagedPreset(source) {
212
+ return createHash("sha256").update(source).digest("hex") === LEGACY_PRESET_0_0_9_SHA256;
213
+ }
214
+ /** Upgrade only the exact package-owned legacy composition; preserve every edited preset. */
215
+ async function synchronizeExistingPreset(ctx, targetDir, sourceDir, presetId) {
216
+ const composition = join(targetDir, "agent.cordis.yml");
217
+ try {
218
+ const current = await readFile(composition, "utf8");
219
+ if (isLegacyManagedPreset(current)) {
220
+ const replacement = await readFile(join(sourceDir, "agent.cordis.yml"), "utf8");
221
+ await writeFile(composition, replacement, "utf8");
222
+ ctx.logger.info("data-agent: migrated preset at %s to profile-preloaded tools (removed dynamic /tool and /command rows)", composition);
223
+ return true;
224
+ }
225
+ if (current.includes("@yejiming/dsh-data-agent/tool") || current.includes("@yejiming/dsh-data-agent/command")) {
226
+ ctx.logger.warn("data-agent: user-edited preset at %s still imports /tool or /command dynamically; remove those rows so the profile-preloaded preset capabilities can activate in DSH Desktop", composition);
227
+ return false;
228
+ }
229
+ ctx.logger.info("data-agent: preset \"%s\" already present at %s, skipping install", presetId, targetDir);
230
+ return true;
231
+ } catch (error) {
232
+ ctx.logger.warn("data-agent: could not inspect existing preset %s (%s); it was not overwritten", composition, error instanceof Error ? error.message : String(error));
233
+ return false;
191
234
  }
192
235
  }
193
236
  /** Exact profile-local package installation command used by diagnostics/docs. */
@@ -196,24 +239,32 @@ function profileInstallCommand(profile) {
196
239
  }
197
240
  /** Actionable diagnostic for a roster-visible preset whose profile lacks this package. */
198
241
  function missingProfileDependencyMessage(profile) {
199
- return `data-agent preset is visible, but profile "${profile}" cannot resolve @yejiming/dsh-data-agent/tool or /command. Run: ${profileInstallCommand(profile)}`;
242
+ return `data-agent preset is visible, but its profile-preloaded capabilities are absent from profile "${profile}". Run: ${profileInstallCommand(profile)}`;
200
243
  }
201
- /** Warn without overwriting when a pre-existing user preset lacks the command row. */
202
- async function diagnoseExistingPreset(ctx, targetDir) {
203
- const composition = join(targetDir, "agent.cordis.yml");
204
- try {
205
- if ((await readFile(composition, "utf8")).includes("@yejiming/dsh-data-agent/command")) return;
206
- const profile = process.env.DSH_PROFILE?.trim();
207
- const installHint = profile !== void 0 && profile.length > 0 ? profileInstallCommand(profile) : `${profileInstallCommand("web")};${profileInstallCommand("dsh-tui")}`;
208
- ctx.logger.warn("data-agent: existing user preset at %s does not contain the database-command row; the file was not overwritten. Back it up, then add name: \"@yejiming/dsh-data-agent/command\". Also install this package in the target profile: %s", composition, installHint);
209
- } catch (error) {
210
- ctx.logger.warn("data-agent: could not inspect existing preset %s (%s); it was not overwritten", composition, error instanceof Error ? error.message : String(error));
211
- }
244
+ /**
245
+ * Register the statically imported database tools and command under the exact
246
+ * standing key owned by the data-agent preset. Selecting the preset performs
247
+ * no package import and only links the agent scope to this key.
248
+ */
249
+ async function mountPresetCapabilities(ctx, key, scopeTag, config) {
250
+ const scoped = ctx.extend({ [scopeTag]: key });
251
+ apply$2(scoped, config);
252
+ apply$1(scoped);
253
+ }
254
+ /** Read the host-owned scope tag from AgentPresets' already-created standing mount. */
255
+ async function standingScopeTag(ctx, presetId, key) {
256
+ const pending = ctx.agentPresets.standing?.get(presetId);
257
+ if (pending === void 0) throw new Error(`data-agent: preset "${presetId}" has no standing scope after standingKeyFor()`);
258
+ const standing = await pending;
259
+ if (standing.key !== key) throw new Error(`data-agent: preset "${presetId}" standing scope changed during profile preload`);
260
+ const tag = Object.getOwnPropertySymbols(standing.scope.ctx).find((candidate) => Reflect.get(standing.scope.ctx, candidate) === key);
261
+ if (tag === void 0) throw new Error(`data-agent: preset "${presetId}" standing context exposes no scope tag`);
262
+ return tag;
212
263
  }
213
264
  /**
214
- * Mount the data-agent host row: connection store, config-seeded
215
- * connections, and preset self-install. HTTP routes are the sibling
216
- * `data-agent-routes` row (`./routes`).
265
+ * Mount the data-agent profile row: connection store, config-seeded
266
+ * connections, preset installation, and profile-preloaded preset capabilities.
267
+ * HTTP routes are the sibling `data-agent-routes` row (`./routes`).
217
268
  * @param ctx - host cordis context.
218
269
  * @param config - validated loader configuration.
219
270
  */
@@ -225,6 +276,7 @@ async function apply(ctx, config) {
225
276
  introspectMaxTables: config.introspectMaxTables,
226
277
  queryTimeoutMs: config.queryTimeoutMs,
227
278
  maxResultChars: config.maxResultChars,
279
+ maxRows: config.maxRows,
228
280
  maxQueryChars: config.maxQueryChars,
229
281
  readonly: config.readonly,
230
282
  persistConnections: config.persistConnections,
@@ -255,6 +307,7 @@ async function apply(ctx, config) {
255
307
  store.set(sessionId, connection);
256
308
  }
257
309
  };
310
+ const presetReady = resolved.installPreset ? await installPreset(ctx, resolved.presetId) : false;
258
311
  if (resolved.persistConnections) {
259
312
  const domain = await (await ensureStorageDomain(ctx)).open(connectionStorageSpec);
260
313
  ctx.effect(() => () => domain.close(), "data-agent: close connection storage domain");
@@ -263,7 +316,17 @@ async function apply(ctx, config) {
263
316
  ctx.logger.warn("data-agent: persistConnections=false; connection state is process-local and cannot restore across Web/TUI");
264
317
  mountService(ctx);
265
318
  }
266
- if (resolved.installPreset) await installPreset(ctx, resolved.presetId);
319
+ if (presetReady) {
320
+ const standingKey = await ctx.agentPresets.standingKeyFor(resolved.presetId);
321
+ await mountPresetCapabilities(ctx, standingKey, await standingScopeTag(ctx, resolved.presetId, standingKey), {
322
+ queryTimeoutMs: resolved.queryTimeoutMs,
323
+ maxResultChars: resolved.maxResultChars,
324
+ maxRows: resolved.maxRows,
325
+ maxQueryChars: resolved.maxQueryChars,
326
+ readonly: resolved.readonly,
327
+ clients: resolved.clients
328
+ });
329
+ }
267
330
  }
268
331
  /**
269
332
  * Reuse a surface-provided storage stack (Web) or mount the same JSON stack
@@ -289,4 +352,4 @@ async function ensureStorageDomain(ctx) {
289
352
  return facility;
290
353
  }
291
354
  //#endregion
292
- export { Config, apply, inject, installPreset, missingProfileDependencyMessage, name, profileInstallCommand, resolveDshHome };
355
+ export { Config, apply, inject, installPreset, isLegacyManagedPreset, missingProfileDependencyMessage, mountPresetCapabilities, name, profileInstallCommand, resolveDshHome };