postgresai 0.12.0-beta.7 → 0.14.0-dev.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.md +47 -0
- package/bin/postgres-ai.ts +177 -0
- package/dist/bin/postgres-ai.js +158 -0
- package/dist/bin/postgres-ai.js.map +1 -1
- package/dist/lib/init.d.ts +64 -0
- package/dist/lib/init.d.ts.map +1 -0
- package/dist/lib/init.js +389 -0
- package/dist/lib/init.js.map +1 -0
- package/dist/package.json +3 -2
- package/lib/init.ts +435 -0
- package/package.json +3 -2
- package/sql/01.role.sql +4 -0
- package/sql/02.permissions.sql +33 -0
- package/sql/03.optional_rds.sql +6 -0
- package/sql/04.optional_self_managed.sql +8 -0
- package/test/init.integration.test.cjs +279 -0
- package/test/init.test.cjs +116 -0
package/README.md
CHANGED
|
@@ -34,6 +34,53 @@ postgresai --help
|
|
|
34
34
|
pgai --help # short alias
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
## init (create monitoring user in Postgres)
|
|
38
|
+
|
|
39
|
+
This command creates (or updates) the `postgres_ai_mon` user and grants the permissions described in the root `README.md` (it is idempotent).
|
|
40
|
+
|
|
41
|
+
Run without installing (positional connection string):
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx postgresai init postgresql://admin@host:5432/dbname
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
It also accepts libpq “conninfo” syntax:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx postgresai init "dbname=dbname host=host user=admin"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
And psql-like options:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npx postgresai init -h host -p 5432 -U admin -d dbname
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Password input options (in priority order):
|
|
60
|
+
- `--password <password>`
|
|
61
|
+
- `PGAI_MON_PASSWORD` environment variable
|
|
62
|
+
- if not provided: a strong password is generated automatically and printed once
|
|
63
|
+
|
|
64
|
+
Optional permissions (RDS/self-managed extras from the root `README.md`) are enabled by default. To skip them:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npx postgresai init postgresql://admin@host:5432/dbname --skip-optional-permissions
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Print SQL / dry run
|
|
71
|
+
|
|
72
|
+
To see what SQL would be executed (passwords redacted by default):
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npx postgresai init postgresql://admin@host:5432/dbname --print-sql
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
To print SQL and exit without applying anything:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npx postgresai init postgresql://admin@host:5432/dbname --dry-run
|
|
82
|
+
```
|
|
83
|
+
|
|
37
84
|
## Quick start
|
|
38
85
|
|
|
39
86
|
### Authentication
|
package/bin/postgres-ai.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { URL } from "url";
|
|
|
15
15
|
import { startMcpServer } from "../lib/mcp-server";
|
|
16
16
|
import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue } from "../lib/issues";
|
|
17
17
|
import { resolveBaseUrls } from "../lib/util";
|
|
18
|
+
import { applyInitPlan, buildInitPlan, resolveAdminConnection, resolveMonitoringPassword } from "../lib/init";
|
|
18
19
|
|
|
19
20
|
const execPromise = promisify(exec);
|
|
20
21
|
const execFilePromise = promisify(execFile);
|
|
@@ -116,6 +117,182 @@ program
|
|
|
116
117
|
"UI base URL for browser routes (overrides PGAI_UI_BASE_URL)"
|
|
117
118
|
);
|
|
118
119
|
|
|
120
|
+
program
|
|
121
|
+
.command("init [conn]")
|
|
122
|
+
.description("Create a monitoring user and grant all required permissions (idempotent)")
|
|
123
|
+
.option("--db-url <url>", "PostgreSQL connection URL (admin) to run the setup against (deprecated; pass it as positional arg)")
|
|
124
|
+
.option("-h, --host <host>", "PostgreSQL host (psql-like)")
|
|
125
|
+
.option("-p, --port <port>", "PostgreSQL port (psql-like)")
|
|
126
|
+
.option("-U, --username <username>", "PostgreSQL user (psql-like)")
|
|
127
|
+
.option("-d, --dbname <dbname>", "PostgreSQL database name (psql-like)")
|
|
128
|
+
.option("--admin-password <password>", "Admin connection password (otherwise uses PGPASSWORD if set)")
|
|
129
|
+
.option("--monitoring-user <name>", "Monitoring role name to create/update", "postgres_ai_mon")
|
|
130
|
+
.option("--password <password>", "Monitoring role password (overrides PGAI_MON_PASSWORD)")
|
|
131
|
+
.option("--skip-optional-permissions", "Skip optional permissions (RDS/self-managed extras)", false)
|
|
132
|
+
.option("--print-sql", "Print SQL steps before applying (passwords redacted by default)", false)
|
|
133
|
+
.option("--show-secrets", "When printing SQL, do not redact secrets (DANGEROUS)", false)
|
|
134
|
+
.option("--dry-run", "Print SQL steps and exit without applying changes", false)
|
|
135
|
+
.action(async (conn: string | undefined, opts: {
|
|
136
|
+
dbUrl?: string;
|
|
137
|
+
host?: string;
|
|
138
|
+
port?: string;
|
|
139
|
+
username?: string;
|
|
140
|
+
dbname?: string;
|
|
141
|
+
adminPassword?: string;
|
|
142
|
+
monitoringUser: string;
|
|
143
|
+
password?: string;
|
|
144
|
+
skipOptionalPermissions?: boolean;
|
|
145
|
+
printSql?: boolean;
|
|
146
|
+
showSecrets?: boolean;
|
|
147
|
+
dryRun?: boolean;
|
|
148
|
+
}) => {
|
|
149
|
+
let adminConn;
|
|
150
|
+
try {
|
|
151
|
+
adminConn = resolveAdminConnection({
|
|
152
|
+
conn,
|
|
153
|
+
dbUrlFlag: opts.dbUrl,
|
|
154
|
+
host: opts.host,
|
|
155
|
+
port: opts.port,
|
|
156
|
+
username: opts.username,
|
|
157
|
+
dbname: opts.dbname,
|
|
158
|
+
adminPassword: opts.adminPassword,
|
|
159
|
+
envPassword: process.env.PGPASSWORD,
|
|
160
|
+
});
|
|
161
|
+
} catch (e) {
|
|
162
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
163
|
+
console.error(`✗ ${msg}`);
|
|
164
|
+
process.exitCode = 1;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const includeOptionalPermissions = !opts.skipOptionalPermissions;
|
|
169
|
+
|
|
170
|
+
console.log(`Connecting to: ${adminConn.display}`);
|
|
171
|
+
console.log(`Monitoring user: ${opts.monitoringUser}`);
|
|
172
|
+
console.log(`Optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
|
|
173
|
+
|
|
174
|
+
const shouldPrintSql = !!opts.printSql || !!opts.dryRun;
|
|
175
|
+
|
|
176
|
+
// Use native pg client instead of requiring psql to be installed
|
|
177
|
+
const { Client } = require("pg");
|
|
178
|
+
const client = new Client(adminConn.clientConfig);
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
await client.connect();
|
|
182
|
+
|
|
183
|
+
const roleRes = await client.query("select 1 from pg_catalog.pg_roles where rolname = $1", [
|
|
184
|
+
opts.monitoringUser,
|
|
185
|
+
]);
|
|
186
|
+
const roleExists = roleRes.rowCount > 0;
|
|
187
|
+
|
|
188
|
+
const dbRes = await client.query("select current_database() as db");
|
|
189
|
+
const database = dbRes.rows?.[0]?.db;
|
|
190
|
+
if (typeof database !== "string" || !database) {
|
|
191
|
+
throw new Error("Failed to resolve current database name");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let monPassword: string;
|
|
195
|
+
try {
|
|
196
|
+
const resolved = await resolveMonitoringPassword({
|
|
197
|
+
passwordFlag: opts.password,
|
|
198
|
+
passwordEnv: process.env.PGAI_MON_PASSWORD,
|
|
199
|
+
monitoringUser: opts.monitoringUser,
|
|
200
|
+
});
|
|
201
|
+
monPassword = resolved.password;
|
|
202
|
+
if (resolved.generated) {
|
|
203
|
+
console.log(`Generated password for monitoring user ${opts.monitoringUser}: ${monPassword}`);
|
|
204
|
+
console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
|
|
205
|
+
}
|
|
206
|
+
} catch (e) {
|
|
207
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
208
|
+
console.error(`✗ ${msg}`);
|
|
209
|
+
process.exitCode = 1;
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const plan = await buildInitPlan({
|
|
214
|
+
database,
|
|
215
|
+
monitoringUser: opts.monitoringUser,
|
|
216
|
+
monitoringPassword: monPassword,
|
|
217
|
+
includeOptionalPermissions,
|
|
218
|
+
roleExists,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (shouldPrintSql) {
|
|
222
|
+
const redact = !opts.showSecrets;
|
|
223
|
+
const redactPasswords = (sql: string): string => {
|
|
224
|
+
if (!redact) return sql;
|
|
225
|
+
// Replace PASSWORD '<literal>' (handles doubled quotes inside).
|
|
226
|
+
return sql.replace(/password\s+'(?:''|[^'])*'/gi, "password '<redacted>'");
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
console.log("\n--- SQL plan ---");
|
|
230
|
+
for (const step of plan.steps) {
|
|
231
|
+
console.log(`\n-- ${step.name}${step.optional ? " (optional)" : ""}`);
|
|
232
|
+
console.log(redactPasswords(step.sql));
|
|
233
|
+
}
|
|
234
|
+
console.log("\n--- end SQL plan ---\n");
|
|
235
|
+
if (redact) {
|
|
236
|
+
console.log("Note: passwords are redacted in the printed SQL (use --show-secrets to print them).");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (opts.dryRun) {
|
|
241
|
+
console.log("✓ dry-run completed (no changes were applied)");
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const { applied, skippedOptional } = await applyInitPlan({ client, plan });
|
|
246
|
+
|
|
247
|
+
console.log("✓ init completed");
|
|
248
|
+
if (skippedOptional.length > 0) {
|
|
249
|
+
console.log("⚠ Some optional steps were skipped (not supported or insufficient privileges):");
|
|
250
|
+
for (const s of skippedOptional) console.log(`- ${s}`);
|
|
251
|
+
}
|
|
252
|
+
// Keep output compact but still useful
|
|
253
|
+
if (process.stdout.isTTY) {
|
|
254
|
+
console.log(`Applied ${applied.length} steps`);
|
|
255
|
+
}
|
|
256
|
+
} catch (error) {
|
|
257
|
+
const errAny = error as any;
|
|
258
|
+
let message = "";
|
|
259
|
+
if (error instanceof Error && error.message) {
|
|
260
|
+
message = error.message;
|
|
261
|
+
} else if (errAny && typeof errAny === "object" && typeof errAny.message === "string" && errAny.message) {
|
|
262
|
+
message = errAny.message;
|
|
263
|
+
} else {
|
|
264
|
+
message = String(error);
|
|
265
|
+
}
|
|
266
|
+
if (!message || message === "[object Object]") {
|
|
267
|
+
message = "Unknown error";
|
|
268
|
+
}
|
|
269
|
+
console.error(`✗ init failed: ${message}`);
|
|
270
|
+
if (errAny && typeof errAny === "object") {
|
|
271
|
+
if (typeof errAny.code === "string" && errAny.code) {
|
|
272
|
+
console.error(`Error code: ${errAny.code}`);
|
|
273
|
+
}
|
|
274
|
+
if (typeof errAny.detail === "string" && errAny.detail) {
|
|
275
|
+
console.error(`Detail: ${errAny.detail}`);
|
|
276
|
+
}
|
|
277
|
+
if (typeof errAny.hint === "string" && errAny.hint) {
|
|
278
|
+
console.error(`Hint: ${errAny.hint}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
|
|
282
|
+
if (errAny.code === "42501") {
|
|
283
|
+
console.error("Hint: connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges).");
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
process.exitCode = 1;
|
|
287
|
+
} finally {
|
|
288
|
+
try {
|
|
289
|
+
await client.end();
|
|
290
|
+
} catch {
|
|
291
|
+
// ignore
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
119
296
|
/**
|
|
120
297
|
* Stub function for not implemented commands
|
|
121
298
|
*/
|
package/dist/bin/postgres-ai.js
CHANGED
|
@@ -49,6 +49,7 @@ const url_1 = require("url");
|
|
|
49
49
|
const mcp_server_1 = require("../lib/mcp-server");
|
|
50
50
|
const issues_1 = require("../lib/issues");
|
|
51
51
|
const util_2 = require("../lib/util");
|
|
52
|
+
const init_1 = require("../lib/init");
|
|
52
53
|
const execPromise = (0, util_1.promisify)(child_process_1.exec);
|
|
53
54
|
const execFilePromise = (0, util_1.promisify)(child_process_1.execFile);
|
|
54
55
|
/**
|
|
@@ -98,6 +99,163 @@ program
|
|
|
98
99
|
.option("--api-key <key>", "API key (overrides PGAI_API_KEY)")
|
|
99
100
|
.option("--api-base-url <url>", "API base URL for backend RPC (overrides PGAI_API_BASE_URL)")
|
|
100
101
|
.option("--ui-base-url <url>", "UI base URL for browser routes (overrides PGAI_UI_BASE_URL)");
|
|
102
|
+
program
|
|
103
|
+
.command("init [conn]")
|
|
104
|
+
.description("Create a monitoring user and grant all required permissions (idempotent)")
|
|
105
|
+
.option("--db-url <url>", "PostgreSQL connection URL (admin) to run the setup against (deprecated; pass it as positional arg)")
|
|
106
|
+
.option("-h, --host <host>", "PostgreSQL host (psql-like)")
|
|
107
|
+
.option("-p, --port <port>", "PostgreSQL port (psql-like)")
|
|
108
|
+
.option("-U, --username <username>", "PostgreSQL user (psql-like)")
|
|
109
|
+
.option("-d, --dbname <dbname>", "PostgreSQL database name (psql-like)")
|
|
110
|
+
.option("--admin-password <password>", "Admin connection password (otherwise uses PGPASSWORD if set)")
|
|
111
|
+
.option("--monitoring-user <name>", "Monitoring role name to create/update", "postgres_ai_mon")
|
|
112
|
+
.option("--password <password>", "Monitoring role password (overrides PGAI_MON_PASSWORD)")
|
|
113
|
+
.option("--skip-optional-permissions", "Skip optional permissions (RDS/self-managed extras)", false)
|
|
114
|
+
.option("--print-sql", "Print SQL steps before applying (passwords redacted by default)", false)
|
|
115
|
+
.option("--show-secrets", "When printing SQL, do not redact secrets (DANGEROUS)", false)
|
|
116
|
+
.option("--dry-run", "Print SQL steps and exit without applying changes", false)
|
|
117
|
+
.action(async (conn, opts) => {
|
|
118
|
+
let adminConn;
|
|
119
|
+
try {
|
|
120
|
+
adminConn = (0, init_1.resolveAdminConnection)({
|
|
121
|
+
conn,
|
|
122
|
+
dbUrlFlag: opts.dbUrl,
|
|
123
|
+
host: opts.host,
|
|
124
|
+
port: opts.port,
|
|
125
|
+
username: opts.username,
|
|
126
|
+
dbname: opts.dbname,
|
|
127
|
+
adminPassword: opts.adminPassword,
|
|
128
|
+
envPassword: process.env.PGPASSWORD,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
133
|
+
console.error(`✗ ${msg}`);
|
|
134
|
+
process.exitCode = 1;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const includeOptionalPermissions = !opts.skipOptionalPermissions;
|
|
138
|
+
console.log(`Connecting to: ${adminConn.display}`);
|
|
139
|
+
console.log(`Monitoring user: ${opts.monitoringUser}`);
|
|
140
|
+
console.log(`Optional permissions: ${includeOptionalPermissions ? "enabled" : "skipped"}`);
|
|
141
|
+
const shouldPrintSql = !!opts.printSql || !!opts.dryRun;
|
|
142
|
+
// Use native pg client instead of requiring psql to be installed
|
|
143
|
+
const { Client } = require("pg");
|
|
144
|
+
const client = new Client(adminConn.clientConfig);
|
|
145
|
+
try {
|
|
146
|
+
await client.connect();
|
|
147
|
+
const roleRes = await client.query("select 1 from pg_catalog.pg_roles where rolname = $1", [
|
|
148
|
+
opts.monitoringUser,
|
|
149
|
+
]);
|
|
150
|
+
const roleExists = roleRes.rowCount > 0;
|
|
151
|
+
const dbRes = await client.query("select current_database() as db");
|
|
152
|
+
const database = dbRes.rows?.[0]?.db;
|
|
153
|
+
if (typeof database !== "string" || !database) {
|
|
154
|
+
throw new Error("Failed to resolve current database name");
|
|
155
|
+
}
|
|
156
|
+
let monPassword;
|
|
157
|
+
try {
|
|
158
|
+
const resolved = await (0, init_1.resolveMonitoringPassword)({
|
|
159
|
+
passwordFlag: opts.password,
|
|
160
|
+
passwordEnv: process.env.PGAI_MON_PASSWORD,
|
|
161
|
+
monitoringUser: opts.monitoringUser,
|
|
162
|
+
});
|
|
163
|
+
monPassword = resolved.password;
|
|
164
|
+
if (resolved.generated) {
|
|
165
|
+
console.log(`Generated password for monitoring user ${opts.monitoringUser}: ${monPassword}`);
|
|
166
|
+
console.log("Store it securely (or rerun with --password / PGAI_MON_PASSWORD to set your own).");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
171
|
+
console.error(`✗ ${msg}`);
|
|
172
|
+
process.exitCode = 1;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const plan = await (0, init_1.buildInitPlan)({
|
|
176
|
+
database,
|
|
177
|
+
monitoringUser: opts.monitoringUser,
|
|
178
|
+
monitoringPassword: monPassword,
|
|
179
|
+
includeOptionalPermissions,
|
|
180
|
+
roleExists,
|
|
181
|
+
});
|
|
182
|
+
if (shouldPrintSql) {
|
|
183
|
+
const redact = !opts.showSecrets;
|
|
184
|
+
const redactPasswords = (sql) => {
|
|
185
|
+
if (!redact)
|
|
186
|
+
return sql;
|
|
187
|
+
// Replace PASSWORD '<literal>' (handles doubled quotes inside).
|
|
188
|
+
return sql.replace(/password\s+'(?:''|[^'])*'/gi, "password '<redacted>'");
|
|
189
|
+
};
|
|
190
|
+
console.log("\n--- SQL plan ---");
|
|
191
|
+
for (const step of plan.steps) {
|
|
192
|
+
console.log(`\n-- ${step.name}${step.optional ? " (optional)" : ""}`);
|
|
193
|
+
console.log(redactPasswords(step.sql));
|
|
194
|
+
}
|
|
195
|
+
console.log("\n--- end SQL plan ---\n");
|
|
196
|
+
if (redact) {
|
|
197
|
+
console.log("Note: passwords are redacted in the printed SQL (use --show-secrets to print them).");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (opts.dryRun) {
|
|
201
|
+
console.log("✓ dry-run completed (no changes were applied)");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const { applied, skippedOptional } = await (0, init_1.applyInitPlan)({ client, plan });
|
|
205
|
+
console.log("✓ init completed");
|
|
206
|
+
if (skippedOptional.length > 0) {
|
|
207
|
+
console.log("⚠ Some optional steps were skipped (not supported or insufficient privileges):");
|
|
208
|
+
for (const s of skippedOptional)
|
|
209
|
+
console.log(`- ${s}`);
|
|
210
|
+
}
|
|
211
|
+
// Keep output compact but still useful
|
|
212
|
+
if (process.stdout.isTTY) {
|
|
213
|
+
console.log(`Applied ${applied.length} steps`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
const errAny = error;
|
|
218
|
+
let message = "";
|
|
219
|
+
if (error instanceof Error && error.message) {
|
|
220
|
+
message = error.message;
|
|
221
|
+
}
|
|
222
|
+
else if (errAny && typeof errAny === "object" && typeof errAny.message === "string" && errAny.message) {
|
|
223
|
+
message = errAny.message;
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
message = String(error);
|
|
227
|
+
}
|
|
228
|
+
if (!message || message === "[object Object]") {
|
|
229
|
+
message = "Unknown error";
|
|
230
|
+
}
|
|
231
|
+
console.error(`✗ init failed: ${message}`);
|
|
232
|
+
if (errAny && typeof errAny === "object") {
|
|
233
|
+
if (typeof errAny.code === "string" && errAny.code) {
|
|
234
|
+
console.error(`Error code: ${errAny.code}`);
|
|
235
|
+
}
|
|
236
|
+
if (typeof errAny.detail === "string" && errAny.detail) {
|
|
237
|
+
console.error(`Detail: ${errAny.detail}`);
|
|
238
|
+
}
|
|
239
|
+
if (typeof errAny.hint === "string" && errAny.hint) {
|
|
240
|
+
console.error(`Hint: ${errAny.hint}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (errAny && typeof errAny === "object" && typeof errAny.code === "string") {
|
|
244
|
+
if (errAny.code === "42501") {
|
|
245
|
+
console.error("Hint: connect as a superuser (or a role with CREATEROLE and sufficient GRANT privileges).");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
process.exitCode = 1;
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
try {
|
|
252
|
+
await client.end();
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// ignore
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
});
|
|
101
259
|
/**
|
|
102
260
|
* Stub function for not implemented commands
|
|
103
261
|
*/
|