@fuzdev/fuz_gitops 0.57.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 (190) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +119 -0
  3. package/dist/ModulesDetail.svelte +180 -0
  4. package/dist/ModulesDetail.svelte.d.ts +10 -0
  5. package/dist/ModulesDetail.svelte.d.ts.map +1 -0
  6. package/dist/ModulesNav.svelte +43 -0
  7. package/dist/ModulesNav.svelte.d.ts +11 -0
  8. package/dist/ModulesNav.svelte.d.ts.map +1 -0
  9. package/dist/ModulesPage.svelte +50 -0
  10. package/dist/ModulesPage.svelte.d.ts +9 -0
  11. package/dist/ModulesPage.svelte.d.ts.map +1 -0
  12. package/dist/PageFooter.svelte +15 -0
  13. package/dist/PageFooter.svelte.d.ts +19 -0
  14. package/dist/PageFooter.svelte.d.ts.map +1 -0
  15. package/dist/PageHeader.svelte +35 -0
  16. package/dist/PageHeader.svelte.d.ts +19 -0
  17. package/dist/PageHeader.svelte.d.ts.map +1 -0
  18. package/dist/PullRequestsDetail.svelte +53 -0
  19. package/dist/PullRequestsDetail.svelte.d.ts +10 -0
  20. package/dist/PullRequestsDetail.svelte.d.ts.map +1 -0
  21. package/dist/PullRequestsPage.svelte +47 -0
  22. package/dist/PullRequestsPage.svelte.d.ts +11 -0
  23. package/dist/PullRequestsPage.svelte.d.ts.map +1 -0
  24. package/dist/ReposTable.svelte +189 -0
  25. package/dist/ReposTable.svelte.d.ts +9 -0
  26. package/dist/ReposTable.svelte.d.ts.map +1 -0
  27. package/dist/ReposTree.svelte +88 -0
  28. package/dist/ReposTree.svelte.d.ts +11 -0
  29. package/dist/ReposTree.svelte.d.ts.map +1 -0
  30. package/dist/ReposTreeNav.svelte +55 -0
  31. package/dist/ReposTreeNav.svelte.d.ts +11 -0
  32. package/dist/ReposTreeNav.svelte.d.ts.map +1 -0
  33. package/dist/TablePage.svelte +46 -0
  34. package/dist/TablePage.svelte.d.ts +9 -0
  35. package/dist/TablePage.svelte.d.ts.map +1 -0
  36. package/dist/TreeItemPage.svelte +75 -0
  37. package/dist/TreeItemPage.svelte.d.ts +10 -0
  38. package/dist/TreeItemPage.svelte.d.ts.map +1 -0
  39. package/dist/TreePage.svelte +64 -0
  40. package/dist/TreePage.svelte.d.ts +9 -0
  41. package/dist/TreePage.svelte.d.ts.map +1 -0
  42. package/dist/changeset_generator.d.ts +38 -0
  43. package/dist/changeset_generator.d.ts.map +1 -0
  44. package/dist/changeset_generator.js +110 -0
  45. package/dist/changeset_reader.d.ts +75 -0
  46. package/dist/changeset_reader.d.ts.map +1 -0
  47. package/dist/changeset_reader.js +167 -0
  48. package/dist/constants.d.ts +9 -0
  49. package/dist/constants.d.ts.map +1 -0
  50. package/dist/constants.js +8 -0
  51. package/dist/dependency_graph.d.ts +120 -0
  52. package/dist/dependency_graph.d.ts.map +1 -0
  53. package/dist/dependency_graph.js +341 -0
  54. package/dist/dependency_updater.d.ts +46 -0
  55. package/dist/dependency_updater.d.ts.map +1 -0
  56. package/dist/dependency_updater.js +213 -0
  57. package/dist/fetch_repo_data.d.ts +19 -0
  58. package/dist/fetch_repo_data.d.ts.map +1 -0
  59. package/dist/fetch_repo_data.js +49 -0
  60. package/dist/fs_fetch_value_cache.d.ts +24 -0
  61. package/dist/fs_fetch_value_cache.d.ts.map +1 -0
  62. package/dist/fs_fetch_value_cache.js +61 -0
  63. package/dist/git_operations.d.ts +54 -0
  64. package/dist/git_operations.d.ts.map +1 -0
  65. package/dist/git_operations.js +144 -0
  66. package/dist/github.d.ts +91 -0
  67. package/dist/github.d.ts.map +1 -0
  68. package/dist/github.js +94 -0
  69. package/dist/github_helpers.d.ts +10 -0
  70. package/dist/github_helpers.d.ts.map +1 -0
  71. package/dist/github_helpers.js +13 -0
  72. package/dist/gitops_analyze.task.d.ts +17 -0
  73. package/dist/gitops_analyze.task.d.ts.map +1 -0
  74. package/dist/gitops_analyze.task.js +188 -0
  75. package/dist/gitops_config.d.ts +56 -0
  76. package/dist/gitops_config.d.ts.map +1 -0
  77. package/dist/gitops_config.js +63 -0
  78. package/dist/gitops_plan.task.d.ts +28 -0
  79. package/dist/gitops_plan.task.d.ts.map +1 -0
  80. package/dist/gitops_plan.task.js +217 -0
  81. package/dist/gitops_publish.task.d.ts +29 -0
  82. package/dist/gitops_publish.task.d.ts.map +1 -0
  83. package/dist/gitops_publish.task.js +178 -0
  84. package/dist/gitops_sync.task.d.ts +18 -0
  85. package/dist/gitops_sync.task.d.ts.map +1 -0
  86. package/dist/gitops_sync.task.js +95 -0
  87. package/dist/gitops_task_helpers.d.ts +63 -0
  88. package/dist/gitops_task_helpers.d.ts.map +1 -0
  89. package/dist/gitops_task_helpers.js +84 -0
  90. package/dist/gitops_validate.task.d.ts +12 -0
  91. package/dist/gitops_validate.task.d.ts.map +1 -0
  92. package/dist/gitops_validate.task.js +210 -0
  93. package/dist/graph_validation.d.ts +39 -0
  94. package/dist/graph_validation.d.ts.map +1 -0
  95. package/dist/graph_validation.js +79 -0
  96. package/dist/local_repo.d.ts +84 -0
  97. package/dist/local_repo.d.ts.map +1 -0
  98. package/dist/local_repo.js +213 -0
  99. package/dist/log_helpers.d.ts +43 -0
  100. package/dist/log_helpers.d.ts.map +1 -0
  101. package/dist/log_helpers.js +98 -0
  102. package/dist/multi_repo_publisher.d.ts +34 -0
  103. package/dist/multi_repo_publisher.d.ts.map +1 -0
  104. package/dist/multi_repo_publisher.js +364 -0
  105. package/dist/npm_install_helpers.d.ts +23 -0
  106. package/dist/npm_install_helpers.d.ts.map +1 -0
  107. package/dist/npm_install_helpers.js +60 -0
  108. package/dist/npm_registry.d.ts +46 -0
  109. package/dist/npm_registry.d.ts.map +1 -0
  110. package/dist/npm_registry.js +96 -0
  111. package/dist/operations.d.ts +409 -0
  112. package/dist/operations.d.ts.map +1 -0
  113. package/dist/operations.js +34 -0
  114. package/dist/operations_defaults.d.ts +19 -0
  115. package/dist/operations_defaults.d.ts.map +1 -0
  116. package/dist/operations_defaults.js +279 -0
  117. package/dist/output_helpers.d.ts +27 -0
  118. package/dist/output_helpers.d.ts.map +1 -0
  119. package/dist/output_helpers.js +39 -0
  120. package/dist/paths.d.ts +11 -0
  121. package/dist/paths.d.ts.map +1 -0
  122. package/dist/paths.js +10 -0
  123. package/dist/preflight_checks.d.ts +47 -0
  124. package/dist/preflight_checks.d.ts.map +1 -0
  125. package/dist/preflight_checks.js +181 -0
  126. package/dist/publishing_plan.d.ts +100 -0
  127. package/dist/publishing_plan.d.ts.map +1 -0
  128. package/dist/publishing_plan.js +353 -0
  129. package/dist/publishing_plan_helpers.d.ts +30 -0
  130. package/dist/publishing_plan_helpers.d.ts.map +1 -0
  131. package/dist/publishing_plan_helpers.js +112 -0
  132. package/dist/publishing_plan_logging.d.ts +18 -0
  133. package/dist/publishing_plan_logging.d.ts.map +1 -0
  134. package/dist/publishing_plan_logging.js +342 -0
  135. package/dist/repo.svelte.d.ts +52 -0
  136. package/dist/repo.svelte.d.ts.map +1 -0
  137. package/dist/repo.svelte.js +70 -0
  138. package/dist/repo_ops.d.ts +57 -0
  139. package/dist/repo_ops.d.ts.map +1 -0
  140. package/dist/repo_ops.js +167 -0
  141. package/dist/resolved_gitops_config.d.ts +9 -0
  142. package/dist/resolved_gitops_config.d.ts.map +1 -0
  143. package/dist/resolved_gitops_config.js +12 -0
  144. package/dist/semver.d.ts +24 -0
  145. package/dist/semver.d.ts.map +1 -0
  146. package/dist/semver.js +140 -0
  147. package/dist/serialization_types.d.ts +57 -0
  148. package/dist/serialization_types.d.ts.map +1 -0
  149. package/dist/serialization_types.js +40 -0
  150. package/dist/version_utils.d.ts +48 -0
  151. package/dist/version_utils.d.ts.map +1 -0
  152. package/dist/version_utils.js +125 -0
  153. package/package.json +107 -0
  154. package/src/lib/changeset_generator.ts +162 -0
  155. package/src/lib/changeset_reader.ts +218 -0
  156. package/src/lib/constants.ts +8 -0
  157. package/src/lib/dependency_graph.ts +423 -0
  158. package/src/lib/dependency_updater.ts +297 -0
  159. package/src/lib/fetch_repo_data.ts +64 -0
  160. package/src/lib/fs_fetch_value_cache.ts +75 -0
  161. package/src/lib/git_operations.ts +208 -0
  162. package/src/lib/github.ts +128 -0
  163. package/src/lib/github_helpers.ts +31 -0
  164. package/src/lib/gitops_analyze.task.ts +261 -0
  165. package/src/lib/gitops_config.ts +123 -0
  166. package/src/lib/gitops_plan.task.ts +272 -0
  167. package/src/lib/gitops_publish.task.ts +227 -0
  168. package/src/lib/gitops_sync.task.ts +109 -0
  169. package/src/lib/gitops_task_helpers.ts +126 -0
  170. package/src/lib/gitops_validate.task.ts +248 -0
  171. package/src/lib/graph_validation.ts +109 -0
  172. package/src/lib/local_repo.ts +359 -0
  173. package/src/lib/log_helpers.ts +147 -0
  174. package/src/lib/multi_repo_publisher.ts +464 -0
  175. package/src/lib/npm_install_helpers.ts +85 -0
  176. package/src/lib/npm_registry.ts +143 -0
  177. package/src/lib/operations.ts +334 -0
  178. package/src/lib/operations_defaults.ts +335 -0
  179. package/src/lib/output_helpers.ts +64 -0
  180. package/src/lib/paths.ts +11 -0
  181. package/src/lib/preflight_checks.ts +269 -0
  182. package/src/lib/publishing_plan.ts +531 -0
  183. package/src/lib/publishing_plan_helpers.ts +145 -0
  184. package/src/lib/publishing_plan_logging.ts +470 -0
  185. package/src/lib/repo.svelte.ts +95 -0
  186. package/src/lib/repo_ops.ts +213 -0
  187. package/src/lib/resolved_gitops_config.ts +27 -0
  188. package/src/lib/semver.ts +166 -0
  189. package/src/lib/serialization_types.ts +90 -0
  190. package/src/lib/version_utils.ts +150 -0
