+108
-52
app.ts
+108
-52
app.ts
···
10
10
import { AppBskyVideoDefs, AtpAgent, BlobRef, RichText } from '@atproto/api';
11
11
12
12
import {
13
-
getEmbeddedUrlAndRecord, getMergeEmbed, getReplyRefs, PAST_HANDLES
13
+
getEmbeddedUrlAndRecord, getMergeEmbed, getReplyRefs
14
14
} from './libs/bskyParams';
15
15
import { checkPastHandles, convertToBskyPostUrl, getBskyPostUrl } from './libs/urlHandler';
16
16
let fetch: any;
···
21
21
const oembetter = require('oembetter')();
22
22
oembetter.endpoints(oembetter.suggestedEndpoints);
23
23
24
-
dotenv.config();
24
+
import yargs from 'yargs';
25
+
import { hideBin } from 'yargs/helpers';
25
26
26
-
const agent = new AtpAgent({
27
-
service: 'https://bsky.social',
28
-
})
29
27
30
-
const SIMULATE = process.env.SIMULATE === "1";
31
-
const API_DELAY = 2500; // https://docs.bsky.app/docs/advanced-guides/rate-limits
32
28
const TWEETS_MAPPING_FILE_NAME = 'tweets_mapping.json'; // store the imported tweets & bsky id mapping
33
-
const DISABLE_IMPORT_REPLY = process.env.DISABLE_IMPORT_REPLY === "1";
34
29
const MAX_FILE_SIZE = 1 * 1000 * 1000; // 1MiB
35
-
30
+
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3';
36
31
37
-
let MIN_DATE: Date | undefined = undefined;
38
-
if (process.env.MIN_DATE != null && process.env.MIN_DATE.length > 0)
39
-
MIN_DATE = new Date(process.env.MIN_DATE as string);
32
+
dotenv.config();
40
33
41
-
let MAX_DATE: Date | undefined = undefined;
42
-
if (process.env.MAX_DATE != null && process.env.MAX_DATE.length > 0)
43
-
MAX_DATE = new Date(process.env.MAX_DATE as string);
34
+
const agent = new AtpAgent({
35
+
service: 'https://bsky.social',
36
+
})
44
37
45
38
let alreadySavedCache = false;
46
-
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3';
47
39
48
40
class RateLimitedAgent {
49
41
private agent: AtpAgent;
···
151
143
}
152
144
153
145
async function cleanTweetText(
154
-
tweetFullText: string,
155
-
urlMappings: Array<{
156
-
url: string;
157
-
expanded_url: string
158
-
}>,
159
-
embeddedUrl: string|null,
160
-
tweets
146
+
twitterHandles: string[],
147
+
blueskyUsername: string,
148
+
tweetFullText: string,
149
+
urlMappings: Array<{
150
+
url: string;
151
+
expanded_url: string
152
+
}>,
153
+
embeddedUrl: string|null,
154
+
tweets
161
155
): Promise<string> {
162
156
let newText = tweetFullText;
163
157
const urls: string[] = [];
···
180
174
return urls[index];
181
175
});
182
176
183
-
if ( checkPastHandles(newUrl)
177
+
if ( checkPastHandles(twitterHandles, newUrl)
184
178
&& newUrl.indexOf("/photo/") == -1
185
179
&& newUrl.indexOf("/video/") == -1
186
180
&& embeddedUrl != newUrl) {
187
181
// self quote exchange ( tweet-> bsky)
188
-
newUrls.push(convertToBskyPostUrl(newUrl, tweets))
182
+
newUrls.push(convertToBskyPostUrl(blueskyUsername, newUrl, tweets))
189
183
}else{
190
184
newUrls.push(newUrl)
191
185
}
···
197
191
newText = URI.withinString(tweetFullText, (url, start, end, source) => {
198
192
// I exclude links to photos, because they have already been inserted into the Bluesky post independently
199
193
// also exclude embeddedUrl (ex. your twitter quote post)
200
-
if ( (checkPastHandles(newUrls[j]) && (newUrls[j].indexOf("/photo/") > 0 || newUrls[j].indexOf("/video/") > 0) )
194
+
if ( (checkPastHandles(twitterHandles, newUrls[j]) && (newUrls[j].indexOf("/photo/") > 0 || newUrls[j].indexOf("/video/") > 0) )
201
195
|| embeddedUrl == newUrls[j]
202
196
) {
203
197
j++;
···
221
215
.replace(/;$/, "");
222
216
}
223
217
224
-
function getTweets(){
218
+
function getTweets(archiveFolder: string){
225
219
// get cache (from last time imported)
226
220
let caches = []
227
221
if(FS.existsSync(TWEETS_MAPPING_FILE_NAME)){
···
229
223
}
230
224
231
225
// get original tweets
232
-
const fTweets = FS.readFileSync(process.env.ARCHIVE_FOLDER + "/data/tweets.js");
226
+
const fTweets = FS.readFileSync(path.join(archiveFolder, 'data', 'tweets.js'));
233
227
let tweets = JSON.parse(cleanTweetFileContent(fTweets));
234
228
235
229
let archiveExists = true;
236
230
for (let i=1; archiveExists; i++) {
237
-
let archiveFile = `${process.env.ARCHIVE_FOLDER}/data/tweets-part${i}.js`;
231
+
let archiveFile = path.join(archiveFolder, 'data', `tweets-part${i}.js`);
238
232
archiveExists = FS.existsSync(archiveFile)
239
233
if( archiveExists ) {
240
234
let fTweetsPart = FS.readFileSync(archiveFile);
···
434
428
}
435
429
436
430
async function main() {
431
+
432
+
const argv = yargs(hideBin(process.argv))
433
+
.option('simulate', {
434
+
type: 'boolean',
435
+
description: 'Simulate the import without making any changes (defaults to false)',
436
+
default: process.env.SIMULATE === '1',
437
+
})
438
+
.option('disable-import-reply', {
439
+
type: 'boolean',
440
+
description: 'Disable importing replies',
441
+
default: process.env.DISABLE_IMPORT_REPLY === '1',
442
+
})
443
+
.option('min-date', {
444
+
type: 'string',
445
+
description: 'Minimum date for tweets to import (YYYY-MM-DD)',
446
+
default: process.env.MIN_DATE,
447
+
})
448
+
.option('max-date', {
449
+
type: 'string',
450
+
description: 'Maximum date for tweets to import (YYYY-MM-DD)',
451
+
default: process.env.MAX_DATE,
452
+
})
453
+
.option('api-delay', {
454
+
type: 'number',
455
+
description: 'Delay between API calls in milliseconds',
456
+
default: process.env.API_DELAY ? parseInt(process.env.API_DELAY) : 2500,
457
+
})
458
+
.option('archive-folder', {
459
+
type: 'string',
460
+
description: 'Path to the archive folder',
461
+
default: process.env.ARCHIVE_FOLDER,
462
+
demandOption: true,
463
+
})
464
+
.option('bluesky-username', {
465
+
type: 'string',
466
+
description: 'Bluesky username',
467
+
default: process.env.BLUESKY_USERNAME,
468
+
demandOption: true,
469
+
})
470
+
.option('bluesky-password', {
471
+
type: 'string',
472
+
description: 'Bluesky password',
473
+
default: process.env.BLUESKY_PASSWORD,
474
+
demandOption: true,
475
+
})
476
+
.option('twitter-handles', {
477
+
type: 'array',
478
+
description: 'Twitter handles to import',
479
+
default: process.env.PAST_HANDLES?.split(','),
480
+
demandOption: true,
481
+
})
482
+
.help()
483
+
.argv;
484
+
485
+
let minDate = argv.minDate ? new Date(argv.minDate) : undefined;
486
+
let maxDate = argv.maxDate ? new Date(argv.maxDate) : undefined;
487
+
437
488
console.log(`Import started at ${new Date().toISOString()}`)
438
-
console.log(`SIMULATE is ${SIMULATE ? "ON" : "OFF"}`);
439
-
console.log(`IMPORT REPLY is ${!DISABLE_IMPORT_REPLY ? "ON" : "OFF"}`);
489
+
console.log(`Simulate is ${argv.simulate ? "ON" : "OFF"}`);
490
+
console.log(`Import Reply is ${!argv.disableImportReply ? "ON" : "OFF"}`);
491
+
console.log(`Min Date is ${minDate ? minDate.toISOString() : "OFF"}`);
492
+
console.log(`Max Date is ${maxDate ? maxDate.toISOString() : "OFF"}`);
493
+
console.log(`API Delay is ${argv.apiDelay}ms`);
494
+
console.log(`Archive Folder is ${argv.archiveFolder}`);
495
+
console.log(`Bluesky Username is ${argv.blueskyUsername}`);
440
496
441
-
const tweets = getTweets();
497
+
const tweets = getTweets(argv.archiveFolder);
442
498
443
499
let importedTweet = 0;
444
500
if (tweets != null && tweets.length > 0) {
···
448
504
return ad - bd;
449
505
});
450
506
451
-
await rateLimitedAgent.login({ identifier: process.env.BLUESKY_USERNAME!, password: process.env.BLUESKY_PASSWORD! });
507
+
await rateLimitedAgent.login({ identifier: argv.blueskyUsername, password: argv.blueskyPassword });
452
508
453
509
process.on('exit', () => saveCache(sortedTweets));
454
510
process.on('SIGINT', () => process.exit());
···
461
517
const tweet_createdAt = tweetDate.toISOString();
462
518
463
519
//this cheks assume that the array is sorted by date (first the oldest)
464
-
if (MIN_DATE != undefined && tweetDate < MIN_DATE)
520
+
if (minDate != undefined && tweetDate < minDate)
465
521
continue;
466
-
if (MAX_DATE != undefined && tweetDate > MAX_DATE)
522
+
if (maxDate != undefined && tweetDate > maxDate)
467
523
break;
468
524
469
525
if(bsky){
···
477
533
console.log(` Created at ${tweet_createdAt}`);
478
534
console.log(` Full text '${tweet.full_text}'`);
479
535
480
-
if (DISABLE_IMPORT_REPLY && tweet.in_reply_to_screen_name) {
536
+
if (argv.disableImportReply && tweet.in_reply_to_screen_name) {
481
537
console.log("Discarded (reply)");
482
538
continue;
483
539
}
484
540
485
541
if (tweet.in_reply_to_screen_name) {
486
-
if (PAST_HANDLES.some(handle => tweet.in_reply_to_screen_name == handle)) {
542
+
if (argv.twitterHandles.some(handle => tweet.in_reply_to_screen_name == handle)) {
487
543
// Remove "@screen_name" from the beginning of the tweet's full text
488
544
const replyPrefix = `@${tweet.in_reply_to_screen_name} `;
489
545
if (tweet.full_text.startsWith(replyPrefix)) {
···
537
593
break;
538
594
}
539
595
540
-
let mediaFilename = `${process.env.ARCHIVE_FOLDER}${path.sep}data${path.sep}tweets_media${path.sep}${tweet.id}-${media?.media_url.substring(i + 1)}`;
596
+
let mediaFilename = `${argv.archiveFolder}${path.sep}data${path.sep}tweets_media${path.sep}${tweet.id}-${media?.media_url.substring(i + 1)}`;
541
597
542
598
let localMediaFileNotFound = true;
543
599
if (FS.existsSync(mediaFilename)) {
···
545
601
}
546
602
547
603
if (localMediaFileNotFound) {
548
-
const wildcardPath = `${process.env.ARCHIVE_FOLDER}${path.sep}data${path.sep}tweets_media${path.sep}${tweet.id}-*`;
604
+
const wildcardPath = `${argv.archiveFolder}${path.sep}data${path.sep}tweets_media${path.sep}${tweet.id}-*`;
549
605
const files = FS.readdirSync(path.dirname(wildcardPath)).filter(file => file.startsWith(`${tweet.id}-`));
550
606
551
607
if (files.length > 0) {
···
567
623
mimeType = 'image/jpeg';
568
624
}
569
625
570
-
if (!SIMULATE) {
626
+
if (!argv.simulate) {
571
627
const blobRecord = await rateLimitedAgent.uploadBlob(imageBuffer, {
572
628
encoding: mimeType
573
629
});
···
590
646
tweet.full_text = tweet.full_text.replace(media?.url, '').replace(/\s\s+/g, ' ').trim();
591
647
}
592
648
593
-
const baseVideoPath = `${process.env.ARCHIVE_FOLDER}/data/tweets_media/${tweet.id}-`;
649
+
const baseVideoPath = `${argv.archiveFolder}/data/tweets_media/${tweet.id}-`;
594
650
let videoFileName = '';
595
651
let videoFilePath = '';
596
652
let localVideoFileNotFound = true;
···
611
667
continue
612
668
}
613
669
614
-
if (!SIMULATE) {
670
+
if (!argv.simulate) {
615
671
const { data: serviceAuth } = await rateLimitedAgent.getServiceAuth(
616
672
{
617
673
aud: `did:web:${rateLimitedAgent.dispatchUrl.host}`,
···
674
730
}
675
731
676
732
// handle bsky embed record
677
-
const { embeddedUrl = null, embeddedRecord = null } = getEmbeddedUrlAndRecord(tweet.entities?.urls, sortedTweets);
733
+
const { embeddedUrl = null, embeddedRecord = null } = getEmbeddedUrlAndRecord(argv.twitterHandles, tweet.entities?.urls, sortedTweets);
678
734
679
735
let replyTo: {}|null = null;
680
-
if ( !DISABLE_IMPORT_REPLY && !SIMULATE && tweet.in_reply_to_screen_name) {
681
-
replyTo = getReplyRefs(tweet,sortedTweets);
736
+
if ( !argv.disableImportReply && !argv.simulate && tweet.in_reply_to_screen_name) {
737
+
replyTo = getReplyRefs(argv.twitterHandles, tweet, sortedTweets);
682
738
}
683
739
684
740
let postText = tweet.full_text as string;
685
-
if (!SIMULATE) {
686
-
postText = await cleanTweetText(tweet.full_text, tweet.entities?.urls, embeddedUrl, sortedTweets);
741
+
if (!argv.simulate) {
742
+
postText = await cleanTweetText(argv.twitterHandles, argv.blueskyUsername, tweet.full_text, tweet.entities?.urls, embeddedUrl, sortedTweets);
687
743
688
744
if (postText.length > 300)
689
745
postText = tweet.full_text;
···
747
803
748
804
console.log(postRecord);
749
805
750
-
if (!SIMULATE) {
806
+
if (!argv.simulate) {
751
807
//I wait 3 seconds so as not to exceed the api rate limits
752
-
await new Promise(resolve => setTimeout(resolve, API_DELAY));
808
+
await new Promise(resolve => setTimeout(resolve, argv.apiDelay));
753
809
754
810
try
755
811
{
756
812
const recordData = await rateLimitedAgent.post(postRecord);
757
813
const i = recordData.uri.lastIndexOf("/");
758
814
if (i > 0) {
759
-
const postUri = getBskyPostUrl(recordData.uri);
815
+
const postUri = getBskyPostUrl(argv.blueskyUsername, recordData.uri);
760
816
console.log("Bluesky post create, URL: " + postUri);
761
817
762
818
importedTweet++;
···
786
842
}
787
843
}
788
844
789
-
if (SIMULATE) {
845
+
if (argv.simulate) {
790
846
// In addition to the delay in AT Proto API calls, we will also consider a 5% delta for URL resolution calls
791
-
const minutes = Math.round((importedTweet * API_DELAY / 1000) / 60) + (1 / 0.1);
847
+
const minutes = Math.round((importedTweet * argv.apiDelay / 1000) / 60) + (1 / 0.1);
792
848
const hours = Math.floor(minutes / 60);
793
849
const min = minutes % 60;
794
850
console.log(`Estimated time for real import: ${hours} hours and ${min} minutes`);
+3
-18
libs/bskyAPI.ts
+3
-18
libs/bskyAPI.ts
···
1
-
import * as dotenv from 'dotenv';
2
-
import * as process from 'process';
3
1
import FS from 'fs';
4
2
5
-
dotenv.config();
6
-
7
3
export const TWEETS_MAPPING_FILE_NAME = 'tweets_mapping.json'; // store the imported tweets & bsky id mapping
8
4
9
-
10
-
let MIN_DATE: Date | undefined = undefined;
11
-
if (process.env.MIN_DATE != null && process.env.MIN_DATE.length > 0)
12
-
MIN_DATE = new Date(process.env.MIN_DATE as string);
13
-
14
-
let MAX_DATE: Date | undefined = undefined;
15
-
if (process.env.MAX_DATE != null && process.env.MAX_DATE.length > 0)
16
-
MAX_DATE = new Date(process.env.MAX_DATE as string);
17
-
18
-
19
-
20
-
export async function deleteBskyPosts(agent, tweets){
5
+
export async function deleteBskyPosts(agent, tweets, minDate: Date, maxDate: Date){
21
6
// Delete bsky posts with a record in TWEETS_MAPPING_FILE_NAME.
22
7
// If something goes wrong, call this method to clear the previously imported posts.
23
8
// You may also use MIN_DATE and MAX_DATE to limit the range.
···
30
15
31
16
const tweetDate = new Date(tweet.created_at);
32
17
33
-
if (MIN_DATE != undefined && tweetDate < MIN_DATE)
18
+
if (minDate != undefined && tweetDate < minDate)
34
19
continue;
35
-
if (MAX_DATE != undefined && tweetDate > MAX_DATE)
20
+
if (maxDate != undefined && tweetDate > maxDate)
36
21
continue;
37
22
38
23
await agent.deletePost(bsky.uri);
+6
-10
libs/bskyParams.ts
+6
-10
libs/bskyParams.ts
···
1
-
import * as dotenv from 'dotenv';
2
-
import * as process from 'process';
3
-
4
1
import { checkPastHandles } from './urlHandler';
5
2
6
-
dotenv.config();
7
-
8
-
export const PAST_HANDLES = process.env.PAST_HANDLES!.split(",");
9
-
10
-
export function getReplyRefs({in_reply_to_screen_name, in_reply_to_status_id}, tweets):{
3
+
export function getReplyRefs(
4
+
twitterHandles: string[],
5
+
{in_reply_to_screen_name, in_reply_to_status_id}, tweets):{
11
6
"root": {
12
7
"uri": string;
13
8
"cid": string;
···
17
12
"cid":string;
18
13
},
19
14
}|null{
20
-
const importReplyScreenNames = PAST_HANDLES || [];
15
+
const importReplyScreenNames = twitterHandles || [];
21
16
if(importReplyScreenNames.every(handle => in_reply_to_screen_name != handle)){
22
17
console.log(`Skip Reply (wrong reply screen name :${in_reply_to_screen_name})`, importReplyScreenNames);
23
18
return null;
···
48
43
49
44
50
45
export function getEmbeddedUrlAndRecord(
46
+
twitterHandles: string[],
51
47
urls: Array<{expanded_url: string}>,
52
48
tweets: Array<{
53
49
tweet: Record<string, string>,
···
68
64
69
65
// get the last one url to embed
70
66
const reversedUrls = urls.reverse();
71
-
embeddedTweetUrl = reversedUrls.find(({expanded_url})=> checkPastHandles(expanded_url))?.expanded_url ?? null;
67
+
embeddedTweetUrl = reversedUrls.find(({expanded_url})=> checkPastHandles(twitterHandles, expanded_url))?.expanded_url ?? null;
72
68
73
69
if(!embeddedTweetUrl){
74
70
return nullResult;
+6
-11
libs/urlHandler.ts
+6
-11
libs/urlHandler.ts
···
1
-
import * as dotenv from 'dotenv';
2
-
import * as process from 'process';
3
1
4
-
dotenv.config();
5
-
6
-
export const PAST_HANDLES = process.env.PAST_HANDLES!.split(",");
7
-
8
-
export function checkPastHandles(url: string): boolean{
9
-
return (PAST_HANDLES || []).some(handle =>
2
+
export function checkPastHandles(twitterHandles: string[], url: string): boolean{
3
+
return (twitterHandles || []).some(handle =>
10
4
url.startsWith(`https://x.com/${handle}/`) ||
11
5
url.startsWith(`https://twitter.com/${handle}/`)
12
6
)
13
7
}
14
8
15
9
export function convertToBskyPostUrl(
10
+
blueskyUsername: string,
16
11
tweetUrl:string ,
17
12
tweets: Array<{
18
13
tweet: Record<string, string>,
···
29
24
if(!tweet?.bsky){
30
25
return tweetUrl;
31
26
}
32
-
return getBskyPostUrl(tweet.bsky.uri);
27
+
return getBskyPostUrl(blueskyUsername, tweet.bsky.uri);
33
28
}
34
29
35
-
export function getBskyPostUrl(bskyUri: string): string {
30
+
export function getBskyPostUrl(blueskyUsername : string, bskyUri: string): string {
36
31
const i = bskyUri.lastIndexOf("/");
37
32
if(i == -1){
38
33
return bskyUri;
39
34
}
40
35
const rkey = bskyUri.substring(i + 1);
41
-
return `https://bsky.app/profile/${process.env.BLUESKY_USERNAME!}/post/${rkey}`;
36
+
return `https://bsky.app/profile/${blueskyUsername!}/post/${rkey}`;
42
37
}
43
38
44
39
+161
-1
package-lock.json
+161
-1
package-lock.json
···
19
19
"process": "^0.11.10",
20
20
"sharp": "^0.33.5",
21
21
"typescript": "^5.6.3",
22
-
"urijs": "^1.19.11"
22
+
"urijs": "^1.19.11",
23
+
"yargs": "17.7.2"
23
24
},
24
25
"devDependencies": {
25
26
"@types/follow-redirects": "^1.14.4",
···
465
466
"integrity": "sha512-XOfUup9r3Y06nFAZh3WvO0rBU4OtlfPB/vgxpjg+NRdGU6CN6djdc6OEiH+PcqHCY6eFLo9Ista73uarf4gnBg==",
466
467
"dev": true
467
468
},
469
+
"node_modules/ansi-regex": {
470
+
"version": "5.0.1",
471
+
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
472
+
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
473
+
"license": "MIT",
474
+
"engines": {
475
+
"node": ">=8"
476
+
}
477
+
},
478
+
"node_modules/ansi-styles": {
479
+
"version": "4.3.0",
480
+
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
481
+
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
482
+
"license": "MIT",
483
+
"dependencies": {
484
+
"color-convert": "^2.0.1"
485
+
},
486
+
"engines": {
487
+
"node": ">=8"
488
+
},
489
+
"funding": {
490
+
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
491
+
}
492
+
},
468
493
"node_modules/async": {
469
494
"version": "0.9.2",
470
495
"resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz",
···
518
543
},
519
544
"funding": {
520
545
"url": "https://github.com/sponsors/fb55"
546
+
}
547
+
},
548
+
"node_modules/cliui": {
549
+
"version": "8.0.1",
550
+
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
551
+
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
552
+
"license": "ISC",
553
+
"dependencies": {
554
+
"string-width": "^4.2.0",
555
+
"strip-ansi": "^6.0.1",
556
+
"wrap-ansi": "^7.0.0"
557
+
},
558
+
"engines": {
559
+
"node": ">=12"
521
560
}
522
561
},
523
562
"node_modules/color": {
···
661
700
"url": "https://dotenvx.com"
662
701
}
663
702
},
703
+
"node_modules/emoji-regex": {
704
+
"version": "8.0.0",
705
+
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
706
+
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
707
+
"license": "MIT"
708
+
},
664
709
"node_modules/encoding-sniffer": {
665
710
"version": "0.2.0",
666
711
"resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz",
···
682
727
},
683
728
"funding": {
684
729
"url": "https://github.com/fb55/entities?sponsor=1"
730
+
}
731
+
},
732
+
"node_modules/escalade": {
733
+
"version": "3.2.0",
734
+
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
735
+
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
736
+
"license": "MIT",
737
+
"engines": {
738
+
"node": ">=6"
685
739
}
686
740
},
687
741
"node_modules/fast-xml-parser": {
···
757
811
"node": ">=12.20.0"
758
812
}
759
813
},
814
+
"node_modules/get-caller-file": {
815
+
"version": "2.0.5",
816
+
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
817
+
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
818
+
"license": "ISC",
819
+
"engines": {
820
+
"node": "6.* || 8.* || >= 10.*"
821
+
}
822
+
},
760
823
"node_modules/graphemer": {
761
824
"version": "1.4.0",
762
825
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
···
804
867
"version": "0.3.2",
805
868
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
806
869
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
870
+
},
871
+
"node_modules/is-fullwidth-code-point": {
872
+
"version": "3.0.0",
873
+
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
874
+
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
875
+
"license": "MIT",
876
+
"engines": {
877
+
"node": ">=8"
878
+
}
807
879
},
808
880
"node_modules/iso-datestring-validator": {
809
881
"version": "2.2.2",
···
934
1006
"node": ">= 0.6.0"
935
1007
}
936
1008
},
1009
+
"node_modules/require-directory": {
1010
+
"version": "2.1.1",
1011
+
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
1012
+
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
1013
+
"license": "MIT",
1014
+
"engines": {
1015
+
"node": ">=0.10.0"
1016
+
}
1017
+
},
937
1018
"node_modules/safer-buffer": {
938
1019
"version": "2.1.2",
939
1020
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
···
994
1075
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
995
1076
"dependencies": {
996
1077
"is-arrayish": "^0.3.1"
1078
+
}
1079
+
},
1080
+
"node_modules/string-width": {
1081
+
"version": "4.2.3",
1082
+
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
1083
+
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
1084
+
"license": "MIT",
1085
+
"dependencies": {
1086
+
"emoji-regex": "^8.0.0",
1087
+
"is-fullwidth-code-point": "^3.0.0",
1088
+
"strip-ansi": "^6.0.1"
1089
+
},
1090
+
"engines": {
1091
+
"node": ">=8"
1092
+
}
1093
+
},
1094
+
"node_modules/strip-ansi": {
1095
+
"version": "6.0.1",
1096
+
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
1097
+
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
1098
+
"license": "MIT",
1099
+
"dependencies": {
1100
+
"ansi-regex": "^5.0.1"
1101
+
},
1102
+
"engines": {
1103
+
"node": ">=8"
997
1104
}
998
1105
},
999
1106
"node_modules/strnum": {
···
1104
1211
"dependencies": {
1105
1212
"tr46": "~0.0.3",
1106
1213
"webidl-conversions": "^3.0.0"
1214
+
}
1215
+
},
1216
+
"node_modules/wrap-ansi": {
1217
+
"version": "7.0.0",
1218
+
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
1219
+
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
1220
+
"license": "MIT",
1221
+
"dependencies": {
1222
+
"ansi-styles": "^4.0.0",
1223
+
"string-width": "^4.1.0",
1224
+
"strip-ansi": "^6.0.0"
1225
+
},
1226
+
"engines": {
1227
+
"node": ">=10"
1228
+
},
1229
+
"funding": {
1230
+
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
1231
+
}
1232
+
},
1233
+
"node_modules/y18n": {
1234
+
"version": "5.0.8",
1235
+
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
1236
+
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
1237
+
"license": "ISC",
1238
+
"engines": {
1239
+
"node": ">=10"
1240
+
}
1241
+
},
1242
+
"node_modules/yargs": {
1243
+
"version": "17.7.2",
1244
+
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
1245
+
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
1246
+
"license": "MIT",
1247
+
"dependencies": {
1248
+
"cliui": "^8.0.1",
1249
+
"escalade": "^3.1.1",
1250
+
"get-caller-file": "^2.0.5",
1251
+
"require-directory": "^2.1.1",
1252
+
"string-width": "^4.2.3",
1253
+
"y18n": "^5.0.5",
1254
+
"yargs-parser": "^21.1.1"
1255
+
},
1256
+
"engines": {
1257
+
"node": ">=12"
1258
+
}
1259
+
},
1260
+
"node_modules/yargs-parser": {
1261
+
"version": "21.1.1",
1262
+
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
1263
+
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
1264
+
"license": "ISC",
1265
+
"engines": {
1266
+
"node": ">=12"
1107
1267
}
1108
1268
},
1109
1269
"node_modules/zod": {