@wfm-cn/dsh-storage 0.0.0-stage → 0.3.0

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 (121) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +21 -0
  3. package/README.md +137 -2
  4. package/cordis.patch.yml +3 -0
  5. package/lib/backends/database.js +578 -0
  6. package/lib/backfill.js +331 -0
  7. package/lib/client/index.js +79 -0
  8. package/lib/client/panel.js +299 -0
  9. package/lib/client/toast.js +66 -0
  10. package/lib/client.js +1 -0
  11. package/lib/compaction.js +108 -0
  12. package/lib/config.js +171 -0
  13. package/lib/ddl.js +323 -0
  14. package/lib/generated/mysql/browser.js +17 -0
  15. package/lib/generated/mysql/client.js +34 -0
  16. package/lib/generated/mysql/commonInputTypes.js +10 -0
  17. package/lib/generated/mysql/enums.js +10 -0
  18. package/lib/generated/mysql/internal/class.js +49 -0
  19. package/lib/generated/mysql/internal/prismaNamespace.js +146 -0
  20. package/lib/generated/mysql/internal/prismaNamespaceBrowser.js +117 -0
  21. package/lib/generated/mysql/models/AiChatHistory.js +1 -0
  22. package/lib/generated/mysql/models/AiMessage.js +1 -0
  23. package/lib/generated/mysql/models.js +1 -0
  24. package/lib/generated/postgresql/browser.js +17 -0
  25. package/lib/generated/postgresql/client.js +34 -0
  26. package/lib/generated/postgresql/commonInputTypes.js +10 -0
  27. package/lib/generated/postgresql/enums.js +10 -0
  28. package/lib/generated/postgresql/internal/class.js +49 -0
  29. package/lib/generated/postgresql/internal/prismaNamespace.js +131 -0
  30. package/lib/generated/postgresql/internal/prismaNamespaceBrowser.js +102 -0
  31. package/lib/generated/postgresql/models/AiChatHistory.js +1 -0
  32. package/lib/generated/postgresql/models/AiMessage.js +1 -0
  33. package/lib/generated/postgresql/models.js +1 -0
  34. package/lib/generated/sqlite/browser.js +17 -0
  35. package/lib/generated/sqlite/client.js +34 -0
  36. package/lib/generated/sqlite/commonInputTypes.js +10 -0
  37. package/lib/generated/sqlite/enums.js +10 -0
  38. package/lib/generated/sqlite/internal/class.js +49 -0
  39. package/lib/generated/sqlite/internal/prismaNamespace.js +128 -0
  40. package/lib/generated/sqlite/internal/prismaNamespaceBrowser.js +99 -0
  41. package/lib/generated/sqlite/models/AiChatHistory.js +1 -0
  42. package/lib/generated/sqlite/models/AiMessage.js +1 -0
  43. package/lib/generated/sqlite/models.js +1 -0
  44. package/lib/generated/sqlserver/browser.js +17 -0
  45. package/lib/generated/sqlserver/client.js +34 -0
  46. package/lib/generated/sqlserver/commonInputTypes.js +10 -0
  47. package/lib/generated/sqlserver/enums.js +10 -0
  48. package/lib/generated/sqlserver/internal/class.js +49 -0
  49. package/lib/generated/sqlserver/internal/prismaNamespace.js +119 -0
  50. package/lib/generated/sqlserver/internal/prismaNamespaceBrowser.js +90 -0
  51. package/lib/generated/sqlserver/models/AiChatHistory.js +1 -0
  52. package/lib/generated/sqlserver/models/AiMessage.js +1 -0
  53. package/lib/generated/sqlserver/models.js +1 -0
  54. package/lib/health.js +180 -0
  55. package/lib/index.js +681 -0
  56. package/lib/memory-tools.js +365 -0
  57. package/lib/notify.js +56 -0
  58. package/lib/projector.js +191 -0
  59. package/lib/status-route.js +172 -0
  60. package/lib/types/backends/database.d.ts +78 -0
  61. package/lib/types/backfill.d.ts +67 -0
  62. package/lib/types/client/index.d.ts +12 -0
  63. package/lib/types/client/panel.d.ts +79 -0
  64. package/lib/types/client/toast.d.ts +38 -0
  65. package/lib/types/compaction.d.ts +3 -0
  66. package/lib/types/config.d.ts +52 -0
  67. package/lib/types/ddl.d.ts +49 -0
  68. package/lib/types/generated/mysql/browser.d.ts +14 -0
  69. package/lib/types/generated/mysql/client.d.ts +33 -0
  70. package/lib/types/generated/mysql/commonInputTypes.d.ts +396 -0
  71. package/lib/types/generated/mysql/enums.d.ts +1 -0
  72. package/lib/types/generated/mysql/internal/class.d.ts +143 -0
  73. package/lib/types/generated/mysql/internal/prismaNamespace.d.ts +709 -0
  74. package/lib/types/generated/mysql/internal/prismaNamespaceBrowser.d.ts +112 -0
  75. package/lib/types/generated/mysql/models/AiChatHistory.d.ts +1258 -0
  76. package/lib/types/generated/mysql/models/AiMessage.d.ts +1359 -0
  77. package/lib/types/generated/mysql/models.d.ts +3 -0
  78. package/lib/types/generated/postgresql/browser.d.ts +14 -0
  79. package/lib/types/generated/postgresql/client.d.ts +33 -0
  80. package/lib/types/generated/postgresql/commonInputTypes.d.ts +392 -0
  81. package/lib/types/generated/postgresql/enums.d.ts +1 -0
  82. package/lib/types/generated/postgresql/internal/class.d.ts +143 -0
  83. package/lib/types/generated/postgresql/internal/prismaNamespace.d.ts +728 -0
  84. package/lib/types/generated/postgresql/internal/prismaNamespaceBrowser.d.ts +95 -0
  85. package/lib/types/generated/postgresql/models/AiChatHistory.d.ts +1373 -0
  86. package/lib/types/generated/postgresql/models/AiMessage.d.ts +1496 -0
  87. package/lib/types/generated/postgresql/models.d.ts +3 -0
  88. package/lib/types/generated/sqlite/browser.d.ts +14 -0
  89. package/lib/types/generated/sqlite/client.d.ts +33 -0
  90. package/lib/types/generated/sqlite/commonInputTypes.d.ts +373 -0
  91. package/lib/types/generated/sqlite/enums.d.ts +1 -0
  92. package/lib/types/generated/sqlite/internal/class.d.ts +143 -0
  93. package/lib/types/generated/sqlite/internal/prismaNamespace.d.ts +705 -0
  94. package/lib/types/generated/sqlite/internal/prismaNamespaceBrowser.d.ts +92 -0
  95. package/lib/types/generated/sqlite/models/AiChatHistory.d.ts +1371 -0
  96. package/lib/types/generated/sqlite/models/AiMessage.d.ts +1493 -0
  97. package/lib/types/generated/sqlite/models.d.ts +3 -0
  98. package/lib/types/generated/sqlserver/browser.d.ts +14 -0
  99. package/lib/types/generated/sqlserver/client.d.ts +33 -0
  100. package/lib/types/generated/sqlserver/commonInputTypes.d.ts +333 -0
  101. package/lib/types/generated/sqlserver/enums.d.ts +1 -0
  102. package/lib/types/generated/sqlserver/internal/class.d.ts +143 -0
  103. package/lib/types/generated/sqlserver/internal/prismaNamespace.d.ts +669 -0
  104. package/lib/types/generated/sqlserver/internal/prismaNamespaceBrowser.d.ts +80 -0
  105. package/lib/types/generated/sqlserver/models/AiChatHistory.d.ts +1257 -0
  106. package/lib/types/generated/sqlserver/models/AiMessage.d.ts +1375 -0
  107. package/lib/types/generated/sqlserver/models.d.ts +3 -0
  108. package/lib/types/health.d.ts +52 -0
  109. package/lib/types/index.d.ts +91 -0
  110. package/lib/types/memory-tools.d.ts +168 -0
  111. package/lib/types/notify.d.ts +35 -0
  112. package/lib/types/projector.d.ts +51 -0
  113. package/lib/types/status-route.d.ts +48 -0
  114. package/lib/types/types.d.ts +58 -0
  115. package/lib/types.js +8 -0
  116. package/package.json +127 -4
  117. package/preview.png +0 -0
  118. package/prisma/schema.mysql.prisma +78 -0
  119. package/prisma/schema.postgresql.prisma +70 -0
  120. package/prisma/schema.sqlite.prisma +70 -0
  121. package/prisma/schema.sqlserver.prisma +76 -0
