@13w/miri 1.1.16 → 1.1.18

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/dist/cli.js CHANGED
@@ -1,18 +1,55 @@
1
- import { readFileSync, realpathSync } from 'node:fs';
2
- import { readFile } from 'node:fs/promises';
3
- Error.stackTraceLimit = Infinity;
1
+ import { homedir } from 'node:os';
2
+ import { lstat, readFile, realpath } from 'node:fs/promises';
3
+ import { createConnection } from 'node:net';
4
4
  import { join } from 'node:path';
5
- /* eslint-disable no-console */
5
+ Error.stackTraceLimit = Infinity;
6
+ import colors from 'colors';
6
7
  import { Command } from 'commander';
7
8
  import { Table } from 'console-table-printer';
8
- import colors from 'colors';
9
- import { createTunnel } from 'tunnel-ssh';
10
9
  import SSHConfig from 'ssh-config';
11
- import connection from './mongodb.js';
10
+ import { createTunnel } from 'tunnel-ssh';
12
11
  import Miri, { IndexStatus, PatchStatus } from './miri.js';
12
+ import connection from './mongodb.js';
13
13
  const pkg = await readFile(join(import.meta.dirname, '../package.json'), 'utf-8').then((content) => JSON.parse(content));
14
14
  const mirirc = await readFile(join(process.cwd(), '.mirirc'), 'utf-8').then((content) => JSON.parse(content), () => ({}));
