@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,109 @@
1
+ /**
2
+ * Shared dependency graph validation logic used across multiple workflows.
3
+ *
4
+ * Consolidates graph building, cycle detection, and publishing order computation
5
+ * that was duplicated in three places: `multi_repo_publisher.ts`, `publishing_plan.ts`,
6
+ * and `gitops_analyze.task.ts`.
7
+ *
8
+ * Options pattern supports different behaviors: analyze can tolerate cycles for
9
+ * reporting, while publish must throw on production cycles.
10
+ *
11
+ * See also: `dependency_graph.ts` for core graph data structure and algorithms.
12
+ */
13
+
14
+ import type {Logger} from '@fuzdev/fuz_util/log.js';
15
+ import {TaskError} from '@ryanatkn/gro';
16
+ import {styleText as st} from 'node:util';
17
+
18
+ import {DependencyGraph, DependencyGraphBuilder} from './dependency_graph.js';
19
+ import type {LocalRepo} from './local_repo.js';
20
+
21
+ export interface GraphValidationResult {
22
+ graph: DependencyGraph;
23
+ publishing_order: Array<string>;
24
+ production_cycles: Array<Array<string>>;
25
+ dev_cycles: Array<Array<string>>;
26
+ sort_error?: string; // Error message if topological sort failed
27
+ }
28
+
29
+ /**
30
+ * Shared utility for building dependency graph, detecting cycles, and computing publishing order.
31
+ * This centralizes logic that was duplicated across multi_repo_publisher, publishing_plan, and gitops_analyze.
32
+ *
33
+ * @param options.throw_on_prod_cycles whether to throw an error if production cycles are detected (default: true)
34
+ * @param options.log_cycles whether to log cycle information (default: true)
35
+ * @param options.log_order whether to log publishing order (default: true)
36
+ * @returns graph validation result with graph, publishing order, and detected cycles
37
+ * @throws {TaskError} if production cycles detected and throw_on_prod_cycles is true
38
+ */
39
+ export const validate_dependency_graph = (
40
+ repos: Array<LocalRepo>,
41
+ options: {
42
+ log?: Logger;
43
+ throw_on_prod_cycles?: boolean;
44
+ log_cycles?: boolean;
45
+ log_order?: boolean;
46
+ } = {},
47
+ ): GraphValidationResult => {
48
+ const {log, throw_on_prod_cycles = true, log_cycles = true, log_order = true} = options;
49
+
50
+ // Build dependency graph
51
+ log?.info('📊 Analyzing dependencies...');
52
+ const builder = new DependencyGraphBuilder();
53
+ const graph = builder.build_from_repos(repos);
54
+
55
+ // Check for cycles
56
+ const {production_cycles, dev_cycles} = graph.detect_cycles_by_type();
57
+
58
+ // Log production cycles
59
+ if (production_cycles.length > 0 && log_cycles) {
60
+ log?.error(st('red', '❌ Production/peer dependency cycles detected:'));
61
+ for (const cycle of production_cycles) {
62
+ log?.error(` ${cycle.join(' → ')}`);
63
+ }
64
+
65
+ if (throw_on_prod_cycles) {
66
+ throw new TaskError(
67
+ `Cannot publish with production/peer dependency cycles. ` +
68
+ `These must be resolved before publishing.`,
69
+ );
70
+ }
71
+ }
72
+
73
+ // Log dev cycles (informational, not an error)
74
+ if (dev_cycles.length > 0 && log_cycles) {
75
+ log?.info(st('dim', 'ℹ️ Dev dependency cycles detected (this is normal):'));
76
+ for (const cycle of dev_cycles) {
77
+ log?.info(st('dim', ` ${cycle.join(' → ')}`));
78
+ }
79
+ }
80
+
81
+ // Compute publishing order
82
+ let publishing_order: Array<string>;
83
+ let sort_error: string | undefined;
84
+ try {
85
+ publishing_order = graph.topological_sort(true); // exclude dev deps to break cycles
86
+ if (log_order && publishing_order.length > 0) {
87
+ log?.info(` Publishing order: ${publishing_order.join(' → ')}`);
88
+ }
89
+ } catch (error) {
90
+ // Capture the sort error message for callers to report
91
+ sort_error = 'Failed to compute publishing order: ' + error;
92
+
93
+ // If topological sort fails (due to cycles), return empty array
94
+ // Only throw if production cycles exist AND throw_on_prod_cycles is true
95
+ if (production_cycles.length > 0 && throw_on_prod_cycles) {
96
+ throw new TaskError(sort_error);
97
+ }
98
+ // Otherwise, return empty publishing order (let caller handle it)
99
+ publishing_order = [];
100
+ }
101
+
102
+ return {
103
+ graph,
104
+ publishing_order,
105
+ production_cycles,
106
+ dev_cycles,
107
+ sort_error,
108
+ };
109
+ };
@@ -0,0 +1,359 @@
1
+ import {strip_end} from '@fuzdev/fuz_util/string.js';
2
+ import type {LibraryJson} from '@fuzdev/fuz_util/library_json.js';
3
+ import {Library} from '@fuzdev/fuz_ui/library.svelte.js';
4
+ import {existsSync} from 'node:fs';
5
+ import {join} from 'node:path';
6
+ import {TaskError} from '@ryanatkn/gro';
7
+ import type {Logger} from '@fuzdev/fuz_util/log.js';
8
+ import {spawn} from '@fuzdev/fuz_util/process.js';
9
+ import type {GitOperations, NpmOperations} from './operations.js';
10
+ import {default_git_operations, default_npm_operations} from './operations_defaults.js';
11
+
12
+ import type {GitopsConfig, GitopsRepoConfig} from './gitops_config.js';
13
+ import type {ResolvedGitopsConfig} from './resolved_gitops_config.js';
14
+
15
+ /**
16
+ * Fully loaded local repo with Library and extracted dependency data.
17
+ * Does not extend LocalRepoPath - Library is source of truth for name/repo_url/etc.
18
+ */
19
+ export interface LocalRepo {
20
+ library: Library;
21
+ library_json: LibraryJson;
22
+ repo_dir: string;
23
+ repo_git_ssh_url: string;
24
+ repo_config: GitopsRepoConfig;
25
+ dependencies?: Map<string, string>;
26
+ dev_dependencies?: Map<string, string>;
27
+ peer_dependencies?: Map<string, string>;
28
+ }
29
+
30
+ /**
31
+ * A repo that has been located on the filesystem (path exists).
32
+ * Used before loading - just filesystem/git concerns.
33
+ */
34
+ export interface LocalRepoPath {
35
+ type: 'local_repo_path';
36
+ repo_name: string; // from URL parsing (for display/logging before Library loaded)
37
+ repo_dir: string;
38
+ repo_url: string;
39
+ repo_git_ssh_url: string;
40
+ repo_config: GitopsRepoConfig;
41
+ }
42
+
43
+ /**
44
+ * A repo that is missing from the filesystem (needs cloning).
45
+ */
46
+ export interface LocalRepoMissing {
47
+ type: 'local_repo_missing';
48
+ repo_name: string;
49
+ repo_url: string;
50
+ repo_git_ssh_url: string;
51
+ repo_config: GitopsRepoConfig;
52
+ }
53
+
54
+ /**
55
+ * Loads repo data with automatic syncing and dependency management.
56
+ *
57
+ * Workflow:
58
+ * 1. Records current commit hash (for detecting changes)
59
+ * 2. Switches to target branch if needed (requires clean workspace)
60
+ * 3. Pulls latest changes from remote (skipped for local-only repos)
61
+ * 4. Validates workspace is clean after pull
62
+ * 5. Auto-installs dependencies if package.json changed
63
+ * 6. Imports library_json from src/routes/library.ts
64
+ * 7. Creates Library and extracts dependency maps
65
+ *
66
+ * This ensures repos are always in sync with their configured branch
67
+ * before being used by gitops commands.
68
+ *
69
+ * @throws {TaskError} if workspace dirty, branch switch fails, install fails, or library.ts missing
70
+ */
71
+ export const local_repo_load = async ({
72
+ local_repo_path,
73
+ log: _log,
74
+ git_ops = default_git_operations,
75
+ npm_ops = default_npm_operations,
76
+ }: {
77
+ local_repo_path: LocalRepoPath;
78
+ log?: Logger;
79
+ git_ops?: GitOperations;
80
+ npm_ops?: NpmOperations;
81
+ }): Promise<LocalRepo> => {
82
+ const {repo_config, repo_dir, repo_name, repo_git_ssh_url} = local_repo_path;
83
+
84
+ // Record commit hash before any changes
85
+ const commit_before_result = await git_ops.current_commit_hash({cwd: repo_dir});
86
+ if (!commit_before_result.ok) {
87
+ throw new TaskError(
88
+ `Failed to get commit hash in ${repo_dir}: ${commit_before_result.message}`,
89
+ );
90
+ }
91
+ const commit_before = commit_before_result.value;
92
+
93
+ // Switch to target branch if needed
94
+ const branch_result = await git_ops.current_branch_name({cwd: repo_dir});
95
+ if (!branch_result.ok) {
96
+ throw new TaskError(`Failed to get current branch in ${repo_dir}: ${branch_result.message}`);
97
+ }
98
+
99
+ const switched_branches = branch_result.value !== repo_config.branch;
100
+ if (switched_branches) {
101
+ const clean_result = await git_ops.check_clean_workspace({cwd: repo_dir});
102
+ if (!clean_result.ok) {
103
+ throw new TaskError(`Failed to check workspace in ${repo_dir}: ${clean_result.message}`);
104
+ }
105
+
106
+ if (!clean_result.value) {
107
+ throw new TaskError(
108
+ `Repo ${repo_dir} is not on branch "${repo_config.branch}" and the workspace is unclean, blocking switch`,
109
+ );
110
+ }
111
+
112
+ const checkout_result = await git_ops.checkout({branch: repo_config.branch, cwd: repo_dir});
113
+ if (!checkout_result.ok) {
114
+ throw new TaskError(
115
+ `Failed to checkout branch "${repo_config.branch}" in ${repo_dir}: ${checkout_result.message}`,
116
+ );
117
+ }
118
+ }
119
+
120
+ // Only pull if remote exists (skip for local-only repos, test fixtures)
121
+ const origin_result = await git_ops.has_remote({remote: 'origin', cwd: repo_dir});
122
+ if (!origin_result.ok) {
123
+ throw new TaskError(`Failed to check for remote in ${repo_dir}: ${origin_result.message}`);
124
+ }
125
+
126
+ if (origin_result.value) {
127
+ const pull_result = await git_ops.pull({cwd: repo_dir});
128
+ if (!pull_result.ok) {
129
+ throw new TaskError(`Failed to pull in ${repo_dir}: ${pull_result.message}`);
130
+ }
131
+ }
132
+
133
+ // Check clean workspace after pull to ensure we're in a good state
134
+ const clean_after_result = await git_ops.check_clean_workspace({cwd: repo_dir});
135
+ if (!clean_after_result.ok) {
136
+ throw new TaskError(`Failed to check workspace in ${repo_dir}: ${clean_after_result.message}`);
137
+ }
138
+
139
+ if (!clean_after_result.value) {
140
+ throw new TaskError(
141
+ `Workspace ${repo_dir} is unclean after pulling branch "${repo_config.branch}"`,
142
+ );
143
+ }
144
+
145
+ // Record commit hash after pull
146
+ const commit_after_result = await git_ops.current_commit_hash({cwd: repo_dir});
147
+ if (!commit_after_result.ok) {
148
+ throw new TaskError(`Failed to get commit hash in ${repo_dir}: ${commit_after_result.message}`);
149
+ }
150
+ const commit_after = commit_after_result.value;
151
+
152
+ // Track if we got new commits
153
+ const got_new_commits = commit_before !== commit_after;
154
+
155
+ // Only install if package.json changed
156
+ if (got_new_commits) {
157
+ const changed_result = await git_ops.has_file_changed({
158
+ from_commit: commit_before,
159
+ to_commit: commit_after,
160
+ file_path: 'package.json',
161
+ cwd: repo_dir,
162
+ });
163
+
164
+ if (!changed_result.ok) {
165
+ throw new TaskError(
166
+ `Failed to check if package.json changed in ${repo_dir}: ${changed_result.message}`,
167
+ );
168
+ }
169
+
170
+ if (changed_result.value) {
171
+ const install_result = await npm_ops.install({cwd: repo_dir});
172
+ if (!install_result.ok) {
173
+ throw new TaskError(
174
+ `Failed to install dependencies in ${repo_dir}: ${install_result.message}${install_result.stderr ? `\n${install_result.stderr}` : ''}`,
175
+ );
176
+ }
177
+ }
178
+ }
179
+
180
+ // Validate and load library.ts
181
+ const library_path = join(repo_dir, 'src/routes/library.ts');
182
+ if (!existsSync(library_path)) {
183
+ throw new TaskError(
184
+ `Repo "${repo_name}" is missing src/routes/library.ts\n` +
185
+ `This file is required for fuz_gitops. To fix:\n` +
186
+ ` 1. Create src/routes/library.gen.ts with:\n` +
187
+ ` import {library_gen} from '@fuzdev/fuz_ui/library_gen.js';\n` +
188
+ ` export const gen = library_gen();\n` +
189
+ ` 2. Run: cd ${repo_dir} && gro gen`,
190
+ );
191
+ }
192
+
193
+ const library_module = await import(library_path);
194
+ const {library_json} = library_module as {library_json: LibraryJson | undefined};
195
+ if (!library_json) {
196
+ throw new TaskError(
197
+ `Repo "${repo_name}" has invalid src/routes/library.ts - missing library_json export\n` +
198
+ `The file must export a library_json object. To fix:\n` +
199
+ ` 1. Ensure src/routes/library.gen.ts uses library_gen from @fuzdev/fuz_ui\n` +
200
+ ` 2. Run: cd ${repo_dir} && gro gen`,
201
+ );
202
+ }
203
+ const library = new Library(library_json);
204
+
205
+ const local_repo: LocalRepo = {
206
+ library,
207
+ library_json,
208
+ repo_dir,
209
+ repo_git_ssh_url,
210
+ repo_config,
211
+ };
212
+
213
+ // Extract dependencies from package_json
214
+ const {package_json} = library;
215
+ if (package_json.dependencies) {
216
+ local_repo.dependencies = new Map(Object.entries(package_json.dependencies));
217
+ }
218
+ if (package_json.devDependencies) {
219
+ local_repo.dev_dependencies = new Map(Object.entries(package_json.devDependencies));
220
+ }
221
+ if (package_json.peerDependencies) {
222
+ local_repo.peer_dependencies = new Map(Object.entries(package_json.peerDependencies));
223
+ }
224
+
225
+ return local_repo;
226
+ };
227
+
228
+ export const local_repos_ensure = async ({
229
+ resolved_config,
230
+ repos_dir,
231
+ gitops_config,
232
+ download,
233
+ log,
234
+ npm_ops = default_npm_operations,
235
+ }: {
236
+ resolved_config: ResolvedGitopsConfig;
237
+ repos_dir: string;
238
+ gitops_config: GitopsConfig;
239
+ download: boolean;
240
+ log?: Logger;
241
+ npm_ops?: NpmOperations;
242
+ }): Promise<Array<LocalRepoPath>> => {
243
+ let local_repo_paths: Array<LocalRepoPath> | null = null;
244
+
245
+ if (!resolved_config.local_repos_missing) {
246
+ local_repo_paths = resolved_config.local_repo_paths;
247
+ } else {
248
+ if (download) {
249
+ const downloaded = await download_repos({
250
+ repos_dir,
251
+ local_repos_missing: resolved_config.local_repos_missing,
252
+ log,
253
+ npm_ops,
254
+ });
255
+ local_repo_paths = (resolved_config.local_repo_paths ?? [])
256
+ .concat(downloaded)
257
+ .sort(
258
+ (a, b) =>
259
+ gitops_config.repos.findIndex((r) => r.repo_url === a.repo_url) -
260
+ gitops_config.repos.findIndex((r) => r.repo_url === b.repo_url),
261
+ );
262
+ } else {
263
+ log?.error(
264
+ `Failed to resolve local repos in ${repos_dir} - do you need to pass \`--download\` or configure the directory?`, // TODO leaking task impl details
265
+ resolved_config.local_repos_missing.map((r) => r.repo_url),
266
+ );
267
+ throw new TaskError('Failed to resolve local configs');
268
+ }
269
+ }
270
+
271
+ if (!local_repo_paths) {
272
+ throw new TaskError('No repos are configured in `gitops_config.ts`');
273
+ }
274
+
275
+ return local_repo_paths;
276
+ };
277
+
278
+ export const local_repos_load = async ({
279
+ local_repo_paths,
280
+ log,
281
+ git_ops = default_git_operations,
282
+ npm_ops = default_npm_operations,
283
+ }: {
284
+ local_repo_paths: Array<LocalRepoPath>;
285
+ log?: Logger;
286
+ git_ops?: GitOperations;
287
+ npm_ops?: NpmOperations;
288
+ }): Promise<Array<LocalRepo>> => {
289
+ const loaded: Array<LocalRepo> = [];
290
+ for (const local_repo_path of local_repo_paths) {
291
+ loaded.push(await local_repo_load({local_repo_path, log, git_ops, npm_ops})); // eslint-disable-line no-await-in-loop
292
+ }
293
+ return loaded;
294
+ };
295
+
296
+ export const local_repo_locate = ({
297
+ repo_config,
298
+ repos_dir,
299
+ }: {
300
+ repo_config: GitopsRepoConfig;
301
+ repos_dir: string;
302
+ }): LocalRepoPath | LocalRepoMissing => {
303
+ const {repo_url} = repo_config;
304
+ const repo_name = strip_end(repo_url, '/').split('/').at(-1);
305
+ if (!repo_name) throw Error('Invalid `repo_config.repo_url` ' + repo_url);
306
+
307
+ const repo_git_ssh_url = to_repo_git_ssh_url(repo_url);
308
+
309
+ const repo_dir = repo_config.repo_dir ?? join(repos_dir, repo_name);
310
+ if (!existsSync(repo_dir)) {
311
+ return {type: 'local_repo_missing', repo_name, repo_url, repo_git_ssh_url, repo_config};
312
+ }
313
+
314
+ return {
315
+ type: 'local_repo_path',
316
+ repo_name,
317
+ repo_dir,
318
+ repo_url,
319
+ repo_git_ssh_url,
320
+ repo_config,
321
+ };
322
+ };
323
+
324
+ const to_repo_git_ssh_url = (repo_url: string): string => {
325
+ const url = new URL(repo_url);
326
+ return `git@${url.hostname}:${url.pathname.substring(1)}`;
327
+ };
328
+
329
+ const download_repos = async ({
330
+ repos_dir,
331
+ local_repos_missing,
332
+ log,
333
+ npm_ops = default_npm_operations,
334
+ }: {
335
+ repos_dir: string;
336
+ local_repos_missing: Array<LocalRepoMissing>;
337
+ log?: Logger;
338
+ npm_ops?: NpmOperations;
339
+ }): Promise<Array<LocalRepoPath>> => {
340
+ const resolved: Array<LocalRepoPath> = [];
341
+ for (const {repo_config, repo_git_ssh_url} of local_repos_missing) {
342
+ log?.info(`cloning repo ${repo_git_ssh_url} to ${repos_dir}`);
343
+ await spawn('git', ['clone', repo_git_ssh_url], {cwd: repos_dir}); // eslint-disable-line no-await-in-loop
344
+ const local_repo = local_repo_locate({repo_config, repos_dir});
345
+ if (local_repo.type === 'local_repo_missing') {
346
+ throw new TaskError(`Failed to clone repo ${repo_git_ssh_url} to ${repos_dir}`);
347
+ }
348
+ // Always install dependencies after cloning
349
+ log?.info(`installing dependencies for newly cloned repo ${local_repo.repo_dir}`);
350
+ const install_result = await npm_ops.install({cwd: local_repo.repo_dir}); // eslint-disable-line no-await-in-loop
351
+ if (!install_result.ok) {
352
+ throw new TaskError(
353
+ `Failed to install dependencies in ${local_repo.repo_dir}: ${install_result.message}${install_result.stderr ? `\n${install_result.stderr}` : ''}`,
354
+ );
355
+ }
356
+ resolved.push(local_repo);
357
+ }
358
+ return resolved;
359
+ };
@@ -0,0 +1,147 @@
1
+ import type {Logger} from '@fuzdev/fuz_util/log.js';
2
+ import {styleText as st} from 'node:util';
3
+
4
+ import type {DependencyGraphBuilder} from './dependency_graph.js';
5
+
6
+ /**
7
+ * Formats wildcard dependencies as styled strings.
8
+ * Returns array of lines for inclusion in output.
9
+ */
10
+ export const format_wildcard_dependencies = (
11
+ analysis: ReturnType<DependencyGraphBuilder['analyze']>,
12
+ ): Array<string> => {
13
+ if (analysis.wildcard_deps.length === 0) return [];
14
+
15
+ const lines: Array<string> = [];
16
+ lines.push(st('yellow', `\n⚠️ Found ${analysis.wildcard_deps.length} wildcard dependencies:`));
17
+ for (const {pkg, dep, version} of analysis.wildcard_deps) {
18
+ lines.push(` ${pkg} → ${dep} ${st('red', version)}`);
19
+ }
20
+ return lines;
21
+ };
22
+
23
+ /**
24
+ * Formats dev circular dependencies as styled strings.
25
+ * Returns array of lines for inclusion in output.
26
+ */
27
+ export const format_dev_cycles = (
28
+ analysis: ReturnType<DependencyGraphBuilder['analyze']>,
29
+ ): Array<string> => {
30
+ if (analysis.dev_cycles.length === 0) return [];
31
+
32
+ const lines: Array<string> = [];
33
+ lines.push(
34
+ st(
35
+ 'dim',
36
+ `\nℹ️ Found ${analysis.dev_cycles.length} dev circular dependencies (normal, non-blocking):`,
37
+ ),
38
+ );
39
+ for (const cycle of analysis.dev_cycles) {
40
+ lines.push(st('dim', ` ${cycle.join(' → ')}`));
41
+ }
42
+ return lines;
43
+ };
44
+
45
+ /**
46
+ * Formats production/peer circular dependencies as styled strings.
47
+ * Returns array of lines for inclusion in output.
48
+ */
49
+ export const format_production_cycles = (
50
+ analysis: ReturnType<DependencyGraphBuilder['analyze']>,
51
+ ): Array<string> => {
52
+ if (analysis.production_cycles.length === 0) return [];
53
+
54
+ const lines: Array<string> = [];
55
+ lines.push(
56
+ st(
57
+ 'red',
58
+ `\n❌ Found ${analysis.production_cycles.length} production/peer circular dependencies (blocks publishing):`,
59
+ ),
60
+ );
61
+ for (const cycle of analysis.production_cycles) {
62
+ lines.push(` ${st('red', cycle.join(' → '))}`);
63
+ }
64
+ return lines;
65
+ };
66
+
67
+ /**
68
+ * Logs wildcard dependencies as warnings.
69
+ * Wildcard dependencies require attention and should be reviewed.
70
+ */
71
+ export const log_wildcard_dependencies = (
72
+ analysis: ReturnType<DependencyGraphBuilder['analyze']>,
73
+ log: Logger,
74
+ indent = '',
75
+ ): void => {
76
+ const lines = format_wildcard_dependencies(analysis);
77
+ for (const line of lines) {
78
+ log.warn(indent + line);
79
+ }
80
+ };
81
+
82
+ /**
83
+ * Logs dev circular dependencies as info.
84
+ * Dev cycles are normal and non-blocking, so they're informational, not warnings.
85
+ */
86
+ export const log_dev_cycles = (
87
+ analysis: ReturnType<DependencyGraphBuilder['analyze']>,
88
+ log: Logger,
89
+ indent = '',
90
+ ): void => {
91
+ const lines = format_dev_cycles(analysis);
92
+ for (const line of lines) {
93
+ log.info(indent + line);
94
+ }
95
+ };
96
+
97
+ /**
98
+ * Logs production/peer circular dependencies as errors.
99
+ * Production cycles block publishing and must be resolved.
100
+ */
101
+ export const log_production_cycles = (
102
+ analysis: ReturnType<DependencyGraphBuilder['analyze']>,
103
+ log: Logger,
104
+ indent = '',
105
+ ): void => {
106
+ const lines = format_production_cycles(analysis);
107
+ for (const line of lines) {
108
+ log.error(indent + line);
109
+ }
110
+ };
111
+
112
+ /**
113
+ * Logs all dependency analysis results (wildcards, production cycles, dev cycles).
114
+ * Convenience function that calls all three logging functions in order.
115
+ */
116
+ export const log_dependency_analysis = (
117
+ analysis: ReturnType<DependencyGraphBuilder['analyze']>,
118
+ log: Logger,
119
+ indent = '',
120
+ ): void => {
121
+ log_wildcard_dependencies(analysis, log, indent);
122
+ log_production_cycles(analysis, log, indent);
123
+ log_dev_cycles(analysis, log, indent);
124
+ };
125
+
126
+ /**
127
+ * Logs a simple bulleted list with a header.
128
+ * Common pattern for warnings, info messages, and other lists.
129
+ */
130
+ export const log_list = (
131
+ items: Array<string>,
132
+ header: string,
133
+ color: 'cyan' | 'yellow' | 'red' | 'dim',
134
+ log: Logger,
135
+ log_method: 'info' | 'warn' | 'error' = 'info',
136
+ ): void => {
137
+ if (items.length === 0) return;
138
+
139
+ // Only add newline prefix if header is non-empty
140
+ const formatted_header = header ? `\n${header}` : '';
141
+ if (formatted_header) {
142
+ log[log_method](st(color, formatted_header));
143
+ }
144
+ for (const item of items) {
145
+ log[log_method](st(color, ` • ${item}`));
146
+ }
147
+ };