@yejiming/dsh-data-agent 0.0.9 → 0.0.11

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/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-5sfdEDsG.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-DuCpwVbl.js";
4
+ import { n as apply$2 } from "./tool-DVh61An-.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";
@@ -40,6 +43,11 @@ const persistedConnectionProfileSchema = z$1.object({
40
43
  database: z$1.string().min(1),
41
44
  readonly: z$1.boolean().optional(),
42
45
  passwordRef: z$1.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).optional(),
46
+ credentialMode: z$1.enum([
47
+ "none",
48
+ "password",
49
+ "reference"
50
+ ]).optional(),
43
51
  updatedAt: z$1.string().min(1)
44
52
  }).strict();
45
53
  /** Durable session-to-profile binding schema. */
@@ -109,24 +117,32 @@ function createDomainConnectionPersistence(domain) {
109
117
  //#endregion
110
118
  //#region src/index.ts
111
119
  /**
112
- * Data Agent server half for the dsh web GUI. The host row provides the
120
+ * Data Agent profile entry. The host row provides the
113
121
  * `dataAgentConnections` service (shared non-secret profile/binding storage;
114
122
  * 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).
123
+ * wildcard default), installs the `data-agent` agent preset into
124
+ * `$DSH_HOME/.agent-presets/`, and preloads the preset-scoped database tools
125
+ * and command through this profile bundle entry.
118
126
  *
119
127
  * The HTTP routes live in the separate `./routes` entry
120
128
  * (`@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.
129
+ * this row keeps working in headless profiles without a webserver. The
130
+ * database implementations still have public `./tool` and `./command`
131
+ * exports, but the shipped preset does not dynamically import those package
132
+ * subpaths. Loading them here keeps Desktop on the same profile-startup path
133
+ * as other UI bundles and avoids Electron ASAR package-resolution drift.
124
134
  * @module @yejiming/dsh-data-agent
125
135
  */
126
136
  /** Cordis plugin name (diagnostics only). */
127
137
  const name = "data-agent";
128
- /** Services required before the store can serve. */
129
- const inject = ["subprocess", "credentials"];
138
+ /** Services required before the profile entry can mount its preset layer. */
139
+ const inject = [
140
+ "agentPresets",
141
+ "commands",
142
+ "credentials",
143
+ "subprocess",
144
+ "tools"
145
+ ];
130
146
  /** Loader schema with deployment defaults (no library defaults). */
131
147
  const Config = z.object({
132
148
  presetId: z.string().default(DEFAULT_PRESET_ID),
@@ -135,6 +151,7 @@ const Config = z.object({
135
151
  introspectMaxTables: z.number().step(1).min(1).default(500),
136
152
  queryTimeoutMs: z.number().step(1).min(1e3).default(DEFAULT_QUERY_TIMEOUT_MS),
137
153
  maxResultChars: z.number().step(1).min(1024).default(DEFAULT_MAX_RESULT_CHARS),
154
+ maxRows: z.number().step(1).min(1).default(100),
138
155
  maxQueryChars: z.number().step(1).min(1024).default(DEFAULT_MAX_QUERY_CHARS),
139
156
  readonly: z.boolean().default(false),
140
157
  persistConnections: z.boolean().default(true),
@@ -168,26 +185,57 @@ function resolveDshHome(env = process.env) {
168
185
  }
169
186
  /**
170
187
  * 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.
188
+ * `$DSH_HOME/.agent-presets/<presetId>/`. Idempotent: an existing target is
189
+ * normally left untouched. The exact package-owned 0.0.9 composition is
190
+ * migrated once because its two dynamic package rows are incompatible with
191
+ * DSH Desktop's unpacked-ASAR loader; user-edited compositions are never
192
+ * overwritten. `installPreset: false` never calls this. Best-effort — a
193
+ * failure logs a warning with manual install instructions instead of failing
194
+ * the boot.
175
195
  */
176
196
  async function installPreset(ctx, presetId) {
177
197
  const targetDir = join(resolveDshHome(), ".agent-presets", presetId);
198
+ const sourceDir = fileURLToPath(new URL("../preset/data-agent/", import.meta.url));
178
199
  try {
179
200
  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;
201
+ return await synchronizeExistingPreset(ctx, targetDir, sourceDir, presetId);
183
202
  } catch {}
184
- const sourceDir = fileURLToPath(new URL("../preset/data-agent/", import.meta.url));
185
203
  try {
186
204
  await mkdir(targetDir, { recursive: true });
187
205
  await cp(sourceDir, targetDir, { recursive: true });
188
206
  ctx.logger.info("data-agent: installed preset \"%s\" to %s", presetId, targetDir);
207
+ return true;
189
208
  } catch (error) {
190
209
  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));
210
+ return false;
211
+ }
212
+ }
213
+ /** SHA-256 of the unmodified 0.0.9 composition that imported /tool and /command dynamically. */
214
+ const LEGACY_PRESET_0_0_9_SHA256 = "bae875a90d638ea78715030246b0f8a9f1a2c3359ca61febb6ceb59d0fcd930a";
215
+ /** Public for regression tests of the non-destructive preset migration gate. */
216
+ function isLegacyManagedPreset(source) {
217
+ return createHash("sha256").update(source).digest("hex") === LEGACY_PRESET_0_0_9_SHA256;
218
+ }
219
+ /** Upgrade only the exact package-owned legacy composition; preserve every edited preset. */
220
+ async function synchronizeExistingPreset(ctx, targetDir, sourceDir, presetId) {
221
+ const composition = join(targetDir, "agent.cordis.yml");
222
+ try {
223
+ const current = await readFile(composition, "utf8");
224
+ if (isLegacyManagedPreset(current)) {
225
+ const replacement = await readFile(join(sourceDir, "agent.cordis.yml"), "utf8");
226
+ await writeFile(composition, replacement, "utf8");
227
+ ctx.logger.info("data-agent: migrated preset at %s to profile-preloaded tools (removed dynamic /tool and /command rows)", composition);
228
+ return true;
229
+ }
230
+ if (current.includes("@yejiming/dsh-data-agent/tool") || current.includes("@yejiming/dsh-data-agent/command")) {
231
+ 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);
232
+ return false;
233
+ }
234
+ ctx.logger.info("data-agent: preset \"%s\" already present at %s, skipping install", presetId, targetDir);
235
+ return true;
236
+ } catch (error) {
237
+ ctx.logger.warn("data-agent: could not inspect existing preset %s (%s); it was not overwritten", composition, error instanceof Error ? error.message : String(error));
238
+ return false;
191
239
  }
192
240
  }
193
241
  /** Exact profile-local package installation command used by diagnostics/docs. */
@@ -196,24 +244,32 @@ function profileInstallCommand(profile) {
196
244
  }
197
245
  /** Actionable diagnostic for a roster-visible preset whose profile lacks this package. */
198
246
  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)}`;
247
+ return `data-agent preset is visible, but its profile-preloaded capabilities are absent from profile "${profile}". Run: ${profileInstallCommand(profile)}`;
200
248
  }
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
- }
249
+ /**
250
+ * Register the statically imported database tools and command under the exact
251
+ * standing key owned by the data-agent preset. Selecting the preset performs
252
+ * no package import and only links the agent scope to this key.
253
+ */
254
+ async function mountPresetCapabilities(ctx, key, scopeTag, config) {
255
+ const scoped = ctx.extend({ [scopeTag]: key });
256
+ apply$2(scoped, config);
257
+ apply$1(scoped);
258
+ }
259
+ /** Read the host-owned scope tag from AgentPresets' already-created standing mount. */
260
+ async function standingScopeTag(ctx, presetId, key) {
261
+ const pending = ctx.agentPresets.standing?.get(presetId);
262
+ if (pending === void 0) throw new Error(`data-agent: preset "${presetId}" has no standing scope after standingKeyFor()`);
263
+ const standing = await pending;
264
+ if (standing.key !== key) throw new Error(`data-agent: preset "${presetId}" standing scope changed during profile preload`);
265
+ const tag = Object.getOwnPropertySymbols(standing.scope.ctx).find((candidate) => Reflect.get(standing.scope.ctx, candidate) === key);
266
+ if (tag === void 0) throw new Error(`data-agent: preset "${presetId}" standing context exposes no scope tag`);
267
+ return tag;
212
268
  }
213
269
  /**
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`).
270
+ * Mount the data-agent profile row: connection store, config-seeded
271
+ * connections, preset installation, and profile-preloaded preset capabilities.
272
+ * HTTP routes are the sibling `data-agent-routes` row (`./routes`).
217
273
  * @param ctx - host cordis context.
