midql-cli 0.1.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.
@@ -0,0 +1,396 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getProfile,
4
+ resolvePassword
5
+ } from "./chunk-3QAR6XBK.js";
6
+ import {
7
+ trackTransactionState
8
+ } from "./chunk-PXDMSYWH.js";
9
+ import {
10
+ ConnectionError,
11
+ ExecutionError
12
+ } from "./chunk-VFC3HWTF.js";
13
+
14
+ // src/adapters/introspect/postgres.ts
15
+ var columnsQuery = `
16
+ SELECT c.table_name, c.column_name, c.data_type, c.is_nullable, c.column_default
17
+ FROM information_schema.columns c
18
+ JOIN information_schema.tables t
19
+ ON t.table_schema = c.table_schema AND t.table_name = c.table_name
20
+ WHERE c.table_schema = 'public' AND t.table_type = 'BASE TABLE'
21
+ ORDER BY c.table_name, c.ordinal_position`;
22
+ var primaryKeysQuery = `
23
+ SELECT rel.relname AS table_name, att.attname AS column_name
24
+ FROM pg_constraint con
25
+ JOIN pg_class rel ON rel.oid = con.conrelid
26
+ JOIN pg_namespace nsp ON nsp.oid = rel.relnamespace
27
+ JOIN unnest(con.conkey) WITH ORDINALITY AS cols(attnum, ord) ON true
28
+ JOIN pg_attribute att ON att.attrelid = rel.oid AND att.attnum = cols.attnum
29
+ WHERE con.contype = 'p' AND nsp.nspname = 'public'
30
+ ORDER BY rel.relname, cols.ord`;
31
+ var foreignKeysQuery = `
32
+ SELECT
33
+ con.conname AS constraint_name,
34
+ rel.relname AS table_name,
35
+ att.attname AS column_name,
36
+ frel.relname AS referenced_table,
37
+ fatt.attname AS referenced_column,
38
+ cols.ord
39
+ FROM pg_constraint con
40
+ JOIN pg_class rel ON rel.oid = con.conrelid
41
+ JOIN pg_class frel ON frel.oid = con.confrelid
42
+ JOIN pg_namespace nsp ON nsp.oid = rel.relnamespace
43
+ JOIN unnest(con.conkey) WITH ORDINALITY AS cols(attnum, ord) ON true
44
+ JOIN unnest(con.confkey) WITH ORDINALITY AS fcols(attnum, ord) ON fcols.ord = cols.ord
45
+ JOIN pg_attribute att ON att.attrelid = rel.oid AND att.attnum = cols.attnum
46
+ JOIN pg_attribute fatt ON fatt.attrelid = frel.oid AND fatt.attnum = fcols.attnum
47
+ WHERE con.contype = 'f' AND nsp.nspname = 'public'
48
+ ORDER BY rel.relname, con.conname, cols.ord`;
49
+ var rowCountsQuery = `
50
+ SELECT relname AS table_name, GREATEST(reltuples, 0)::bigint AS approx_rows
51
+ FROM pg_class rel
52
+ JOIN pg_namespace nsp ON nsp.oid = rel.relnamespace
53
+ WHERE nsp.nspname = 'public' AND rel.relkind = 'r'`;
54
+ function classifyPostgresType(dataType) {
55
+ const type = dataType.toLowerCase();
56
+ if (type.includes("int") || type.includes("numeric") || type.includes("decimal")) {
57
+ return "number";
58
+ }
59
+ if (type.includes("double") || type.includes("real") || type.includes("money") || type.includes("serial")) {
60
+ return "number";
61
+ }
62
+ if (type.includes("bool")) {
63
+ return "boolean";
64
+ }
65
+ if (type.includes("timestamp") || type.includes("date") || type.includes("time")) {
66
+ return "date";
67
+ }
68
+ if (type.includes("json")) {
69
+ return "json";
70
+ }
71
+ if (type.includes("uuid")) {
72
+ return "uuid";
73
+ }
74
+ if (type.includes("char") || type.includes("text") || type.includes("citext")) {
75
+ return "text";
76
+ }
77
+ return "other";
78
+ }
79
+ function assembleSchema(columnRows, primaryKeyRows, foreignKeyRows, rowCountRows) {
80
+ const primaryKeys = /* @__PURE__ */ new Map();
81
+ for (const row of primaryKeyRows) {
82
+ const existing = primaryKeys.get(row.table_name) ?? [];
83
+ existing.push(row.column_name);
84
+ primaryKeys.set(row.table_name, existing);
85
+ }
86
+ const foreignKeys = /* @__PURE__ */ new Map();
87
+ for (const row of foreignKeyRows) {
88
+ const tableFks = foreignKeys.get(row.table_name) ?? /* @__PURE__ */ new Map();
89
+ const fk = tableFks.get(row.constraint_name) ?? {
90
+ columns: [],
91
+ referencedTable: row.referenced_table,
92
+ referencedColumns: []
93
+ };
94
+ fk.columns.push(row.column_name);
95
+ fk.referencedColumns.push(row.referenced_column);
96
+ tableFks.set(row.constraint_name, fk);
97
+ foreignKeys.set(row.table_name, tableFks);
98
+ }
99
+ const rowCounts = /* @__PURE__ */ new Map();
100
+ for (const row of rowCountRows) {
101
+ rowCounts.set(row.table_name, Number(row.approx_rows));
102
+ }
103
+ const tables = /* @__PURE__ */ new Map();
104
+ for (const row of columnRows) {
105
+ const pk = primaryKeys.get(row.table_name) ?? [];
106
+ const table = tables.get(row.table_name) ?? {
107
+ name: row.table_name,
108
+ columns: [],
109
+ primaryKey: pk,
110
+ foreignKeys: [...foreignKeys.get(row.table_name)?.values() ?? []],
111
+ approxRowCount: rowCounts.get(row.table_name) ?? 0
112
+ };
113
+ const column = {
114
+ name: row.column_name,
115
+ dataType: row.data_type,
116
+ kind: classifyPostgresType(row.data_type),
117
+ nullable: row.is_nullable === "YES",
118
+ isPrimaryKey: pk.includes(row.column_name),
119
+ hasDefault: row.column_default !== null
120
+ };
121
+ table.columns.push(column);
122
+ tables.set(row.table_name, table);
123
+ }
124
+ return { tables: [...tables.values()] };
125
+ }
126
+
127
+ // src/adapters/postgres.ts
128
+ var PostgresAdapter = class {
129
+ dialect = "postgres";
130
+ database;
131
+ options;
132
+ pool = null;
133
+ sessionClient = null;
134
+ transactionOpen = false;
135
+ constructor(options) {
136
+ this.options = options;
137
+ this.database = options.database;
138
+ }
139
+ async connect() {
140
+ const { Pool, Client } = (await import("pg")).default ?? await import("pg");
141
+ const config = {
142
+ host: this.options.host,
143
+ port: this.options.port,
144
+ database: this.options.database,
145
+ user: this.options.user,
146
+ password: this.options.password ?? void 0,
147
+ ssl: this.options.ssl ? { rejectUnauthorized: false } : void 0,
148
+ max: 3
149
+ };
150
+ try {
151
+ this.pool = new Pool(config);
152
+ this.sessionClient = new Client(config);
153
+ await this.sessionClient.connect();
154
+ await this.pool.query("SELECT 1");
155
+ } catch (error) {
156
+ await this.close();
157
+ throw new ConnectionError(
158
+ `Could not connect to postgres://${this.options.user}@${this.options.host}:${this.options.port}/${this.options.database}: ${error.message}`
159
+ );
160
+ }
161
+ }
162
+ async query(sql, params = []) {
163
+ if (!this.pool) {
164
+ throw new ConnectionError("Not connected");
165
+ }
166
+ return this.run(() => this.pool.query(sql, params), sql);
167
+ }
168
+ async sessionQuery(sql, params = []) {
169
+ if (!this.sessionClient) {
170
+ throw new ConnectionError("Not connected");
171
+ }
172
+ const result = await this.run(
173
+ () => this.sessionClient.query(sql, params),
174
+ sql
175
+ );
176
+ this.transactionOpen = trackTransactionState(sql, this.transactionOpen);
177
+ return result;
178
+ }
179
+ async run(execute, sql) {
180
+ const started = performance.now();
181
+ let result;
182
+ try {
183
+ result = await execute();
184
+ } catch (error) {
185
+ throw new ExecutionError(error.message);
186
+ }
187
+ return {
188
+ columns: result.fields?.map((field) => field.name) ?? [],
189
+ rows: result.rows ?? [],
190
+ rowCount: result.rowCount ?? result.rows?.length ?? 0,
191
+ durationMs: Math.round(performance.now() - started),
192
+ command: result.command ?? sql.trim().split(/\s+/)[0]?.toUpperCase() ?? ""
193
+ };
194
+ }
195
+ async introspect() {
196
+ const [columns, primaryKeys, foreignKeys, rowCounts] = await Promise.all([
197
+ this.query(columnsQuery),
198
+ this.query(primaryKeysQuery),
199
+ this.query(foreignKeysQuery),
200
+ this.query(rowCountsQuery)
201
+ ]);
202
+ return assembleSchema(
203
+ columns.rows,
204
+ primaryKeys.rows,
205
+ foreignKeys.rows,
206
+ rowCounts.rows
207
+ );
208
+ }
209
+ inTransaction() {
210
+ return this.transactionOpen;
211
+ }
212
+ async close() {
213
+ if (this.sessionClient) {
214
+ try {
215
+ await this.sessionClient.end();
216
+ } catch {
217
+ }
218
+ this.sessionClient = null;
219
+ }
220
+ if (this.pool) {
221
+ try {
222
+ await this.pool.end();
223
+ } catch {
224
+ }
225
+ this.pool = null;
226
+ }
227
+ this.transactionOpen = false;
228
+ }
229
+ };
230
+
231
+ // src/connections/manager.ts
232
+ function parseConnectionUrl(url) {
233
+ let parsed;
234
+ try {
235
+ parsed = new URL(url);
236
+ } catch {
237
+ throw new ConnectionError(`Invalid connection URL: ${url}`);
238
+ }
239
+ const protocol = parsed.protocol.replace(":", "");
240
+ let dialect;
241
+ if (protocol === "postgres" || protocol === "postgresql") {
242
+ dialect = "postgres";
243
+ } else if (protocol === "mysql") {
244
+ dialect = "mysql";
245
+ } else {
246
+ throw new ConnectionError(`Unsupported protocol "${protocol}"; use postgres:// or mysql://`);
247
+ }
248
+ const database = parsed.pathname.replace(/^\//, "");
249
+ if (!database) {
250
+ throw new ConnectionError("Connection URL must include a database name");
251
+ }
252
+ return {
253
+ dialect,
254
+ options: {
255
+ host: parsed.hostname || "localhost",
256
+ port: parsed.port ? Number(parsed.port) : dialect === "postgres" ? 5432 : 3306,
257
+ database,
258
+ user: decodeURIComponent(parsed.username || "postgres"),
259
+ password: parsed.password ? decodeURIComponent(parsed.password) : null,
260
+ ssl: parsed.searchParams.get("ssl") === "true" || parsed.searchParams.get("sslmode") === "require"
261
+ },
262
+ env: "dev",
263
+ readonly: false
264
+ };
265
+ }
266
+ function targetFromProfile(profile) {
267
+ return {
268
+ dialect: profile.dialect,
269
+ options: {
270
+ host: profile.host,
271
+ port: profile.port,
272
+ database: profile.database,
273
+ user: profile.user,
274
+ password: resolvePassword(profile),
275
+ ssl: profile.ssl
276
+ },
277
+ env: profile.env,
278
+ readonly: profile.readonly
279
+ };
280
+ }
281
+ function resolveTarget(profileNameOrUrl) {
282
+ if (profileNameOrUrl.includes("://")) {
283
+ return parseConnectionUrl(profileNameOrUrl);
284
+ }
285
+ const profile = getProfile(profileNameOrUrl);
286
+ if (!profile) {
287
+ throw new ConnectionError(
288
+ `No saved profile named "${profileNameOrUrl}". Use a connection URL or save a profile first.`
289
+ );
290
+ }
291
+ return targetFromProfile(profile);
292
+ }
293
+ async function createAdapter(target) {
294
+ if (target.dialect === "postgres") {
295
+ return new PostgresAdapter(target.options);
296
+ }
297
+ const { MysqlAdapter } = await import("./mysql-GQSXUUEK.js");
298
+ return new MysqlAdapter(target.options);
299
+ }
300
+ var ConnectionManager = class {
301
+ connections = /* @__PURE__ */ new Map();
302
+ activeAlias = null;
303
+ async connect(profileNameOrUrl, alias) {
304
+ const target = resolveTarget(profileNameOrUrl);
305
+ const resolvedAlias = alias ?? this.defaultAlias(profileNameOrUrl, target);
306
+ if (this.connections.has(resolvedAlias)) {
307
+ throw new ConnectionError(
308
+ `Alias "${resolvedAlias}" is already connected; use \\use ${resolvedAlias} or pick another alias`
309
+ );
310
+ }
311
+ const adapter = await createAdapter(target);
312
+ await adapter.connect();
313
+ const schema = await adapter.introspect();
314
+ const connection = {
315
+ alias: resolvedAlias,
316
+ adapter,
317
+ dialect: target.dialect,
318
+ database: target.options.database,
319
+ env: target.env,
320
+ readonly: target.readonly,
321
+ schema,
322
+ schemaLoadedAt: Date.now(),
323
+ options: target.options
324
+ };
325
+ this.connections.set(resolvedAlias, connection);
326
+ this.activeAlias = resolvedAlias;
327
+ return connection;
328
+ }
329
+ defaultAlias(profileNameOrUrl, target) {
330
+ const base = profileNameOrUrl.includes("://") ? target.options.database : profileNameOrUrl;
331
+ if (!this.connections.has(base)) {
332
+ return base;
333
+ }
334
+ let suffix = 2;
335
+ while (this.connections.has(`${base}${suffix}`)) {
336
+ suffix += 1;
337
+ }
338
+ return `${base}${suffix}`;
339
+ }
340
+ use(alias) {
341
+ const connection = this.connections.get(alias);
342
+ if (!connection) {
343
+ throw new ConnectionError(`No connection with alias "${alias}"`);
344
+ }
345
+ this.activeAlias = alias;
346
+ return connection;
347
+ }
348
+ async disconnect(alias) {
349
+ const connection = this.connections.get(alias);
350
+ if (!connection) {
351
+ throw new ConnectionError(`No connection with alias "${alias}"`);
352
+ }
353
+ await connection.adapter.close();
354
+ this.connections.delete(alias);
355
+ if (this.activeAlias === alias) {
356
+ this.activeAlias = this.connections.keys().next().value ?? null;
357
+ }
358
+ }
359
+ list() {
360
+ return [...this.connections.values()];
361
+ }
362
+ active() {
363
+ if (!this.activeAlias) {
364
+ return null;
365
+ }
366
+ return this.connections.get(this.activeAlias) ?? null;
367
+ }
368
+ async refreshSchema(alias) {
369
+ const connection = alias ? this.connections.get(alias) : this.active();
370
+ if (!connection) {
371
+ throw new ConnectionError("No active connection");
372
+ }
373
+ connection.schema = await connection.adapter.introspect();
374
+ connection.schemaLoadedAt = Date.now();
375
+ return connection;
376
+ }
377
+ setReadonly(readonly) {
378
+ const connection = this.active();
379
+ if (!connection) {
380
+ throw new ConnectionError("No active connection");
381
+ }
382
+ connection.readonly = readonly;
383
+ return connection;
384
+ }
385
+ async closeAll() {
386
+ await Promise.all(this.list().map((connection) => connection.adapter.close()));
387
+ this.connections.clear();
388
+ this.activeAlias = null;
389
+ }
390
+ };
391
+
392
+ export {
393
+ PostgresAdapter,
394
+ resolveTarget,
395
+ ConnectionManager
396
+ };
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ findTool,
4
+ runTool
5
+ } from "./chunk-NCN3ZBOJ.js";
6
+ import {
7
+ ExecutionError
8
+ } from "./chunk-VFC3HWTF.js";
9
+
10
+ // src/backup/restore.ts
11
+ import { existsSync } from "fs";
12
+ async function restoreDatabase(dialect, options, file, tools) {
13
+ if (!existsSync(file)) {
14
+ throw new ExecutionError(`Backup file not found: ${file}`);
15
+ }
16
+ if (dialect === "postgres") {
17
+ await restorePostgres(options, file, tools);
18
+ } else {
19
+ await restoreMysql(options, file, tools);
20
+ }
21
+ }
22
+ async function restorePostgres(options, file, tools) {
23
+ const env = { PGPASSWORD: options.password ?? void 0 };
24
+ const connectionArgs = [
25
+ "--no-password",
26
+ "-h",
27
+ options.host,
28
+ "-p",
29
+ String(options.port),
30
+ "-U",
31
+ options.user,
32
+ "-d",
33
+ options.database
34
+ ];
35
+ if (file.endsWith(".sql")) {
36
+ const binary2 = await findTool("psql", tools.psqlPath);
37
+ const run2 = await runTool(binary2, [...connectionArgs, "-f", file], env, {});
38
+ if (run2.code !== 0) {
39
+ throw new ExecutionError(
40
+ `psql restore failed: ${run2.stderr.trim() || `exit code ${run2.code}`}`
41
+ );
42
+ }
43
+ return;
44
+ }
45
+ const binary = await findTool("pg_restore", tools.pgRestorePath);
46
+ const run = await runTool(binary, [...connectionArgs, "--clean", "--if-exists", file], env, {});
47
+ if (run.code !== 0) {
48
+ throw new ExecutionError(`pg_restore failed: ${run.stderr.trim() || `exit code ${run.code}`}`);
49
+ }
50
+ }
51
+ async function restoreMysql(options, file, tools) {
52
+ const binary = await findTool("mysql", tools.mysqlPath);
53
+ const args = [
54
+ "-h",
55
+ options.host,
56
+ "-P",
57
+ String(options.port),
58
+ "-u",
59
+ options.user,
60
+ options.database
61
+ ];
62
+ const run = await runTool(
63
+ binary,
64
+ args,
65
+ { MYSQL_PWD: options.password ?? void 0 },
66
+ { fromFile: file }
67
+ );
68
+ if (run.code !== 0) {
69
+ throw new ExecutionError(
70
+ `mysql restore failed: ${run.stderr.trim() || `exit code ${run.code}`}`
71
+ );
72
+ }
73
+ }
74
+
75
+ export {
76
+ restoreDatabase
77
+ };
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ConfigError
4
+ } from "./chunk-VFC3HWTF.js";
5
+
6
+ // src/config/config.ts
7
+ import { existsSync, readFileSync, writeFileSync } from "fs";
8
+
9
+ // src/config/paths.ts
10
+ import { mkdirSync } from "fs";
11
+ import { homedir } from "os";
12
+ import { join } from "path";
13
+ function configDir() {
14
+ return process.env.MIDQL_HOME ?? join(homedir(), ".midql");
15
+ }
16
+ function configFilePath() {
17
+ return join(configDir(), "config.json");
18
+ }
19
+ function keyFilePath() {
20
+ return join(configDir(), ".key");
21
+ }
22
+ function historyFilePath() {
23
+ return join(configDir(), "history.jsonl");
24
+ }
25
+ function ensureConfigDir() {
26
+ const dir = configDir();
27
+ mkdirSync(dir, { recursive: true });
28
+ return dir;
29
+ }
30
+
31
+ // src/config/config.ts
32
+ var defaultTools = {
33
+ pgDumpPath: null,
34
+ pgRestorePath: null,
35
+ psqlPath: null,
36
+ mysqldumpPath: null,
37
+ mysqlPath: null
38
+ };
39
+ function defaultConfig() {
40
+ return {
41
+ profiles: [],
42
+ safety: "on",
43
+ maxJoinDepth: 3,
44
+ tools: { ...defaultTools }
45
+ };
46
+ }
47
+ var environmentTags = ["dev", "staging", "prod"];
48
+ function validateProfile(raw, index) {
49
+ if (typeof raw !== "object" || raw === null) {
50
+ throw new ConfigError(`Profile at index ${index} is not an object`);
51
+ }
52
+ const p = raw;
53
+ if (typeof p.name !== "string" || p.name.length === 0) {
54
+ throw new ConfigError(`Profile at index ${index} is missing a name`);
55
+ }
56
+ if (p.dialect !== "postgres" && p.dialect !== "mysql") {
57
+ throw new ConfigError(`Profile "${p.name}" has invalid dialect "${String(p.dialect)}"`);
58
+ }
59
+ if (typeof p.host !== "string" || typeof p.database !== "string" || typeof p.user !== "string") {
60
+ throw new ConfigError(`Profile "${p.name}" is missing host, database, or user`);
61
+ }
62
+ const env = environmentTags.includes(p.env) ? p.env : "dev";
63
+ return {
64
+ name: p.name,
65
+ dialect: p.dialect,
66
+ host: p.host,
67
+ port: typeof p.port === "number" ? p.port : p.dialect === "postgres" ? 5432 : 3306,
68
+ database: p.database,
69
+ user: p.user,
70
+ encryptedPassword: typeof p.encryptedPassword === "string" ? p.encryptedPassword : null,
71
+ passwordEnv: typeof p.passwordEnv === "string" ? p.passwordEnv : null,
72
+ env,
73
+ readonly: p.readonly === true,
74
+ ssl: p.ssl === true
75
+ };
76
+ }
77
+ function loadConfig() {
78
+ const path = configFilePath();
79
+ if (!existsSync(path)) {
80
+ return defaultConfig();
81
+ }
82
+ let parsed;
83
+ try {
84
+ parsed = JSON.parse(readFileSync(path, "utf8"));
85
+ } catch (error) {
86
+ throw new ConfigError(`Failed to parse ${path}: ${error.message}`);
87
+ }
88
+ if (typeof parsed !== "object" || parsed === null) {
89
+ throw new ConfigError(`${path} must contain a JSON object`);
90
+ }
91
+ const raw = parsed;
92
+ const profiles = Array.isArray(raw.profiles) ? raw.profiles.map((entry, index) => validateProfile(entry, index)) : [];
93
+ const tools = typeof raw.tools === "object" && raw.tools !== null ? { ...defaultTools, ...raw.tools } : { ...defaultTools };
94
+ return {
95
+ profiles,
96
+ safety: raw.safety === "off" ? "off" : "on",
97
+ maxJoinDepth: typeof raw.maxJoinDepth === "number" ? raw.maxJoinDepth : 3,
98
+ tools
99
+ };
100
+ }
101
+ function saveConfig(config) {
102
+ ensureConfigDir();
103
+ writeFileSync(configFilePath(), `${JSON.stringify(config, null, 2)}
104
+ `, "utf8");
105
+ }
106
+
107
+ // src/connections/secrets.ts
108
+ import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
109
+ import { chmodSync, existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
110
+ var KEY_LENGTH = 32;
111
+ var IV_LENGTH = 12;
112
+ function loadOrCreateKey() {
113
+ const path = keyFilePath();
114
+ if (existsSync2(path)) {
115
+ const key2 = Buffer.from(readFileSync2(path, "utf8").trim(), "base64");
116
+ if (key2.length !== KEY_LENGTH) {
117
+ throw new ConfigError(`Key file ${path} is corrupted; delete it to generate a new key`);
118
+ }
119
+ return key2;
120
+ }
121
+ ensureConfigDir();
122
+ const key = randomBytes(KEY_LENGTH);
123
+ writeFileSync2(path, key.toString("base64"), { mode: 384 });
124
+ try {
125
+ chmodSync(path, 384);
126
+ } catch {
127
+ }
128
+ return key;
129
+ }
130
+ function encryptSecret(plaintext) {
131
+ const key = loadOrCreateKey();
132
+ const iv = randomBytes(IV_LENGTH);
133
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
134
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
135
+ const tag = cipher.getAuthTag();
136
+ return Buffer.concat([iv, tag, encrypted]).toString("base64");
137
+ }
138
+ function decryptSecret(payload) {
139
+ const key = loadOrCreateKey();
140
+ const raw = Buffer.from(payload, "base64");
141
+ const iv = raw.subarray(0, IV_LENGTH);
142
+ const tag = raw.subarray(IV_LENGTH, IV_LENGTH + 16);
143
+ const encrypted = raw.subarray(IV_LENGTH + 16);
144
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
145
+ decipher.setAuthTag(tag);
146
+ return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString("utf8");
147
+ }
148
+
149
+ // src/connections/profiles.ts
150
+ function listProfiles() {
151
+ return loadConfig().profiles;
152
+ }
153
+ function getProfile(name) {
154
+ return loadConfig().profiles.find((profile) => profile.name === name) ?? null;
155
+ }
156
+ function saveProfile(profile, password) {
157
+ const config = loadConfig();
158
+ const stored = {
159
+ ...profile,
160
+ encryptedPassword: password ? encryptSecret(password) : profile.encryptedPassword
161
+ };
162
+ const index = config.profiles.findIndex((existing) => existing.name === profile.name);
163
+ if (index >= 0) {
164
+ config.profiles[index] = stored;
165
+ } else {
166
+ config.profiles.push(stored);
167
+ }
168
+ saveConfig(config);
169
+ }
170
+ function deleteProfile(name) {
171
+ const config = loadConfig();
172
+ const index = config.profiles.findIndex((profile) => profile.name === name);
173
+ if (index < 0) {
174
+ throw new ConfigError(`Profile "${name}" not found`);
175
+ }
176
+ config.profiles.splice(index, 1);
177
+ saveConfig(config);
178
+ }
179
+ function resolvePassword(profile) {
180
+ if (profile.passwordEnv) {
181
+ const value = process.env[profile.passwordEnv];
182
+ if (value) {
183
+ return value;
184
+ }
185
+ }
186
+ if (profile.encryptedPassword) {
187
+ return decryptSecret(profile.encryptedPassword);
188
+ }
189
+ return null;
190
+ }
191
+
192
+ export {
193
+ historyFilePath,
194
+ loadConfig,
195
+ listProfiles,
196
+ getProfile,
197
+ saveProfile,
198
+ deleteProfile,
199
+ resolvePassword
200
+ };