From 9159682e0b2df2b459115e1f658daf92073fd5f3 Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Wed, 25 Sep 2024 15:03:13 -0600 Subject: [PATCH] feat: use oclif/multi-stage-output --- .eslintrc.json | 3 +- README.md | 3 +- package-lock.json | 217 ++++++++++++++++++++++++++++++++++++++- package.json | 1 + src/commands/refresh.ts | 68 ++++++++++++ src/commands/refresh.tsx | 108 ------------------- 6 files changed, 286 insertions(+), 114 deletions(-) create mode 100644 src/commands/refresh.ts delete mode 100644 src/commands/refresh.tsx diff --git a/.eslintrc.json b/.eslintrc.json index 3aaef43..3e8d01c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -5,6 +5,7 @@ "no-await-in-loop": "off", "no-useless-escape": "off", "unicorn/no-await-expression-member": "off", - "lines-between-class-members": "off" + "lines-between-class-members": "off", + "react/jsx-tag-spacing": "off" } } diff --git a/README.md b/README.md index ea80bae..7d1f8f4 100644 --- a/README.md +++ b/README.md @@ -248,7 +248,8 @@ Open a repository in github. ``` USAGE - $ multi open REPO [-f | -t actions|discussions|issues|pulls|pulse|security|settings|wiki] + $ multi open REPO [-f | -t + actions|discussions|issues|pulls|pulse|security|settings|wiki] ARGUMENTS REPO [default: .] Name of repository. diff --git a/package-lock.json b/package-lock.json index 21db421..54d9cc0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "multiple-repo-manager", - "version": "4.16.0", + "version": "4.16.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "multiple-repo-manager", - "version": "4.16.0", + "version": "4.16.1", "license": "BSD-3-Clause", "os": [ "darwin", @@ -16,6 +16,7 @@ "@inkjs/ui": "^1", "@inquirer/input": "^3", "@oclif/core": "^4", + "@oclif/multi-stage-output": "^0.6.1", "@oclif/table": "^0.1.9", "@octokit/plugin-paginate-graphql": "^5.0.0", "@octokit/plugin-request-log": "^5.0.0", @@ -1987,6 +1988,216 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@oclif/multi-stage-output": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@oclif/multi-stage-output/-/multi-stage-output-0.6.1.tgz", + "integrity": "sha512-ZYCIEfbGwYB8vlFGHMewkM+qKblB63AKj9jLG+eBUSHrhQn3WdNn9XHK19GiY0A3K8R0DXVvAWSaUNI9stQC1g==", + "dependencies": { + "@oclif/core": "^4", + "@types/react": "^18.3.8", + "cli-spinners": "^2", + "figures": "^6.1.0", + "ink": "^5.0.1", + "react": "^18.3.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@oclif/multi-stage-output/node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@oclif/multi-stage-output/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@oclif/multi-stage-output/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==" + }, + "node_modules/@oclif/multi-stage-output/node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@oclif/multi-stage-output/node_modules/ink": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ink/-/ink-5.0.1.tgz", + "integrity": "sha512-ae4AW/t8jlkj/6Ou21H2av0wxTk8vrGzXv+v2v7j4in+bl1M5XRMVbfNghzhBokV++FjF8RBDJvYo+ttR9YVRg==", + "dependencies": { + "@alcalzone/ansi-tokenize": "^0.1.3", + "ansi-escapes": "^7.0.0", + "ansi-styles": "^6.2.1", + "auto-bind": "^5.0.1", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "cli-cursor": "^4.0.0", + "cli-truncate": "^4.0.0", + "code-excerpt": "^4.0.0", + "indent-string": "^5.0.0", + "is-in-ci": "^0.1.0", + "lodash": "^4.17.21", + "patch-console": "^2.0.0", + "react-reconciler": "^0.29.0", + "scheduler": "^0.23.0", + "signal-exit": "^3.0.7", + "slice-ansi": "^7.1.0", + "stack-utils": "^2.0.6", + "string-width": "^7.0.0", + "type-fest": "^4.8.3", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0", + "ws": "^8.15.0", + "yoga-wasm-web": "~0.3.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "react": ">=18.0.0", + "react-devtools-core": "^4.19.1" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-devtools-core": { + "optional": true + } + } + }, + "node_modules/@oclif/multi-stage-output/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@oclif/multi-stage-output/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/@oclif/multi-stage-output/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/@oclif/multi-stage-output/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@oclif/multi-stage-output/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@oclif/multi-stage-output/node_modules/type-fest": { + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@oclif/multi-stage-output/node_modules/widest-line": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", + "dependencies": { + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@oclif/multi-stage-output/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@oclif/plugin-help": { "version": "6.2.12", "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-6.2.12.tgz", @@ -8725,7 +8936,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", - "dev": true, "dependencies": { "is-unicode-supported": "^2.0.0" }, @@ -10964,7 +11174,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "dev": true, "engines": { "node": ">=18" }, diff --git a/package.json b/package.json index 3f19b38..071c8ee 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@inkjs/ui": "^1", "@inquirer/input": "^3", "@oclif/core": "^4", + "@oclif/multi-stage-output": "^0.6.1", "@oclif/table": "^0.1.9", "@octokit/plugin-paginate-graphql": "^5.0.0", "@octokit/plugin-request-log": "^5.0.0", diff --git a/src/commands/refresh.ts b/src/commands/refresh.ts new file mode 100644 index 0000000..e301b45 --- /dev/null +++ b/src/commands/refresh.ts @@ -0,0 +1,68 @@ +import {Flags} from '@oclif/core' +import {ParallelMultiStageOutput} from '@oclif/multi-stage-output' + +import BaseCommand from '../base-command.js' +import {Repos} from '../repos.js' + +export class Refresh extends BaseCommand { + public static description = 'Refresh the list of repositories and corresponding metadata.' + public static flags = { + all: Flags.boolean({ + char: 'a', + description: 'Refresh all orgs.', + exclusive: ['org'], + }), + 'dry-run': Flags.boolean({ + char: 'd', + description: 'Show what would be done without doing it.', + }), + 'no-cache': Flags.boolean({ + description: 'Find repos by looking at configured repos directory instead of using the cached repos.json file.', + }), + org: Flags.string({ + char: 'o', + description: 'Github org to refresh.', + exclusive: ['all'], + multiple: true, + }), + } + + public async run(): Promise { + const {flags} = await this.parse(Refresh) + const repos = await new Repos().init() + const orgs = flags.all ? repos.getOrgs() : (flags.org ?? []) + + const repoCounts = Object.fromEntries(orgs.map((org) => [org, repos.getReposOfOrg(org, true).length])) + + if (flags['no-cache']) { + await repos.hydrateCache() + } + + const mso = new ParallelMultiStageOutput<{repoCounts: typeof repoCounts}>({ + jsonEnabled: this.jsonEnabled(), + stageSpecificBlock: orgs.map((org) => ({ + get: (data) => data?.repoCounts[org].toString(), + label: 'repos', + stage: org, + type: 'static-key-value', + })), + stages: orgs, + title: flags['dry-run'] ? '[DRY RUN] Refreshing repositories' : 'Refreshing repositories', + }) + + mso.updateData({repoCounts}) + await Promise.all( + orgs.map(async (org) => { + mso.startStage(org) + await repos.refresh(org, true) + mso.stopStage(org) + }), + ) + + mso.stop() + + if (!flags['dry-run']) { + await repos.write() + } + } +} diff --git a/src/commands/refresh.tsx b/src/commands/refresh.tsx deleted file mode 100644 index e89d191..0000000 --- a/src/commands/refresh.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import {Flags} from '@oclif/core' -import {render} from 'ink' -import sortBy from 'lodash/sortBy.js' -import PQueue from 'p-queue' -import React from 'react' - -import BaseCommand from '../base-command.js' -import {TaskTracker} from '../components/index.js' -import {Repos} from '../repos.js' - -type Props = { - readonly all: boolean - readonly concurrency?: number - readonly dryRun: boolean - readonly header: string - readonly noCache?: boolean - readonly orgs: string[] -} - -class RefreshTaskTracker extends TaskTracker { - async componentDidMount(): Promise { - const repos = await new Repos().init() - - if (this.props.noCache) { - await repos.hydrateCache() - } - - const orgs = this.props.all ? repos.getOrgs() : this.props.orgs - - const repositories = sortBy( - orgs.flatMap((org) => repos.getReposOfOrg(org, true)), - 'fullName', - ) - - this.setState(() => ({ - tasks: repositories.map((repo) => ({key: repo.org, name: repo.fullName, status: 'pending'})), - waiting: false, - })) - const queue = new PQueue({concurrency: this.props.concurrency ?? orgs.length}) - - for (const org of orgs) { - // eslint-disable-next-line no-void - void queue.add(async () => { - this.setState((state) => ({ - tasks: state.tasks.map((c) => (c.key === org ? {...c, status: 'loading'} : c)), - })) - - try { - await (this.props.dryRun ? this.noop() : repos.refresh(org, true)) - this.setState((state) => ({ - tasks: state.tasks.map((c) => (c.key === org ? {...c, status: 'success'} : c)), - })) - } catch { - this.setState((state) => ({ - tasks: state.tasks.map((c) => (c.key === org ? {...c, status: 'error'} : c)), - })) - } - }) - } - - await queue.onIdle() - if (!this.props.dryRun) await repos.write() - this.setState(() => ({timeToComplete: Date.now() - this.startTime})) - } -} - -export class Refresh extends BaseCommand { - public static description = 'Refresh the list of repositories and corresponding metadata.' - public static flags = { - all: Flags.boolean({ - char: 'a', - description: 'Refresh all orgs.', - exclusive: ['org'], - }), - concurrency: Flags.integer({ - char: 'c', - description: 'Number of concurrent refreshes. Defaults to the number of orgs.', - min: 1, - }), - 'dry-run': Flags.boolean({ - char: 'd', - description: 'Show what would be done without doing it.', - }), - 'no-cache': Flags.boolean({ - description: 'Find repos by looking at configured repos directory instead of using the cached repos.json file.', - }), - org: Flags.string({ - char: 'o', - description: 'Github org to refresh.', - exclusive: ['all'], - multiple: true, - }), - } - - public async run(): Promise { - const {flags} = await this.parse(Refresh) - render( - , - ) - } -}