@@ -0,0 +1,261 @@
1
+ import type {Task} from '@ryanatkn/gro';
2
+ import {z} from 'zod';
3
+ import {styleText as st} from 'node:util';
4
+ import type {Logger} from '@fuzdev/fuz_util/log.js';
5
+
6
+ import {get_gitops_ready} from './gitops_task_helpers.js';
7
+ import {type DependencyGraph, DependencyGraphBuilder} from './dependency_graph.js';
8
+ import type {LocalRepo} from './local_repo.js';
9
+ import {validate_dependency_graph} from './graph_validation.js';
10
+ import {
11
+ format_wildcard_dependencies,
12
+ format_dev_cycles,
13
+ format_production_cycles,
14
+ } from './log_helpers.js';
15
+ import {format_and_output, type OutputFormatters} from './output_helpers.js';
16
+
17
+ /** @nodocs */
18
+ export const Args = z.strictObject({
19
+ path: z
20
+ .string()
21
+ .meta({description: 'path to the gitops config file, absolute or relative to the cwd'})
22
+ .default('gitops.config.ts'),
23
+ dir: z
24
+ .string()
25
+ .meta({description: 'path containing the repos, defaults to the parent of the `path` dir'})
26
+ .optional(),
27
+ format: z
28
+ .enum(['stdout', 'json', 'markdown'])
29
+ .meta({description: 'output format'})
30
+ .default('stdout'),
31
+ outfile: z.string().meta({description: 'write output to file instead of logging'}).optional(),
32
+ });
33
+ export type Args = z.infer<typeof Args>;
34
+
35
+ /** @nodocs */
36
+ export const task: Task<Args> = {
37
+ Args,
38
+ summary: 'analyze dependency structure and relationships across repos',
39
+ run: async ({args, log}) => {
40
+ const {path, dir, format, outfile} = args;
41
+
42
+ // Get repos ready (without downloading)
43
+ const {local_repos} = await get_gitops_ready({path, dir, download: false, log});
44
+
45
+ // Build dependency graph and validate (but don't throw on cycles for analyze)
46
+ const {graph, publishing_order: order} = validate_dependency_graph(local_repos, {
47
+ log,
48
+ throw_on_prod_cycles: false, // Analyze should report, not throw
49
+ log_cycles: false, // We'll show cycles in our formatted output
50
+ log_order: false, // We'll show order in our formatted output
51
+ });
52
+
53
+ // Perform additional analysis
54
+ const builder = new DependencyGraphBuilder();
55
+ const analysis = builder.analyze(graph);
56
+
57
+ // Publishing order (may be null if prod cycles exist)
58
+ const publishing_order = order.length > 0 ? order : null;
59
+
60
+ // Format and output using output_helpers
61
+ const data = {
62
+ repos: local_repos,
63
+ graph,
64
+ analysis,
65
+ publishing_order,
66
+ };
67
+
68
+ await format_and_output(data, create_formatters(), {format, outfile, log});
69
+ },
70
+ };
71
+
72
+ // Data type for analysis output
73
+ interface AnalysisData {
74
+ repos: Array<LocalRepo>;
75
+ graph: DependencyGraph;
76
+ analysis: ReturnType<DependencyGraphBuilder['analyze']>;
77
+ publishing_order: Array<string> | null;
78
+ }
79
+
80
+ // Create formatters for output_helpers
81
+ const create_formatters = (): OutputFormatters<AnalysisData> => ({
82
+ json: (data) => format_json(data.graph, data.analysis, data.publishing_order),
83
+ markdown: (data) => format_markdown(data.repos, data.graph, data.analysis, data.publishing_order),
84
+ stdout: (data, log) =>
85
+ format_stdout(data.repos, data.graph, data.analysis, data.publishing_order, log),
86
+ });
87
+
88
+ // Helper to calculate common statistics
89
+ const calculate_stats = (graph: DependencyGraph) => {
90
+ const total_deps = Array.from(graph.nodes.values()).reduce(
91
+ (sum, node) => sum + node.dependencies.size,
92
+ 0,
93
+ );
94
+ const internal_deps = Array.from(graph.nodes.values()).reduce(
95
+ (sum, node) =>
96
+ sum + Array.from(node.dependencies.keys()).filter((name) => graph.nodes.has(name)).length,
97
+ 0,
98
+ );
99
+ return {total_deps, internal_deps};
100
+ };
101
+
102
+ const format_json = (
103
+ graph: DependencyGraph,
104
+ analysis: ReturnType<DependencyGraphBuilder['analyze']>,
105
+ publishing_order: Array<string> | null,
106
+ ): string => {
107
+ const output = {
108
+ graph: graph.toJSON(),
109
+ analysis,
110
+ publishing_order,
111
+ };
112
+ return JSON.stringify(output, null, 2);
113
+ };
114
+
115
+ const format_markdown = (
116
+ repos: Array<LocalRepo>,
117
+ graph: DependencyGraph,
118
+ analysis: ReturnType<DependencyGraphBuilder['analyze']>,
119
+ publishing_order: Array<string> | null,
120
+ ): Array<string> => {
121
+ const lines: Array<string> = ['# Dependency Analysis'];
122
+
123
+ // Summary stats
124
+ const {total_deps, internal_deps} = calculate_stats(graph);
125
+
126
+ lines.push('', '## Summary', '');
127
+ lines.push(`- **Total packages**: ${repos.length}`);
128
+ lines.push(`- **Total dependencies**: ${total_deps}`);
129
+ lines.push(`- **Internal dependencies**: ${internal_deps}`);
130
+ lines.push(`- **Wildcard dependencies**: ${analysis.wildcard_deps.length}`);
131
+ lines.push(`- **Production/peer cycles**: ${analysis.production_cycles.length}`);
132
+ lines.push(`- **Dev cycles**: ${analysis.dev_cycles.length}`);
133
+
134
+ // Publishing order
135
+ if (publishing_order) {
136
+ lines.push('', '## Publishing Order', '');
137
+ publishing_order.forEach((name, i) => {
138
+ const node = graph.get_node(name);
139
+ const version = node ? `v${node.version}` : '';
140
+ lines.push(`${i + 1}. \`${name}\` ${version}`);
141
+ });
142
+ }
143
+
144
+ // Cycles (show problems first)
145
+ if (analysis.production_cycles.length > 0) {
146
+ lines.push('', '## ❌ Production/Peer Circular Dependencies', '');
147
+ lines.push('> **These block publishing and must be resolved!**');
148
+ lines.push('');
149
+ for (const cycle of analysis.production_cycles) {
150
+ lines.push(`- ${cycle.map((n) => `\`${n}\``).join(' → ')}`);
151
+ }
152
+ }
153
+
154
+ if (analysis.dev_cycles.length > 0) {
155
+ lines.push('', '## ⚠️ Dev Circular Dependencies', '');
156
+ lines.push('> These are normal and do not block publishing.');
157
+ lines.push('');
158
+ for (const cycle of analysis.dev_cycles) {
159
+ lines.push(`- ${cycle.map((n) => `\`${n}\``).join(' → ')}`);
160
+ }
161
+ }
162
+
163
+ // Wildcard dependencies
164
+ if (analysis.wildcard_deps.length > 0) {
165
+ lines.push('', '## ⚠️ Wildcard Dependencies', '');
166
+ lines.push('| Package | Dependency | Version |');
167
+ lines.push('|---------|------------|---------|');
168
+ for (const {pkg, dep, version} of analysis.wildcard_deps) {
169
+ lines.push(`| \`${pkg}\` | \`${dep}\` | \`${version}\` |`);
170
+ }
171
+ }
172
+
173
+ // Dependency graph (simplified)
174
+ lines.push('', '## Internal Dependencies', '');
175
+ for (const node of graph.nodes.values()) {
176
+ const internal_deps = Array.from(node.dependencies.entries()).filter(([name]) =>
177
+ graph.nodes.has(name),
178
+ );
179
+ if (internal_deps.length > 0) {
180
+ lines.push(`- **${node.name}**`);
181
+ for (const [dep_name, spec] of internal_deps) {
182
+ const badge = spec.type === 'peer' ? '(peer)' : spec.type === 'dev' ? '(dev)' : '';
183
+ lines.push(` - ${dep_name} ${badge}`);
184
+ }
185
+ }
186
+ }
187
+
188
+ return lines;
189
+ };
190
+
191
+ const format_stdout = (
192
+ repos: Array<LocalRepo>,
193
+ graph: DependencyGraph,
194
+ analysis: ReturnType<DependencyGraphBuilder['analyze']>,
195
+ publishing_order: Array<string> | null,
196
+ log: Logger,
197
+ ): void => {
198
+ log.info(st('cyan', `📊 Analyzing ${repos.length} repositories...`));
199
+
200
+ // Publishing order
201
+ if (publishing_order) {
202
+ log.info(st('yellow', 'Publishing order:'));
203
+ publishing_order.forEach((name, i) => {
204
+ const node = graph.get_node(name);
205
+ const version = node ? node.version : 'unknown';
206
+ log.info(` ${st('dim', `${i + 1}.`)} ${name} ${st('dim', `(${version})`)}`);
207
+ });
208
+ log.info('');
209
+ }
210
+
211
+ // Dependencies summary
212
+ log.info(st('yellow', 'Dependency relationships:'));
213
+ for (const node of graph.nodes.values()) {
214
+ const internal_deps = Array.from(node.dependencies.entries()).filter(([name]) =>
215
+ graph.nodes.has(name),
216
+ );
217
+ if (internal_deps.length > 0) {
218
+ log.info(` ${st('cyan', node.name)}`);
219
+ for (const [dep_name, spec] of internal_deps) {
220
+ const type_color = spec.type === 'peer' ? 'magenta' : spec.type === 'dev' ? 'dim' : 'white';
221
+ log.info(
222
+ ` ${st(type_color, '→')} ${dep_name} ${st('dim', `(${spec.type}: ${spec.version})`)}`,
223
+ );
224
+ }
225
+ }
226
+ }
227
+ log.info('');
228
+
229
+ // Dependency analysis
230
+ for (const line of format_wildcard_dependencies(analysis)) {
231
+ log.info(line);
232
+ }
233
+ for (const line of format_production_cycles(analysis)) {
234
+ log.info(line);
235
+ }
236
+ for (const line of format_dev_cycles(analysis)) {
237
+ log.info(line);
238
+ }
239
+
240
+ // Success message based on cycle detection
241
+ const has_prod_cycles = analysis.production_cycles.length > 0;
242
+ const has_dev_cycles = analysis.dev_cycles.length > 0;
243
+
244
+ if (!has_prod_cycles && !has_dev_cycles) {
245
+ log.info(st('green', '✅ No circular dependencies detected'));
246
+ } else if (!has_prod_cycles) {
247
+ log.info(st('green', '✓ Publishing order computed successfully (dev deps excluded)'));
248
+ }
249
+
250
+ // Summary
251
+ const {total_deps, internal_deps} = calculate_stats(graph);
252
+
253
+ log.info('');
254
+ log.info(st('cyan', 'Summary:'));
255
+ log.info(` Total packages: ${repos.length}`);
256
+ log.info(` Total dependencies: ${total_deps}`);
257
+ log.info(` Internal dependencies: ${internal_deps}`);
258
+ log.info(` Wildcard dependencies: ${analysis.wildcard_deps.length}`);
259
+ log.info(` Production/peer circular dependencies: ${analysis.production_cycles.length}`);
260
+ log.info(` Dev circular dependencies: ${analysis.dev_cycles.length}`);
261
+ };
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Configuration types and normalization for gitops multi-repo management.
3
+ *
4
+ * Two-phase configuration system:
5
+ * - `RawGitopsConfig` - User-friendly format with optional fields and flexible types
6
+ * - `GitopsConfig` - Internal format with required fields and strict types
7
+ *
8
+ * This allows users to provide minimal configs (e.g., just URLs as strings) while
9
+ * the system works with normalized configs internally for type safety.
10
+ */
11
+
12
+ import type {Url} from '@fuzdev/fuz_util/url.js';
13
+ import {existsSync} from 'node:fs';
14
+ import {strip_end} from '@fuzdev/fuz_util/string.js';
15
+ import type {GitBranch} from '@fuzdev/fuz_util/git.js';
16
+
17
+ import {DEFAULT_REPOS_DIR} from './paths.js';
18
+
19
+ export interface GitopsConfig {
20
+ repos: Array<GitopsRepoConfig>;
21
+ repos_dir: string;
22
+ }
23
+
24
+ export type CreateGitopsConfig = (
25
+ base_config: GitopsConfig,
26
+ ) => RawGitopsConfig | Promise<RawGitopsConfig>;
27
+
28
+ export interface RawGitopsConfig {
29
+ repos?: Array<Url | RawGitopsRepoConfig>;
30
+ repos_dir?: string;
31
+ }
32
+
33
+ export interface GitopsRepoConfig {
34
+ /**
35
+ * The HTTPS URL to the repo. Does not include a `.git` suffix.
36
+ * @example 'https://github.com/fuzdev/fuz_ui'
37
+ */
38
+ repo_url: Url;
39
+
40
+ /**
41
+ * Relative or absolute path to the repo's local directory.
42
+ * If `null`, the directory is inferred from the URL and cwd.
43
+ * @example 'relative/path/to/repo'
44
+ * @example '/absolute/path/to/repo'
45
+ */
46
+ repo_dir: string | null;
47
+
48
+ /**
49
+ * The branch name to use when fetching the repo. Defaults to `main`.
50
+ */
51
+ branch: GitBranch;
52
+ }
53
+
54
+ export interface RawGitopsRepoConfig {
55
+ repo_url: Url;
56
+ repo_dir?: string | null;
57
+ branch?: GitBranch;
58
+ }
59
+
60
+ export const create_empty_gitops_config = (): GitopsConfig => ({
61
+ repos: [],
62
+ repos_dir: DEFAULT_REPOS_DIR,
63
+ });
64
+
65
+ /**
66
+ * Transforms a `RawGitopsConfig` to the more strict `GitopsConfig`.
67
+ * This allows users to provide a more relaxed config.
68
+ */
69
+ export const normalize_gitops_config = (raw_config: RawGitopsConfig): GitopsConfig => {
70
+ const empty_config = create_empty_gitops_config();
71
+ // All of the raw config properties are optional,
72
+ // so fall back to the empty values when `undefined`.
73
+ const {repos, repos_dir} = raw_config;
74
+ return {
75
+ repos: repos ? repos.map((r) => parse_fuz_repo_config(r)) : empty_config.repos,
76
+ // Default to two dirs up from config if not specified
77
+ repos_dir: repos_dir ?? DEFAULT_REPOS_DIR,
78
+ };
79
+ };
80
+
81
+ const parse_fuz_repo_config = (r: Url | RawGitopsRepoConfig): GitopsRepoConfig => {
82
+ if (typeof r === 'string') {
83
+ return {repo_url: r, repo_dir: null, branch: 'main' as GitBranch}; // TODO @zts use flavored for GitBranch
84
+ }
85
+ return {
86
+ repo_url: strip_end(r.repo_url, '.git'),
87
+ repo_dir: r.repo_dir ?? null,
88
+ branch: r.branch ?? ('main' as GitBranch), // TODO @zts use flavored for GitBranch
89
+ };
90
+ };
91
+
92
+ export interface GitopsConfigModule {
93
+ readonly default: RawGitopsConfig | CreateGitopsConfig;
94
+ }
95
+
96
+ export const load_gitops_config = async (config_path: string): Promise<GitopsConfig | null> => {
97
+ if (!existsSync(config_path)) {
98
+ // No user config file found.
99
+ return null;
100
+ }
101
+ // Import the user's `gitops.config.ts`.
102
+ const config_module = await import(config_path);
103
+ validate_gitops_config_module(config_module, config_path);
104
+ return normalize_gitops_config(
105
+ typeof config_module.default === 'function'
106
+ ? await config_module.default(create_empty_gitops_config())
107
+ : config_module.default,
108
+ );
109
+ };
110
+
111
+ export const validate_gitops_config_module: (
112
+ config_module: any,
113
+ config_path: string,
114
+ ) => asserts config_module is GitopsConfigModule = (config_module, config_path) => {
115
+ const config = config_module.default;
116
+ if (!config) {
117
+ throw Error(`Invalid Fuz config module at ${config_path}: expected a default export`);
118
+ } else if (!(typeof config === 'function' || typeof config === 'object')) {
119
+ throw Error(
120
+ `Invalid Fuz config module at ${config_path}: the default export must be a function or object`,
121
+ );
122
+ }
123
+ };
@@ -0,0 +1,272 @@
1
+ import type {Task} from '@ryanatkn/gro';
2
+ import {z} from 'zod';
3
+ import {styleText as st} from 'node:util';
4
+
5
+ import {get_gitops_ready} from './gitops_task_helpers.js';
6
+ import {
7
+ generate_publishing_plan,
8
+ log_publishing_plan,
9
+ type PublishingPlan,
10
+ type LogPlanOptions,
11
+ } from './publishing_plan.js';
12
+ import {format_and_output, type OutputFormatters} from './output_helpers.js';
13
+
14
+ /** @nodocs */
15
+ export const Args = z.strictObject({
16
+ path: z
17
+ .string()
18
+ .meta({description: 'path to the gitops config file, absolute or relative to the cwd'})
19
+ .default('gitops.config.ts'),
20
+ dir: z
21
+ .string()
22
+ .meta({description: 'path containing the repos, defaults to the parent of the `path` dir'})
23
+ .optional(),
24
+ format: z
25
+ .enum(['stdout', 'json', 'markdown'])
26
+ .meta({description: 'output format'})
27
+ .default('stdout'),
28
+ outfile: z.string().meta({description: 'write output to file instead of logging'}).optional(),
29
+ verbose: z.boolean().meta({description: 'show additional details'}).default(false),
30
+ });
31
+ export type Args = z.infer<typeof Args>;
32
+
33
+ /**
34
+ * Generate a publishing plan showing what would happen during multi-repo publishing.
35
+ * Shows version changes, dependency updates, and breaking change cascades.
36
+ *
37
+ * Usage:
38
+ * gro gitops_plan
39
+ * gro gitops_plan --dir ../repos
40
+ * gro gitops_plan --path ./custom.config.ts
41
+ *
42
+ * @nodocs
43
+ */
44
+ export const task: Task<Args> = {
45
+ summary: 'generate a publishing plan based on changesets',
46
+ Args,
47
+ run: async ({args, log}): Promise<void> => {
48
+ const {dir, path, format, outfile, verbose} = args;
49
+
50
+ log.info(st('cyan', 'Generating multi-repo publishing plan...'));
51
+
52
+ // Load local repos
53
+ const {local_repos} = await get_gitops_ready({
54
+ path,
55
+ dir,
56
+ download: false, // Don't download if missing
57
+ log,
58
+ });
59
+
60
+ if (local_repos.length === 0) {
61
+ log.error('No local repos found');
62
+ return;
63
+ }
64
+
65
+ log.info(` Found ${local_repos.length} local repos`);
66
+
67
+ // Generate publishing plan
68
+ const plan = await generate_publishing_plan(local_repos, {log, verbose});
69
+
70
+ // Format and output using output_helpers
71
+ await format_and_output(plan, create_plan_formatters({verbose}), {format, outfile, log});
72
+
73
+ // Exit with error if there are blocking issues
74
+ if (plan.errors.length > 0) {
75
+ throw new Error('Publishing plan found errors that would block publishing');
76
+ }
77
+ },
78
+ };
79
+
80
+ const create_plan_formatters = (
81
+ options: LogPlanOptions = {},
82
+ ): OutputFormatters<PublishingPlan> => ({
83
+ json: (plan) => {
84
+ const output = {
85
+ publishing_order: plan.publishing_order,
86
+ version_changes: plan.version_changes,
87
+ dependency_updates: plan.dependency_updates,
88
+ breaking_cascades: Object.fromEntries(plan.breaking_cascades),
89
+ warnings: plan.warnings,
90
+ info: plan.info,
91
+ errors: plan.errors,
92
+ };
93
+ return JSON.stringify(output, null, 2);
94
+ },
95
+ markdown: (plan) => format_plan_as_markdown(plan),
96
+ stdout: (plan, log) => log_publishing_plan(plan, log, options),
97
+ });
98
+
99
+ const format_plan_as_markdown = (plan: PublishingPlan): Array<string> => {
100
+ const lines: Array<string> = [];
101
+ const {
102
+ publishing_order,
103
+ version_changes,
104
+ dependency_updates,
105
+ breaking_cascades,
106
+ warnings,
107
+ info,
108
+ errors,
109
+ } = plan;
110
+
111
+ lines.push('# Publishing Plan');
112
+ lines.push('');
113
+
114
+ // Errors
115
+ if (errors.length > 0) {
116
+ lines.push('## ❌ Errors');
117
+ lines.push('');
118
+ for (const error of errors) {
119
+ lines.push(`- ${error}`);
120
+ }
121
+ lines.push('');
122
+ }
123
+
124
+ // Publishing order
125
+ if (publishing_order.length > 0) {
126
+ lines.push('## Publishing Order');
127
+ lines.push('');
128
+ lines.push(publishing_order.map((p) => `\`${p}\``).join(' → '));
129
+ lines.push('');
130
+ }
131
+
132
+ // Version changes
133
+ if (version_changes.length > 0) {
134
+ const with_changesets = version_changes.filter(
135
+ (vc) => vc.has_changesets && !vc.needs_bump_escalation,
136
+ );
137
+ const with_escalation = version_changes.filter((vc) => vc.needs_bump_escalation);
138
+ const with_auto_changesets = version_changes.filter((vc) => vc.will_generate_changeset);
139
+
140
+ if (with_changesets.length > 0) {
141
+ lines.push('## Version Changes (from changesets)');
142
+ lines.push('');
143
+ lines.push('| Package | From | To | Bump | Major |');
144
+ lines.push('|---------|------|----|------|-------|');
145
+ for (const change of with_changesets) {
146
+ const is_major = change.bump_type === 'major' ? '💥 Yes' : 'No';
147
+ lines.push(
148
+ `| \`${change.package_name}\` | ${change.from} | ${change.to} | ${change.bump_type} | ${is_major} |`,
149
+ );
150
+ }
151
+ lines.push('');
152
+ }
153
+
154
+ if (with_escalation.length > 0) {
155
+ lines.push('## Version Changes (bump escalation required)');
156
+ lines.push('');
157
+ lines.push('| Package | From | To | Changesets Bump | Required Bump | Major |');
158
+ lines.push('|---------|------|-----|-----------------|---------------|-------|');
159
+ for (const change of with_escalation) {
160
+ const is_major = change.bump_type === 'major' ? '💥 Yes' : 'No';
161
+ lines.push(
162
+ `| \`${change.package_name}\` | ${change.from} | ${change.to} | ${change.existing_bump} | ${change.required_bump} | ${is_major} |`,
163
+ );
164
+ }
165
+ lines.push('');
166
+ lines.push(
167
+ '> ⬆️ These packages have changesets, but dependencies require a larger version bump.',
168
+ );
169
+ lines.push('');
170
+ }
171
+
172
+ if (with_auto_changesets.length > 0) {
173
+ lines.push('## Version Changes (auto-generated for dependency updates)');
174
+ lines.push('');
175
+ lines.push('| Package | From | To | Bump | Major |');
176
+ lines.push('|---------|------|-----|------|-------|');
177
+ for (const change of with_auto_changesets) {
178
+ const is_major = change.bump_type === 'major' ? '💥 Yes' : 'No';
179
+ lines.push(
180
+ `| \`${change.package_name}\` | ${change.from} | ${change.to} | ${change.bump_type} | ${is_major} |`,
181
+ );
182
+ }
183
+ lines.push('');
184
+ }
185
+ } else {
186
+ lines.push('## No Packages to Publish');
187
+ lines.push('');
188
+ lines.push('No packages have changesets to publish.');
189
+ lines.push('');
190
+ }
191
+
192
+ // Dependency cascades
193
+ if (breaking_cascades.size > 0) {
194
+ lines.push('## Dependency Cascades');
195
+ lines.push('');
196
+ for (const [pkg, affected] of breaking_cascades) {
197
+ lines.push(`- \`${pkg}\` affects: ${affected.map((a) => `\`${a}\``).join(', ')}`);
198
+ }
199
+ lines.push('');
200
+ }
201
+
202
+ // Dependency updates - show diff-style
203
+ if (dependency_updates.length > 0) {
204
+ // Group by package
205
+ const updates_by_package: Map<string, typeof dependency_updates> = new Map();
206
+ for (const update of dependency_updates) {
207
+ const updates = updates_by_package.get(update.dependent_package) || [];
208
+ updates.push(update);
209
+ updates_by_package.set(update.dependent_package, updates);
210
+ }
211
+
212
+ lines.push('## Dependency Updates');
213
+ lines.push('');
214
+ for (const [pkg, updates] of updates_by_package) {
215
+ const has_version_change = version_changes.some((vc) => vc.package_name === pkg);
216
+ const label = has_version_change ? '' : ' (no republish)';
217
+ lines.push(`### ${pkg}${label}`);
218
+ lines.push('');
219
+ lines.push('```diff');
220
+ for (const update of updates) {
221
+ const type_label =
222
+ update.type === 'dependencies'
223
+ ? 'prod'
224
+ : update.type === 'peerDependencies'
225
+ ? 'peer'
226
+ : 'dev';
227
+ lines.push(
228
+ `- "${update.updated_dependency}": "${update.current_version}" # ${type_label}`,
229
+ );
230
+ lines.push(`+ "${update.updated_dependency}": "${update.new_version}" # ${type_label}`);
231
+ }
232
+ lines.push('```');
233
+ lines.push('');
234
+ }
235
+ }
236
+
237
+ // Warnings (actual issues requiring attention)
238
+ if (warnings.length > 0) {
239
+ lines.push('## ⚠️ Warnings');
240
+ lines.push('');
241
+ lines.push('*Issues that require attention:*');
242
+ lines.push('');
243
+ for (const warning of warnings) {
244
+ lines.push(`- ${warning}`);
245
+ }
246
+ lines.push('');
247
+ }
248
+
249
+ // Info (packages with no changes - normal status)
250
+ if (info.length > 0) {
251
+ lines.push('## ℹ️ No Changes to Publish');
252
+ lines.push('');
253
+ lines.push('*These packages have no changesets and no dependency updates:*');
254
+ lines.push('');
255
+ for (const pkg of info) {
256
+ lines.push(`- \`${pkg}\``);
257
+ }
258
+ lines.push('');
259
+ }
260
+
261
+ // Summary
262
+ const major_bump_count = version_changes.filter((vc) => vc.bump_type === 'major').length;
263
+ lines.push('## Summary');
264
+ lines.push('');
265
+ lines.push(`- **Packages to publish**: ${version_changes.length}`);
266
+ lines.push(`- **Dependency updates**: ${dependency_updates.length}`);
267
+ lines.push(`- **Major version bumps**: ${major_bump_count}`);
268
+ lines.push(`- **Warnings**: ${warnings.length}`);
269
+ lines.push(`- **Errors**: ${errors.length}`);
270
+
271
+ return lines;
272
+ };