@humaan/patch-patrol 0.3.1 → 0.4.0

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,6 +1,6 @@
1
1
  import { intro, outro } from '@clack/prompts';
2
2
  import { loadRules } from './advisories.js';
3
- import { actionRepo } from './repo-actions.js';
3
+ import { actionRepos } from './repo-actions.js';
4
4
  import { finish, printRules } from './output.js';
5
5
  import { runScan } from './scanner.js';
6
6
  import { getRuleOrThrow, parseArgs } from './options.js';
@@ -33,9 +33,7 @@ export async function runCli(argv = process.argv.slice(2)) {
33
33
  finish(options, 'No projects selected. Nothing changed.');
34
34
  return;
35
35
  }
36
- for (const affectedRepo of selectedRepos) {
37
- await actionRepo(affectedRepo, rule, options);
38
- }
36
+ await actionRepos(selectedRepos, rule, options);
39
37
  if (options.interactive)
40
38
  outro('Selected projects processed.');
41
39
  }
package/dist/git.js CHANGED
@@ -6,6 +6,11 @@ export function ensureClean(repo) {
6
6
  if (status.stdout.trim())
7
7
  throw new Error(`${repo} has uncommitted changes; skipping to avoid overwriting local work.`);
8
8
  }
9
+ export async function ensureCleanAsync(repo) {
10
+ const status = await runAsync('git', ['status', '--porcelain'], repo, { capture: true });
11
+ if (status.stdout.trim())
12
+ throw new Error(`${repo} has uncommitted changes; skipping to avoid overwriting local work.`);
13
+ }
9
14
  export function checkoutFreshBranch(repo, rule, options, quiet = false) {
10
15
  const defaultBranch = getDefaultBranch(repo);
11
16
  run('git', ['checkout', defaultBranch], repo, { quiet });
@@ -13,7 +18,7 @@ export function checkoutFreshBranch(repo, rule, options, quiet = false) {
13
18
  run('git', ['checkout', '-B', getBranchName(rule, options)], repo, { quiet });
14
19
  }
15
20
  export async function checkoutFreshBranchAsync(repo, rule, options, quiet = false) {
16
- const defaultBranch = getDefaultBranch(repo);
21
+ const defaultBranch = await getDefaultBranchAsync(repo);
17
22
  await runAsync('git', ['checkout', defaultBranch], repo, { quiet });
18
23
  await runAsync('git', ['pull', '--ff-only'], repo, { quiet });
19
24
  await runAsync('git', ['checkout', '-B', getBranchName(rule, options)], repo, { quiet });
@@ -23,6 +28,11 @@ export function getCheckoutState(repo) {
23
28
  const branch = run('git', ['branch', '--show-current'], repo, { capture: true }).stdout.trim();
24
29
  return { ref: branch || commit, commit, detached: !branch };
25
30
  }
31
+ export async function getCheckoutStateAsync(repo) {
32
+ const commit = (await runAsync('git', ['rev-parse', 'HEAD'], repo, { capture: true })).stdout.trim();
33
+ const branch = (await runAsync('git', ['branch', '--show-current'], repo, { capture: true })).stdout.trim();
34
+ return { ref: branch || commit, commit, detached: !branch };
35
+ }
26
36
  export function restoreCheckout(repo, state, quiet = false) {
27
37
  run('git', ['checkout', state.ref], repo, { quiet });
28
38
  }
@@ -34,7 +44,7 @@ export function commitChanges(repo, rule, quiet = false) {
34
44
  run('git', ['commit', '-m', rule.commitMessage], repo, { quiet });
35
45
  }
36
46
  export async function commitChangesAsync(repo, rule, quiet = false) {
37
- await runAsync('git', ['add', ...getChangedFiles(repo)], repo, { quiet });
47
+ await runAsync('git', ['add', ...await getChangedFilesAsync(repo)], repo, { quiet });
38
48
  await runAsync('git', ['commit', '-m', rule.commitMessage], repo, { quiet });
39
49
  }
40
50
  export async function pushAndCreatePr(repo, rule, options, quiet = false) {
@@ -68,6 +78,13 @@ function getDefaultBranch(repo) {
68
78
  const current = run('git', ['branch', '--show-current'], repo, { capture: true });
69
79
  return current.stdout.trim() || 'main';
70
80
  }
81
+ async function getDefaultBranchAsync(repo) {
82
+ const symbolic = await runAsync('git', ['symbolic-ref', 'refs/remotes/origin/HEAD', '--short'], repo, { capture: true, allowFailure: true });
83
+ if (symbolic?.stdout.trim())
84
+ return symbolic.stdout.trim().replace(/^origin\//, '');
85
+ const current = await runAsync('git', ['branch', '--show-current'], repo, { capture: true });
86
+ return current.stdout.trim() || 'main';
87
+ }
71
88
  function getBranchName(rule, options) {
72
89
  return `${options.branchPrefix}/${rule.branchName}`;
73
90
  }
@@ -83,15 +100,24 @@ function getPrBody(rule) {
83
100
  function hasChanges(repo) {
84
101
  return Boolean(run('git', ['status', '--porcelain'], repo, { capture: true }).stdout.trim());
85
102
  }
103
+ async function hasChangesAsync(repo) {
104
+ return Boolean((await runAsync('git', ['status', '--porcelain'], repo, { capture: true })).stdout.trim());
105
+ }
86
106
  export function getChangedFiles(repo) {
87
107
  return run('git', ['status', '--porcelain'], repo, { capture: true }).stdout
88
108
  .split('\n')
89
109
  .filter(Boolean)
90
110
  .map((line) => line.slice(3));
91
111
  }
92
- export { hasChanges };
112
+ export async function getChangedFilesAsync(repo) {
113
+ return (await runAsync('git', ['status', '--porcelain'], repo, { capture: true })).stdout
114
+ .split('\n')
115
+ .filter(Boolean)
116
+ .map((line) => line.slice(3));
117
+ }
118
+ export { hasChanges, hasChangesAsync };
93
119
  async function ensureGithubRemote(repo) {
94
- const remotes = listRemotes(repo);
120
+ const remotes = await listRemotesAsync(repo);
95
121
  const github = remotes.find((remote) => remote.github);
96
122
  if (github)
97
123
  return github;
@@ -99,13 +125,16 @@ async function ensureGithubRemote(repo) {
99
125
  const suggestion = origin ? convertOriginToGithub(origin.url) : '';
100
126
  const url = await promptForGithubRemote(path.basename(repo), suggestion, parseRemote);
101
127
  const name = remotes.some((remote) => remote.name === 'github') ? 'github-pr' : 'github';
102
- run('git', ['remote', 'add', name, url], repo, { quiet: true });
128
+ await runAsync('git', ['remote', 'add', name, url], repo, { quiet: true });
103
129
  return parseRemote(name, url);
104
130
  }
105
- function listRemotes(repo) {
106
- const result = run('git', ['remote', '-v'], repo, { capture: true });
131
+ async function listRemotesAsync(repo) {
132
+ const result = await runAsync('git', ['remote', '-v'], repo, { capture: true });
133
+ return parseRemotes(result.stdout);
134
+ }
135
+ function parseRemotes(stdout) {
107
136
  const remotes = new Map();
108
- for (const line of result.stdout.trim().split('\n').filter(Boolean)) {
137
+ for (const line of stdout.trim().split('\n').filter(Boolean)) {
109
138
  const match = line.match(/^(\S+)\s+(\S+)\s+\(fetch\)$/);
110
139
  if (match)
111
140
  remotes.set(match[1], parseRemote(match[1], match[2]));
package/dist/options.js CHANGED
@@ -10,7 +10,7 @@ export function parseArgs(args) {
10
10
  base: undefined,
11
11
  branchPrefix: DEFAULT_BRANCH_PREFIX,
12
12
  advisoryDir: DEFAULT_ADVISORY_DIR,
13
- bumpLevels: new Set(BUMP_LEVELS),
13
+ bumpLevels: new Set(["minor", "patch"]),
14
14
  dryRun: false,
15
15
  yes: false,
16
16
  createPr: true,
@@ -117,7 +117,7 @@ Options:
117
117
  --base <branch> Base branch for PRs. Defaults to GitHub default branch.
118
118
  --branch-prefix <name> Branch namespace. Default: ${DEFAULT_BRANCH_PREFIX}
119
119
  --limit <number> Stop after processing this many affected repos.
120
- --bump <levels> Only include bump levels: major, minor, patch. Comma-separated.
120
+ --bump <levels> Only include bump levels: major, minor, patch. Comma-separated. Default: minor,patch.
121
121
  --major Only include major version bumps. Can combine with --minor/--patch.
122
122
  --minor Only include minor version bumps. Can combine with --major/--patch.
123
123
  --patch Only include patch version bumps. Can combine with --major/--minor.
@@ -1,18 +1,57 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { spinner } from '@clack/prompts';
3
+ import { log } from '@clack/prompts';
4
4
  import { getUpdates } from './advisories.js';
5
- import { checkoutFreshBranch, checkoutFreshBranchAsync, commitChanges, commitChangesAsync, ensureClean, getCheckoutState, hasChanges, pushAndCreatePr, pushAndCreatePrAsync, restoreCheckout, restoreCheckoutAsync } from './git.js';
6
- import { refreshLockfile, refreshLockfileAsync } from './package-manager.js';
5
+ import { checkoutFreshBranchAsync, commitChangesAsync, ensureCleanAsync, getCheckoutStateAsync, hasChangesAsync, pushAndCreatePrAsync, restoreCheckoutAsync } from './git.js';
6
+ import { refreshLockfileAsync } from './package-manager.js';
7
+ import { paddedSpinner } from './spinner.js';
7
8
  import { cloneGithubRepo } from './targets.js';
8
- export async function actionRepo(affectedRepo, rule, options) {
9
+ const actionConcurrency = 3;
10
+ export async function actionRepos(affectedRepos, rule, options) {
11
+ if (affectedRepos.length === 1) {
12
+ await actionRepo(affectedRepos[0], rule, options);
13
+ return;
14
+ }
15
+ let completed = 0;
16
+ let succeeded = 0;
17
+ let nextIndex = 0;
18
+ const failures = [];
19
+ const processNext = async () => {
20
+ const index = nextIndex;
21
+ nextIndex += 1;
22
+ if (index >= affectedRepos.length)
23
+ return;
24
+ try {
25
+ const result = await actionRepo(affectedRepos[index], rule, options, null);
26
+ succeeded += 1;
27
+ printActionSuccess(result, options);
28
+ }
29
+ catch (error) {
30
+ const failure = error instanceof Error ? error : new Error(String(error));
31
+ failures.push(failure);
32
+ printActionFailure(path.basename(affectedRepos[index].repo), failure, options);
33
+ }
34
+ finally {
35
+ completed += 1;
36
+ }
37
+ await processNext();
38
+ };
39
+ await Promise.all(Array.from({ length: Math.min(actionConcurrency, affectedRepos.length) }, processNext));
40
+ if (failures.length > 0) {
41
+ printActionFailureSummary(`Processed ${completed}/${affectedRepos.length} repos; ${failures.length} failed.`, options);
42
+ throw failures[0];
43
+ }
44
+ printActionSuccess(`Processed ${completed}/${affectedRepos.length} repos; ${succeeded} succeeded.`, options);
45
+ }
46
+ export async function actionRepo(affectedRepo, rule, options, statusOverride) {
9
47
  const repoName = path.basename(affectedRepo.repo);
10
- const status = createActionStatus(options, repoName);
48
+ const status = statusOverride === undefined ? createActionStatus(options, repoName) : statusOverride;
11
49
  try {
12
50
  const result = affectedRepo.github
13
51
  ? await actionGithubRepo(affectedRepo, rule, options, status, repoName)
14
52
  : await actionLocalRepo(affectedRepo, rule, options, status, repoName);
15
53
  status?.stop(result);
54
+ return result;
16
55
  }
17
56
  catch (error) {
18
57
  status?.stop(`Processing ${repoName} failed.`);
@@ -52,13 +91,10 @@ async function actionLocalRepo(affectedRepo, rule, options, status, repoName) {
52
91
  console.log(` [${update.bumpLevel}] ${update.section}.${update.name}: ${update.from} -> ${update.to} (${update.reason})`);
53
92
  }
54
93
  }
55
- ensureClean(repo);
56
- const originalCheckout = getCheckoutState(repo);
94
+ await ensureCleanAsync(repo);
95
+ const originalCheckout = await getCheckoutStateAsync(repo);
57
96
  status?.message(`Preparing update branch for ${repoName}`);
58
- if (options.interactive)
59
- await checkoutFreshBranchAsync(repo, rule, options, true);
60
- else
61
- checkoutFreshBranch(repo, rule, options);
97
+ await checkoutFreshBranchAsync(repo, rule, options, options.interactive);
62
98
  const packageJsonText = await fs.readFile(packageJsonPath, 'utf8');
63
99
  const packageJson = JSON.parse(packageJsonText);
64
100
  const freshUpdates = getUpdates(packageJson, rule, options.bumpLevels);
@@ -71,39 +107,50 @@ async function actionLocalRepo(affectedRepo, rule, options, status, repoName) {
71
107
  await fs.writeFile(packageJsonPath, applyUpdates(packageJsonText, freshUpdates));
72
108
  if (!options.skipInstall) {
73
109
  status?.message(`Refreshing lockfile for ${repoName}`);
74
- if (options.interactive)
75
- await refreshLockfileAsync(repo, true);
76
- else
77
- refreshLockfile(repo);
110
+ await refreshLockfileAsync(repo, options.interactive);
78
111
  }
79
- if (!hasChanges(repo)) {
112
+ const changed = await hasChangesAsync(repo);
113
+ if (!changed) {
80
114
  if (!options.interactive)
81
115
  console.log(' No changes after update; skipping.');
82
116
  return `Skipped ${repoName}: no changes after update.`;
83
117
  }
84
118
  status?.message(`Committing changes for ${repoName}`);
85
- if (options.interactive)
86
- await commitChangesAsync(repo, rule, true);
87
- else
88
- commitChanges(repo, rule);
119
+ await commitChangesAsync(repo, rule, options.interactive);
89
120
  if (!options.createPr) {
90
121
  if (!options.interactive)
91
122
  console.log(' Updated locally; PR creation disabled.');
92
123
  return `Updated ${repoName} locally; PR creation disabled.`;
93
124
  }
94
125
  status?.message(`Creating PR for ${repoName}`);
95
- const prUrl = options.interactive ? await pushAndCreatePrAsync(repo, rule, options, true) : await pushAndCreatePr(repo, rule, options);
126
+ const prUrl = await pushAndCreatePrAsync(repo, rule, options, options.interactive);
96
127
  status?.message(`Restoring checkout for ${repoName}`);
128
+ await restoreCheckoutAsync(repo, originalCheckout, options.interactive);
129
+ return prUrl ? `Created PR for ${repoName}: ${prUrl}` : `Created PR for ${repoName}.`;
130
+ }
131
+ function printActionSuccess(message, options) {
97
132
  if (options.interactive)
98
- await restoreCheckoutAsync(repo, originalCheckout, true);
133
+ log.success(message);
99
134
  else
100
- restoreCheckout(repo, originalCheckout);
101
- return prUrl ? `Created PR for ${repoName}: ${prUrl}` : `Created PR for ${repoName}.`;
135
+ console.log(` ${message}`);
136
+ }
137
+ function printActionFailure(repoName, error, options) {
138
+ const message = `${repoName} failed: ${error.message}`;
139
+ if (options.interactive)
140
+ log.error(message);
141
+ else
142
+ console.error(` ${message}`);
143
+ }
144
+ function printActionFailureSummary(message, options) {
145
+ if (options.interactive)
146
+ log.error(message);
147
+ else
148
+ console.error(` ${message}`);
102
149
  }
103
150
  function createActionStatus(options, repoName) {
104
151
  if (!options.interactive)
105
152
  return null;
106
- const statusSpinner = spinner();
153
+ const statusSpinner = paddedSpinner();
107
154
  statusSpinner.start(`Processing ${repoName}`);
108
155
  return statusSpinner;
109
156
  }
package/dist/scanner.js CHANGED
@@ -1,18 +1,21 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { spinner } from '@clack/prompts';
4
3
  import { getUpdates } from './advisories.js';
5
4
  import { exists } from './files.js';
6
- import { run } from './shell.js';
5
+ import { runAsync } from './shell.js';
6
+ import { paddedSpinner } from './spinner.js';
7
+ const scanConcurrency = 8;
7
8
  export async function runScan(repos, root, rule, options) {
8
9
  const targetDescription = options.source === 'github' ? `${repos.length} GitHub repos via the GitHub API` : `${repos.length} repos in ${root}`;
9
10
  if (!options.interactive) {
10
11
  console.log(`Scanning ${targetDescription} using rule: ${rule.title}`);
11
12
  return scanRepos(repos, rule, options);
12
13
  }
13
- const scanSpinner = spinner();
14
+ const scanSpinner = paddedSpinner();
14
15
  scanSpinner.start(`Scanning ${targetDescription}`);
15
- const affectedRepos = await scanRepos(repos, rule, options);
16
+ const affectedRepos = await scanRepos(repos, rule, options, (progress) => {
17
+ scanSpinner.message(formatScanProgress(progress));
18
+ });
16
19
  scanSpinner.stop(`Found ${affectedRepos.length} affected repo${affectedRepos.length === 1 ? '' : 's'}.`);
17
20
  return affectedRepos;
18
21
  }
@@ -33,17 +36,36 @@ export async function findRepos(root) {
33
36
  async function isNodeRepo(repoPath) {
34
37
  return await exists(path.join(repoPath, '.git')) && await exists(path.join(repoPath, 'package.json'));
35
38
  }
36
- async function scanRepos(repos, rule, options) {
37
- const affectedRepos = [];
38
- for (const repo of repos) {
39
+ async function scanRepos(repos, rule, options, onProgress) {
40
+ const affectedRepos = Array.from({ length: repos.length }, () => null);
41
+ let scanned = 0;
42
+ let affected = 0;
43
+ let nextIndex = 0;
44
+ const scanNext = async () => {
45
+ const index = nextIndex;
46
+ nextIndex += 1;
47
+ if (index >= repos.length || hasReachedLimit(affected, options))
48
+ return;
49
+ const repo = repos[index];
39
50
  const affectedRepo = await scanRepo(repo, rule, options);
40
- if (!affectedRepo)
41
- continue;
42
- affectedRepos.push(affectedRepo);
43
- if (options.limit > 0 && affectedRepos.length >= options.limit)
44
- break;
45
- }
46
- return affectedRepos;
51
+ scanned += 1;
52
+ if (affectedRepo && !hasReachedLimit(affected, options)) {
53
+ affectedRepos[index] = affectedRepo;
54
+ affected += 1;
55
+ }
56
+ onProgress?.({ scanned, total: repos.length, affected });
57
+ if (!hasReachedLimit(affected, options))
58
+ await scanNext();
59
+ };
60
+ await Promise.all(Array.from({ length: Math.min(scanConcurrency, repos.length) }, scanNext));
61
+ return affectedRepos.filter((repo) => Boolean(repo));
62
+ }
63
+ function formatScanProgress(progress) {
64
+ const affectedLabel = progress.affected === 1 ? 'affected repo' : 'affected repos';
65
+ return `Scanned ${progress.scanned}/${progress.total} repos, ${progress.affected} ${affectedLabel}.`;
66
+ }
67
+ function hasReachedLimit(affected, options) {
68
+ return options.limit > 0 && affected >= options.limit;
47
69
  }
48
70
  async function scanRepo(repo, rule, options) {
49
71
  if (repo.source === 'github')
@@ -56,16 +78,21 @@ async function scanLocalRepo(repo, rule, options) {
56
78
  const updates = getUpdates(packageJson, rule, options.bumpLevels);
57
79
  if (updates.length === 0)
58
80
  return null;
81
+ const githubRepo = await getGithubRepoFromLocalRemote(repo);
82
+ if (githubRepo && await hasOpenAdvisoryPr(githubRepo, rule, options))
83
+ return null;
59
84
  return { repo, packageJsonPath, updates };
60
85
  }
61
86
  async function scanGithubRepo(repo, rule, options) {
62
- const packageJsonText = getGithubPackageJson(repo);
87
+ const packageJsonText = await getGithubPackageJson(repo);
63
88
  if (!packageJsonText)
64
89
  return null;
65
90
  const packageJson = JSON.parse(packageJsonText);
66
91
  const updates = getUpdates(packageJson, rule, options.bumpLevels);
67
92
  if (updates.length === 0)
68
93
  return null;
94
+ if (await hasOpenAdvisoryPr(repo.nameWithOwner, rule, options))
95
+ return null;
69
96
  return {
70
97
  repo: repo.nameWithOwner,
71
98
  packageJsonPath: 'package.json',
@@ -73,8 +100,8 @@ async function scanGithubRepo(repo, rule, options) {
73
100
  github: repo,
74
101
  };
75
102
  }
76
- function getGithubPackageJson(repo) {
77
- const result = run('gh', [
103
+ async function getGithubPackageJson(repo) {
104
+ const result = await runAsync('gh', [
78
105
  'api',
79
106
  `repos/${repo.nameWithOwner}/contents/package.json`,
80
107
  '--method',
@@ -86,3 +113,45 @@ function getGithubPackageJson(repo) {
86
113
  ], process.cwd(), { capture: true, allowFailure: true });
87
114
  return result?.stdout.trim() ? result.stdout : null;
88
115
  }
116
+ async function hasOpenAdvisoryPr(repo, rule, options) {
117
+ const result = await runAsync('gh', [
118
+ 'pr',
119
+ 'list',
120
+ '--repo',
121
+ repo,
122
+ '--state',
123
+ 'open',
124
+ '--head',
125
+ getAdvisoryBranchName(rule, options),
126
+ '--json',
127
+ 'number',
128
+ '--limit',
129
+ '1',
130
+ ], process.cwd(), { capture: true, allowFailure: true });
131
+ if (!result?.stdout.trim())
132
+ return false;
133
+ return JSON.parse(result.stdout).length > 0;
134
+ }
135
+ async function getGithubRepoFromLocalRemote(repo) {
136
+ const result = await runAsync('git', ['remote', '-v'], repo, { capture: true, allowFailure: true });
137
+ if (!result?.stdout.trim())
138
+ return null;
139
+ for (const line of result.stdout.trim().split('\n')) {
140
+ const match = line.match(/^\S+\s+(?<url>\S+)\s+\(fetch\)$/);
141
+ const ownerRepo = match?.groups?.url ? parseGithubOwnerRepo(match.groups.url) : null;
142
+ if (ownerRepo)
143
+ return ownerRepo;
144
+ }
145
+ return null;
146
+ }
147
+ function parseGithubOwnerRepo(url) {
148
+ const normalized = url
149
+ .replace(/^git@github\.com:/, 'https://github.com/')
150
+ .replace(/^ssh:\/\/git@github\.com\//, 'https://github.com/')
151
+ .replace(/\.git$/, '');
152
+ const match = normalized.match(/github\.com[/:](?<owner>[^/]+)\/(?<repo>[^/]+)$/);
153
+ return match ? `${match.groups?.owner}/${match.groups?.repo}` : null;
154
+ }
155
+ function getAdvisoryBranchName(rule, options) {
156
+ return `${options.branchPrefix}/${rule.branchName}`;
157
+ }
@@ -0,0 +1,12 @@
1
+ import { spinner } from '@clack/prompts';
2
+ const bottomPaddingLines = 2;
3
+ export function paddedSpinner() {
4
+ reserveBottomPadding();
5
+ return spinner();
6
+ }
7
+ function reserveBottomPadding() {
8
+ if (!process.stdout.isTTY)
9
+ return;
10
+ process.stdout.write('\n'.repeat(bottomPaddingLines));
11
+ process.stdout.write(`\x1b[${bottomPaddingLines}A`);
12
+ }
package/dist/targets.js CHANGED
@@ -2,9 +2,9 @@ import fs from 'node:fs/promises';
2
2
  import fsSync from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
- import { spinner } from '@clack/prompts';
6
5
  import { run, runAsync } from './shell.js';
7
6
  import { findRepos } from './scanner.js';
7
+ import { paddedSpinner } from './spinner.js';
8
8
  const cleanupPaths = new Set();
9
9
  let cleanupHooksRegistered = false;
10
10
  export async function resolveTargets(options) {
@@ -37,21 +37,21 @@ export async function cloneGithubRepo(repo, quiet = false) {
37
37
  }
38
38
  }
39
39
  async function resolveGithubTargets(target, options) {
40
- const targetSpinner = options.interactive ? spinner() : null;
40
+ const targetSpinner = options.interactive ? paddedSpinner() : null;
41
41
  targetSpinner?.start(`Loading GitHub repos from ${target}`);
42
- const repos = getGithubRepos(target);
42
+ const repos = await getGithubRepos(target);
43
43
  targetSpinner?.stop(`Loaded ${repos.length} GitHub repo${repos.length === 1 ? '' : 's'}.`);
44
44
  return { root: normalizeGithubTarget(target), repos: repos.map(toGithubRepoTarget) };
45
45
  }
46
- function getGithubRepos(target) {
46
+ async function getGithubRepos(target) {
47
47
  const normalizedTarget = normalizeGithubTarget(target);
48
48
  if (isGithubRepoTarget(normalizedTarget))
49
- return [getGithubRepo(normalizedTarget)];
50
- const result = run('gh', ['repo', 'list', normalizedTarget, '--json', 'nameWithOwner,sshUrl,defaultBranchRef', '--limit', '1000'], process.cwd(), { capture: true });
49
+ return [await getGithubRepo(normalizedTarget)];
50
+ const result = await runAsync('gh', ['repo', 'list', normalizedTarget, '--json', 'nameWithOwner,sshUrl,defaultBranchRef', '--limit', '1000'], process.cwd(), { capture: true });
51
51
  return JSON.parse(result.stdout);
52
52
  }
53
- function getGithubRepo(target) {
54
- const result = run('gh', ['repo', 'view', target, '--json', 'nameWithOwner,sshUrl,defaultBranchRef'], process.cwd(), { capture: true });
53
+ async function getGithubRepo(target) {
54
+ const result = await runAsync('gh', ['repo', 'view', target, '--json', 'nameWithOwner,sshUrl,defaultBranchRef'], process.cwd(), { capture: true });
55
55
  return JSON.parse(result.stdout);
56
56
  }
57
57
  function toGithubRepoTarget(repo) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@humaan/patch-patrol",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Scan local repositories for advisory-driven package bumps, update them, and open pull requests.",
5
5
  "type": "module",
6
6
  "bin": {