├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── github.5m.js ├── package.json ├── screenshot.png └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Koki Sato 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | mkdir -p $(HOME)/Library/Application\ Support/xbar/plugins 3 | ln -sf $(CURDIR)/github.5m.js $(HOME)/Library/Application\ Support/xbar/plugins/github.5m.js 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xbar Plugin for GitHub 2 | 3 |

4 | 5 |

6 | 7 | ## Prerequisites 8 | 9 | `node` is required to be installed with Homebrew. 10 | 11 | ```console 12 | $ brew install node 13 | ``` 14 | 15 | ## Installation 16 | 17 | ### Clone repository 18 | 19 | ```console 20 | $ git clone git@github.com:koki-develop/xbar-plugin-github 21 | $ cd xbar-plugin-github 22 | $ make 23 | ``` 24 | 25 | A symbolic link is created in `~/Library/Application Support/xbar/plugins/github.5m.js`. 26 | 27 | ### Install directly 28 | 29 | ```console 30 | $ wget https://raw.githubusercontent.com/koki-develop/xbar-plugin-github/main/github.5m.js -P ~/Library/Application\ Support/xbar/plugins/ 31 | $ chmod +x ~/Library/Application\ Support/xbar/plugins/github.5m.js 32 | ``` 33 | 34 | ## Configuration 35 | 36 | - `GITHUB_TOKEN` : Your GitHub Personal Access Token. 37 | - `SHOW_REVIEW_REQUESTED` : Show Pull Requests that are requested to review. 38 | - `SHOW_MY_PULL_REQUESTS` : Show your Pull Requests. 39 | - `SHOW_NOTIFICATIONS` : Show your notifications. 40 | - `SHOW_PULL_REQUEST_STATUS` : Show Pull Request's status. 41 | - `SHOW_PULL_REQUEST_BRANCHES` : Show Pull Request's base/head branches. 42 | - `SHOW_NOTIFICATION_REASON` : Show notification's reason. 43 | - `INCLUDE_BOT_PULL_REQUESTS` : Include Pull Requests created by bots. 44 | - `GITHUB_HOST` : Your GitHub Enterprise Host. Leave blank for GitHub.com. 45 | 46 | ## LICENSE 47 | 48 | [MIT](./LICENSE) 49 | -------------------------------------------------------------------------------- /github.5m.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S PATH="${PATH}:/opt/homebrew/bin:/usr/local/bin" node 2 | 3 | // meta 4 | // GitHub 5 | // koki 6 | // koki-develop 7 | // node 8 | 9 | // variables 10 | // string(GITHUB_TOKEN=""): Your GitHub Personal Access Token. 11 | // boolean(SHOW_REVIEW_REQUESTED=true): Show Pull Requests that are requested to review. 12 | // boolean(SHOW_MY_PULL_REQUESTS=true): Show your Pull Requests. 13 | // boolean(SHOW_ISSUES_ASSIGNED=false): Show your Issues. 14 | // boolean(SHOW_NOTIFICATIONS=true): Show your notifications. 15 | // boolean(SHOW_PULL_REQUEST_STATUS=true): Show Pull Request's status. 16 | // boolean(SHOW_PULL_REQUEST_BRANCHES=true): Show Pull Request's base/head branches. 17 | // boolean(SHOW_NOTIFICATION_REASON=true): Show notification's reason. 18 | // boolean(INCLUDE_BOT_PULL_REQUESTS=false): Include Pull Requests created by bots. 19 | // string(GITHUB_HOST=""): Your GitHub Enterprise Host. Leave blank for GitHub.com. 20 | 21 | const config = { 22 | image: 23 | "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", 24 | githubHost: process.env["GITHUB_HOST"] || "github.com", 25 | token: process.env["GITHUB_TOKEN"], 26 | showReviewRequested: process.env["SHOW_REVIEW_REQUESTED"] === "true", 27 | showMyPullRequests: process.env["SHOW_MY_PULL_REQUESTS"] === "true", 28 | showIssuesAssigned: process.env["SHOW_ISSUES_ASSIGNED"] === "true", 29 | showNotifications: process.env["SHOW_NOTIFICATIONS"] === "true", 30 | showPullRequestStatus: process.env["SHOW_PULL_REQUEST_STATUS"] === "true", 31 | showBranches: process.env["SHOW_PULL_REQUEST_BRANCHES"] === "true", 32 | showNotificationReason: process.env["SHOW_NOTIFICATION_REASON"] === "true", 33 | includeBotPullRequests: process.env["INCLUDE_BOT_PULL_REQUESTS"] === "true", 34 | }; 35 | 36 | const botNames = ["renovate", "dependabot"]; 37 | const restApiEndpoint = 38 | config.githubHost === "github.com" 39 | ? "api.github.com" 40 | : `${config.githubHost}/api/v3`; 41 | const graphqlApiEndpoint = 42 | config.githubHost === "github.com" 43 | ? "api.github.com" 44 | : `${config.githubHost}/api`; 45 | 46 | /* 47 | * type definitions 48 | */ 49 | 50 | /** 51 | * @typedef {Object} GitHubUser 52 | * @property {string} login 53 | */ 54 | 55 | /** 56 | * @typedef {Object} GitHubRepository 57 | * @property {string} name 58 | * @property {GitHubUser} owner 59 | * @property {string} url 60 | */ 61 | 62 | /** 63 | * @typedef {Object} GitHubPullRequest 64 | * @property {string} title 65 | * @property {string} url 66 | * @property {number} number 67 | * @property {string} headRefName 68 | * @property {string} baseRefName 69 | * @property {boolean} isDraft 70 | * @property {GitHubRepository} repository 71 | */ 72 | 73 | /** 74 | * @typedef {Object} GitHubIssue 75 | * @property {string} title 76 | * @property {string} url 77 | * @property {GitHubRepository} repository 78 | * @property {string} number 79 | */ 80 | 81 | /** 82 | * @typedef {Object} GitHubNotification 83 | * @property {string} id 84 | * @property {string} reason 85 | * @property {string} title 86 | * @property {string} html_url 87 | * @property {GitHubRepository} repository 88 | * @property {GitHubNotificationSubject} subject 89 | */ 90 | 91 | /** 92 | * @typedef {Object} GitHubNotificationSubject 93 | * @property {string} title 94 | * @property {string} url 95 | * @property {string | null} latest_comment_url 96 | */ 97 | 98 | /** 99 | * @returns {string} 100 | */ 101 | const buildQueryPullRequestsReviewRequested = () => { 102 | const filters = ["is:pr", "is:open", "review-requested:@me"]; 103 | if (!config.includeBotPullRequests) 104 | filters.push(...botNames.map((botName) => `-author:app/${botName}`)); 105 | return filters.join(" "); 106 | }; 107 | 108 | /** 109 | * @returns {Promise} 110 | */ 111 | const fetchPullRequestsReviewRequested = async () => { 112 | return await searchPullRequests(buildQueryPullRequestsReviewRequested()); 113 | }; 114 | 115 | /** 116 | * @returns {string} 117 | */ 118 | const buildQueryPullRequestsMine = () => { 119 | const filters = ["is:pr", "is:open", "author:@me"]; 120 | if (!config.includeBotPullRequests) 121 | filters.push(...botNames.map((botName) => `-author:app/${botName}`)); 122 | return filters.join(" "); 123 | }; 124 | 125 | /** 126 | * @returns {Promise} 127 | */ 128 | const fetchPullRequestsMine = async () => { 129 | return await searchPullRequests(buildQueryPullRequestsMine()); 130 | }; 131 | 132 | /** 133 | * @param {string} q 134 | * @returns {Promise} 135 | */ 136 | const searchPullRequests = async (q) => { 137 | const query = ` 138 | query { 139 | search(query: "${q}", type: ISSUE, first: 100) { 140 | edges { 141 | node { 142 | ... on PullRequest { 143 | title 144 | url 145 | number 146 | headRefName 147 | baseRefName 148 | isDraft 149 | repository { 150 | name 151 | owner { 152 | login 153 | } 154 | } 155 | commits(last: 1) { 156 | nodes { 157 | commit { 158 | checkSuites(last: 1) { 159 | nodes { 160 | conclusion 161 | } 162 | } 163 | } 164 | } 165 | } 166 | } 167 | } 168 | } 169 | } 170 | } 171 | `; 172 | 173 | data = await fetch(`https://${graphqlApiEndpoint}/graphql`, { 174 | method: "POST", 175 | headers: { 176 | Authorization: `Bearer ${config.token}`, 177 | }, 178 | body: JSON.stringify({ query }), 179 | }).then(async (resp) => { 180 | const data = await resp.json(); 181 | if (!resp.ok) { 182 | throw new Error(JSON.stringify(data)); 183 | } 184 | return data; 185 | }); 186 | 187 | return data.data.search.edges.map((edge) => edge.node); 188 | }; 189 | 190 | /** 191 | * @returns {string} 192 | */ 193 | const buildQueryIssuesAssigned = () => { 194 | const filters = ["is:issue", "is:open", "assignee:@me"]; 195 | return filters.join(" "); 196 | }; 197 | 198 | /** 199 | * @returns {Promise} 200 | */ 201 | const fetchIssuesAssigned = async () => { 202 | return await searchIssues(buildQueryIssuesAssigned()); 203 | }; 204 | 205 | /** 206 | * @param {string} q 207 | * @returns {Promise} 208 | */ 209 | const searchIssues = async (q) => { 210 | const query = ` 211 | query { 212 | search(query: "${q}", type: ISSUE, first: 100) { 213 | edges { 214 | node { 215 | ... on Issue { 216 | title 217 | url 218 | number 219 | repository { 220 | name 221 | owner { 222 | login 223 | } 224 | } 225 | } 226 | } 227 | } 228 | } 229 | } 230 | `; 231 | 232 | data = await fetch(`https://${graphqlApiEndpoint}/graphql`, { 233 | method: "POST", 234 | headers: { 235 | Authorization: `Bearer ${config.token}`, 236 | }, 237 | body: JSON.stringify({ query }), 238 | }).then(async (resp) => { 239 | const data = await resp.json(); 240 | if (!resp.ok) { 241 | throw new Error(JSON.stringify(data)); 242 | } 243 | return data; 244 | }); 245 | 246 | return data.data.search.edges.map((edge) => edge.node); 247 | }; 248 | 249 | /** 250 | * @param {number} max 251 | * @returns {Promise<[GitHubNotification[], boolean]>} 252 | */ 253 | const fetchNotifications = async (max) => { 254 | const notifications = await fetch( 255 | `https://${restApiEndpoint}/notifications?per_page=${max + 1}`, 256 | { 257 | headers: { 258 | Authorization: `Bearer ${config.token}`, 259 | }, 260 | }, 261 | ).then(async (resp) => { 262 | const data = await resp.json(); 263 | if (!resp.ok) { 264 | throw new Error(JSON.stringify(data)); 265 | } 266 | return data; 267 | }); 268 | 269 | return [ 270 | await Promise.all( 271 | notifications.slice(0, max).map(async (notification) => { 272 | const resourceUrl = 273 | notification.subject.latest_comment_url ?? notification.subject.url; 274 | if (!resourceUrl) { 275 | return notification; 276 | } 277 | const resource = await fetch(resourceUrl, { 278 | headers: { 279 | Authorization: `Bearer ${config.token}`, 280 | }, 281 | }).then((resp) => resp.json()); 282 | return { 283 | ...notification, 284 | html_url: resource.html_url, 285 | }; 286 | }), 287 | ), 288 | notifications.length > max, 289 | ]; 290 | }; 291 | 292 | const readNotification = async (id) => { 293 | await fetch(`https://${restApiEndpoint}/notifications/threads/${id}`, { 294 | method: "PATCH", 295 | headers: { 296 | Authorization: `Bearer ${config.token}`, 297 | }, 298 | }); 299 | }; 300 | 301 | const readAllNotifications = async () => { 302 | await fetch(`https://${restApiEndpoint}/notifications`, { 303 | method: "PUT", 304 | headers: { 305 | Authorization: `Bearer ${config.token}`, 306 | }, 307 | body: JSON.stringify({ 308 | last_read_at: new Date().toISOString(), 309 | read: true, 310 | }), 311 | }); 312 | }; 313 | 314 | /** 315 | * @param {any} resources 316 | * @returns {Record} 317 | */ 318 | const groupResourcesByRepo = (resources) => { 319 | const repositories = resources.reduce((acc, pr) => { 320 | const key = `${pr.repository.owner.login}/${pr.repository.name}`; 321 | if (!acc[key]) acc[key] = []; 322 | acc[key].push(pr); 323 | return acc; 324 | }, {}); 325 | 326 | return Object.entries(repositories).sort(([a], [b]) => (a > b ? 1 : -1)); 327 | }; 328 | 329 | /** 330 | * @param {string} str 331 | * @returns string 332 | */ 333 | const escapePipe = (str) => str.replaceAll(/\|/g, "ǀ"); 334 | 335 | /** 336 | * @param {GitHubPullRequest[]} pullRequests 337 | * @returns {string[]} 338 | */ 339 | const pullRequestsToLines = (pullRequests) => { 340 | const lines = []; 341 | for (const pullRequest of pullRequests) { 342 | const prefix = (() => { 343 | /** @type {string[]} */ 344 | const cols = []; 345 | 346 | if (config.showPullRequestStatus) { 347 | const conclusion = 348 | pullRequest.commits.nodes[0].commit.checkSuites.nodes[0]?.conclusion; 349 | const emoji = conclustionToEmoji(conclusion); 350 | if (emoji) cols.push(emoji); 351 | } 352 | 353 | if (pullRequest.isDraft) cols.push("(Draft)"); 354 | 355 | if (cols.length === 0) return ""; 356 | return `${cols.join(" ")} `; 357 | })(); 358 | lines.push( 359 | `${prefix}${escapePipe(pullRequest.title)} #${pullRequest.number} | href=${pullRequest.url}`, 360 | ); 361 | if (config.showBranches) { 362 | lines.push( 363 | `${escapePipe(`${pullRequest.baseRefName} ← ${pullRequest.headRefName}`)} | size=10`, 364 | ); 365 | } 366 | } 367 | return lines; 368 | }; 369 | 370 | /** 371 | * @param {GitHubIssue[]} issues 372 | * @returns {string[]} 373 | * */ 374 | const issuesToLines = (issues) => { 375 | const lines = []; 376 | for (const issue of issues) { 377 | lines.push( 378 | `${escapePipe(issue.title)} #${issue.number} | href=${issue.url}`, 379 | ); 380 | } 381 | return lines; 382 | }; 383 | 384 | /** 385 | * @param {string} conclusion 386 | * @returns {string | null} 387 | */ 388 | const conclustionToEmoji = (conclusion) => { 389 | switch (conclusion) { 390 | case "SUCCESS": 391 | return ":white_check_mark:"; 392 | case "FAILURE": 393 | case "TIMED_OUT": 394 | case "STARTUP_FAILURE": 395 | return ":x:"; 396 | case "CANCELLED": 397 | return ":no_entry:"; 398 | case "ACTION_REQUIRED": 399 | return ":clock12:"; 400 | default: 401 | return null; 402 | } 403 | }; 404 | 405 | (async () => { 406 | const [executable, script, ...args] = process.argv; 407 | 408 | if (args.length > 0) { 409 | // NOTE: When executed from the menu, xbar.var is not set, so get the token from the arguments. 410 | config.token = args[0]; 411 | const command = args[1]; 412 | 413 | switch (command) { 414 | case "read-notification": 415 | const id = args[2]; 416 | await readNotification(id); 417 | break; 418 | case "read-all-notifications": 419 | await readAllNotifications(); 420 | break; 421 | default: 422 | throw new Error(`Unknown command: ${command}`); 423 | } 424 | 425 | return; 426 | } 427 | 428 | if (!config.token) { 429 | console.log(`:warning: | image=${config.image}`); 430 | console.log("---"); 431 | console.log("GITHUB_TOKEN not set. Please set it in the plugin settings."); 432 | return; 433 | } 434 | 435 | /** @type {Promise} */ 436 | const promises = []; 437 | /** @type {Record} */ 438 | const countsMap = {}; 439 | /** @type {string[]} */ 440 | const reviewRequestedLines = []; 441 | /** @type {string[]} */ 442 | const mineLines = []; 443 | /** @type {string[]} */ 444 | const issuesAssignedLines = []; 445 | /** @type {string[]} */ 446 | const notificationsLines = []; 447 | 448 | /* 449 | * Review Requested 450 | */ 451 | if (config.showReviewRequested) { 452 | const promise = fetchPullRequestsReviewRequested().then((pullRequests) => { 453 | reviewRequestedLines.push( 454 | `:eyes: Review Requested (${pullRequests.length}) | color=red href=https://${config.githubHost}/search?q=${encodeURIComponent(buildQueryPullRequestsReviewRequested())}`, 455 | ); 456 | 457 | countsMap.reviewRequested = pullRequests.length; 458 | if (pullRequests.length === 0) { 459 | reviewRequestedLines.push("No pull requests"); 460 | reviewRequestedLines.push("---"); 461 | return; 462 | } 463 | 464 | const byRepo = groupResourcesByRepo(pullRequests); 465 | for (const [repo, pullRequests] of byRepo) { 466 | reviewRequestedLines.push( 467 | `${repo} | size=12 color=red`, 468 | ...pullRequestsToLines(pullRequests), 469 | ); 470 | } 471 | reviewRequestedLines.push("---"); 472 | }); 473 | promises.push(promise); 474 | } 475 | 476 | /* 477 | * My Pull Requests 478 | */ 479 | if (config.showMyPullRequests) { 480 | const promise = fetchPullRequestsMine().then((pullRequests) => { 481 | mineLines.push( 482 | `:seedling: My Pull Requests (${pullRequests.length}) | color=green href=https://${config.githubHost}/search?q=${encodeURIComponent(buildQueryPullRequestsMine())}`, 483 | ); 484 | 485 | countsMap.mine = pullRequests.length; 486 | if (pullRequests.length === 0) { 487 | mineLines.push("No pull requests"); 488 | mineLines.push("---"); 489 | return; 490 | } 491 | 492 | const byRepo = groupResourcesByRepo(pullRequests); 493 | for (const [repo, pullRequests] of byRepo) { 494 | mineLines.push( 495 | `${repo} | size=12 color=green`, 496 | ...pullRequestsToLines(pullRequests), 497 | ); 498 | } 499 | mineLines.push("---"); 500 | }); 501 | promises.push(promise); 502 | } 503 | 504 | /* 505 | * Issues 506 | */ 507 | if (config.showIssuesAssigned) { 508 | const promise = fetchIssuesAssigned().then((issues) => { 509 | issuesAssignedLines.push( 510 | `:pushpin: Issues Assigned (${issues.length}) | color=pink href=https://${config.githubHost}/search?q=${encodeURIComponent(buildQueryIssuesAssigned())}`, 511 | ); 512 | 513 | countsMap.issuesAssigned = issues.length; 514 | if (issues.length === 0) { 515 | issuesAssignedLines.push("No issue assigned"); 516 | issuesAssignedLines.push("---"); 517 | return; 518 | } 519 | 520 | const byRepo = groupResourcesByRepo(issues); 521 | for (const [repo, issues] of byRepo) { 522 | issuesAssignedLines.push( 523 | `${repo} | size=12 color=red`, 524 | ...issuesToLines(issues), 525 | ); 526 | } 527 | issuesAssignedLines.push("---"); 528 | }); 529 | promises.push(promise); 530 | } 531 | 532 | /* 533 | * Notifications 534 | */ 535 | if (config.showNotifications) { 536 | const max = 20; 537 | const promise = fetchNotifications(max).then(([notifications, hasMore]) => { 538 | const count = hasMore ? `${max}+` : notifications.length.toString(); 539 | 540 | notificationsLines.push( 541 | `:bell: Notifications (${count}) | color=yellow href=https://${config.githubHost}/notifications`, 542 | ); 543 | 544 | countsMap.notifications = count; 545 | if (notifications.length === 0) { 546 | notificationsLines.push("No notifications"); 547 | notificationsLines.push("---"); 548 | return; 549 | } 550 | 551 | notificationsLines.push( 552 | `--Mark all as read | shell="${executable}" param1="${script}" param2=${config.token} param3=read-all-notifications refresh=true`, 553 | ); 554 | 555 | const byRepo = groupResourcesByRepo(notifications); 556 | for (const [repo, notifications] of byRepo) { 557 | notificationsLines.push(`${repo} | size=12 color=yellow`); 558 | 559 | for (const notification of notifications) { 560 | const prefix = config.showNotificationReason 561 | ? `(${notification.reason}) ` 562 | : ""; 563 | notificationsLines.push( 564 | `${prefix}${escapePipe(notification.subject.title)} | href=${notification.html_url}`, 565 | `--Mark as read | shell="${executable}" param1="${script}" param2=${config.token} param3=read-notification param4=${notification.id} refresh=true`, 566 | ); 567 | } 568 | } 569 | }); 570 | promises.push(promise); 571 | } 572 | 573 | // Wait for all promises to complete 574 | await Promise.all(promises); 575 | 576 | /* 577 | * Menu bar 578 | */ 579 | 580 | /** @type {number[]} */ 581 | const counts = []; 582 | if (config.showReviewRequested) counts.push(countsMap.reviewRequested); 583 | if (config.showMyPullRequests) counts.push(countsMap.mine); 584 | if (config.showIssuesAssigned) counts.push(countsMap.issuesAssigned); 585 | if (config.showNotifications) counts.push(countsMap.notifications); 586 | 587 | /** @type {string[]} */ 588 | const menubarLines = []; 589 | menubarLines.push( 590 | `(${counts.map((i) => i.toString()).join("/")}) | templateImage=${config.image}`, 591 | "---", 592 | `Last updated at ${new Date().toLocaleString()} | size=12`, 593 | "---", 594 | ); 595 | 596 | /* 597 | * Output 598 | */ 599 | 600 | /** @type {string[]} */ 601 | const lines = []; 602 | lines.push(...menubarLines); 603 | if (config.showReviewRequested) lines.push(...reviewRequestedLines); 604 | if (config.showMyPullRequests) lines.push(...mineLines); 605 | if (config.showIssuesAssigned) lines.push(...issuesAssignedLines); 606 | if (config.showNotifications) lines.push(...notificationsLines); 607 | 608 | console.log(lines.join("\n")); 609 | })(); 610 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xbar-plugin-github", 3 | "main": "github.5m.js", 4 | "author": "koki-develop ", 5 | "license": "MIT", 6 | "devDependencies": { 7 | "prettier": "^3.2.2" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koki-develop/xbar-plugin-github/1811aacdce4f3ec17f220c9c765c15172df5dd09/screenshot.png -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | prettier@^3.2.2: 6 | version "3.2.2" 7 | resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.2.tgz#96e580f7ca9c96090ad054616c0c4597e2844b65" 8 | integrity sha512-HTByuKZzw7utPiDO523Tt2pLtEyK7OibUD9suEJQrPUCYQqrHr74GGX6VidMrovbf/I50mPqr8j/II6oBAuc5A== 9 | --------------------------------------------------------------------------------