@yejiming/dsh-data-agent 0.0.6 → 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.
Files changed (39) hide show
  1. package/README.en.md +142 -119
  2. package/README.md +138 -118
  3. package/cordis.patch.yml +8 -9
  4. package/lib/client.js +42423 -524
  5. package/lib/client.js.map +1 -1
  6. package/lib/command-LFgLb6el.js +875 -0
  7. package/lib/command.js +2 -0
  8. package/lib/connections-WmjuUrDj.js +1608 -0
  9. package/lib/defaults-DP4RyRh1.js +21 -0
  10. package/lib/index.js +265 -68
  11. package/lib/routes.js +94 -170
  12. package/lib/tool-Dka6RyEp.js +1128 -0
  13. package/lib/tool.js +1 -426
  14. package/lib/types/analysis.d.ts +1071 -0
  15. package/lib/types/client/AnalysisChart.d.ts +26 -0
  16. package/lib/types/client/AnalysisDashboard.d.ts +30 -0
  17. package/lib/types/client/DataAgentWorkbench.d.ts +2 -2
  18. package/lib/types/client/analysis-charts.d.ts +40 -0
  19. package/lib/types/client/analysis-view-model.d.ts +44 -0
  20. package/lib/types/client/index.d.ts +3 -4
  21. package/lib/types/client/locales.d.ts +66 -0
  22. package/lib/types/client/persistence.d.ts +6 -1
  23. package/lib/types/client-discovery.d.ts +45 -0
  24. package/lib/types/clients.d.ts +17 -10
  25. package/lib/types/command.d.ts +41 -0
  26. package/lib/types/connections.d.ts +115 -40
  27. package/lib/types/defaults.d.ts +2 -0
  28. package/lib/types/index.d.ts +109 -63
  29. package/lib/types/routes.d.ts +25 -91
  30. package/lib/types/sql.d.ts +1 -1
  31. package/lib/types/storage.d.ts +70 -0
  32. package/lib/types/structured-read.d.ts +50 -0
  33. package/lib/types/tool.d.ts +29 -20
  34. package/lib/types/tui-connection-form.d.ts +98 -0
  35. package/package.json +65 -4
  36. package/preset/data-agent/agent.cordis.yml +22 -25
  37. package/preset/data-agent/preset.yml +1 -1
  38. package/lib/defaults-Bac6QvNt.js +0 -911
  39. package/lib/query-CmhTFklw.js +0 -86
