@fuzdev/fuz_gitops 0.78.1 → 0.79.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.
Files changed (179) hide show
  1. package/README.md +78 -24
  2. package/dist/ModulesDetail.svelte +15 -22
  3. package/dist/ModulesDetail.svelte.d.ts.map +1 -1
  4. package/dist/ModulesNav.svelte +4 -1
  5. package/dist/ModulesPage.svelte +1 -3
  6. package/dist/ModulesPage.svelte.d.ts.map +1 -1
  7. package/dist/PageHeader.svelte +2 -2
  8. package/dist/PageHeader.svelte.d.ts +0 -3
  9. package/dist/PageHeader.svelte.d.ts.map +1 -1
  10. package/dist/PullRequestsDetail.svelte +10 -9
  11. package/dist/PullRequestsDetail.svelte.d.ts.map +1 -1
  12. package/dist/PullRequestsPage.svelte +1 -3
  13. package/dist/PullRequestsPage.svelte.d.ts.map +1 -1
  14. package/dist/ReposTable.svelte +27 -48
  15. package/dist/ReposTable.svelte.d.ts.map +1 -1
  16. package/dist/ReposTree.svelte +5 -19
  17. package/dist/ReposTree.svelte.d.ts.map +1 -1
  18. package/dist/ReposTreeNav.svelte +5 -7
  19. package/dist/ReposTreeNav.svelte.d.ts.map +1 -1
  20. package/dist/TablePage.svelte +1 -3
  21. package/dist/TablePage.svelte.d.ts.map +1 -1
  22. package/dist/TreeItemPage.svelte +1 -3
  23. package/dist/TreeItemPage.svelte.d.ts.map +1 -1
  24. package/dist/TreePage.svelte +1 -3
  25. package/dist/TreePage.svelte.d.ts.map +1 -1
  26. package/dist/ci_reconcile.d.ts +6 -10
  27. package/dist/ci_reconcile.d.ts.map +1 -1
  28. package/dist/ci_reconcile.js +6 -8
  29. package/dist/dependency_graph.d.ts +26 -49
  30. package/dist/dependency_graph.d.ts.map +1 -1
  31. package/dist/dependency_graph.js +37 -80
  32. package/dist/dependency_updater.d.ts +1 -1
  33. package/dist/dependency_updater.d.ts.map +1 -1
  34. package/dist/dependency_updater.js +6 -1
  35. package/dist/fetch_repo_data.d.ts +34 -8
  36. package/dist/fetch_repo_data.d.ts.map +1 -1
  37. package/dist/fetch_repo_data.js +44 -22
  38. package/dist/fs_fetch_value_cache.d.ts +1 -1
  39. package/dist/fs_fetch_value_cache.js +1 -1
  40. package/dist/git_operations.d.ts +6 -49
  41. package/dist/git_operations.d.ts.map +1 -1
  42. package/dist/git_operations.js +13 -126
  43. package/dist/github.d.ts +17 -2
  44. package/dist/github.d.ts.map +1 -1
  45. package/dist/github.js +15 -6
  46. package/dist/github_helpers.d.ts.map +1 -1
  47. package/dist/github_helpers.js +3 -11
  48. package/dist/gitops_analyze.task.d.ts +24 -2
  49. package/dist/gitops_analyze.task.d.ts.map +1 -1
  50. package/dist/gitops_analyze.task.js +43 -30
  51. package/dist/gitops_config.d.ts +34 -79
  52. package/dist/gitops_config.d.ts.map +1 -1
  53. package/dist/gitops_config.js +76 -53
  54. package/dist/gitops_constants.d.ts +6 -4
  55. package/dist/gitops_constants.d.ts.map +1 -1
  56. package/dist/gitops_constants.js +6 -4
  57. package/dist/gitops_plan.task.d.ts +29 -3
  58. package/dist/gitops_plan.task.d.ts.map +1 -1
  59. package/dist/gitops_plan.task.js +80 -58
  60. package/dist/gitops_publish.task.d.ts +62 -2
  61. package/dist/gitops_publish.task.d.ts.map +1 -1
  62. package/dist/gitops_publish.task.js +187 -102
  63. package/dist/gitops_run.task.d.ts +74 -0
  64. package/dist/gitops_run.task.d.ts.map +1 -1
  65. package/dist/gitops_run.task.js +174 -160
  66. package/dist/gitops_sync.task.d.ts +65 -3
  67. package/dist/gitops_sync.task.d.ts.map +1 -1
  68. package/dist/gitops_sync.task.js +154 -30
  69. package/dist/gitops_task_helpers.d.ts +86 -59
  70. package/dist/gitops_task_helpers.d.ts.map +1 -1
  71. package/dist/gitops_task_helpers.js +120 -79
  72. package/dist/gitops_validate.task.d.ts +1 -2
  73. package/dist/gitops_validate.task.d.ts.map +1 -1
  74. package/dist/gitops_validate.task.js +15 -22
  75. package/dist/graph_validation.d.ts +16 -27
  76. package/dist/graph_validation.d.ts.map +1 -1
  77. package/dist/graph_validation.js +23 -64
  78. package/dist/local_repo.d.ts +50 -62
  79. package/dist/local_repo.d.ts.map +1 -1
  80. package/dist/local_repo.js +80 -193
  81. package/dist/log_helpers.d.ts +8 -8
  82. package/dist/log_helpers.d.ts.map +1 -1
  83. package/dist/multi_repo_publisher.d.ts +10 -0
  84. package/dist/multi_repo_publisher.d.ts.map +1 -1
  85. package/dist/multi_repo_publisher.js +115 -51
  86. package/dist/npm_registry.d.ts +23 -19
  87. package/dist/npm_registry.d.ts.map +1 -1
  88. package/dist/npm_registry.js +20 -47
  89. package/dist/operations.d.ts +75 -180
  90. package/dist/operations.d.ts.map +1 -1
  91. package/dist/operations.js +15 -7
  92. package/dist/operations_defaults.d.ts +17 -2
  93. package/dist/operations_defaults.d.ts.map +1 -1
  94. package/dist/operations_defaults.js +104 -121
  95. package/dist/output_helpers.d.ts +36 -0
  96. package/dist/output_helpers.d.ts.map +1 -1
  97. package/dist/output_helpers.js +37 -3
  98. package/dist/preflight_checks.d.ts +20 -27
  99. package/dist/preflight_checks.d.ts.map +1 -1
  100. package/dist/preflight_checks.js +37 -126
  101. package/dist/publish_steps.d.ts +2 -4
  102. package/dist/publish_steps.d.ts.map +1 -1
  103. package/dist/publish_steps.js +2 -6
  104. package/dist/publishing_event.d.ts +2 -8
  105. package/dist/publishing_event.d.ts.map +1 -1
  106. package/dist/publishing_event.js +5 -4
  107. package/dist/publishing_event_handler.d.ts +10 -8
  108. package/dist/publishing_event_handler.d.ts.map +1 -1
  109. package/dist/publishing_event_handler.js +14 -12
  110. package/dist/publishing_plan.d.ts +16 -1
  111. package/dist/publishing_plan.d.ts.map +1 -1
  112. package/dist/publishing_plan.js +328 -250
  113. package/dist/publishing_plan_logging.d.ts +1 -1
  114. package/dist/publishing_plan_logging.d.ts.map +1 -1
  115. package/dist/publishing_plan_logging.js +21 -14
  116. package/dist/repo.svelte.d.ts +7 -0
  117. package/dist/repo.svelte.d.ts.map +1 -1
  118. package/dist/repo.svelte.js +3 -0
  119. package/dist/repo_readiness.d.ts +210 -0
  120. package/dist/repo_readiness.d.ts.map +1 -0
  121. package/dist/repo_readiness.js +516 -0
  122. package/dist/repos_status.d.ts +3340 -0
  123. package/dist/repos_status.d.ts.map +1 -0
  124. package/dist/repos_status.js +555 -0
  125. package/dist/repos_status_load.d.ts +65 -0
  126. package/dist/repos_status_load.d.ts.map +1 -0
  127. package/dist/repos_status_load.js +131 -0
  128. package/package.json +7 -27
  129. package/src/lib/ci_reconcile.ts +7 -11
  130. package/src/lib/dependency_graph.ts +45 -97
  131. package/src/lib/dependency_updater.ts +7 -1
  132. package/src/lib/fetch_repo_data.ts +66 -29
  133. package/src/lib/fs_fetch_value_cache.ts +1 -1
  134. package/src/lib/git_operations.ts +15 -187
  135. package/src/lib/github.ts +20 -8
  136. package/src/lib/github_helpers.ts +5 -12
  137. package/src/lib/gitops_analyze.task.ts +79 -41
  138. package/src/lib/gitops_config.ts +88 -137
  139. package/src/lib/gitops_constants.ts +6 -4
  140. package/src/lib/gitops_plan.task.ts +113 -61
  141. package/src/lib/gitops_publish.task.ts +255 -122
  142. package/src/lib/gitops_run.task.ts +250 -178
  143. package/src/lib/gitops_sync.task.ts +208 -33
  144. package/src/lib/gitops_task_helpers.ts +166 -110
  145. package/src/lib/gitops_validate.task.ts +17 -27
  146. package/src/lib/graph_validation.ts +25 -86
  147. package/src/lib/local_repo.ts +109 -316
  148. package/src/lib/log_helpers.ts +8 -18
  149. package/src/lib/multi_repo_publisher.ts +138 -53
  150. package/src/lib/npm_registry.ts +37 -62
  151. package/src/lib/operations.ts +78 -150
  152. package/src/lib/operations_defaults.ts +108 -154
  153. package/src/lib/output_helpers.ts +55 -3
  154. package/src/lib/preflight_checks.ts +47 -183
  155. package/src/lib/publish_steps.ts +15 -13
  156. package/src/lib/publishing_event.ts +5 -4
  157. package/src/lib/publishing_event_handler.ts +18 -16
  158. package/src/lib/publishing_plan.ts +451 -287
  159. package/src/lib/publishing_plan_logging.ts +32 -23
  160. package/src/lib/repo.svelte.ts +8 -0
  161. package/src/lib/repo_readiness.ts +647 -0
  162. package/src/lib/repos_status.ts +671 -0
  163. package/src/lib/repos_status_load.ts +173 -0
  164. package/dist/config_reconcile.d.ts +0 -46
  165. package/dist/config_reconcile.d.ts.map +0 -1
  166. package/dist/config_reconcile.js +0 -64
  167. package/dist/paths.d.ts +0 -7
  168. package/dist/paths.d.ts.map +0 -1
  169. package/dist/paths.js +0 -6
  170. package/dist/repo_ops.d.ts +0 -59
  171. package/dist/repo_ops.d.ts.map +0 -1
  172. package/dist/repo_ops.js +0 -170
  173. package/dist/resolved_gitops_config.d.ts +0 -9
  174. package/dist/resolved_gitops_config.d.ts.map +0 -1
  175. package/dist/resolved_gitops_config.js +0 -12
  176. package/src/lib/config_reconcile.ts +0 -98
  177. package/src/lib/paths.ts +0 -6
  178. package/src/lib/repo_ops.ts +0 -216
  179. package/src/lib/resolved_gitops_config.ts +0 -27