@@ -0,0 +1,119 @@
1
+ /* !!! This is code generated by Prisma. Do not edit directly. !!! */
2
+ /* eslint-disable */
3
+ // biome-ignore-all lint: generated file
4
+ // @ts-nocheck
5
+ /*
6
+ * WARNING: This is an internal file that is subject to change!
7
+ *
8
+ * 🛑 Under no circumstances should you import this file directly! 🛑
9
+ *
10
+ * All exports from this file are wrapped under a `Prisma` namespace object in the client.ts file.
11
+ * While this enables partial backward compatibility, it is not part of the stable public API.
12
+ *
13
+ * If you are looking for your Models, Enums, and Input Types, please import them from the respective
14
+ * model files in the `model` directory!
15
+ */
16
+ import * as runtime from "@prisma/client/runtime/client";
17
+ /**
18
+ * Prisma Errors
19
+ */
20
+ export const PrismaClientKnownRequestError = runtime.PrismaClientKnownRequestError;
21
+ export const PrismaClientUnknownRequestError = runtime.PrismaClientUnknownRequestError;
22
+ export const PrismaClientRustPanicError = runtime.PrismaClientRustPanicError;
23
+ export const PrismaClientInitializationError = runtime.PrismaClientInitializationError;
24
+ export const PrismaClientValidationError = runtime.PrismaClientValidationError;
25
+ /**
26
+ * Re-export of sql-template-tag
27
+ */
28
+ export const sql = runtime.sqltag;
29
+ export const empty = runtime.empty;
30
+ export const join = runtime.join;
31
+ export const raw = runtime.raw;
32
+ export const Sql = runtime.Sql;
33
+ /**
34
+ * Decimal.js
35
+ */
36
+ export const Decimal = runtime.Decimal;
37
+ export const getExtensionContext = runtime.Extensions.getExtensionContext;
38
+ /**
39
+ * Prisma Client JS version: 7.10.1
40
+ * Query Engine version: d1b3f377ca835d73fd50954a5e22128b9ebdd6b8
41
+ */
42
+ export const prismaVersion = {
43
+ client: "7.10.1",
44
+ engine: "d1b3f377ca835d73fd50954a5e22128b9ebdd6b8"
45
+ };
46
+ export const NullTypes = {
47
+ DbNull: runtime.NullTypes.DbNull,
48
+ JsonNull: runtime.NullTypes.JsonNull,
49
+ AnyNull: runtime.NullTypes.AnyNull,
50
+ };
51
+ /**
52
+ * Helper for filtering JSON entries that have `null` on the database (empty on the db)
53
+ *
54
+ * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
55
+ */
56
+ export const DbNull = runtime.DbNull;
57
+ /**
58
+ * Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
59
+ *
60
+ * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
61
+ */
62
+ export const JsonNull = runtime.JsonNull;
63
+ /**
64
+ * Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
65
+ *
66
+ * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
67
+ */
68
+ export const AnyNull = runtime.AnyNull;
69
+ export const ModelName = {
70
+ AiMessage: 'AiMessage',
71
+ AiChatHistory: 'AiChatHistory'
72
+ };
73
+ /**
74
+ * Enums
75
+ */
76
+ export const TransactionIsolationLevel = runtime.makeStrictEnum({
77
+ ReadUncommitted: 'ReadUncommitted',
78
+ ReadCommitted: 'ReadCommitted',
79
+ RepeatableRead: 'RepeatableRead',
80
+ Serializable: 'Serializable',
81
+ Snapshot: 'Snapshot'
82
+ });
83
+ export const AiMessageScalarFieldEnum = {
84
+ id: 'id',
85
+ sessionId: 'sessionId',
86
+ historyId: 'historyId',
87
+ type: 'type',
88
+ content: 'content',
89
+ thoughts: 'thoughts',
90
+ model: 'model',
91
+ tokens: 'tokens',
92
+ toolCalls: 'toolCalls',
93
+ agentId: 'agentId',
94
+ metadata: 'metadata',
95
+ createdAt: 'createdAt',
96
+ updatedAt: 'updatedAt',
97
+ deletedAt: 'deletedAt'
98
+ };
99
+ export const AiChatHistoryScalarFieldEnum = {
100
+ id: 'id',
101
+ sessionId: 'sessionId',
102
+ title: 'title',
103
+ summary: 'summary',
104
+ messageCount: 'messageCount',
105
+ totalTokens: 'totalTokens',
106
+ firstMessageAt: 'firstMessageAt',
107
+ lastMessageAt: 'lastMessageAt',
108
+ metadata: 'metadata',
109
+ deletedAt: 'deletedAt'
110
+ };
111
+ export const SortOrder = {
112
+ asc: 'asc',
113
+ desc: 'desc'
114
+ };
115
+ export const NullsOrder = {
116
+ first: 'first',
117
+ last: 'last'
118
+ };
119
+ export const defineExtension = runtime.Extensions.defineExtension;
@@ -0,0 +1,90 @@
1
+ /* !!! This is code generated by Prisma. Do not edit directly. !!! */
2
+ /* eslint-disable */
3
+ // biome-ignore-all lint: generated file
4
+ // @ts-nocheck
5
+ /*
6
+ * WARNING: This is an internal file that is subject to change!
7
+ *
8
+ * 🛑 Under no circumstances should you import this file directly! 🛑
9
+ *
10
+ * All exports from this file are wrapped under a `Prisma` namespace object in the browser.ts file.
11
+ * While this enables partial backward compatibility, it is not part of the stable public API.
12
+ *
13
+ * If you are looking for your Models, Enums, and Input Types, please import them from the respective
14
+ * model files in the `model` directory!
15
+ */
16
+ import * as runtime from "@prisma/client/runtime/index-browser";
17
+ export const Decimal = runtime.Decimal;
18
+ export const NullTypes = {
19
+ DbNull: runtime.NullTypes.DbNull,
20
+ JsonNull: runtime.NullTypes.JsonNull,
21
+ AnyNull: runtime.NullTypes.AnyNull,
22
+ };
23
+ /**
24
+ * Helper for filtering JSON entries that have `null` on the database (empty on the db)
25
+ *
26
+ * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
27
+ */
28
+ export const DbNull = runtime.DbNull;
29
+ /**
30
+ * Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
31
+ *
32
+ * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
33
+ */
34
+ export const JsonNull = runtime.JsonNull;
35
+ /**
36
+ * Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
37
+ *
38
+ * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
39
+ */
40
+ export const AnyNull = runtime.AnyNull;
41
+ export const ModelName = {
42
+ AiMessage: 'AiMessage',
43
+ AiChatHistory: 'AiChatHistory'
44
+ };
45
+ /*
46
+ * Enums
47
+ */
48
+ export const TransactionIsolationLevel = runtime.makeStrictEnum({
49
+ ReadUncommitted: 'ReadUncommitted',
50
+ ReadCommitted: 'ReadCommitted',
51
+ RepeatableRead: 'RepeatableRead',
52
+ Serializable: 'Serializable',
53
+ Snapshot: 'Snapshot'
54
+ });
55
+ export const AiMessageScalarFieldEnum = {
56
+ id: 'id',
57
+ sessionId: 'sessionId',
58
+ historyId: 'historyId',
59
+ type: 'type',
60
+ content: 'content',
61
+ thoughts: 'thoughts',
62
+ model: 'model',
63
+ tokens: 'tokens',
64
+ toolCalls: 'toolCalls',
65
+ agentId: 'agentId',
66
+ metadata: 'metadata',
67
+ createdAt: 'createdAt',
68
+ updatedAt: 'updatedAt',
69
+ deletedAt: 'deletedAt'
70
+ };
71
+ export const AiChatHistoryScalarFieldEnum = {
72
+ id: 'id',
73
+ sessionId: 'sessionId',
74
+ title: 'title',
75
+ summary: 'summary',
76
+ messageCount: 'messageCount',
77
+ totalTokens: 'totalTokens',
78
+ firstMessageAt: 'firstMessageAt',
79
+ lastMessageAt: 'lastMessageAt',
80
+ metadata: 'metadata',
81
+ deletedAt: 'deletedAt'
82
+ };
83
+ export const SortOrder = {
84
+ asc: 'asc',
85
+ desc: 'desc'
86
+ };
87
+ export const NullsOrder = {
88
+ first: 'first',
89
+ last: 'last'
90
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
package/lib/health.js ADDED
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Health monitor for the sync backend: a small state machine around a probe
3
+ * (connect + SELECT 1 + table existence check) with exponential-backoff
4
+ * retries, plus the one-shot `testConnection` used by the settings panel.
5
+ */
6
+ import { createAdapter } from './backends/database.js';
7
+ import { configToUrl } from './config.js';
8
+ /** Thrown by the probe when the sync config itself is unusable — no retries until it is fixed. */
9
+ export class ConfigError extends Error {
10
+ }
11
+ /**
12
+ * States: 'disabled' (not started) → 'ok'/'down' on probe results, or
13
+ * 'config-error' when the probe reports a ConfigError (retries suspended
14
+ * until an explicit `recheck()`). Same-state results never re-fire
15
+ * transition callbacks.
16
+ */
17
+ export class HealthMonitor {
18
+ probe;
19
+ options;
20
+ state = 'disabled';
21
+ lastOkAt;
22
+ lastError;
23
+ retryCount = 0;
24
+ running = false;
25
+ timer;
26
+ listeners = new Set();
27
+ constructor(probe, options = {}) {
28
+ this.probe = probe;
29
+ this.options = options;
30
+ }
31
+ start() {
32
+ if (this.running)
33
+ return;
34
+ this.running = true;
35
+ void this.runProbe();
36
+ }
37
+ stop() {
38
+ this.running = false;
39
+ this.clearTimer();
40
+ this.retryCount = 0;
41
+ this.lastError = undefined;
42
+ this.lastOkAt = undefined;
43
+ this.transition('disabled');
44
+ }
45
+ snapshot() {
46
+ return {
47
+ state: this.state,
48
+ ...(this.lastOkAt !== undefined ? { lastOkAt: this.lastOkAt } : {}),
49
+ ...(this.lastError !== undefined ? { lastError: this.lastError } : {}),
50
+ retryCount: this.retryCount,
51
+ };
52
+ }
53
+ onTransition(cb) {
54
+ this.listeners.add(cb);
55
+ return () => this.listeners.delete(cb);
56
+ }
57
+ /** Run one probe cycle immediately (re-arms after a config error). */
58
+ async recheck() {
59
+ if (!this.running)
60
+ return;
61
+ this.clearTimer();
62
+ await this.runProbe();
63
+ }
64
+ clearTimer() {
65
+ if (this.timer !== undefined) {
66
+ clearTimeout(this.timer);
67
+ this.timer = undefined;
68
+ }
69
+ }
70
+ async runProbe() {
71
+ try {
72
+ await this.probe();
73
+ if (this.running)
74
+ this.onSuccess();
75
+ }
76
+ catch (error) {
77
+ if (this.running)
78
+ this.onFailure(error);
79
+ }
80
+ }
81
+ onSuccess() {
82
+ this.lastOkAt = Date.now();
83
+ this.lastError = undefined;
84
+ this.retryCount = 0;
85
+ this.transition('ok');
86
+ this.scheduleRetry(this.options.intervalMs ?? 30_000);
87
+ }
88
+ onFailure(error) {
89
+ this.lastError = error instanceof Error ? error.message : String(error);
90
+ if (error instanceof ConfigError) {
91
+ // The config is unusable — retrying cannot help until it is fixed.
92
+ this.retryCount = 0;
93
+ this.transition('config-error');
94
+ return;
95
+ }
96
+ this.transition('down');
97
+ // Exponential backoff: 1s → 2s → 4s → ... capped at 60s.
98
+ this.scheduleRetry(Math.min(1000 * 2 ** this.retryCount, 60_000));
99
+ this.retryCount += 1;
100
+ }
101
+ scheduleRetry(delayMs) {
102
+ this.clearTimer();
103
+ this.timer = setTimeout(() => {
104
+ this.timer = undefined;
105
+ void this.runProbe();
106
+ }, delayMs);
107
+ }
108
+ transition(to) {
109
+ const from = this.state;
110
+ if (from === to)
111
+ return;
112
+ this.state = to;
113
+ const snapshot = this.snapshot();
114
+ for (const cb of [...this.listeners])
115
+ cb(to, from, snapshot);
116
+ }
117
+ }
118
+ function tableCheck(provider) {
119
+ switch (provider) {
120
+ case 'mysql':
121
+ return `SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'ai_messages'`;
122
+ case 'postgresql':
123
+ return `SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'ai_messages'`;
124
+ case 'sqlite':
125
+ return `SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'ai_messages'`;
126
+ case 'sqlserver':
127
+ return `SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'ai_messages'`;
128
+ }
129
+ }
130
+ /** Collapse driver errors into stable, human-readable messages. */
131
+ function humanizeError(error) {
132
+ const message = error instanceof Error ? error.message : String(error);
133
+ if (/ECONNREFUSED|connection refused/i.test(message))
134
+ return 'connection refused';
135
+ if (/access denied|ER_ACCESS_DENIED_ERROR|authentication failed|password authentication|login failed/i.test(message))
136
+ return 'authentication denied (check user/password)';
137
+ if (/ER_BAD_DB_ERROR|unknown database|database .* does not exist|no such file/i.test(message))
138
+ return 'database not found';
139
+ return message;
140
+ }
141
+ /** Build an adapter from the config, connect, SELECT 1, check tables, dispose. */
142
+ async function connectAndCheck(cfg) {
143
+ const factory = (await createAdapter({
144
+ provider: cfg.provider,
145
+ url: configToUrl(cfg),
146
+ timezone: cfg.timezone,
147
+ }));
148
+ const connection = await factory.connect();
149
+ try {
150
+ await connection.queryRaw({ sql: 'SELECT 1', args: [] });
151
+ const tables = await connection.queryRaw({ sql: tableCheck(cfg.provider), args: [] });
152
+ return tables.rows.length > 0 ? 'ok' : 'no-tables';
153
+ }
154
+ finally {
155
+ await connection.dispose?.().catch((error) => {
156
+ console.error('[dsh-storage] connection dispose failed:', error);
157
+ });
158
+ }
159
+ }
160
+ /** Probe for HealthMonitor: resolves when the database is live and migrated. */
161
+ export async function probeDatabase(cfg) {
162
+ let result;
163
+ try {
164
+ result = await connectAndCheck(cfg);
165
+ }
166
+ catch (error) {
167
+ throw new Error(humanizeError(error));
168
+ }
169
+ if (result === 'no-tables')
170
+ throw new Error('ai_messages table missing — run dsh-storage migrations first');
171
+ }
172
+ /** One-shot panel test: never throws — every failure maps to 'fail:<reason>'. */
173
+ export async function testConnection(cfg) {
174
+ try {
175
+ return await connectAndCheck(cfg);
176
+ }
177
+ catch (error) {
178
+ return `fail:${humanizeError(error)}`;
179
+ }
180
+ }