@@ -0,0 +1,21 @@
1
+ //#region src/defaults.ts
2
+ /**
3
+ * Package-wide defaults shared by the server half (`src/index.ts`) and the
4
+ * database tool half (`src/tool.ts`). Loader schemas carry these as their
5
+ * defaults so a deployment may override every one of them in cordis.yml.
6
+ * @module @yejiming/dsh-data-agent/defaults
7
+ */
8
+ /** Preset directory name installed into `$DSH_HOME/.agent-presets/`. */
9
+ const DEFAULT_PRESET_ID = "data-agent";
10
+ /** End-to-end deadline for one `/connect` connectivity check, milliseconds. */
11
+ const DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
12
+ /** End-to-end deadline for one database-tool query, milliseconds. */
13
+ const DEFAULT_QUERY_TIMEOUT_MS = 3e4;
14
+ /** In-memory cap on database-tool captured output (stdout and stderr each). */
15
+ const DEFAULT_MAX_RESULT_CHARS = 2e4;
16
+ /** Cap on one /query SQL text length (abuse guard; the wire body stays small). */
17
+ const DEFAULT_MAX_QUERY_CHARS = 65536;
18
+ /** Grace period for the subprocess terminate escalation. */
19
+ const DEFAULT_GRACE_MS = 5e3;
20
+ //#endregion
21
+ export { DEFAULT_PRESET_ID as a, DEFAULT_MAX_RESULT_CHARS as i, DEFAULT_GRACE_MS as n, DEFAULT_QUERY_TIMEOUT_MS as o, DEFAULT_MAX_QUERY_CHARS as r, DEFAULT_CONNECT_TIMEOUT_MS as t };
package/lib/index.js CHANGED
@@ -1,67 +1,143 @@
1
- import { a as DEFAULT_PRESET_ID, d as clientsSchema, i as DEFAULT_MAX_RESULT_CHARS, o as DEFAULT_QUERY_TIMEOUT_MS, t as DEFAULT_CONNECT_TIMEOUT_MS } from "./defaults-Bac6QvNt.js";
2
- import { access, cp, mkdir } from "node:fs/promises";
1
+ import { o as clientsSchema, t as createConnectionService } from "./connections-WmjuUrDj.js";
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 { 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";
3
7
  import { homedir } from "node:os";
4
8
  import { join, resolve } from "node:path";
5
9
  import { fileURLToPath } from "node:url";
10
+ import Storage from "@deepseek-ai/dsh-storage";
11
+ import * as storageDomainPlugin from "@deepseek-ai/dsh-storage-domain";
12
+ import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
13
+ import * as storageJsonPlugin from "@deepseek-ai/dsh-storage-json";
6
14
  import z from "schemastery";
7
- /** Build the password-stripped copy of one connection. */
8
- function summarize(connection) {
9
- const summary = {
10
- type: connection.type,
11
- database: connection.database
12
- };
13
- if (connection.host !== void 0) summary.host = connection.host;
14
- if (connection.port !== void 0) summary.port = connection.port;
15
- if (connection.user !== void 0) summary.user = connection.user;
16
- if (connection.readonly !== void 0) summary.readonly = connection.readonly;
17
- if (connection.tables !== void 0) summary.tables = [...connection.tables];
18
- return summary;
19
- }
20
- /** Create a fresh connection store (per-process singleton, one per plugin instance). */
21
- function createConnectionStore() {
22
- const connections = /* @__PURE__ */ new Map();
23
- /** Exact entry first; the wildcard (`'*'`) entry is the fallback. */
24
- const exactOrWildcard = (sessionId) => connections.get(sessionId) ?? connections.get("*");
15
+ import { z as z$1 } from "zod";
16
+ //#region src/storage.ts
17
+ /**
18
+ * Durable, non-secret connection profiles, session bindings, and form drafts.
19
+ *
20
+ * The domain intentionally excludes passwords, resolved credentials, SQL,
21
+ * table metadata, and client output. Form drafts likewise accept no secret
22
+ * fields. Runtime secrets stay in
23
+ * {@link DataAgentConnectionService}; durable records only retain enough
24
+ * information to rebuild a connection description in another DSH surface.
25
+ * @module @yejiming/dsh-data-agent/storage
26
+ */
27
+ /** Storage-domain identity. Bump the version only with an explicit migration. */
28
+ const CONNECTION_STORAGE_DOMAIN = "data_agent_connections";
29
+ /** Durable profile schema. There is deliberately no `password` field. */
30
+ const persistedConnectionProfileSchema = z$1.object({
31
+ name: z$1.string().min(1).optional(),
32
+ type: z$1.enum([
33
+ "mysql",
34
+ "postgres",
35
+ "sqlite",
36
+ "oracle",
37
+ "hive",
38
+ "impala"
39
+ ]),
40
+ host: z$1.string().optional(),
41
+ port: z$1.number().int().min(1).max(65535).optional(),
42
+ user: z$1.string().optional(),
43
+ database: z$1.string().min(1),
44
+ readonly: z$1.boolean().optional(),
45
+ passwordRef: z$1.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).optional(),
46
+ updatedAt: z$1.string().min(1)
47
+ }).strict();
48
+ /** Durable session-to-profile binding schema. */
49
+ const sessionConnectionBindingSchema = z$1.object({
50
+ profileId: z$1.string().min(1),
51
+ updatedAt: z$1.string().min(1)
52
+ }).strict();
53
+ /** Session form draft schema. Secret-shaped fields are rejected by strict mode. */
54
+ const persistedConnectionFormDraftSchema = z$1.object({
55
+ type: z$1.enum([
56
+ "mysql",
57
+ "postgres",
58
+ "sqlite",
59
+ "oracle",
60
+ "hive",
61
+ "impala"
62
+ ]),
63
+ host: z$1.string(),
64
+ port: z$1.string(),
65
+ user: z$1.string(),
66
+ database: z$1.string(),
67
+ readonly: z$1.boolean(),
68
+ updatedAt: z$1.string().min(1)
69
+ }).strict();
70
+ /** Single source of truth for the storage layout and durable validation. */
71
+ const connectionStorageSpec = defineDomain({
72
+ name: CONNECTION_STORAGE_DOMAIN,
73
+ version: 1,
74
+ tables: {
75
+ profiles: domainTable(persistedConnectionProfileSchema),
76
+ bindings: domainTable(sessionConnectionBindingSchema),
77
+ drafts: domainTable(persistedConnectionFormDraftSchema)
78
+ }
79
+ });
80
+ /** Project a typed DSH domain handle onto the service's persistence seam. */
81
+ function createDomainConnectionPersistence(domain) {
82
+ const profiles = domain.table("profiles");
83
+ const bindings = domain.table("bindings");
84
+ const drafts = domain.table("drafts");
25
85
  return {
26
- set(sessionId, connection) {
27
- connections.set(sessionId, connection);
86
+ getProfile(profileId) {
87
+ return profiles.get(profileId);
88
+ },
89
+ putProfile(profileId, profile) {
90
+ return profiles.put(profileId, profile);
91
+ },
92
+ deleteProfile(profileId) {
93
+ return profiles.delete(profileId);
28
94
  },
29
- get(sessionId) {
30
- const connection = exactOrWildcard(sessionId);
31
- return connection === void 0 ? void 0 : summarize(connection);
95
+ getBinding(sessionId) {
96
+ return bindings.get(sessionId);
32
97
  },
33
- getWithSecret(sessionId) {
34
- return exactOrWildcard(sessionId);
98
+ putBinding(sessionId, binding) {
99
+ return bindings.put(sessionId, binding);
35
100
  },
36
- has(sessionId) {
37
- return exactOrWildcard(sessionId) !== void 0;
101
+ deleteBinding(sessionId) {
102
+ return bindings.delete(sessionId);
38
103
  },
39
- clear(sessionId) {
40
- connections.delete(sessionId);
104
+ getDraft(sessionId) {
105
+ return drafts.get(sessionId);
106
+ },
107
+ putDraft(sessionId, draft) {
108
+ return drafts.put(sessionId, draft);
41
109
  }
42
110
  };
43
111
  }
44
112
  //#endregion
45
113
  //#region src/index.ts
46
114
  /**
47
- * Data Agent server half for the dsh web GUI. The host row provides the
48
- * `dataAgentConnections` service (session-scoped in-memory store; passwords
49
- * never leave memory), seeds config connections (`connections`, `'*'` =
50
- * wildcard default), and installs the `data-agent` agent preset into
51
- * `$DSH_HOME/.agent-presets/` (idempotent, never overwrites a user-edited
52
- * directory).
115
+ * Data Agent profile entry. The host row provides the
116
+ * `dataAgentConnections` service (shared non-secret profile/binding storage;
117
+ * temporary passwords stay process-local), seeds config connections (`connections`, `'*'` =
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.
53
121
  *
54
122
  * The HTTP routes live in the separate `./routes` entry
55
123
  * (`@yejiming/dsh-data-agent/routes`, cordis row `data-agent-routes`) so
56
- * this row keeps working in headless profiles without a webserver; the
57
- * database tools themselves live in the `./tool` entry and are mounted only
58
- * 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.
59
129
  * @module @yejiming/dsh-data-agent
60
130
  */
61
131
  /** Cordis plugin name (diagnostics only). */
62
132
  const name = "data-agent";
63
- /** Services required before the store can serve. */
64
- const inject = ["subprocess"];
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
+ ];
65
141
  /** Loader schema with deployment defaults (no library defaults). */
66
142
  const Config = z.object({
67
143
  presetId: z.string().default(DEFAULT_PRESET_ID),
@@ -70,7 +146,10 @@ const Config = z.object({
70
146
  introspectMaxTables: z.number().step(1).min(1).default(500),
71
147
  queryTimeoutMs: z.number().step(1).min(1e3).default(DEFAULT_QUERY_TIMEOUT_MS),
72
148
  maxResultChars: z.number().step(1).min(1024).default(DEFAULT_MAX_RESULT_CHARS),
149
+ maxRows: z.number().step(1).min(1).default(100),
150
+ maxQueryChars: z.number().step(1).min(1024).default(DEFAULT_MAX_QUERY_CHARS),
73
151
  readonly: z.boolean().default(false),
152
+ persistConnections: z.boolean().default(true),
74
153
  clients: clientsSchema,
75
154
  connections: z.dict(z.object({
76
155
  type: z.union([
@@ -85,7 +164,9 @@ const Config = z.object({
85
164
  port: z.natural(),
86
165
  user: z.string(),
87
166
  database: z.string(),
88
- readonly: z.boolean()
167
+ readonly: z.boolean(),
168
+ passwordRef: z.string().pattern(/^[A-Za-z_][A-Za-z0-9_]*$/),
169
+ password: z.never().hidden()
89
170
  })).default({})
90
171
  });
91
172
  /**
@@ -99,35 +180,95 @@ function resolveDshHome(env = process.env) {
99
180
  }
100
181
  /**
101
182
  * Install the packaged `preset/data-agent/` directory into
102
- * `$DSH_HOME/.agent-presets/<presetId>/`. Idempotent: an existing target
103
- * directory is left untouched (user edits survive); `installPreset: false`
104
- * never calls this. Best-effort a failure logs a warning with manual
105
- * 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.
106
190
  */
107
191
  async function installPreset(ctx, presetId) {
108
192
  const targetDir = join(resolveDshHome(), ".agent-presets", presetId);
193
+ const sourceDir = fileURLToPath(new URL("../preset/data-agent/", import.meta.url));
109
194
  try {
110
195
  await access(targetDir);
111
- ctx.logger.info("data-agent: preset \"%s\" already present at %s, skipping install", presetId, targetDir);
112
- return;
196
+ return await synchronizeExistingPreset(ctx, targetDir, sourceDir, presetId);
113
197
  } catch {}
114
- const sourceDir = fileURLToPath(new URL("../preset/data-agent/", import.meta.url));
115
198
  try {
116
199
  await mkdir(targetDir, { recursive: true });
117
200
  await cp(sourceDir, targetDir, { recursive: true });
118
201
  ctx.logger.info("data-agent: installed preset \"%s\" to %s", presetId, targetDir);
202
+ return true;
119
203
  } catch (error) {
120
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;
121
206
  }
122
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;
234
+ }
235
+ }
236
+ /** Exact profile-local package installation command used by diagnostics/docs. */
237
+ function profileInstallCommand(profile) {
238
+ return `dsh plugin --profile ${profile} add @yejiming/dsh-data-agent`;
239
+ }
240
+ /** Actionable diagnostic for a roster-visible preset whose profile lacks this package. */
241
+ function missingProfileDependencyMessage(profile) {
242
+ return `data-agent preset is visible, but its profile-preloaded capabilities are absent from profile "${profile}". Run: ${profileInstallCommand(profile)}`;
243
+ }
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;
263
+ }
123
264
  /**
124
- * Mount the data-agent host row: connection store, config-seeded
125
- * connections, and preset self-install. HTTP routes are the sibling
126
- * `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`).
127
268
  * @param ctx - host cordis context.
128
269
  * @param config - validated loader configuration.
129
270
  */
130
- function apply(ctx, config) {
271
+ async function apply(ctx, config) {
131
272
  const resolved = {
132
273
  presetId: config.presetId,
133
274
  installPreset: config.installPreset,
@@ -135,24 +276,80 @@ function apply(ctx, config) {
135
276
  introspectMaxTables: config.introspectMaxTables,
136
277
  queryTimeoutMs: config.queryTimeoutMs,
137
278
  maxResultChars: config.maxResultChars,
279
+ maxRows: config.maxRows,
280
+ maxQueryChars: config.maxQueryChars,
138
281
  readonly: config.readonly,
282
+ persistConnections: config.persistConnections,
139
283
  clients: config.clients,
140
284
  connections: config.connections
141
285
  };
142
- const store = createConnectionStore();
143
- ctx.provide("dataAgentConnections", store);
144
- for (const [sessionId, spec] of Object.entries(config.connections)) {
145
- const connection = {
146
- type: spec.type,
147
- database: spec.type === "sqlite" ? resolve(process.cwd(), spec.database) : spec.database,
148
- ...spec.host !== void 0 ? { host: spec.host } : {},
149
- ...spec.port !== void 0 ? { port: spec.port } : {},
150
- ...spec.user !== void 0 ? { user: spec.user } : {},
151
- ...spec.readonly !== void 0 ? { readonly: spec.readonly } : {}
152
- };
153
- store.set(sessionId, connection);
286
+ const mountService = (scope, persistence) => {
287
+ const store = createConnectionService(scope, {
288
+ connectTimeoutMs: resolved.connectTimeoutMs,
289
+ queryTimeoutMs: resolved.queryTimeoutMs,
290
+ maxResultChars: resolved.maxResultChars,
291
+ maxQueryChars: resolved.maxQueryChars,
292
+ introspectMaxTables: resolved.introspectMaxTables,
293
+ readonly: resolved.readonly,
294
+ clients: resolved.clients
295
+ }, persistence);
296
+ scope.provide("dataAgentConnections", store);
297
+ for (const [sessionId, spec] of Object.entries(resolved.connections)) {
298
+ const connection = {
299
+ type: spec.type,
300
+ database: spec.type === "sqlite" ? resolve(process.cwd(), spec.database) : spec.database,
301
+ ...spec.host !== void 0 ? { host: spec.host } : {},
302
+ ...spec.port !== void 0 ? { port: spec.port } : {},
303
+ ...spec.user !== void 0 ? { user: spec.user } : {},
304
+ ...spec.passwordRef !== void 0 ? { passwordRef: spec.passwordRef } : {},
305
+ ...spec.readonly !== void 0 ? { readonly: spec.readonly } : {}
306
+ };
307
+ store.set(sessionId, connection);
308
+ }
309
+ };
310
+ const presetReady = resolved.installPreset ? await installPreset(ctx, resolved.presetId) : false;
311
+ if (resolved.persistConnections) {
312
+ const domain = await (await ensureStorageDomain(ctx)).open(connectionStorageSpec);
313
+ ctx.effect(() => () => domain.close(), "data-agent: close connection storage domain");
314
+ mountService(ctx, createDomainConnectionPersistence(domain));
315
+ } else {
316
+ ctx.logger.warn("data-agent: persistConnections=false; connection state is process-local and cannot restore across Web/TUI");
317
+ mountService(ctx);
318
+ }
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
+ }
330
+ }
331
+ /**
332
+ * Reuse a surface-provided storage stack (Web) or mount the same JSON stack
333
+ * when an interactive profile such as dsh-tui does not ship one.
334
+ */
335
+ async function ensureStorageDomain(ctx) {
336
+ const existing = ctx.get("storageDomain");
337
+ if (existing !== void 0) return existing;
338
+ ctx.logger.info("data-agent: storageDomain is absent; mounting the JSON storage stack for this profile");
339
+ let storage = ctx.get("storage");
340
+ if (storage === void 0) {
341
+ await ctx.plugin(Storage);
342
+ storage = ctx.get("storage");
343
+ }
344
+ if (storage === void 0) throw new Error("data-agent: failed to mount DSH storage hub");
345
+ if (!storage.backend.names().includes("json")) await ctx.plugin(storageJsonPlugin, { root: join(resolveDshHome(), "storages") });
346
+ let facility = ctx.get("storageDomain");
347
+ if (facility === void 0) {
348
+ await ctx.plugin(storageDomainPlugin, { backend: "json" });
349
+ facility = ctx.get("storageDomain");
154
350
  }
155
- if (resolved.installPreset) installPreset(ctx, resolved.presetId);
351
+ if (facility === void 0) throw new Error("data-agent: failed to mount DSH storage-domain facility");
352
+ return facility;
156
353
  }
157
354
  //#endregion
158
- export { Config, apply, inject, installPreset, name, resolveDshHome };
355
+ export { Config, apply, inject, installPreset, isLegacyManagedPreset, missingProfileDependencyMessage, mountPresetCapabilities, name, profileInstallCommand, resolveDshHome };