@@ -1,14 +1,6 @@
1
1
  import { spawn_out, spawn_result_to_message } from '@fuzdev/fuz_util/process.ts';
2
2
  import type { SpawnOptions } from 'node:child_process';
3
- import {
4
- git_check_clean_workspace as gro_git_check_clean_workspace,
5
- git_checkout as gro_git_checkout,
6
- git_pull as gro_git_pull,
7
- git_current_branch_name as gro_git_current_branch_name,
8
- git_current_commit_hash as gro_git_current_commit_hash,
9
- type GitBranch,
10
- type GitOrigin
11
- } from '@fuzdev/fuz_util/git.ts';
3
+ import { git_current_commit_hash as gro_git_current_commit_hash } from '@fuzdev/fuz_util/git.ts';
12
4
 
13
5
  /**
14
6
  * Adds files to git staging area and throws if anything goes wrong.
@@ -27,201 +19,37 @@ export const git_add = async (
27
19
  };
28
20
 
29
21
  /**
30
- * Commits staged changes with a message and throws if anything goes wrong.
22
+ * Commits `files` alone with a message, leaving anything else staged out of
23
+ * the commit, and throws if anything goes wrong. `files` must be non-empty:
24
+ * an empty list would commit the whole index.
31
25
  */
32
- export const git_commit = async (message: string, options?: SpawnOptions): Promise<void> => {
33
- const { result, stderr } = await spawn_out('git', ['commit', '-m', message], options);
34
- if (!result.ok) {
35
- throw Error(
36
- `git_commit failed with ${spawn_result_to_message(result)}${stderr ? ': ' + stderr.trim() : ''}`
37
- );
38
- }
39
- };
40
-
41
- /**
42
- * Adds files and commits in one operation and throws if anything goes wrong.
43
- */
44
- export const git_add_and_commit = async (
45
- files: string | Array<string>,
26
+ export const git_commit = async (
46
27
  message: string,
28
+ files: Array<string>,
47
29
  options?: SpawnOptions
48
30
  ): Promise<void> => {
49
- await git_add(files, options);
50
- await git_commit(message, options);
51
- };
52
-
53
- /**
54
- * Creates a git tag and throws if anything goes wrong.
55
- */
56
- export const git_tag = async (
57
- tag_name: string,
58
- message?: string,
59
- options?: SpawnOptions
60
- ): Promise<void> => {
61
- const args = message ? ['tag', '-a', tag_name, '-m', message] : ['tag', tag_name];
62
-
63
- const { result, stderr } = await spawn_out('git', args, options);
64
- if (!result.ok) {
65
- throw Error(
66
- `git_tag failed for tag '${tag_name}' with ${spawn_result_to_message(result)}${stderr ? ': ' + stderr.trim() : ''}`
67
- );
68
- }
69
- };
70
-
71
- /**
72
- * Pushes a tag to origin and throws if anything goes wrong.
73
- */
74
- export const git_push_tag = async (
75
- tag_name: string,
76
- origin: GitOrigin = 'origin' as GitOrigin,
77
- options?: SpawnOptions
78
- ): Promise<void> => {
79
- const { result, stderr } = await spawn_out('git', ['push', origin, tag_name], options);
80
- if (!result.ok) {
81
- throw Error(
82
- `git_push_tag failed for tag '${tag_name}' with ${spawn_result_to_message(result)}${stderr ? ': ' + stderr.trim() : ''}`
83
- );
31
+ if (files.length === 0) {
32
+ throw Error('git_commit needs at least one file: an empty list would commit the whole index');
84
33
  }
85
- };
86
-
87
- /**
88
- * Returns `true` if the working tree has any changes — staged, unstaged, or
89
- * untracked (`git status --porcelain`). Broader than `git_list_uncommitted_files`,
90
- * which reports only tracked working-tree changes relative to HEAD.
91
- */
92
- export const git_has_changes = async (options?: SpawnOptions): Promise<boolean> => {
93
- const { stdout } = await spawn_out('git', ['status', '--porcelain'], options);
94
- return stdout ? stdout.trim().length > 0 : false;
95
- };
96
-
97
- /**
98
- * Lists uncommitted files in the working tree (`git diff --name-only HEAD`).
99
- */
100
- export const git_list_uncommitted_files = async (
101
- options?: SpawnOptions
102
- ): Promise<Array<string>> => {
103
- const { stdout } = await spawn_out('git', ['diff', '--name-only', 'HEAD'], options);
104
- if (!stdout) return [];
105
-
106
- return stdout
107
- .split('\n')
108
- .map((f) => f.trim())
109
- .filter(Boolean);
110
- };
111
-
112
- export const git_has_file_changed = async (
113
- from_commit: string,
114
- to_commit: string,
115
- file_path: string,
116
- options?: SpawnOptions
117
- ): Promise<boolean> => {
118
- const { stdout } = await spawn_out(
34
+ const { result, stderr } = await spawn_out(
119
35
  'git',
120
- ['diff', '--name-only', from_commit, to_commit, '--', file_path],
36
+ ['commit', '-m', message, '--', ...files],
121
37
  options
122
38
  );
123
- return stdout ? stdout.trim().length > 0 : false;
124
- };
125
-
126
- /**
127
- * Stashes current changes and throws if anything goes wrong.
128
- */
129
- export const git_stash = async (message?: string, options?: SpawnOptions): Promise<void> => {
130
- const args = message ? ['stash', 'push', '-m', message] : ['stash', 'push'];
131
-
132
- const { result, stderr } = await spawn_out('git', args, options);
133
39
  if (!result.ok) {
134
40
  throw Error(
135
- `git_stash failed with ${spawn_result_to_message(result)}${stderr ? ': ' + stderr.trim() : ''}`
136
- );
137
- }
138
- };
139
-
140
- /**
141
- * Applies stashed changes and throws if anything goes wrong.
142
- */
143
- export const git_stash_pop = async (options?: SpawnOptions): Promise<void> => {
144
- const { result, stderr } = await spawn_out('git', ['stash', 'pop'], options);
145
- if (!result.ok) {
146
- throw Error(
147
- `git_stash_pop failed with ${spawn_result_to_message(result)}${stderr ? ': ' + stderr.trim() : ''}`
41
+ `git_commit failed with ${spawn_result_to_message(result)}${stderr ? ': ' + stderr.trim() : ''}`
148
42
  );
149
43
  }
150
44
  };
151
45
 
152
46
  /**
153
- * Switches to a branch with safety checks and throws if workspace is not clean.
47
+ * Wrapper for gro's `git_current_commit_hash` that reads `HEAD` and throws if null.
154
48
  */
155
- export const git_switch_branch = async (
156
- branch: GitBranch,
157
- pull: boolean = true,
158
- options?: SpawnOptions
159
- ): Promise<void> => {
160
- // Check if workspace is clean first
161
- const error = await gro_git_check_clean_workspace(options);
162
- if (error) {
163
- throw Error(`Cannot switch branch: ${error}`);
164
- }
165
-
166
- // Checkout the branch
167
- await gro_git_checkout(branch, options);
168
-
169
- // Pull latest changes if requested
170
- if (pull) {
171
- await gro_git_pull(undefined, undefined, options);
172
- }
173
-
174
- // Verify workspace is still clean
175
- const error_after = await gro_git_check_clean_workspace(options);
176
- if (error_after) {
177
- throw Error(`Workspace unclean after switching to ${branch}: ${error_after}`);
178
- }
179
- };
180
-
181
- /**
182
- * Wrapper for gro's `git_current_branch_name` that throws if null.
183
- */
184
- export const git_current_branch_name_required = async (options?: SpawnOptions): Promise<string> => {
185
- const branch = await gro_git_current_branch_name(options);
186
- if (!branch) {
187
- throw new Error('Failed to get current branch name');
188
- }
189
- return branch;
190
- };
191
-
192
- /**
193
- * Wrapper for gro's `git_current_commit_hash` that throws if null.
194
- */
195
- export const git_current_commit_hash_required = async (
196
- branch?: string,
197
- options?: SpawnOptions
198
- ): Promise<string> => {
199
- const hash = await gro_git_current_commit_hash(branch, options);
49
+ export const git_current_commit_hash_required = async (options?: SpawnOptions): Promise<string> => {
50
+ const hash = await gro_git_current_commit_hash(undefined, options);
200
51
  if (!hash) {
201
- throw new Error(`Failed to get commit hash for branch: ${branch || 'current'}`);
52
+ throw new Error('Failed to get the current commit hash');
202
53
  }
203
54
  return hash;
204
55
  };
205
-
206
- /**
207
- * Wrapper for gro's `git_check_clean_workspace` that returns a boolean.
208
- */
209
- export const git_check_clean_workspace_as_boolean = async (
210
- options?: SpawnOptions
211
- ): Promise<boolean> => {
212
- const error = await gro_git_check_clean_workspace(options);
213
- return error === null;
214
- };
215
-
216
- export const git_has_remote = async (
217
- remote: string = 'origin',
218
- options?: SpawnOptions
219
- ): Promise<boolean> => {
220
- const { stdout } = await spawn_out('git', ['remote'], options);
221
- if (!stdout) return false;
222
- const remotes = stdout
223
- .split('\n')
224
- .map((r) => r.trim())
225
- .filter(Boolean);
226
- return remotes.includes(remote);
227
- };
package/src/lib/github.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import type { Logger } from '@fuzdev/fuz_util/log.ts';
2
2
  import { z } from 'zod';
3
3
  import { fetch_value, type FetchValueCache } from '@fuzdev/fuz_util/fetch.ts';
4
+ import type { Result } from '@fuzdev/fuz_util/result.ts';
4
5
 
5
6
  /**
6
- * Minimal interface for GitHub API calls - works with both `Pkg` and `Repo`.
7
+ * Minimal interface for GitHub API calls, satisfied structurally by fuz_ui's `Library`.
7
8
  */
8
9
  export interface GithubRepoInfo {
9
10
  owner_name: string | null;
@@ -35,9 +36,10 @@ export const fetch_github_pull_requests = async (
35
36
  log?: Logger;
36
37
  token?: string;
37
38
  api_version?: string;
39
+ fetch?: typeof globalThis.fetch;
38
40
  } = {}
39
41
  ): Promise<GithubPullRequests | null> => {
40
- const { cache, log, token, api_version } = options;
42
+ const { cache, log, token, api_version, fetch } = options;
41
43
  if (!repo_info.owner_name) throw Error('owner_name is required');
42
44
  const headers = api_version ? new Headers({ 'x-github-api-version': api_version }) : undefined;
43
45
  const url = `https://api.github.com/repos/${repo_info.owner_name}/${repo_info.repo_name}/pulls`;
@@ -46,7 +48,8 @@ export const fetch_github_pull_requests = async (
46
48
  parse: GithubPullRequests.parse,
47
49
  token,
48
50
  cache,
49
- log
51
+ log,
52
+ fetch
50
53
  });
51
54
  if (!fetched.ok) {
52
55
  // TODO @many this is messy but I think it's the main case we need to worry about?
@@ -77,6 +80,13 @@ export const GithubCheckRuns = z.object({
77
80
  export type GithubCheckRuns = z.infer<typeof GithubCheckRuns>;
78
81
 
79
82
  /**
83
+ * Fetches the check runs on `ref` and reduces them to one overall status and
84
+ * conclusion.
85
+ *
86
+ * @param repo_info - the repo's GitHub owner and name
87
+ * @param options - the cache, logger, token, API version, ref (`main` by default), and `fetch`
88
+ * @returns the reduced check runs, `null` for a ref with none (a repo without CI, or a commit CI skipped), or a failure when the request or its parse failed
89
+ * @throws {Error} on a 401 response (check `SECRET_GITHUB_API_TOKEN`) or a repo without an owner
80
90
  * @see https://docs.github.com/en/rest/checks/runs?apiVersion=2022-11-28#list-check-runs-for-a-git-reference
81
91
  */
82
92
  export const fetch_github_check_runs = async (
@@ -87,9 +97,10 @@ export const fetch_github_check_runs = async (
87
97
  token?: string;
88
98
  api_version?: string;
89
99
  ref?: string;
100
+ fetch?: typeof globalThis.fetch;
90
101
  } = {}
91
- ): Promise<GithubCheckRunsItem | null> => {
92
- const { cache, log, token, api_version, ref = 'main' } = options;
102
+ ): Promise<Result<{ value: GithubCheckRunsItem | null }, { status: number; message: string }>> => {
103
+ const { cache, log, token, api_version, ref = 'main', fetch } = options;
93
104
  if (!repo_info.owner_name) throw Error('owner_name is required');
94
105
  const headers = api_version ? new Headers({ 'x-github-api-version': api_version }) : undefined;
95
106
  const url = `https://api.github.com/repos/${repo_info.owner_name}/${repo_info.repo_name}/commits/${ref}/check-runs`;
@@ -98,16 +109,17 @@ export const fetch_github_check_runs = async (
98
109
  parse: (v) => reduce_check_runs(GithubCheckRuns.parse(v).check_runs),
99
110
  token,
100
111
  cache,
101
- log
112
+ log,
113
+ fetch
102
114
  });
103
115
  if (!fetched.ok) {
104
116
  // TODO @many this is messy but I think it's the main case we need to worry about?
105
117
  if (fetched.status === 401) {
106
118
  throw Error('401 response fetching github CI status - check your SECRET_GITHUB_API_TOKEN');
107
119
  }
108
- return null;
120
+ return { ok: false, status: fetched.status, message: fetched.message };
109
121
  }
110
- return fetched.value;
122
+ return { ok: true, value: fetched.value };
111
123
  };
112
124
 
113
125
  const reduce_check_runs = (check_runs: Array<GithubCheckRunsItem>): GithubCheckRunsItem | null => {
@@ -14,18 +14,11 @@ export const to_pull_requests = (
14
14
  repos: Array<Repo>,
15
15
  filter_pull_request?: FilterPullRequest
16
16
  ): Array<PullRequestMeta> =>
17
- repos
18
- .flatMap((repo) => {
19
- if (!repo.pull_requests) return null;
20
- // TODO hacky, figure out the data structure
21
- return repo.pull_requests.map((pull_request) =>
22
- repo.package_json.homepage &&
23
- (!filter_pull_request || filter_pull_request(pull_request, repo))
24
- ? { repo, pull_request }
25
- : null
26
- );
27
- })
28
- .filter((v) => v !== null);
17
+ repos.flatMap((repo) =>
18
+ (repo.pull_requests ?? [])
19
+ .filter((pull_request) => !filter_pull_request || filter_pull_request(pull_request, repo))
20
+ .map((pull_request) => ({ repo, pull_request }))
21
+ );
29
22
 
30
23
  export const to_pull_url = (repo_url: string, pull: GithubPullRequest): string =>
31
24
  ensure_end(repo_url, '/') + 'pull/' + pull.number;
@@ -3,16 +3,25 @@ import { z } from 'zod';
3
3
  import { styleText as st } from 'node:util';
4
4
  import type { Logger } from '@fuzdev/fuz_util/log.ts';
5
5
 
6
- import { get_gitops_ready } from './gitops_task_helpers.ts';
7
- import type { DependencyGraph } from './dependency_graph.ts';
8
- import { repo_is_npm } from './local_repo.ts';
9
- import { analyze_repos, type DependencyAnalysis } from './graph_validation.ts';
6
+ import {
7
+ get_gitops_ready,
8
+ log_readiness_block,
9
+ type ResolveGitopsReposOptions
10
+ } from './gitops_task_helpers.ts';
11
+ import type { DependencyAnalysis, DependencyGraph } from './dependency_graph.ts';
12
+ import { repo_is_npm, type LocalRepo } from './local_repo.ts';
13
+ import { analyze_repos } from './graph_validation.ts';
10
14
  import {
11
15
  format_wildcard_dependencies,
12
16
  format_dev_cycles,
13
17
  format_production_cycles
14
18
  } from './log_helpers.ts';
15
- import { format_and_output, type OutputFormatters } from './output_helpers.ts';
19
+ import {
20
+ format_and_output,
21
+ output_is_machine,
22
+ route_human_output,
23
+ type OutputFormatters
24
+ } from './output_helpers.ts';
16
25
  import { GITOPS_CONFIG_PATH_DEFAULT } from './gitops_constants.ts';
17
26
 
18
27
  /** @nodocs */
@@ -21,22 +30,18 @@ export const Args = z.strictObject({
21
30
  .string()
22
31
  .meta({ description: 'path to the gitops config file, absolute or relative to the cwd' })
23
32
  .default(GITOPS_CONFIG_PATH_DEFAULT),
24
- dir: z
33
+ registry: z
25
34
  .string()
26
- .meta({ description: 'path containing the repos, defaults to the parent of the config dir' })
35
+ .meta({
36
+ description:
37
+ 'path to the repos.toml registry, when `repos` would not find it walking up from the cwd'
38
+ })
27
39
  .optional(),
28
40
  format: z
29
41
  .enum(['stdout', 'json', 'markdown'])
30
42
  .meta({ description: 'output format' })
31
43
  .default('stdout'),
32
- outfile: z.string().meta({ description: 'write output to file instead of logging' }).optional(),
33
- sync: z
34
- .boolean()
35
- .meta({
36
- description:
37
- 'sync repos (switch branch, pull, install) before analyzing instead of reading the working tree as-is'
38
- })
39
- .default(false)
44
+ outfile: z.string().meta({ description: 'write output to file instead of logging' }).optional()
40
45
  });
41
46
  export type Args = z.infer<typeof Args>;
42
47
 
@@ -45,36 +50,69 @@ export const task: Task<Args> = {
45
50
  Args,
46
51
  summary: 'analyze dependency structure and relationships across repos',
47
52
  run: async ({ args, log }) => {
48
- const { config, dir, format, outfile, sync } = args;
49
-
50
- // Get repos ready (without downloading); read the working tree as-is unless `--sync`
51
- const { local_repos } = await get_gitops_ready({ config, dir, download: false, sync, log });
52
-
53
- // Only npm packages form the dependency graph; note any non-npm repos (e.g. cargo)
54
- // that are excluded so the omission isn't silent.
55
- const non_npm_repos = local_repos.filter((r) => !repo_is_npm(r));
56
- if (non_npm_repos.length > 0) {
57
- log.info(
58
- st(
59
- 'dim',
60
- `excluding ${non_npm_repos.length} non-npm repo(s) from analysis (dashboard-only): ` +
61
- non_npm_repos.map((r) => r.library.name).join(', ')
62
- )
63
- );
64
- }
53
+ await run_gitops_analyze(args, log);
54
+ }
55
+ };
65
56
 
66
- // Build the dependency graph and analyze cycles/wildcards (tolerating cycles)
67
- const { graph, analysis, publishing_order } = analyze_repos(local_repos);
57
+ /**
58
+ * The side effects `run_gitops_analyze` reaches through, injectable for tests.
59
+ *
60
+ * @nodocs
61
+ */
62
+ export interface GitopsAnalyzeDeps {
63
+ /** Loads the configured repos as they sit (`get_gitops_ready`). */
64
+ load_repos: (options: ResolveGitopsReposOptions) => Promise<{ local_repos: Array<LocalRepo> }>;
65
+ }
66
+
67
+ const default_gitops_analyze_deps: GitopsAnalyzeDeps = {
68
+ load_repos: get_gitops_ready
69
+ };
68
70
 
69
- // Format and output using output_helpers
70
- const data = {
71
- graph,
72
- analysis,
73
- publishing_order
74
- };
71
+ /**
72
+ * Runs `gro gitops_analyze`: loads the repos as they sit, logs the readiness
73
+ * block, and outputs the dependency analysis. Under `--format json` or
74
+ * `markdown` without `--outfile`, the log goes to stderr and stdout carries
75
+ * the document alone (`route_human_output`).
76
+ *
77
+ * @nodocs
78
+ */
79
+ export const run_gitops_analyze = async (
80
+ args: Args,
81
+ log: Logger,
82
+ deps: Partial<GitopsAnalyzeDeps> = {}
83
+ ): Promise<void> => {
84
+ const { load_repos } = { ...default_gitops_analyze_deps, ...deps };
85
+ const { config, registry, format, outfile } = args;
86
+ const write_stdout = route_human_output(log, output_is_machine(format, outfile));
75
87
 
76
- await format_and_output(data, create_formatters(), { format, outfile, log });
88
+ // Read the working trees as they sit, and say which aren't at rest
89
+ const { local_repos } = await load_repos({ config, registry, log });
90
+ log_readiness_block(local_repos, log);
91
+
92
+ // Only npm packages form the dependency graph; note any non-npm repos (e.g. cargo)
93
+ // that are excluded so the omission isn't silent.
94
+ const non_npm_repos = local_repos.filter((r) => !repo_is_npm(r));
95
+ if (non_npm_repos.length > 0) {
96
+ log.info(
97
+ st(
98
+ 'dim',
99
+ `excluding ${non_npm_repos.length} non-npm repo(s) from analysis (dashboard-only): ` +
100
+ non_npm_repos.map((r) => r.library.name).join(', ')
101
+ )
102
+ );
77
103
  }
104
+
105
+ // Build the dependency graph and analyze cycles/wildcards (tolerating cycles)
106
+ const { graph, analysis, publishing_order } = analyze_repos(local_repos);
107
+
108
+ // Format and output using output_helpers
109
+ const data = {
110
+ graph,
111
+ analysis,
112
+ publishing_order
113
+ };
114
+
115
+ await format_and_output(data, create_formatters(), { format, outfile, log, write_stdout });
78
116
  };
79
117
 
80
118
  // Data type for analysis output