218
274
  * @param config - validated loader configuration.
219
275
  */
@@ -225,6 +281,7 @@ async function apply(ctx, config) {
225
281
  introspectMaxTables: config.introspectMaxTables,
226
282
  queryTimeoutMs: config.queryTimeoutMs,
227
283
  maxResultChars: config.maxResultChars,
284
+ maxRows: config.maxRows,
228
285
  maxQueryChars: config.maxQueryChars,
229
286
  readonly: config.readonly,
230
287
  persistConnections: config.persistConnections,
@@ -255,6 +312,7 @@ async function apply(ctx, config) {
255
312
  store.set(sessionId, connection);
256
313
  }
257
314
  };
315
+ const presetReady = resolved.installPreset ? await installPreset(ctx, resolved.presetId) : false;
258
316
  if (resolved.persistConnections) {
259
317
  const domain = await (await ensureStorageDomain(ctx)).open(connectionStorageSpec);
260
318
  ctx.effect(() => () => domain.close(), "data-agent: close connection storage domain");
@@ -263,7 +321,17 @@ async function apply(ctx, config) {
263
321
  ctx.logger.warn("data-agent: persistConnections=false; connection state is process-local and cannot restore across Web/TUI");
264
322
  mountService(ctx);
265
323
  }
266
- if (resolved.installPreset) await installPreset(ctx, resolved.presetId);
324
+ if (presetReady) {
325
+ const standingKey = await ctx.agentPresets.standingKeyFor(resolved.presetId);
326
+ await mountPresetCapabilities(ctx, standingKey, await standingScopeTag(ctx, resolved.presetId, standingKey), {
327
+ queryTimeoutMs: resolved.queryTimeoutMs,
328
+ maxResultChars: resolved.maxResultChars,
329
+ maxRows: resolved.maxRows,
330
+ maxQueryChars: resolved.maxQueryChars,
331
+ readonly: resolved.readonly,
332
+ clients: resolved.clients
333
+ });
334
+ }
267
335
  }
