@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.
- package/README.en.md +31 -7
- package/README.md +31 -7
- package/cordis.patch.yml +8 -9
- package/lib/client.js +520 -578
- package/lib/client.js.map +1 -1
- package/lib/command-LFgLb6el.js +875 -0
- package/lib/command.js +1 -874
- package/lib/{connections-DeauhaZi.js → connections-WmjuUrDj.js} +300 -11
- package/lib/index.js +99 -36
- package/lib/tool-Dka6RyEp.js +1128 -0
- package/lib/tool.js +1 -1127
- package/lib/types/client/DataAgentWorkbench.d.ts +2 -2
- package/lib/types/client/index.d.ts +3 -4
- package/lib/types/client/locales.d.ts +18 -0
- package/lib/types/client-discovery.d.ts +45 -0
- package/lib/types/clients.d.ts +7 -1
- package/lib/types/defaults.d.ts +2 -0
- package/lib/types/index.d.ts +42 -16
- package/lib/types/routes.d.ts +0 -14
- package/lib/types/tool.d.ts +4 -0
- package/package.json +5 -1
- package/preset/data-agent/agent.cordis.yml +6 -25
- package/preset/data-agent/preset.yml +1 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import "./defaults-DP4RyRh1.js";
|
|
2
|
-
import {
|
|
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
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
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:
|
|
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-
|
|
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 {
|
|
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
|
|
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),
|
|
116
|
-
* `$DSH_HOME/.agent-presets
|
|
117
|
-
*
|
|
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
|
|
122
|
-
* database
|
|
123
|
-
*
|
|
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
|
|
129
|
-
const inject = [
|
|
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
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
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
|
-
|
|
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}"
|
|
242
|
+
return `data-agent preset is visible, but its profile-preloaded capabilities are absent from profile "${profile}". Run: ${profileInstallCommand(profile)}`;
|
|
200
243
|
}
|
|
201
|
-
/**
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
|
215
|
-
* connections,
|
|
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 (
|
|
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 };
|