@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,423 @@
1
+ /**
2
+ * Dependency graph data structure and algorithms for multi-repo publishing.
3
+ *
4
+ * Provides `DependencyGraph` class with topological sort and cycle detection.
5
+ * For validation workflow and publishing order computation, see `graph_validation.ts`.
6
+ */
7
+
8
+ import type {LocalRepo} from './local_repo.js';
9
+ import {EMPTY_OBJECT} from '@fuzdev/fuz_util/object.js';
10
+
11
+ export const DEPENDENCY_TYPE = {
12
+ PROD: 'prod',
13
+ PEER: 'peer',
14
+ DEV: 'dev',
15
+ } as const;
16
+
17
+ export type DependencyType = (typeof DEPENDENCY_TYPE)[keyof typeof DEPENDENCY_TYPE];
18
+
19
+ export interface DependencySpec {
20
+ type: DependencyType;
21
+ version: string;
22
+ resolved?: string;
23
+ }
24
+
25
+ export interface DependencyGraphJson {
26
+ nodes: Array<{
27
+ name: string;
28
+ version: string;
29
+ dependencies: Array<{name: string; spec: DependencySpec}>;
30
+ dependents: Array<string>;
31
+ publishable: boolean;
32
+ }>;
33
+ edges: Array<{from: string; to: string}>;
34
+ }
35
+
36
+ export interface DependencyNode {
37
+ name: string;
38
+ version: string;
39
+ repo?: LocalRepo;
40
+ dependencies: Map<string, DependencySpec>;
41
+ dependents: Set<string>;
42
+ publishable: boolean;
43
+ }
44
+
45
+ export class DependencyGraph {
46
+ nodes: Map<string, DependencyNode>;
47
+ edges: Map<string, Set<string>>; // pkg -> dependents
48
+
49
+ constructor() {
50
+ this.nodes = new Map();
51
+ this.edges = new Map();
52
+ }
53
+
54
+ public init_from_repos(repos: Array<LocalRepo>): void {
55
+ // First pass: create nodes
56
+ for (const repo of repos) {
57
+ const {library} = repo;
58
+ const node: DependencyNode = {
59
+ name: library.name,
60
+ version: library.package_json.version || '0.0.0',
61
+ repo,
62
+ dependencies: new Map(),
63
+ dependents: new Set(),
64
+ publishable: !!library.package_json.private === false, // eslint-disable-line @typescript-eslint/no-unnecessary-boolean-literal-compare
65
+ };
66
+
67
+ // Extract dependencies
68
+ const deps = library.package_json.dependencies || (EMPTY_OBJECT as Record<string, string>);
69
+ const dev_deps =
70
+ library.package_json.devDependencies || (EMPTY_OBJECT as Record<string, string>);
71
+ const peer_deps =
72
+ library.package_json.peerDependencies || (EMPTY_OBJECT as Record<string, string>);
73
+
74
+ // Add dependencies, prioritizing prod/peer over dev
75
+ // (if a package appears in multiple dep types, use the stronger constraint)
76
+ for (const [name, version] of Object.entries(deps)) {
77
+ node.dependencies.set(name, {type: DEPENDENCY_TYPE.PROD, version});
78
+ }
79
+ for (const [name, version] of Object.entries(peer_deps)) {
80
+ node.dependencies.set(name, {type: DEPENDENCY_TYPE.PEER, version});
81
+ }
82
+ for (const [name, version] of Object.entries(dev_deps)) {
83
+ // Only add dev deps if not already present as prod/peer
84
+ if (!node.dependencies.has(name)) {
85
+ node.dependencies.set(name, {type: DEPENDENCY_TYPE.DEV, version});
86
+ }
87
+ }
88
+
89
+ this.nodes.set(library.name, node);
90
+ this.edges.set(library.name, new Set());
91
+ }
92
+
93
+ // Second pass: build edges (dependents)
94
+ for (const node of this.nodes.values()) {
95
+ for (const [dep_name] of node.dependencies) {
96
+ if (this.nodes.has(dep_name)) {
97
+ // Internal dependency
98
+ const dep_node = this.nodes.get(dep_name)!;
99
+ dep_node.dependents.add(node.name);
100
+ this.edges.get(dep_name)!.add(node.name);
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ get_node(name: string): DependencyNode | undefined {
107
+ return this.nodes.get(name);
108
+ }
109
+
110
+ get_dependents(name: string): Set<string> {
111
+ return this.edges.get(name) || new Set();
112
+ }
113
+
114
+ get_dependencies(name: string): Map<string, DependencySpec> {
115
+ const node = this.nodes.get(name);
116
+ return node ? node.dependencies : new Map();
117
+ }
118
+
119
+ /**
120
+ * Computes topological sort order for dependency graph.
121
+ *
122
+ * Uses Kahn's algorithm with alphabetical ordering within tiers for
123
+ * deterministic results. Throws if cycles detected.
124
+ *
125
+ * @param exclude_dev if true, excludes dev dependencies to break cycles.
126
+ * Publishing uses exclude_dev=true to handle circular dev deps.
127
+ * @returns array of package names in dependency order (dependencies before dependents)
128
+ * @throws {Error} if circular dependencies detected in included dependency types
129
+ */
130
+ topological_sort(exclude_dev = false): Array<string> {
131
+ const visited: Set<string> = new Set();
132
+ const result: Array<string> = [];
133
+
134
+ // Count incoming edges for each node
135
+ const in_degree: Map<string, number> = new Map();
136
+ for (const name of this.nodes.keys()) {
137
+ in_degree.set(name, 0);
138
+ }
139
+ for (const node of this.nodes.values()) {
140
+ for (const [dep_name, spec] of node.dependencies) {
141
+ // Skip dev dependencies if requested
142
+ if (exclude_dev && spec.type === DEPENDENCY_TYPE.DEV) continue;
143
+
144
+ if (this.nodes.has(dep_name)) {
145
+ in_degree.set(node.name, in_degree.get(node.name)! + 1);
146
+ }
147
+ }
148
+ }
149
+
150
+ // Start with nodes that have no dependencies
151
+ const queue: Array<string> = [];
152
+ for (const [name, degree] of in_degree) {
153
+ if (degree === 0) {
154
+ queue.push(name);
155
+ }
156
+ }
157
+
158
+ // Sort initial queue alphabetically for deterministic ordering within tier
159
+ queue.sort();
160
+
161
+ // Process nodes
162
+ while (queue.length > 0) {
163
+ const name = queue.shift()!;
164
+ result.push(name);
165
+ visited.add(name);
166
+
167
+ // Reduce in-degree for dependents
168
+ const node = this.nodes.get(name);
169
+ if (node) {
170
+ // Find packages that depend on this one
171
+ // Sort nodes to ensure deterministic iteration order
172
+ const sorted_nodes = Array.from(this.nodes.values()).sort((a, b) =>
173
+ a.name.localeCompare(b.name),
174
+ );
175
+ for (const other_node of sorted_nodes) {
176
+ for (const [dep_name, spec] of other_node.dependencies) {
177
+ // Skip dev dependencies if requested
178
+ if (exclude_dev && spec.type === DEPENDENCY_TYPE.DEV) continue;
179
+
180
+ if (dep_name === name) {
181
+ const new_degree = in_degree.get(other_node.name)! - 1;
182
+ in_degree.set(other_node.name, new_degree);
183
+ if (new_degree === 0) {
184
+ queue.push(other_node.name);
185
+ }
186
+ }
187
+ }
188
+ }
189
+ }
190
+ }
191
+
192
+ // Check for cycles
193
+ if (result.length !== this.nodes.size) {
194
+ const unvisited = Array.from(this.nodes.keys()).filter((n) => !visited.has(n));
195
+ throw new Error(`Circular dependency detected involving: ${unvisited.join(', ')}`);
196
+ }
197
+
198
+ return result;
199
+ }
200
+
201
+ detect_cycles(): Array<Array<string>> {
202
+ const cycles: Array<Array<string>> = [];
203
+ const visited: Set<string> = new Set();
204
+ const rec_stack: Set<string> = new Set();
205
+
206
+ const dfs = (name: string, path: Array<string>): void => {
207
+ visited.add(name);
208
+ rec_stack.add(name);
209
+ path.push(name);
210
+
211
+ const node = this.nodes.get(name);
212
+ if (node) {
213
+ for (const [dep_name] of node.dependencies) {
214
+ if (this.nodes.has(dep_name)) {
215
+ if (!visited.has(dep_name)) {
216
+ dfs(dep_name, [...path]);
217
+ } else if (rec_stack.has(dep_name)) {
218
+ // Found a cycle
219
+ const cycle_start = path.indexOf(dep_name);
220
+ cycles.push(path.slice(cycle_start).concat(dep_name));
221
+ }
222
+ }
223
+ }
224
+ }
225
+
226
+ rec_stack.delete(name);
227
+ };
228
+
229
+ for (const name of this.nodes.keys()) {
230
+ if (!visited.has(name)) {
231
+ dfs(name, []);
232
+ }
233
+ }
234
+
235
+ return cycles;
236
+ }
237
+
238
+ /**
239
+ * Detects circular dependencies, categorized by severity.
240
+ *
241
+ * Production/peer cycles prevent publishing (impossible to order packages).
242
+ * Dev cycles are normal (test utils, shared configs) and safely ignored.
243
+ *
244
+ * Uses DFS traversal with recursion stack to identify back edges.
245
+ * Deduplicates cycles using sorted cycle keys.
246
+ *
247
+ * @returns object with production_cycles (errors) and dev_cycles (info)
248
+ */
249
+ detect_cycles_by_type(): {
250
+ production_cycles: Array<Array<string>>;
251
+ dev_cycles: Array<Array<string>>;
252
+ } {
253
+ const production_cycles: Array<Array<string>> = [];
254
+ const dev_cycles: Array<Array<string>> = [];
255
+ const visited_prod: Set<string> = new Set();
256
+ const visited_dev: Set<string> = new Set();
257
+ const rec_stack_prod: Set<string> = new Set();
258
+ const rec_stack_dev: Set<string> = new Set();
259
+
260
+ // DFS for production/peer dependencies only
261
+ const dfs_prod = (name: string, path: Array<string>): void => {
262
+ visited_prod.add(name);
263
+ rec_stack_prod.add(name);
264
+ path.push(name);
265
+
266
+ const node = this.nodes.get(name);
267
+ if (node) {
268
+ for (const [dep_name, spec] of node.dependencies) {
269
+ // Skip dev dependencies
270
+ if (spec.type === DEPENDENCY_TYPE.DEV) continue;
271
+
272
+ if (this.nodes.has(dep_name)) {
273
+ if (!visited_prod.has(dep_name)) {
274
+ dfs_prod(dep_name, [...path]);
275
+ } else if (rec_stack_prod.has(dep_name)) {
276
+ // Found a production cycle
277
+ const cycle_start = path.indexOf(dep_name);
278
+ const cycle = path.slice(cycle_start).concat(dep_name);
279
+ // Check if this cycle is unique
280
+ const cycle_key = [...cycle].sort().join(',');
281
+ const exists = production_cycles.some((c) => [...c].sort().join(',') === cycle_key);
282
+ if (!exists) {
283
+ production_cycles.push(cycle);
284
+ }
285
+ }
286
+ }
287
+ }
288
+ }
289
+
290
+ rec_stack_prod.delete(name);
291
+ };
292
+
293
+ // DFS for dev dependencies only
294
+ const dfs_dev = (name: string, path: Array<string>): void => {
295
+ visited_dev.add(name);
296
+ rec_stack_dev.add(name);
297
+ path.push(name);
298
+
299
+ const node = this.nodes.get(name);
300
+ if (node) {
301
+ for (const [dep_name, spec] of node.dependencies) {
302
+ // Only check dev dependencies
303
+ if (spec.type !== DEPENDENCY_TYPE.DEV) continue;
304
+
305
+ if (this.nodes.has(dep_name)) {
306
+ if (!visited_dev.has(dep_name)) {
307
+ dfs_dev(dep_name, [...path]);
308
+ } else if (rec_stack_dev.has(dep_name)) {
309
+ // Found a dev cycle
310
+ const cycle_start = path.indexOf(dep_name);
311
+ const cycle = path.slice(cycle_start).concat(dep_name);
312
+ // Check if this cycle is unique
313
+ const cycle_key = [...cycle].sort().join(',');
314
+ const exists = dev_cycles.some((c) => [...c].sort().join(',') === cycle_key);
315
+ if (!exists) {
316
+ dev_cycles.push(cycle);
317
+ }
318
+ }
319
+ }
320
+ }
321
+ }
322
+
323
+ rec_stack_dev.delete(name);
324
+ };
325
+
326
+ // Check for production/peer cycles
327
+ for (const name of this.nodes.keys()) {
328
+ if (!visited_prod.has(name)) {
329
+ dfs_prod(name, []);
330
+ }
331
+ }
332
+
333
+ // Check for dev cycles
334
+ for (const name of this.nodes.keys()) {
335
+ if (!visited_dev.has(name)) {
336
+ dfs_dev(name, []);
337
+ }
338
+ }
339
+
340
+ return {production_cycles, dev_cycles};
341
+ }
342
+
343
+ toJSON(): DependencyGraphJson {
344
+ const nodes = Array.from(this.nodes.values()).map((node) => ({
345
+ name: node.name,
346
+ version: node.version,
347
+ dependencies: Array.from(node.dependencies.entries()).map(([name, spec]) => ({
348
+ name,
349
+ spec,
350
+ })),
351
+ dependents: Array.from(node.dependents),
352
+ publishable: node.publishable,
353
+ }));
354
+
355
+ const edges: Array<{from: string; to: string}> = [];
356
+ for (const [from, tos] of this.edges) {
357
+ for (const to of tos) {
358
+ edges.push({from, to});
359
+ }
360
+ }
361
+
362
+ return {nodes, edges};
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Builder for creating and analyzing dependency graphs.
368
+ */
369
+ export class DependencyGraphBuilder {
370
+ /**
371
+ * Constructs dependency graph from local repos.
372
+ *
373
+ * Two-pass algorithm: first creates nodes, then builds edges (dependents).
374
+ * Prioritizes prod/peer deps over dev deps when same package appears in
375
+ * multiple dependency types (stronger constraint wins).
376
+ *
377
+ * @returns fully initialized dependency graph with all nodes and edges
378
+ */
379
+ build_from_repos(repos: Array<LocalRepo>): DependencyGraph {
380
+ const graph = new DependencyGraph();
381
+ graph.init_from_repos(repos);
382
+ return graph;
383
+ }
384
+
385
+ /**
386
+ * Computes publishing order using topological sort with dev deps excluded.
387
+ *
388
+ * Excludes dev dependencies to break circular dev dependency cycles while
389
+ * preserving production/peer dependency ordering. This allows patterns like
390
+ * shared test utilities that depend on each other for development.
391
+ *
392
+ * @returns package names in safe publishing order (dependencies before dependents)
393
+ * @throws {Error} if production/peer cycles detected (cannot be resolved by exclusion)
394
+ */
395
+ compute_publishing_order(graph: DependencyGraph): Array<string> {
396
+ return graph.topological_sort(true); // Exclude dev dependencies
397
+ }
398
+
399
+ analyze(graph: DependencyGraph): {
400
+ production_cycles: Array<Array<string>>;
401
+ dev_cycles: Array<Array<string>>;
402
+ wildcard_deps: Array<{pkg: string; dep: string; version: string}>;
403
+ missing_peers: Array<{pkg: string; dep: string}>;
404
+ } {
405
+ const {production_cycles, dev_cycles} = graph.detect_cycles_by_type();
406
+ const wildcard_deps: Array<{pkg: string; dep: string; version: string}> = [];
407
+ const missing_peers: Array<{pkg: string; dep: string}> = [];
408
+
409
+ for (const node of graph.nodes.values()) {
410
+ for (const [dep_name, spec] of node.dependencies) {
411
+ if (spec.version === '*') {
412
+ wildcard_deps.push({pkg: node.name, dep: dep_name, version: spec.version});
413
+ }
414
+ if (spec.type === DEPENDENCY_TYPE.PEER && !graph.nodes.has(dep_name)) {
415
+ // External peer dependency
416
+ missing_peers.push({pkg: node.name, dep: dep_name});
417
+ }
418
+ }
419
+ }
420
+
421
+ return {production_cycles, dev_cycles, wildcard_deps, missing_peers};
422
+ }
423
+ }
@@ -0,0 +1,297 @@
1
+ import type {Logger} from '@fuzdev/fuz_util/log.js';
2
+ import {join} from 'node:path';
3
+
4
+ import type {LocalRepo} from './local_repo.js';
5
+ import type {PublishedVersion} from './multi_repo_publisher.js';
6
+ import {
7
+ create_changeset_for_dependency_updates,
8
+ create_dependency_updates,
9
+ } from './changeset_generator.js';
10
+ import {needs_update, get_update_prefix} from './version_utils.js';
11
+ import type {GitOperations, FsOperations} from './operations.js';
12
+ import {default_git_operations, default_fs_operations} from './operations_defaults.js';
13
+
14
+ export type VersionStrategy = 'exact' | 'caret' | 'tilde' | 'gte';
15
+
16
+ export interface UpdatePackageJsonOptions {
17
+ strategy?: VersionStrategy;
18
+ published_versions?: Map<string, PublishedVersion>;
19
+ log?: Logger;
20
+ git_ops?: GitOperations;
21
+ fs_ops?: FsOperations;
22
+ }
23
+
24
+ /**
25
+ * Updates package.json dependencies and creates changeset if needed.
26
+ *
27
+ * Workflow:
28
+ * 1. Updates all dependency types (dependencies, devDependencies, peerDependencies)
29
+ * 2. Writes updated package.json with tabs formatting
30
+ * 3. Creates auto-changeset if published_versions provided (for transitive updates)
31
+ * 4. Commits both package.json and changeset with standard message
32
+ *
33
+ * Uses version strategy to determine prefix (exact, caret, tilde) while preserving
34
+ * existing prefixes when possible.
35
+ *
36
+ * @throws {Error} if file operations or git operations fail
37
+ */
38
+ export const update_package_json = async (
39
+ repo: LocalRepo,
40
+ updates: Map<string, string>,
41
+ options: UpdatePackageJsonOptions = {},
42
+ ): Promise<void> => {
43
+ const {
44
+ strategy = 'caret',
45
+ published_versions,
46
+ log,
47
+ git_ops = default_git_operations,
48
+ fs_ops = default_fs_operations,
49
+ } = options;
50
+ if (updates.size === 0) return;
51
+
52
+ const package_json_path = join(repo.repo_dir, 'package.json');
53
+
54
+ // Read current package.json
55
+ const content_result = await fs_ops.readFile({path: package_json_path, encoding: 'utf8'});
56
+ if (!content_result.ok) {
57
+ throw new Error(`Failed to read package.json: ${content_result.message}`);
58
+ }
59
+ const package_json = JSON.parse(content_result.value);
60
+
61
+ // Apply version strategy
62
+ const prefix =
63
+ strategy === 'exact' ? '' : strategy === 'caret' ? '^' : strategy === 'gte' ? '>=' : '~';
64
+
65
+ let updated = false;
66
+
67
+ // Update dependencies
68
+ if (package_json.dependencies) {
69
+ for (const [name, version] of updates) {
70
+ if (name in package_json.dependencies) {
71
+ const current = package_json.dependencies[name];
72
+ const update_prefix = get_update_prefix(current, prefix);
73
+ package_json.dependencies[name] = update_prefix + version;
74
+ updated = true;
75
+ }
76
+ }
77
+ }
78
+
79
+ // Update devDependencies
80
+ if (package_json.devDependencies) {
81
+ for (const [name, version] of updates) {
82
+ if (name in package_json.devDependencies) {
83
+ const current = package_json.devDependencies[name];
84
+ const update_prefix = get_update_prefix(current, prefix);
85
+ package_json.devDependencies[name] = update_prefix + version;
86
+ updated = true;
87
+ }
88
+ }
89
+ }
90
+
91
+ // Update peerDependencies
92
+ if (package_json.peerDependencies) {
93
+ for (const [name, version] of updates) {
94
+ if (name in package_json.peerDependencies) {
95
+ const current = package_json.peerDependencies[name];
96
+ const update_prefix = get_update_prefix(current, prefix);
97
+ package_json.peerDependencies[name] = update_prefix + version;
98
+ updated = true;
99
+ }
100
+ }
101
+ }
102
+
103
+ if (!updated) return;
104
+
105
+ // Write updated package.json
106
+ const write_result = await fs_ops.writeFile({
107
+ path: package_json_path,
108
+ content: JSON.stringify(package_json, null, '\t') + '\n',
109
+ });
110
+ if (!write_result.ok) {
111
+ throw new Error(`Failed to write package.json: ${write_result.message}`);
112
+ }
113
+
114
+ // Create changeset if we have published version info
115
+ if (published_versions && published_versions.size > 0) {
116
+ // Build dependency updates info for changeset
117
+ const all_deps: Map<string, string> = new Map();
118
+
119
+ // Collect all current dependencies with their versions
120
+ if (repo.dependencies) {
121
+ for (const [name, version] of repo.dependencies) {
122
+ if (updates.has(name)) {
123
+ all_deps.set(name, version);
124
+ }
125
+ }
126
+ }
127
+ if (repo.dev_dependencies) {
128
+ for (const [name, version] of repo.dev_dependencies) {
129
+ if (updates.has(name)) {
130
+ all_deps.set(name, version);
131
+ }
132
+ }
133
+ }
134
+ if (repo.peer_dependencies) {
135
+ for (const [name, version] of repo.peer_dependencies) {
136
+ if (updates.has(name)) {
137
+ all_deps.set(name, version);
138
+ }
139
+ }
140
+ }
141
+
142
+ const dependency_updates = create_dependency_updates(all_deps, published_versions);
143
+
144
+ if (dependency_updates.length > 0) {
145
+ const changeset_path = await create_changeset_for_dependency_updates(
146
+ repo,
147
+ dependency_updates,
148
+ {log},
149
+ );
150
+
151
+ // Add changeset to git
152
+ const add_result = await git_ops.add({files: changeset_path, cwd: repo.repo_dir});
153
+ if (!add_result.ok) {
154
+ throw new Error(`Failed to stage changeset: ${add_result.message}`);
155
+ }
156
+ }
157
+ }
158
+
159
+ // Commit the changes (including both package.json and changeset)
160
+ const add_pkg_result = await git_ops.add({files: 'package.json', cwd: repo.repo_dir});
161
+ if (!add_pkg_result.ok) {
162
+ throw new Error(`Failed to stage package.json: ${add_pkg_result.message}`);
163
+ }
164
+
165
+ const commit_result = await git_ops.commit({
166
+ message: `update dependencies after publishing`,
167
+ cwd: repo.repo_dir,
168
+ });
169
+ if (!commit_result.ok) {
170
+ throw new Error(`Failed to commit: ${commit_result.message}`);
171
+ }
172
+ };
173
+
174
+ export interface UpdateAllReposOptions {
175
+ strategy?: VersionStrategy;
176
+ log?: Logger;
177
+ git_ops?: GitOperations;
178
+ fs_ops?: FsOperations;
179
+ }
180
+
181
+ export const update_all_repos = async (
182
+ repos: Array<LocalRepo>,
183
+ published: Map<string, string>,
184
+ options: UpdateAllReposOptions = {},
185
+ ): Promise<{updated: number; failed: Array<{repo: string; error: Error}>}> => {
186
+ const {
187
+ strategy = 'caret',
188
+ log,
189
+ git_ops = default_git_operations,
190
+ fs_ops = default_fs_operations,
191
+ } = options;
192
+ let updated_count = 0;
193
+ const failed: Array<{repo: string; error: Error}> = [];
194
+
195
+ for (const repo of repos) {
196
+ const updates: Map<string, string> = new Map();
197
+
198
+ // Find dependencies that were published
199
+ if (repo.dependencies) {
200
+ for (const [dep_name] of repo.dependencies) {
201
+ const new_version = published.get(dep_name);
202
+ if (new_version) {
203
+ updates.set(dep_name, new_version);
204
+ }
205
+ }
206
+ }
207
+
208
+ if (repo.dev_dependencies) {
209
+ for (const [dep_name] of repo.dev_dependencies) {
210
+ const new_version = published.get(dep_name);
211
+ if (new_version) {
212
+ updates.set(dep_name, new_version);
213
+ }
214
+ }
215
+ }
216
+
217
+ if (repo.peer_dependencies) {
218
+ for (const [dep_name] of repo.peer_dependencies) {
219
+ const new_version = published.get(dep_name);
220
+ if (new_version) {
221
+ updates.set(dep_name, new_version);
222
+ }
223
+ }
224
+ }
225
+
226
+ if (updates.size === 0) continue;
227
+
228
+ try {
229
+ await update_package_json(repo, updates, {strategy, log, git_ops, fs_ops}); // eslint-disable-line no-await-in-loop
230
+ updated_count++;
231
+ log?.info(` Updated ${updates.size} dependencies in ${repo.library.name}`);
232
+ } catch (error) {
233
+ const err = error instanceof Error ? error : new Error(String(error));
234
+ failed.push({repo: repo.library.name, error: err});
235
+ log?.error(` Failed to update ${repo.library.name}: ${err.message}`);
236
+ }
237
+ }
238
+
239
+ return {updated: updated_count, failed};
240
+ };
241
+
242
+ export const find_updates_needed = (
243
+ repo: LocalRepo,
244
+ published: Map<string, string>,
245
+ ): Map<
246
+ string,
247
+ {current: string; new: string; type: 'dependencies' | 'devDependencies' | 'peerDependencies'}
248
+ > => {
249
+ const updates: Map<
250
+ string,
251
+ {current: string; new: string; type: 'dependencies' | 'devDependencies' | 'peerDependencies'}
252
+ > = new Map();
253
+
254
+ // Check dependencies
255
+ if (repo.dependencies) {
256
+ for (const [dep_name, current_version] of repo.dependencies) {
257
+ const new_version = published.get(dep_name);
258
+ if (new_version && needs_update(current_version, new_version)) {
259
+ updates.set(dep_name, {
260
+ current: current_version,
261
+ new: new_version,
262
+ type: 'dependencies',
263
+ });
264
+ }
265
+ }
266
+ }
267
+
268
+ // Check devDependencies
269
+ if (repo.dev_dependencies) {
270
+ for (const [dep_name, current_version] of repo.dev_dependencies) {
271
+ const new_version = published.get(dep_name);
272
+ if (new_version && needs_update(current_version, new_version)) {
273
+ updates.set(dep_name, {
274
+ current: current_version,
275
+ new: new_version,
276
+ type: 'devDependencies',
277
+ });
278
+ }
279
+ }
280
+ }
281
+
282
+ // Check peerDependencies
283
+ if (repo.peer_dependencies) {
284
+ for (const [dep_name, current_version] of repo.peer_dependencies) {
285
+ const new_version = published.get(dep_name);
286
+ if (new_version && needs_update(current_version, new_version)) {
287
+ updates.set(dep_name, {
288
+ current: current_version,
289
+ new: new_version,
290
+ type: 'peerDependencies',
291
+ });
292
+ }
293
+ }
294
+ }
295
+
296
+ return updates;
297
+ };