268
336
  /**
269
337
  * Reuse a surface-provided storage stack (Web) or mount the same JSON stack
@@ -289,4 +357,4 @@ async function ensureStorageDomain(ctx) {
289
357
  return facility;
290
358
  }
291
359
  //#endregion
292
- export { Config, apply, inject, installPreset, missingProfileDependencyMessage, name, profileInstallCommand, resolveDshHome };
360
+ export { Config, apply, inject, installPreset, isLegacyManagedPreset, missingProfileDependencyMessage, mountPresetCapabilities, name, profileInstallCommand, resolveDshHome };
package/lib/routes.js CHANGED
@@ -90,8 +90,12 @@ function apply(ctx, _config) {
90
90
  if (req.method === "GET" && routeIs(segments, "status")) {
91
91
  const sessionId = requireString(url.searchParams.get("sessionId"), "sessionId");
92
92
  const summary = await scope.dataAgentConnections.status(sessionId);
93
- writeJson(200, summary === void 0 ? { connected: false } : {
94
- connected: true,
93
+ writeJson(200, summary === void 0 ? {
94
+ connected: false,
95
+ reconnectRequired: false
96
+ } : {
97
+ connected: summary.ready === true,
98
+ reconnectRequired: summary.reconnectRequired === true,
95
99
  summary
96
100
  });
97
101
  return;