15
- const { SSH_AUTH_SOCK, HOME } = process.env;
15
+ const { SSH_AUTH_SOCK } = process.env;
16
+ const fileExists = (filename) => lstat(filename).then(() => true, () => false);
17
+ const isKeyEncrypted = (key) => key.toString('utf-8')
18
+ .split('\n')
19
+ .slice(1, -1)
20
+ .join('')
21
+ .includes('bcrypt');
22
+ const isKeyPublic = (key) => /^(ssh-(rsa|ed25519)|ecdsa-sha2-nistp\d+)\s+[A-Za-z0-9+/=]+/.test(key.toString().trim());
23
+ const listSshAgentKeys = async () => {
24
+ const keys = [];
25
+ if (!SSH_AUTH_SOCK || !(await lstat(SSH_AUTH_SOCK)).isSocket()) {
26
+ return keys;
27
+ }
28
+ const conn = createConnection(SSH_AUTH_SOCK, () => {
29
+ const request = Buffer.from([0, 0, 0, 1, 0x0B]); // SSH_AGENTC_REQUEST_IDENTITIES
30
+ conn.end(request);
31
+ });
32
+ console.log(colors.dim(' reading keys from SSH-Agent...'));
33
+ for await (const chunk of conn) {
34
+ if (chunk.length < 5 || chunk.readUInt8(4) !== 12) {
35
+ continue;
36
+ }
37
+ const numKeys = chunk.readUInt32BE(5);
38
+ let offset = 9;
39
+ for (let i = 0; i < numKeys; i += 1) {
40
+ const keyLen = chunk.readUInt32BE(offset);
41
+ offset += 4;
42
+ const key = chunk.slice(offset, offset + keyLen).toString("base64");
43
+ offset += keyLen;
44
+ offset += chunk.readUInt32BE(offset) + 4;
45
+ keys.push(key);
46
+ }
47
+ }
48
+ if (keys.length) {
49
+ console.log(colors.dim(` ${keys.length} loaded...`));
50
+ }
51
+ return keys;
52
+ };
16
53
  const askPassword = async (message = 'Password: ') => {
17
54
  process.stdin.resume();
18
55
  process.stdin.setEncoding('utf8');
@@ -41,8 +78,9 @@ const askPassword = async (message = 'Password: ') => {
41
78
  // Ctrl C
42
79
  process.stdin.off('data', readPass);
43
80
  reject(new Error('Cancelled'));
81
+ return;
44
82
  default:
45
- // More passsword characters
83
+ // More password characters
46
84
  password += str;
47
85
  break;
48
86
  }
@@ -54,7 +92,7 @@ const askPassword = async (message = 'Password: ') => {
54
92
  const program = new Command();
55
93
  program.version(pkg.version);
56
94
  program.option('-e --env <environment>', 'Environment name from .mirirc', 'default');
57
- program.option('-m --migrations <folder>', `Folder with migrations (default: "${process.cwd(), join(process.cwd(), 'migrations')}")`);
95
+ program.option('-m --migrations <folder>', `Folder with migrations (default: "${join(process.cwd(), 'migrations')}")`);
58
96
  program.option('-d --db <mongo-uri>', 'MongoDB Connection URI (default: "mongodb://localhost:27017/test")');
59
97
  program.option('--no-direct-connection', 'Disable direct connection', true);
60
98
  program.option('--ssh-profile <profile>', 'Connect via SSH using profile');
@@ -62,9 +100,8 @@ program.option('--ssh-host <host>', 'Connect via SSH proxy Host');
62
100
  program.option('--ssh-port <port>', 'Connect via SSH proxy Port');
63
101
  program.option('--ssh-user <user>', 'Connect via SSH proxy User');
64
102
  program.option('--ssh-key <path/to/key>', 'Connect via SSH proxy IdentityKey');
65
- program.option('--ssh-ask-pass', 'Ask for the private key password');
66
103
  let configCache;
67
- const getConfig = (programOpts) => {
104
+ const getConfig = async (programOpts) => {
68
105
  if (configCache) {
69
106
  return configCache;
70
107
  }
@@ -72,48 +109,77 @@ const getConfig = (programOpts) => {
72
109
  const env = envs[programOpts.env ?? 'default'] ?? {};
73
110
  const config = Object.assign({}, mirirc, env, programOpts);
74
111
  if (config.sshProfile) {
75
- const sshConfigPath = join(HOME ?? '', '.ssh/config');
76
- const sshConfigContent = readFileSync(sshConfigPath, 'utf-8');
112
+ const sshConfigPath = join(homedir() ?? '', '.ssh/config');
113
+ const sshConfigContent = await readFile(sshConfigPath, 'utf-8');
77
114
  console.log(`Reading profile ${config.sshProfile} from SSH config ${sshConfigPath}`);
78
115
  const parsed = SSHConfig.parse(sshConfigContent).compute(config.sshProfile);
79
116
  config.sshHost = programOpts.sshHost ?? parsed.Hostname;
80
117
  config.sshPort = programOpts.sshPort ?? parsed.Port;
81
118
  config.sshKey = programOpts.sshKey ?? parsed.IdentityFile?.[0];
82
119
  config.sshUser = programOpts.sshUser ?? parsed.User;
120
+ config.forceSshAgent = Boolean(programOpts.sshAgent);
121
+ }
122
+ if (config.sshKey) {
123
+ if (config.sshKey.startsWith('~/')) {
124
+ config.sshKey = config.sshKey.replace(/^~/, homedir());
125
+ }
126
+ config.sshKey = await realpath(config.sshKey);
127
+ if (config.sshKey.endsWith('.pub')) {
128
+ if (await fileExists(config.sshKey)) {
129
+ config.sshPublicKey = config.sshKey;
130
+ }
131
+ const sshKey = config.sshKey.slice(0, -4);
132
+ delete config.sshKey;
133
+ if (await fileExists(sshKey)) {
134
+ config.sshKey = sshKey;
135
+ }
136
+ }
137
+ else if (await fileExists(`${config.sshKey}.pub`)) {
138
+ config.sshPublicKey = `${config.sshKey}.pub`;
139
+ }
83
140
  }
84
141
  return configCache = config;
85
142
  };
86
143
  const createSSHTunnel = async (opts) => {
87
- const config = getConfig(opts);
88
- const passphrase = config.sshKey && config.sshAskPass ? await askPassword() : void 0;
144
+ const config = await getConfig(opts);
89
145
  const sshOptions = {
90
146
  host: config.sshHost,
91
147
  port: Number(config.sshPort ?? 22),
92
148
  username: config.sshUser,
93
149
  };
94
- if (config.sshKey) {
95
- sshOptions.privateKey = readFileSync(realpathSync(config.sshKey));
96
- if (passphrase) {
97
- sshOptions.passphrase = passphrase;
150
+ sshOptions.agent = SSH_AUTH_SOCK;
151
+ if (config.sshPublicKey) {
152
+ const agentKeys = await listSshAgentKeys();
153
+ const publicKeyBody = await readFile(config.sshPublicKey, 'utf-8').catch(() => '');
154
+ if (agentKeys.includes(publicKeyBody.split(' ')[1])) {
155
+ console.log(colors.dim(' Using SSH-Agent'));
156
+ // key already loaded
157
+ }
158
+ else if (config.sshKey) {
159
+ const privateKey = await readFile(config.sshKey).catch(() => void 0);
160
+ if (privateKey && !isKeyPublic(privateKey)) {
161
+ sshOptions.privateKey = privateKey;
162
+ console.log(colors.dim(` Using private key: ${config.sshKey}`));
163
+ if (isKeyEncrypted(privateKey)) {
164
+ sshOptions.passphrase = await askPassword();
165
+ }
166
+ }
98
167
  }
99
168
  }
100
- else if (SSH_AUTH_SOCK) {
101
- sshOptions.agent = SSH_AUTH_SOCK;
102
- }
103
- console.dir([config, sshOptions]);
169
+ // console.dir([config, sshOptions])
104
170
  const dst = new URL(config.db);
105
171
  const forwardOptions = {
106
172
  dstPort: Number(dst.port ?? 27017),
107
173
  dstAddr: dst.hostname,
108
174
  };
109
- const [server] = await createTunnel({ autoClose: true }, {}, sshOptions, forwardOptions);
175
+ const [server] = await createTunnel({ autoClose: true, reconnectOnError: true }, {}, sshOptions, forwardOptions);
110
176
  const addressInfo = server.address();
111
177
  dst.host = addressInfo.family === 'IPv6' ? `[${String(addressInfo.address)}]` : String(addressInfo.address);
112
178
  dst.port = String(addressInfo.port);
113
179
  return dst.toString();
114
180
  };
115
181
  const getMiri = async () => {
116
- const config = getConfig(program.opts());
182
+ const config = await getConfig(program.opts());
117
183
  const db = config.sshHost ? await createSSHTunnel(config) : config.db;
118
184
  const dbUri = new URL(db);
119
185
  dbUri.searchParams.append('directConnection', String(config.directConnection ?? true));
@@ -157,7 +223,7 @@ const status = async (remote = false, group, all = false) => {
157
223
  };
158
224
  const syncIndexes = async (miri, coll) => {
159
225
  let group = '';
160
- console.group('Starting synchronisation...');
226
+ console.group('Starting synchronization...');
161
227
  for await (const { collection, status, name, error } of miri.indexesSync(coll)) {
162
228
  if (group !== collection) {
163
229
  if (group) {
@@ -231,11 +297,13 @@ indexesProgram.command('status')
231
297
  }
232
298
  const color = {
233
299
  [IndexStatus.New]: colors.green,
300
+ [IndexStatus.Updated]: colors.green,
234
301
  [IndexStatus.Applied]: colors.white,
235
302
  [IndexStatus.Removed]: colors.red,
236
303
  }[detail.status];
237
304
  const point = {
238
305
  [IndexStatus.New]: colors.green.bold('+ '),
306
+ [IndexStatus.Updated]: colors.green.bold('~ '),
239
307
  [IndexStatus.Applied]: colors.cyan('\u00b7 '),
240
308
  [IndexStatus.Removed]: colors.red.bold('- '),
241
309
  }[detail.status];
@@ -289,4 +357,9 @@ patchProgram.command('remove')
289
357
  await miri[Symbol.asyncDispose]();
290
358
  });
291
359
  program.parse();
360
+ for (const eventName of ['unhandledRejection', 'rejectionHandled', 'uncaughtException']) {
361
+ process.on(eventName, (error) => {
362
+ console.error(colors.red(`${eventName}:\n ${error.name}: ${error.message}`));
363
+ });
364
+ }
292
365
  //# sourceMappingURL=cli.js.map
package/dist/evaluator.js CHANGED
@@ -1,7 +1,6 @@
1
- /* eslint-disable no-console */
2
1
  import { EventEmitter } from 'node:events';
3
- import { createContext, runInContext, SourceTextModule, SyntheticModule } from 'node:vm';
4
2
  import { inspect } from 'node:util';
3
+ import { createContext, runInContext, SourceTextModule, SyntheticModule } from 'node:vm';
5
4
  import { CliServiceProvider } from '@mongosh/service-provider-server';
6
5
  import { ShellInstanceState } from '@mongosh/shell-api';
7
6
  import { ShellEvaluator } from '@mongosh/shell-evaluator';
@@ -46,7 +45,7 @@ export async function evaluateMongo(client, code, filename = '[no file]') {
46
45
  onLoad() {
47
46
  throw new Error('Load isn\'t supported');
48
47
  },
49
- async onExit() {
48
+ onExit() {
50
49
  throw new Error('Exit isn\'t supported');
51
50
  },
52
51
  });
package/dist/miri.js CHANGED
@@ -1,10 +1,9 @@
1
- /* eslint-disable no-console */
2
1
  import { createHash } from 'node:crypto';
3
2
  import { readdir, readFile, realpath } from 'node:fs/promises';
4
- import { resolve, extname, basename } from 'node:path';
3
+ import { basename, extname, resolve } from 'node:path';
4
+ import colors from 'colors';
5
5
  import { ObjectId } from 'mongodb';
6
6
  import { evaluateJs, evaluateMongo } from './evaluator.js';
7
- import colors from 'colors';
8
7
  export var PatchStatus;
9
8
  (function (PatchStatus) {
10
9
  PatchStatus[PatchStatus["Ok"] = 0] = "Ok";
@@ -18,7 +17,8 @@ export var IndexStatus;
18
17
  (function (IndexStatus) {
19
18
  IndexStatus[IndexStatus["New"] = 0] = "New";
20
19
  IndexStatus[IndexStatus["Applied"] = 1] = "Applied";
21
- IndexStatus[IndexStatus["Removed"] = 2] = "Removed";
20
+ IndexStatus[IndexStatus["Updated"] = 2] = "Updated";
21
+ IndexStatus[IndexStatus["Removed"] = 3] = "Removed";
22
22
  })(IndexStatus || (IndexStatus = {}));
23
23
  const sortPatches = (a, b) => {
24
24
  if (a.group === b.group) {
package/dist/mongodb.js CHANGED
@@ -1,4 +1,3 @@
1
- /* eslint-disable no-console */
2
1
  import { MongoClient } from 'mongodb';
3
2
  let client;
4
3
  export default async function connect(uri = 'mongodb://localhost:27017/test') {
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@13w/miri",
3
3
  "description": "MongoDB patch manager",
4
- "version": "1.1.16",
4
+ "version": "1.1.18",
5
5
  "type": "module",
6
6
  "engines": {
7
- "node": "v20"
7
+ "node": "v22"
8
8
  },
9
9
  "keywords": [
10
10
  "MongoDB",
@@ -25,29 +25,29 @@
25
25
  "main": "bin/miri",
26
26
  "types": "types/miri.d.ts",
27
27
  "dependencies": {
28
- "@mongosh/service-provider-server": "^2.2.15",
29
- "@mongosh/shell-api": "^2.2.15",
30
- "@mongosh/shell-evaluator": "^2.2.15",
28
+ "@mongosh/service-provider-server": "^2.3.2",
29
+ "@mongosh/shell-api": "^3.8.0",
30
+ "@mongosh/shell-evaluator": "^3.8.0",
31
31
  "colors": "^1.4.0",
32
- "commander": "^12.1.0",
32
+ "commander": "^13.1.0",
33
33
  "console-table-printer": "^2.12.1",
34
- "mongodb": "^6.8.0",
35
- "ssh-config": "^4.4.4",
36
- "tunnel-ssh": "^5.1.2"
34
+ "mongodb": "^6.14.2",
35
+ "ssh-config": "^5.0.3",
36
+ "tunnel-ssh": "^5.2.0"
37
37
  },
38
38
  "devDependencies": {
39
- "@types/node": "^20.14.15",
40
- "@typescript-eslint/eslint-plugin": "^7.18.0",
41
- "@typescript-eslint/parser": "^7.18.0",
42
- "eslint": "^8.57.0",
43
- "eslint-plugin-deprecation": "^2.0.0",
39
+ "@eslint/js": "^9.21.0",
40
+ "@types/node": "^22.13.9",
41
+ "eslint": "^9.21.0",
42
+ "mongodb-log-writer": "^2.4.0",
44
43
  "ts-node": "^10.9.2",
45
- "typescript": "^5.5.4"
44
+ "typescript": "^5.8.2",
45
+ "typescript-eslint": "^8.26.0"
46
46
  },
47
47
  "license": "MIT",
48
48
  "scripts": {
49
49
  "build": "tsc -p tsconfig.json",
50
- "lint": "eslint src/ --ext .ts --quiet",
50
+ "lint": "eslint --quiet src/",
51
51
  "prepublish": "pnpm run build"
52
52
  }
53
53
  }