@13w/miri 1.1.18 → 1.1.20
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 +36 -36
- package/dist/evaluator.js +18 -11
- package/dist/miri.js +22 -15
- package/package.json +12 -12
package/dist/cli.js
CHANGED
|
@@ -14,11 +14,7 @@ const pkg = await readFile(join(import.meta.dirname, '../package.json'), 'utf-8'
|
|
|
14
14
|
const mirirc = await readFile(join(process.cwd(), '.mirirc'), 'utf-8').then((content) => JSON.parse(content), () => ({}));
|
|
15
15
|
const { SSH_AUTH_SOCK } = process.env;
|
|
16
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');
|
|
17
|
+
const isKeyEncrypted = (key) => key.toString('utf-8').split('\n').slice(1, -1).join('').includes('bcrypt');
|
|
22
18
|
const isKeyPublic = (key) => /^(ssh-(rsa|ed25519)|ecdsa-sha2-nistp\d+)\s+[A-Za-z0-9+/=]+/.test(key.toString().trim());
|
|
23
19
|
const listSshAgentKeys = async () => {
|
|
24
20
|
const keys = [];
|
|
@@ -26,10 +22,10 @@ const listSshAgentKeys = async () => {
|
|
|
26
22
|
return keys;
|
|
27
23
|
}
|
|
28
24
|
const conn = createConnection(SSH_AUTH_SOCK, () => {
|
|
29
|
-
const request = Buffer.from([0, 0, 0, 1,
|
|
25
|
+
const request = Buffer.from([0, 0, 0, 1, 0x0b]); // SSH_AGENTC_REQUEST_IDENTITIES
|
|
30
26
|
conn.end(request);
|
|
31
27
|
});
|
|
32
|
-
console.log(colors.
|
|
28
|
+
console.log(colors.gray(' reading keys from SSH-Agent...'));
|
|
33
29
|
for await (const chunk of conn) {
|
|
34
30
|
if (chunk.length < 5 || chunk.readUInt8(4) !== 12) {
|
|
35
31
|
continue;
|
|
@@ -39,14 +35,14 @@ const listSshAgentKeys = async () => {
|
|
|
39
35
|
for (let i = 0; i < numKeys; i += 1) {
|
|
40
36
|
const keyLen = chunk.readUInt32BE(offset);
|
|
41
37
|
offset += 4;
|
|
42
|
-
const key = chunk.slice(offset, offset + keyLen).toString(
|
|
38
|
+
const key = chunk.slice(offset, offset + keyLen).toString('base64');
|
|
43
39
|
offset += keyLen;
|
|
44
40
|
offset += chunk.readUInt32BE(offset) + 4;
|
|
45
41
|
keys.push(key);
|
|
46
42
|
}
|
|
47
43
|
}
|
|
48
44
|
if (keys.length) {
|
|
49
|
-
console.log(colors.
|
|
45
|
+
console.log(colors.gray(` ${keys.length} loaded...`));
|
|
50
46
|
}
|
|
51
47
|
return keys;
|
|
52
48
|
};
|
|
@@ -56,7 +52,7 @@ const askPassword = async (message = 'Password: ') => {
|
|
|
56
52
|
process.stdin.setRawMode(true);
|
|
57
53
|
return new Promise((resolve, reject) => {
|
|
58
54
|
let password = '';
|
|
59
|
-
const readPass =
|
|
55
|
+
const readPass = (chunk) => {
|
|
60
56
|
const str = chunk.toString();
|
|
61
57
|
// backspace
|
|
62
58
|
if (str.charCodeAt(0) === 127) {
|
|
@@ -138,7 +134,7 @@ const getConfig = async (programOpts) => {
|
|
|
138
134
|
config.sshPublicKey = `${config.sshKey}.pub`;
|
|
139
135
|
}
|
|
140
136
|
}
|
|
141
|
-
return configCache = config;
|
|
137
|
+
return (configCache = config);
|
|
142
138
|
};
|
|
143
139
|
const createSSHTunnel = async (opts) => {
|
|
144
140
|
const config = await getConfig(opts);
|
|
@@ -152,14 +148,14 @@ const createSSHTunnel = async (opts) => {
|
|
|
152
148
|
const agentKeys = await listSshAgentKeys();
|
|
153
149
|
const publicKeyBody = await readFile(config.sshPublicKey, 'utf-8').catch(() => '');
|
|
154
150
|
if (agentKeys.includes(publicKeyBody.split(' ')[1])) {
|
|
155
|
-
console.log(colors.
|
|
151
|
+
console.log(colors.gray(' Using SSH-Agent'));
|
|
156
152
|
// key already loaded
|
|
157
153
|
}
|
|
158
154
|
else if (config.sshKey) {
|
|
159
155
|
const privateKey = await readFile(config.sshKey).catch(() => void 0);
|
|
160
156
|
if (privateKey && !isKeyPublic(privateKey)) {
|
|
161
157
|
sshOptions.privateKey = privateKey;
|
|
162
|
-
console.log(colors.
|
|
158
|
+
console.log(colors.gray(` Using private key: ${config.sshKey}`));
|
|
163
159
|
if (isKeyEncrypted(privateKey)) {
|
|
164
160
|
sshOptions.passphrase = await askPassword();
|
|
165
161
|
}
|
|
@@ -244,11 +240,13 @@ const syncIndexes = async (miri, coll) => {
|
|
|
244
240
|
}
|
|
245
241
|
console.groupEnd();
|
|
246
242
|
};
|
|
247
|
-
program
|
|
243
|
+
program
|
|
244
|
+
.command('status')
|
|
248
245
|
.option('--all', 'show all applied patches')
|
|
249
246
|
.description('Displays list of applied migrations')
|
|
250
247
|
.action(({ all }) => status(true, void 0, all));
|
|
251
|
-
program
|
|
248
|
+
program
|
|
249
|
+
.command('sync')
|
|
252
250
|
.description('Applies all available patches from migrations folder')
|
|
253
251
|
.option('--degraded', 'Re-apply patches on degraded migrations')
|
|
254
252
|
.option('--all', 'Re-apply all patches')
|
|
@@ -259,30 +257,30 @@ program.command('sync')
|
|
|
259
257
|
await miri.sync(opts);
|
|
260
258
|
await miri[Symbol.asyncDispose]();
|
|
261
259
|
});
|
|
262
|
-
const initProgram = program.command('init')
|
|
263
|
-
|
|
264
|
-
|
|
260
|
+
const initProgram = program.command('init').description('Manage initial scripts');
|
|
261
|
+
initProgram
|
|
262
|
+
.command('apply')
|
|
265
263
|
.argument('[patch]', 'patch name')
|
|
266
|
-
.option('--no-exec',
|
|
264
|
+
.option('--no-exec', "Don' execute patch, just set as done")
|
|
267
265
|
.option('--force', 'Force apply patch')
|
|
268
266
|
.action(async (patch, opts) => {
|
|
269
267
|
const miri = await getMiri();
|
|
270
268
|
await miri.init(opts, patch);
|
|
271
269
|
await miri[Symbol.asyncDispose]();
|
|
272
270
|
});
|
|
273
|
-
initProgram
|
|
271
|
+
initProgram
|
|
272
|
+
.command('remove')
|
|
274
273
|
.argument('<patch>', 'patch name')
|
|
275
|
-
.option('--no-exec',
|
|
274
|
+
.option('--no-exec', "Don' execute patch, just set as done")
|
|
276
275
|
.action(async (patch, opts) => {
|
|
277
276
|
const miri = await getMiri();
|
|
278
277
|
await miri.remove('init', patch, Boolean(opts?.exec));
|
|
279
278
|
await miri[Symbol.asyncDispose]();
|
|
280
279
|
});
|
|
281
|
-
initProgram.command('status')
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
.
|
|
285
|
-
indexesProgram.command('status')
|
|
280
|
+
initProgram.command('status').action(() => status(false, 'init'));
|
|
281
|
+
const indexesProgram = program.command('indexes').description('Manage indexes');
|
|
282
|
+
indexesProgram
|
|
283
|
+
.command('status')
|
|
286
284
|
.argument('[collection]', 'MongoDB Collection name')
|
|
287
285
|
.option('-q --quiet', 'Show only changes', false)
|
|
288
286
|
.action(async (collection, { quiet }) => {
|
|
@@ -317,18 +315,18 @@ indexesProgram.command('status')
|
|
|
317
315
|
}
|
|
318
316
|
});
|
|
319
317
|
const indexSyncProgram = indexesProgram.command('sync');
|
|
320
|
-
indexSyncProgram.argument('[collection]', 'MongoDB Collection name', '')
|
|
321
|
-
.action(async (collection) => {
|
|
318
|
+
indexSyncProgram.argument('[collection]', 'MongoDB Collection name', '').action(async (collection) => {
|
|
322
319
|
const miri = await getMiri();
|
|
323
320
|
await syncIndexes(miri, collection);
|
|
324
321
|
await miri[Symbol.asyncDispose]();
|
|
325
322
|
});
|
|
326
|
-
const patchProgram = program.command('patch')
|
|
327
|
-
|
|
328
|
-
|
|
323
|
+
const patchProgram = program.command('patch').description('Applies patch to database');
|
|
324
|
+
patchProgram
|
|
325
|
+
.command('diff')
|
|
329
326
|
.description('Displays difference between local and applied migrations')
|
|
330
327
|
.action(() => status());
|
|
331
|
-
patchProgram
|
|
328
|
+
patchProgram
|
|
329
|
+
.command('sync')
|
|
332
330
|
.description('Applies migrations')
|
|
333
331
|
.option('--remote', 'Remote only')
|
|
334
332
|
.option('--degraded', 'Re-apply patches on degraded migrations')
|
|
@@ -338,19 +336,21 @@ patchProgram.command('sync')
|
|
|
338
336
|
await miri.sync(opts);
|
|
339
337
|
await miri[Symbol.asyncDispose]();
|
|
340
338
|
});
|
|
341
|
-
patchProgram
|
|
339
|
+
patchProgram
|
|
340
|
+
.command('apply')
|
|
342
341
|
.argument('<group>', 'group name')
|
|
343
342
|
.argument('<patch>', 'patch name')
|
|
344
|
-
.option('--no-exec',
|
|
343
|
+
.option('--no-exec', "Don' execute patch, just set as done")
|
|
345
344
|
.action(async (group, patch, opts) => {
|
|
346
345
|
const miri = await getMiri();
|
|
347
346
|
await miri.applySingle(group, patch, Boolean(opts.exec));
|
|
348
347
|
await miri[Symbol.asyncDispose]();
|
|
349
348
|
});
|
|
350
|
-
patchProgram
|
|
349
|
+
patchProgram
|
|
350
|
+
.command('remove')
|
|
351
351
|
.argument('<group>', 'group name')
|
|
352
352
|
.argument('<patch>', 'patch name')
|
|
353
|
-
.option('--no-exec',
|
|
353
|
+
.option('--no-exec', "Don' execute patch, just set as done")
|
|
354
354
|
.action(async (group, patch, opts) => {
|
|
355
355
|
const miri = await getMiri();
|
|
356
356
|
await miri.remove(group, patch, Boolean(opts.exec));
|
package/dist/evaluator.js
CHANGED
|
@@ -10,9 +10,7 @@ const print = (values, type) => {
|
|
|
10
10
|
if (type === 'print') {
|
|
11
11
|
return String(object);
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
return inspect(object, { colors: false, customInspect: true, depth: 22 });
|
|
15
|
-
}
|
|
13
|
+
return inspect(object, { colors: false, customInspect: true, depth: 22 });
|
|
16
14
|
};
|
|
17
15
|
for (const value of values) {
|
|
18
16
|
const printable = value.printable ?? value.rawValue;
|
|
@@ -33,24 +31,34 @@ export async function evaluateMongo(client, code, filename = '[no file]') {
|
|
|
33
31
|
productName: 'MIRI: Migration manager',
|
|
34
32
|
productDocsLink: 'https://example.com/',
|
|
35
33
|
});
|
|
36
|
-
|
|
34
|
+
// Add compatibility layer for missing clientBulkWrite method
|
|
35
|
+
// Properly extend the service provider to preserve all prototype methods
|
|
36
|
+
const serviceProviderWithBulkWrite = Object.create(Object.getPrototypeOf(cliServiceProvider));
|
|
37
|
+
Object.assign(serviceProviderWithBulkWrite, cliServiceProvider);
|
|
38
|
+
serviceProviderWithBulkWrite.clientBulkWrite = (operations, options) => {
|
|
39
|
+
// Fallback implementation - delegate to client if available
|
|
40
|
+
if ('bulkWrite' in client && typeof client.bulkWrite === 'function') {
|
|
41
|
+
return client.bulkWrite(operations, options);
|
|
42
|
+
}
|
|
43
|
+
throw new Error('clientBulkWrite not supported in this MongoDB version');
|
|
44
|
+
};
|
|
45
|
+
const instanceState = new ShellInstanceState(serviceProviderWithBulkWrite);
|
|
37
46
|
instanceState.setCtx(context);
|
|
38
47
|
instanceState.setEvaluationListener({
|
|
39
48
|
onPrint(values, type) {
|
|
40
49
|
return print(values, type);
|
|
41
50
|
},
|
|
42
51
|
onPrompt() {
|
|
43
|
-
throw new Error(
|
|
52
|
+
throw new Error("Prompt isn't supported");
|
|
44
53
|
},
|
|
45
54
|
onLoad() {
|
|
46
|
-
throw new Error(
|
|
55
|
+
throw new Error("Load isn't supported");
|
|
47
56
|
},
|
|
48
57
|
onExit() {
|
|
49
|
-
throw new Error(
|
|
58
|
+
throw new Error("Exit isn't supported");
|
|
50
59
|
},
|
|
51
60
|
});
|
|
52
|
-
const output = await new ShellEvaluator(instanceState)
|
|
53
|
-
.customEval(runInContext, `${code};`, context, filename);
|
|
61
|
+
const output = await new ShellEvaluator(instanceState).customEval(runInContext, `${code};`, context, filename);
|
|
54
62
|
return output.rawValue;
|
|
55
63
|
}
|
|
56
64
|
export async function evaluateJs(code, identifier = '[no file]') {
|
|
@@ -63,8 +71,7 @@ export async function evaluateJs(code, identifier = '[no file]') {
|
|
|
63
71
|
}, { context: referencingModule.context });
|
|
64
72
|
});
|
|
65
73
|
});
|
|
66
|
-
await module.evaluate()
|
|
67
|
-
.catch((error) => {
|
|
74
|
+
await module.evaluate().catch((error) => {
|
|
68
75
|
error.message += ` # at file ${identifier}`;
|
|
69
76
|
throw error;
|
|
70
77
|
});
|
package/dist/miri.js
CHANGED
|
@@ -53,8 +53,8 @@ export default class Migrator {
|
|
|
53
53
|
if (localPatch.content.up?.hash !== remotePatch.content.up?.hash) {
|
|
54
54
|
localPatch.status = PatchStatus.Changed;
|
|
55
55
|
}
|
|
56
|
-
else if (localPatch.content.test?.hash !== remotePatch.content.test?.hash
|
|
57
|
-
|
|
56
|
+
else if (localPatch.content.test?.hash !== remotePatch.content.test?.hash ||
|
|
57
|
+
localPatch.content.down?.hash !== remotePatch.content.down?.hash) {
|
|
58
58
|
localPatch.status = PatchStatus.Updated;
|
|
59
59
|
}
|
|
60
60
|
if (localPatch.status !== PatchStatus.Ok) {
|
|
@@ -81,10 +81,9 @@ export default class Migrator {
|
|
|
81
81
|
return -1;
|
|
82
82
|
}
|
|
83
83
|
const code = Buffer.from(content.body, 'base64').toString();
|
|
84
|
-
return evaluateMongo(this.client, wrap ? `(async ${code})();` : code, filename)
|
|
85
|
-
.catch((error) => {
|
|
84
|
+
return (evaluateMongo(this.client, wrap ? `(async ${code})();` : code, filename).catch((error) => {
|
|
86
85
|
throw error;
|
|
87
|
-
}) ?? 0;
|
|
86
|
+
}) ?? 0);
|
|
88
87
|
}
|
|
89
88
|
async sync({ remote = false, all = false, degraded = false } = {}) {
|
|
90
89
|
const patches = remote ? await this.getRemotePatches() : await this.diff();
|
|
@@ -147,9 +146,12 @@ export default class Migrator {
|
|
|
147
146
|
if (!['.js', '.json'].includes(extension)) {
|
|
148
147
|
continue;
|
|
149
148
|
}
|
|
149
|
+
// Construct full file path manually for newer Node.js types compatibility
|
|
150
|
+
const filePath = file.parentPath || resolve(migrationsDir, file.name);
|
|
151
|
+
const fullFilePath = resolve(filePath, file.name);
|
|
150
152
|
const patchObject = {
|
|
151
153
|
_id: new ObjectId(),
|
|
152
|
-
group:
|
|
154
|
+
group: filePath.substring(migrationsDir.length + 1),
|
|
153
155
|
name: file.name,
|
|
154
156
|
title: basename(file.name, extension),
|
|
155
157
|
content: {},
|
|
@@ -161,13 +163,12 @@ export default class Migrator {
|
|
|
161
163
|
continue;
|
|
162
164
|
}
|
|
163
165
|
patches.push(patchObject);
|
|
164
|
-
const patchContent = await readFile(
|
|
166
|
+
const patchContent = await readFile(fullFilePath);
|
|
165
167
|
if (raw) {
|
|
166
168
|
patchObject.raw = patchContent.toString('base64');
|
|
167
169
|
continue;
|
|
168
170
|
}
|
|
169
|
-
const patchExports = await evaluateJs(patchContent.toString())
|
|
170
|
-
.catch(() => ({}));
|
|
171
|
+
const patchExports = await evaluateJs(patchContent.toString()).catch(() => ({}));
|
|
171
172
|
for (const key of ['test', 'up', 'down']) {
|
|
172
173
|
const func = patchExports[key];
|
|
173
174
|
if (typeof func !== 'function') {
|
|
@@ -192,7 +193,8 @@ export default class Migrator {
|
|
|
192
193
|
if (name) {
|
|
193
194
|
filter.name = name;
|
|
194
195
|
}
|
|
195
|
-
const patches = await this.#collection
|
|
196
|
+
const patches = await this.#collection
|
|
197
|
+
.find(filter, {
|
|
196
198
|
projection: {
|
|
197
199
|
_id: 1,
|
|
198
200
|
group: 1,
|
|
@@ -212,7 +214,8 @@ export default class Migrator {
|
|
|
212
214
|
},
|
|
213
215
|
},
|
|
214
216
|
},
|
|
215
|
-
})
|
|
217
|
+
})
|
|
218
|
+
.toArray();
|
|
216
219
|
patches.sort(sortPatches);
|
|
217
220
|
return patches;
|
|
218
221
|
}
|
|
@@ -225,7 +228,9 @@ export default class Migrator {
|
|
|
225
228
|
structure[patch.title] = {};
|
|
226
229
|
const collection = structure[patch.title];
|
|
227
230
|
// console.log(`Reading indexes from ${patch.title}...`)
|
|
228
|
-
const remoteIndexes = await db
|
|
231
|
+
const remoteIndexes = await db
|
|
232
|
+
.collection(patch.title)
|
|
233
|
+
.indexes({ full: true })
|
|
229
234
|
.catch(() => []);
|
|
230
235
|
// console.dir([patch.title, remoteIndexes], { colors: true, customInspect: true, depth: 22 })
|
|
231
236
|
for (const index of indexes) {
|
|
@@ -233,7 +238,7 @@ export default class Migrator {
|
|
|
233
238
|
if (typeof index === 'string') {
|
|
234
239
|
continue;
|
|
235
240
|
}
|
|
236
|
-
const [key, options] =
|
|
241
|
+
const [key, options] = Array.isArray(index) ? index : [index, {}];
|
|
237
242
|
const name = Object.entries(key).reduce((result, [key, value]) => `${result ? `${result}_` : ''}${key}_${value}`, '');
|
|
238
243
|
const remoteIndex = remoteIndexes.find(({ name: indexName }) => indexName === name);
|
|
239
244
|
collection[name] = { key, options, status: remoteIndex ? IndexStatus.Applied : IndexStatus.New };
|
|
@@ -259,14 +264,16 @@ export default class Migrator {
|
|
|
259
264
|
const coll = db.collection(collection);
|
|
260
265
|
for (const [name, { key, status, options }] of Object.entries(indexes)) {
|
|
261
266
|
if (status === IndexStatus.Removed) {
|
|
262
|
-
const error = await coll
|
|
267
|
+
const error = await coll
|
|
268
|
+
.dropIndex(name)
|
|
263
269
|
.then(() => { })
|
|
264
270
|
.catch((error) => error);
|
|
265
271
|
yield { collection, name, error, status };
|
|
266
272
|
continue;
|
|
267
273
|
}
|
|
268
274
|
if (status === IndexStatus.New) {
|
|
269
|
-
const error = await coll
|
|
275
|
+
const error = await coll
|
|
276
|
+
.createIndex(key, options)
|
|
270
277
|
.then(() => { })
|
|
271
278
|
.catch((error) => error);
|
|
272
279
|
yield { collection, name, error, status };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@13w/miri",
|
|
3
3
|
"description": "MongoDB patch manager",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.20",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "v22"
|
|
@@ -26,23 +26,23 @@
|
|
|
26
26
|
"types": "types/miri.d.ts",
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@mongosh/service-provider-server": "^2.3.2",
|
|
29
|
-
"@mongosh/shell-api": "^3.
|
|
30
|
-
"@mongosh/shell-evaluator": "^3.
|
|
29
|
+
"@mongosh/shell-api": "^3.20.0",
|
|
30
|
+
"@mongosh/shell-evaluator": "^3.20.0",
|
|
31
31
|
"colors": "^1.4.0",
|
|
32
|
-
"commander": "^
|
|
33
|
-
"console-table-printer": "^2.
|
|
34
|
-
"mongodb": "^6.
|
|
32
|
+
"commander": "^14.0.0",
|
|
33
|
+
"console-table-printer": "^2.14.6",
|
|
34
|
+
"mongodb": "^6.19.0",
|
|
35
35
|
"ssh-config": "^5.0.3",
|
|
36
36
|
"tunnel-ssh": "^5.2.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@eslint/js": "^9.
|
|
40
|
-
"@types/node": "^
|
|
41
|
-
"eslint": "^9.
|
|
42
|
-
"mongodb-log-writer": "^2.4.
|
|
39
|
+
"@eslint/js": "^9.34.0",
|
|
40
|
+
"@types/node": "^24.3.0",
|
|
41
|
+
"eslint": "^9.34.0",
|
|
42
|
+
"mongodb-log-writer": "^2.4.1",
|
|
43
43
|
"ts-node": "^10.9.2",
|
|
44
|
-
"typescript": "^5.
|
|
45
|
-
"typescript-eslint": "^8.
|
|
44
|
+
"typescript": "^5.9.2",
|
|
45
|
+
"typescript-eslint": "^8.42.0"
|
|
46
46
|
},
|
|
47
47
|
"license": "MIT",
|
|
48
48
|
"scripts": {
|