@fuzdev/fuz_gitops 0.78.0 → 0.79.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/README.md +91 -17
  2. package/dist/ModulesDetail.svelte +22 -27
  3. package/dist/ModulesDetail.svelte.d.ts.map +1 -1
  4. package/dist/ModulesNav.svelte +8 -5
  5. package/dist/ModulesNav.svelte.d.ts.map +1 -1
  6. package/dist/ModulesPage.svelte +3 -5
  7. package/dist/ModulesPage.svelte.d.ts.map +1 -1
  8. package/dist/PageFooter.svelte +2 -2
  9. package/dist/PageHeader.svelte +6 -6
  10. package/dist/PageHeader.svelte.d.ts +0 -3
  11. package/dist/PageHeader.svelte.d.ts.map +1 -1
  12. package/dist/PullRequestsDetail.svelte +29 -20
  13. package/dist/PullRequestsDetail.svelte.d.ts.map +1 -1
  14. package/dist/PullRequestsPage.svelte +4 -6
  15. package/dist/PullRequestsPage.svelte.d.ts.map +1 -1
  16. package/dist/ReposTable.svelte +43 -55
  17. package/dist/ReposTable.svelte.d.ts.map +1 -1
  18. package/dist/ReposTree.svelte +9 -21
  19. package/dist/ReposTree.svelte.d.ts.map +1 -1
  20. package/dist/ReposTreeNav.svelte +9 -12
  21. package/dist/ReposTreeNav.svelte.d.ts.map +1 -1
  22. package/dist/TablePage.svelte +3 -5
  23. package/dist/TablePage.svelte.d.ts.map +1 -1
  24. package/dist/TreeItemPage.svelte +3 -5
  25. package/dist/TreeItemPage.svelte.d.ts.map +1 -1
  26. package/dist/TreePage.svelte +3 -5
  27. package/dist/TreePage.svelte.d.ts.map +1 -1
  28. package/dist/changeset_generator.d.ts.map +1 -1
  29. package/dist/changeset_generator.js +2 -2
  30. package/dist/changeset_reader.d.ts.map +1 -1
  31. package/dist/changeset_reader.js +3 -3
  32. package/dist/ci_reconcile.d.ts +6 -10
  33. package/dist/ci_reconcile.d.ts.map +1 -1
  34. package/dist/ci_reconcile.js +6 -8
  35. package/dist/dependency_graph.d.ts +26 -49
  36. package/dist/dependency_graph.d.ts.map +1 -1
  37. package/dist/dependency_graph.js +40 -83
  38. package/dist/dependency_updater.d.ts +1 -1
  39. package/dist/dependency_updater.d.ts.map +1 -1
  40. package/dist/dependency_updater.js +14 -9
  41. package/dist/fetch_repo_data.d.ts +34 -8
  42. package/dist/fetch_repo_data.d.ts.map +1 -1
  43. package/dist/fetch_repo_data.js +44 -22
  44. package/dist/fs_fetch_value_cache.d.ts +1 -1
  45. package/dist/fs_fetch_value_cache.d.ts.map +1 -1
  46. package/dist/fs_fetch_value_cache.js +3 -3
  47. package/dist/git_operations.d.ts +6 -49
  48. package/dist/git_operations.d.ts.map +1 -1
  49. package/dist/git_operations.js +13 -126
  50. package/dist/github.d.ts +17 -2
  51. package/dist/github.d.ts.map +1 -1
  52. package/dist/github.js +17 -8
  53. package/dist/github_helpers.d.ts.map +1 -1
  54. package/dist/github_helpers.js +3 -11
  55. package/dist/gitops_analyze.task.d.ts +24 -2
  56. package/dist/gitops_analyze.task.d.ts.map +1 -1
  57. package/dist/gitops_analyze.task.js +47 -34
  58. package/dist/gitops_config.d.ts +34 -79
  59. package/dist/gitops_config.d.ts.map +1 -1
  60. package/dist/gitops_config.js +76 -53
  61. package/dist/gitops_constants.d.ts +6 -4
  62. package/dist/gitops_constants.d.ts.map +1 -1
  63. package/dist/gitops_constants.js +6 -4
  64. package/dist/gitops_plan.task.d.ts +29 -3
  65. package/dist/gitops_plan.task.d.ts.map +1 -1
  66. package/dist/gitops_plan.task.js +83 -61
  67. package/dist/gitops_publish.task.d.ts +62 -2
  68. package/dist/gitops_publish.task.d.ts.map +1 -1
  69. package/dist/gitops_publish.task.js +193 -105
  70. package/dist/gitops_run.task.d.ts +74 -0
  71. package/dist/gitops_run.task.d.ts.map +1 -1
  72. package/dist/gitops_run.task.js +176 -162
  73. package/dist/gitops_sync.task.d.ts +65 -3
  74. package/dist/gitops_sync.task.d.ts.map +1 -1
  75. package/dist/gitops_sync.task.js +163 -37
  76. package/dist/gitops_task_helpers.d.ts +86 -59
  77. package/dist/gitops_task_helpers.d.ts.map +1 -1
  78. package/dist/gitops_task_helpers.js +120 -79
  79. package/dist/gitops_validate.task.d.ts +1 -2
  80. package/dist/gitops_validate.task.d.ts.map +1 -1
  81. package/dist/gitops_validate.task.js +25 -32
  82. package/dist/graph_validation.d.ts +16 -27
  83. package/dist/graph_validation.d.ts.map +1 -1
  84. package/dist/graph_validation.js +24 -65
  85. package/dist/local_repo.d.ts +50 -62
  86. package/dist/local_repo.d.ts.map +1 -1
  87. package/dist/local_repo.js +83 -196
  88. package/dist/log_helpers.d.ts +8 -8
  89. package/dist/log_helpers.d.ts.map +1 -1
  90. package/dist/multi_repo_publisher.d.ts +10 -0
  91. package/dist/multi_repo_publisher.d.ts.map +1 -1
  92. package/dist/multi_repo_publisher.js +125 -61
  93. package/dist/npm_registry.d.ts +23 -19
  94. package/dist/npm_registry.d.ts.map +1 -1
  95. package/dist/npm_registry.js +21 -48
  96. package/dist/operations.d.ts +75 -180
  97. package/dist/operations.d.ts.map +1 -1
  98. package/dist/operations.js +15 -7
  99. package/dist/operations_defaults.d.ts +17 -2
  100. package/dist/operations_defaults.d.ts.map +1 -1
  101. package/dist/operations_defaults.js +111 -128
  102. package/dist/output_helpers.d.ts +36 -0
  103. package/dist/output_helpers.d.ts.map +1 -1
  104. package/dist/output_helpers.js +37 -3
  105. package/dist/preflight_checks.d.ts +20 -27
  106. package/dist/preflight_checks.d.ts.map +1 -1
  107. package/dist/preflight_checks.js +37 -126
  108. package/dist/publish_gate.d.ts.map +1 -1
  109. package/dist/publish_steps.d.ts +2 -4
  110. package/dist/publish_steps.d.ts.map +1 -1
  111. package/dist/publish_steps.js +4 -8
  112. package/dist/publishing_event.d.ts +2 -8
  113. package/dist/publishing_event.d.ts.map +1 -1
  114. package/dist/publishing_event.js +20 -17
  115. package/dist/publishing_event_handler.d.ts +10 -8
  116. package/dist/publishing_event_handler.d.ts.map +1 -1
  117. package/dist/publishing_event_handler.js +19 -17
  118. package/dist/publishing_plan.d.ts +16 -1
  119. package/dist/publishing_plan.d.ts.map +1 -1
  120. package/dist/publishing_plan.js +332 -254
  121. package/dist/publishing_plan_helpers.d.ts.map +1 -1
  122. package/dist/publishing_plan_helpers.js +3 -3
  123. package/dist/publishing_plan_logging.d.ts +1 -1
  124. package/dist/publishing_plan_logging.d.ts.map +1 -1
  125. package/dist/publishing_plan_logging.js +22 -15
  126. package/dist/repo.svelte.d.ts +7 -0
  127. package/dist/repo.svelte.d.ts.map +1 -1
  128. package/dist/repo.svelte.js +3 -0
  129. package/dist/repo_readiness.d.ts +210 -0
  130. package/dist/repo_readiness.d.ts.map +1 -0
  131. package/dist/repo_readiness.js +516 -0
  132. package/dist/repos_status.d.ts +3340 -0
  133. package/dist/repos_status.d.ts.map +1 -0
  134. package/dist/repos_status.js +555 -0
  135. package/dist/repos_status_load.d.ts +65 -0
  136. package/dist/repos_status_load.d.ts.map +1 -0
  137. package/dist/repos_status_load.js +131 -0
  138. package/dist/version_utils.js +1 -1
  139. package/package.json +15 -34
  140. package/src/lib/cargo_toml.ts +3 -3
  141. package/src/lib/changeset_generator.ts +19 -19
  142. package/src/lib/changeset_reader.ts +18 -18
  143. package/src/lib/ci_reconcile.ts +12 -16
  144. package/src/lib/dependency_graph.ts +61 -113
  145. package/src/lib/dependency_updater.ts +36 -30
  146. package/src/lib/fetch_repo_data.ts +71 -34
  147. package/src/lib/fs_fetch_value_cache.ts +15 -11
  148. package/src/lib/git_operations.ts +22 -194
  149. package/src/lib/github.ts +33 -21
  150. package/src/lib/github_helpers.ts +9 -16
  151. package/src/lib/gitops_analyze.task.ts +104 -66
  152. package/src/lib/gitops_config.ts +90 -139
  153. package/src/lib/gitops_constants.ts +6 -4
  154. package/src/lib/gitops_plan.task.ts +128 -76
  155. package/src/lib/gitops_publish.task.ts +280 -144
  156. package/src/lib/gitops_run.task.ts +265 -193
  157. package/src/lib/gitops_sync.task.ts +236 -52
  158. package/src/lib/gitops_task_helpers.ts +170 -114
  159. package/src/lib/gitops_validate.task.ts +44 -54
  160. package/src/lib/graph_validation.ts +28 -89
  161. package/src/lib/local_repo.ts +128 -335
  162. package/src/lib/log_helpers.ts +18 -28
  163. package/src/lib/multi_repo_publisher.ts +192 -110
  164. package/src/lib/npm_registry.ts +42 -67
  165. package/src/lib/operations.ts +99 -160
  166. package/src/lib/operations_defaults.ts +157 -201
  167. package/src/lib/output_helpers.ts +58 -6
  168. package/src/lib/preflight_checks.ts +52 -188
  169. package/src/lib/publish_gate.ts +7 -9
  170. package/src/lib/publish_steps.ts +27 -25
  171. package/src/lib/publishing_event.ts +26 -23
  172. package/src/lib/publishing_event_handler.ts +24 -22
  173. package/src/lib/publishing_plan.ts +474 -310
  174. package/src/lib/publishing_plan_helpers.ts +16 -12
  175. package/src/lib/publishing_plan_logging.ts +53 -44
  176. package/src/lib/repo.svelte.ts +16 -8
  177. package/src/lib/repo_readiness.ts +647 -0
  178. package/src/lib/repos_status.ts +671 -0
  179. package/src/lib/repos_status_load.ts +173 -0
  180. package/src/lib/version_utils.ts +3 -3
  181. package/dist/config_reconcile.d.ts +0 -46
  182. package/dist/config_reconcile.d.ts.map +0 -1
  183. package/dist/config_reconcile.js +0 -60
  184. package/dist/paths.d.ts +0 -7
  185. package/dist/paths.d.ts.map +0 -1
  186. package/dist/paths.js +0 -6
  187. package/dist/repo_ops.d.ts +0 -59
  188. package/dist/repo_ops.d.ts.map +0 -1
  189. package/dist/repo_ops.js +0 -170
  190. package/dist/resolved_gitops_config.d.ts +0 -9
  191. package/dist/resolved_gitops_config.d.ts.map +0 -1
  192. package/dist/resolved_gitops_config.js +0 -12
  193. package/src/lib/config_reconcile.ts +0 -94
  194. package/src/lib/paths.ts +0 -6
  195. package/src/lib/repo_ops.ts +0 -216
  196. package/src/lib/resolved_gitops_config.ts +0 -27
@@ -1,42 +1,47 @@
1
- import type {Task} from '@fuzdev/gro';
2
- import {z} from 'zod';
3
- import {styleText as st} from 'node:util';
4
- import type {Logger} from '@fuzdev/fuz_util/log.ts';
5
-
6
- import {get_gitops_ready} from './gitops_task_helpers.ts';
7
- import type {DependencyGraph} from './dependency_graph.ts';
8
- import {repo_is_npm} from './local_repo.ts';
9
- import {analyze_repos, type DependencyAnalysis} from './graph_validation.ts';
1
+ import type { Task } from '@fuzdev/gro';
2
+ import { z } from 'zod';
3
+ import { styleText as st } from 'node:util';
4
+ import type { Logger } from '@fuzdev/fuz_util/log.ts';
5
+
6
+ import {
7
+ get_gitops_ready,
8
+ log_readiness_block,
9
+ type ResolveGitopsReposOptions
10
+ } from './gitops_task_helpers.ts';
11
+ import type { DependencyAnalysis, DependencyGraph } from './dependency_graph.ts';
12
+ import { repo_is_npm, type LocalRepo } from './local_repo.ts';
13
+ import { analyze_repos } from './graph_validation.ts';
10
14
  import {
11
15
  format_wildcard_dependencies,
12
16
  format_dev_cycles,
13
- format_production_cycles,
17
+ format_production_cycles
14
18
  } from './log_helpers.ts';
15
- import {format_and_output, type OutputFormatters} from './output_helpers.ts';
16
- import {GITOPS_CONFIG_PATH_DEFAULT} from './gitops_constants.ts';
19
+ import {
20
+ format_and_output,
21
+ output_is_machine,
22
+ route_human_output,
23
+ type OutputFormatters
24
+ } from './output_helpers.ts';
25
+ import { GITOPS_CONFIG_PATH_DEFAULT } from './gitops_constants.ts';
17
26
 
18
27
  /** @nodocs */
19
28
  export const Args = z.strictObject({
20
29
  config: z
21
30
  .string()
22
- .meta({description: 'path to the gitops config file, absolute or relative to the cwd'})
31
+ .meta({ description: 'path to the gitops config file, absolute or relative to the cwd' })
23
32
  .default(GITOPS_CONFIG_PATH_DEFAULT),
24
- dir: z
33
+ registry: z
25
34
  .string()
26
- .meta({description: 'path containing the repos, defaults to the parent of the config dir'})
35
+ .meta({
36
+ description:
37
+ 'path to the repos.toml registry, when `repos` would not find it walking up from the cwd'
38
+ })
27
39
  .optional(),
28
40
  format: z
29
41
  .enum(['stdout', 'json', 'markdown'])
30
- .meta({description: 'output format'})
42
+ .meta({ description: 'output format' })
31
43
  .default('stdout'),
32
- outfile: z.string().meta({description: 'write output to file instead of logging'}).optional(),
33
- sync: z
34
- .boolean()
35
- .meta({
36
- description:
37
- 'sync repos (switch branch, pull, install) before analyzing instead of reading the working tree as-is',
38
- })
39
- .default(false),
44
+ outfile: z.string().meta({ description: 'write output to file instead of logging' }).optional()
40
45
  });
41
46
  export type Args = z.infer<typeof Args>;
42
47
 
@@ -44,37 +49,70 @@ export type Args = z.infer<typeof Args>;
44
49
  export const task: Task<Args> = {
45
50
  Args,
46
51
  summary: 'analyze dependency structure and relationships across repos',
47
- run: async ({args, log}) => {
48
- const {config, dir, format, outfile, sync} = args;
49
-
50
- // Get repos ready (without downloading); read the working tree as-is unless `--sync`
51
- const {local_repos} = await get_gitops_ready({config, dir, download: false, sync, log});
52
-
53
- // Only npm packages form the dependency graph; note any non-npm repos (e.g. cargo)
54
- // that are excluded so the omission isn't silent.
55
- const non_npm_repos = local_repos.filter((r) => !repo_is_npm(r));
56
- if (non_npm_repos.length > 0) {
57
- log.info(
58
- st(
59
- 'dim',
60
- `excluding ${non_npm_repos.length} non-npm repo(s) from analysis (dashboard-only): ` +
61
- non_npm_repos.map((r) => r.library.name).join(', '),
62
- ),
63
- );
64
- }
52
+ run: async ({ args, log }) => {
53
+ await run_gitops_analyze(args, log);
54
+ }
55
+ };
65
56
 
66
- // Build the dependency graph and analyze cycles/wildcards (tolerating cycles)
67
- const {graph, analysis, publishing_order} = analyze_repos(local_repos);
57
+ /**
58
+ * The side effects `run_gitops_analyze` reaches through, injectable for tests.
59
+ *
60
+ * @nodocs
61
+ */
62
+ export interface GitopsAnalyzeDeps {
63
+ /** Loads the configured repos as they sit (`get_gitops_ready`). */
64
+ load_repos: (options: ResolveGitopsReposOptions) => Promise<{ local_repos: Array<LocalRepo> }>;
65
+ }
68
66
 
69
- // Format and output using output_helpers
70
- const data = {
71
- graph,
72
- analysis,
73
- publishing_order,
74
- };
67
+ const default_gitops_analyze_deps: GitopsAnalyzeDeps = {
68
+ load_repos: get_gitops_ready
69
+ };
75
70
 
76
- await format_and_output(data, create_formatters(), {format, outfile, log});
77
- },
71
+ /**
72
+ * Runs `gro gitops_analyze`: loads the repos as they sit, logs the readiness
73
+ * block, and outputs the dependency analysis. Under `--format json` or
74
+ * `markdown` without `--outfile`, the log goes to stderr and stdout carries
75
+ * the document alone (`route_human_output`).
76
+ *
77
+ * @nodocs
78
+ */
79
+ export const run_gitops_analyze = async (
80
+ args: Args,
81
+ log: Logger,
82
+ deps: Partial<GitopsAnalyzeDeps> = {}
83
+ ): Promise<void> => {
84
+ const { load_repos } = { ...default_gitops_analyze_deps, ...deps };
85
+ const { config, registry, format, outfile } = args;
86
+ const write_stdout = route_human_output(log, output_is_machine(format, outfile));
87
+
88
+ // Read the working trees as they sit, and say which aren't at rest
89
+ const { local_repos } = await load_repos({ config, registry, log });
90
+ log_readiness_block(local_repos, log);
91
+
92
+ // Only npm packages form the dependency graph; note any non-npm repos (e.g. cargo)
93
+ // that are excluded so the omission isn't silent.
94
+ const non_npm_repos = local_repos.filter((r) => !repo_is_npm(r));
95
+ if (non_npm_repos.length > 0) {
96
+ log.info(
97
+ st(
98
+ 'dim',
99
+ `excluding ${non_npm_repos.length} non-npm repo(s) from analysis (dashboard-only): ` +
100
+ non_npm_repos.map((r) => r.library.name).join(', ')
101
+ )
102
+ );
103
+ }
104
+
105
+ // Build the dependency graph and analyze cycles/wildcards (tolerating cycles)
106
+ const { graph, analysis, publishing_order } = analyze_repos(local_repos);
107
+
108
+ // Format and output using output_helpers
109
+ const data = {
110
+ graph,
111
+ analysis,
112
+ publishing_order
113
+ };
114
+
115
+ await format_and_output(data, create_formatters(), { format, outfile, log, write_stdout });
78
116
  };
79
117
 
80
118
  // Data type for analysis output
@@ -88,32 +126,32 @@ interface AnalysisData {
88
126
  const create_formatters = (): OutputFormatters<AnalysisData> => ({
89
127
  json: (data) => format_json(data.graph, data.analysis, data.publishing_order),
90
128
  markdown: (data) => format_markdown(data.graph, data.analysis, data.publishing_order),
91
- stdout: (data, log) => format_stdout(data.graph, data.analysis, data.publishing_order, log),
129
+ stdout: (data, log) => format_stdout(data.graph, data.analysis, data.publishing_order, log)
92
130
  });
93
131
 
94
132
  // Helper to calculate common statistics
95
133
  const calculate_stats = (graph: DependencyGraph) => {
96
134
  const total_deps = Array.from(graph.nodes.values()).reduce(
97
135
  (sum, node) => sum + node.dependencies.size,
98
- 0,
136
+ 0
99
137
  );
100
138
  const internal_deps = Array.from(graph.nodes.values()).reduce(
101
139
  (sum, node) =>
102
140
  sum + Array.from(node.dependencies.keys()).filter((name) => graph.nodes.has(name)).length,
103
- 0,
141
+ 0
104
142
  );
105
- return {total_deps, internal_deps};
143
+ return { total_deps, internal_deps };
106
144
  };
107
145
 
108
146
  const format_json = (
109
147
  graph: DependencyGraph,
110
148
  analysis: DependencyAnalysis,
111
- publishing_order: Array<string> | null,
149
+ publishing_order: Array<string> | null
112
150
  ): string => {
113
151
  const output = {
114
152
  graph: graph.toJSON(),
115
153
  analysis,
116
- publishing_order,
154
+ publishing_order
117
155
  };
118
156
  return JSON.stringify(output, null, 2);
119
157
  };
@@ -121,12 +159,12 @@ const format_json = (
121
159
  const format_markdown = (
122
160
  graph: DependencyGraph,
123
161
  analysis: DependencyAnalysis,
124
- publishing_order: Array<string> | null,
162
+ publishing_order: Array<string> | null
125
163
  ): Array<string> => {
126
164
  const lines: Array<string> = ['# Dependency Analysis'];
127
165
 
128
166
  // Summary stats
129
- const {total_deps, internal_deps} = calculate_stats(graph);
167
+ const { total_deps, internal_deps } = calculate_stats(graph);
130
168
 
131
169
  lines.push('', '## Summary', '');
132
170
  lines.push(`- **Total packages**: ${graph.nodes.size}`);
@@ -170,7 +208,7 @@ const format_markdown = (
170
208
  lines.push('', '## ⚠️ Wildcard Dependencies', '');
171
209
  lines.push('| Package | Dependency | Version |');
172
210
  lines.push('|---------|------------|---------|');
173
- for (const {pkg, dep, version} of analysis.wildcard_deps) {
211
+ for (const { pkg, dep, version } of analysis.wildcard_deps) {
174
212
  lines.push(`| \`${pkg}\` | \`${dep}\` | \`${version}\` |`);
175
213
  }
176
214
  }
@@ -179,7 +217,7 @@ const format_markdown = (
179
217
  lines.push('', '## Internal Dependencies', '');
180
218
  for (const node of graph.nodes.values()) {
181
219
  const internal_deps = Array.from(node.dependencies.entries()).filter(([name]) =>
182
- graph.nodes.has(name),
220
+ graph.nodes.has(name)
183
221
  );
184
222
  if (internal_deps.length > 0) {
185
223
  lines.push(`- **${node.name}**`);
@@ -197,7 +235,7 @@ const format_stdout = (
197
235
  graph: DependencyGraph,
198
236
  analysis: DependencyAnalysis,
199
237
  publishing_order: Array<string> | null,
200
- log: Logger,
238
+ log: Logger
201
239
  ): void => {
202
240
  log.info(st('cyan', `📊 Analyzing ${graph.nodes.size} repositories...`));
203
241
 
@@ -216,14 +254,14 @@ const format_stdout = (
216
254
  log.info(st('yellow', 'Dependency relationships:'));
217
255
  for (const node of graph.nodes.values()) {
218
256
  const internal_deps = Array.from(node.dependencies.entries()).filter(([name]) =>
219
- graph.nodes.has(name),
257
+ graph.nodes.has(name)
220
258
  );
221
259
  if (internal_deps.length > 0) {
222
260
  log.info(` ${st('cyan', node.name)}`);
223
261
  for (const [dep_name, spec] of internal_deps) {
224
262
  const type_color = spec.type === 'peer' ? 'magenta' : spec.type === 'dev' ? 'dim' : 'white';
225
263
  log.info(
226
- ` ${st(type_color, '→')} ${dep_name} ${st('dim', `(${spec.type}: ${spec.version})`)}`,
264
+ ` ${st(type_color, '→')} ${dep_name} ${st('dim', `(${spec.type}: ${spec.version})`)}`
227
265
  );
228
266
  }
229
267
  }
@@ -252,7 +290,7 @@ const format_stdout = (
252
290
  }
253
291
 
254
292
  // Summary
255
- const {total_deps, internal_deps} = calculate_stats(graph);
293
+ const { total_deps, internal_deps } = calculate_stats(graph);
256
294
 
257
295
  log.info('');
258
296
  log.info(st('cyan', 'Summary:'));
@@ -1,174 +1,125 @@
1
1
  /**
2
- * Configuration types and normalization for gitops multi-repo management.
2
+ * The gitops config: which repos a project's gitops tasks work over, as a flat
3
+ * list of `repos.toml` registry keys.
3
4
  *
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.
5
+ * Every other fact about a repo — its dir, URL, branch, visibility, `ci`, and
6
+ * `archived` — comes from the registry, through `repos status --json`, so the
7
+ * config declares nothing that could drift from it.
10
8
  *
11
9
  * @module
12
10
  */
13
11
 
14
- import type {Url} from '@fuzdev/fuz_util/url.ts';
15
- import {existsSync} from 'node:fs';
16
- import {strip_end} from '@fuzdev/fuz_util/string.ts';
17
- import type {GitBranch} from '@fuzdev/fuz_util/git.ts';
12
+ import { existsSync } from 'node:fs';
13
+ import { z } from 'zod';
18
14
 
19
- import {DEFAULT_REPOS_DIR} from './paths.ts';
15
+ import type { ReposEntryStatus } from './repos_status.ts';
20
16
 
21
- export interface GitopsConfig {
22
- repos: Array<GitopsRepoConfig>;
23
- repos_dir: string;
24
- }
17
+ /** A project's gitops config. */
18
+ export const GitopsConfig = z.strictObject({
19
+ repos: z
20
+ .array(z.string().min(1))
21
+ .meta({ description: 'repos.toml registry keys of owned repos, in the order tasks list them' })
22
+ });
23
+ export type GitopsConfig = z.infer<typeof GitopsConfig>;
25
24
 
26
- export type CreateGitopsConfig = (
27
- base_config: GitopsConfig,
28
- ) => RawGitopsConfig | Promise<RawGitopsConfig>;
25
+ /** A config module's default export in function form. */
26
+ export type CreateGitopsConfig = () => GitopsConfig | Promise<GitopsConfig>;
29
27
 
30
- export interface RawGitopsConfig {
31
- repos?: Array<Url | RawGitopsRepoConfig>;
32
- repos_dir?: string;
28
+ export interface GitopsConfigModule {
29
+ readonly default: GitopsConfig | CreateGitopsConfig;
33
30
  }
34
31
 
35
32
  /**
36
- * Visibility of a repo on its host, mirroring the host's own model
37
- * (e.g. GitHub's `visibility` field). Named to avoid confusion with the npm
38
- * `package.json` `private` flag, which is a separate publishing concern.
33
+ * The private repos a public host package must not publish. `gitops_sync` writes
34
+ * every configured repo's GitHub metadata into the host project's generated
35
+ * `repos.json` — a public site's data when the host package is public — so a
36
+ * private repo in that config would leak. Empty when the host is private.
37
+ *
38
+ * @param entries - the configured repos' registry entries
39
+ * @param host_is_private - whether the host `package.json` sets `private: true`
39
40
  */
40
- export type GitopsRepoVisibility = 'public' | 'private';
41
-
42
- export interface GitopsRepoConfig {
43
- /**
44
- * The HTTPS URL to the repo. Does not include a `.git` suffix.
45
- * @example
46
- * ```ts
47
- * 'https://github.com/fuzdev/fuz_ui'
48
- * ```
49
- */
50
- repo_url: Url;
51
-
52
- /**
53
- * Relative or absolute path to the repo's local directory.
54
- * If `null`, the directory is inferred from the URL and cwd.
55
- * @example
56
- * ```ts
57
- * 'relative/path/to/repo'
58
- * ```
59
- * @example
60
- * ```ts
61
- * '/absolute/path/to/repo'
62
- * ```
63
- */
64
- repo_dir: string | null;
65
-
66
- /**
67
- * The branch name to use when fetching the repo. Defaults to `main`.
68
- */
69
- branch: GitBranch;
70
-
71
- /**
72
- * Visibility of the repo on its host. Defaults to `'public'`.
73
- */
74
- visibility: GitopsRepoVisibility;
75
-
76
- /**
77
- * Whether the repo runs CI. Defaults to `true` for public repos and `false`
78
- * for private repos, unless set explicitly.
79
- */
80
- ci: boolean;
81
-
82
- /**
83
- * Whether the repo is archived (read-only) on its host. Defaults to `false`.
84
- */
85
- archived: boolean;
86
- }
87
-
88
- export interface RawGitopsRepoConfig {
89
- repo_url: Url;
90
- repo_dir?: string | null;
91
- branch?: GitBranch;
92
- /** Visibility of the repo on its host. Defaults to `'public'`. */
93
- visibility?: GitopsRepoVisibility;
94
- /** Whether the repo runs CI. Defaults to `true` for public, `false` for private. */
95
- ci?: boolean;
96
- /** Whether the repo is archived (read-only) on its host. Defaults to `false`. */
97
- archived?: boolean;
98
- }
99
-
100
- export const create_empty_gitops_config = (): GitopsConfig => ({
101
- repos: [],
102
- repos_dir: DEFAULT_REPOS_DIR,
103
- });
41
+ export const gitops_config_leaked_private_repos = (
42
+ entries: ReadonlyArray<ReposEntryStatus>,
43
+ host_is_private: boolean
44
+ ): Array<ReposEntryStatus> =>
45
+ host_is_private ? [] : entries.filter((e) => e.visibility === 'private');
104
46
 
105
47
  /**
106
- * Transforms a `RawGitopsConfig` to the more strict `GitopsConfig`.
107
- * This allows users to provide a more relaxed config.
48
+ * Loads a gitops config module and validates it.
49
+ *
50
+ * @returns the config, or `null` when no file exists at `config_path`
51
+ * @throws {Error} if the module's default export isn't a valid config
108
52
  */
109
- export const normalize_gitops_config = (raw_config: RawGitopsConfig): GitopsConfig => {
110
- const empty_config = create_empty_gitops_config();
111
- // All of the raw config properties are optional,
112
- // so fall back to the empty values when `undefined`.
113
- const {repos, repos_dir} = raw_config;
114
- return {
115
- repos: repos ? repos.map((r) => parse_fuz_repo_config(r)) : empty_config.repos,
116
- // Default to two dirs up from config if not specified
117
- repos_dir: repos_dir ?? DEFAULT_REPOS_DIR,
118
- };
119
- };
120
-
121
- const parse_fuz_repo_config = (r: Url | RawGitopsRepoConfig): GitopsRepoConfig => {
122
- if (typeof r === 'string') {
123
- return {
124
- repo_url: r,
125
- repo_dir: null,
126
- branch: 'main' as GitBranch, // TODO @zts use flavored for GitBranch
127
- visibility: 'public',
128
- ci: true,
129
- archived: false,
130
- };
131
- }
132
- const visibility = r.visibility ?? 'public';
133
- return {
134
- repo_url: strip_end(r.repo_url, '.git'),
135
- repo_dir: r.repo_dir ?? null,
136
- branch: r.branch ?? ('main' as GitBranch), // TODO @zts use flavored for GitBranch
137
- visibility,
138
- ci: r.ci ?? visibility === 'public',
139
- archived: r.archived ?? false,
140
- };
141
- };
142
-
143
- export interface GitopsConfigModule {
144
- readonly default: RawGitopsConfig | CreateGitopsConfig;
145
- }
146
-
147
53
  export const load_gitops_config = async (config_path: string): Promise<GitopsConfig | null> => {
148
54
  if (!existsSync(config_path)) {
149
55
  // No user config file found.
150
56
  return null;
151
57
  }
152
- // Import the user's `gitops.config.ts`.
153
- const config_module = await import(config_path);
58
+ // Import the user's `gitops.config.ts`. An import or call failure keeps its
59
+ // stack, which carries the config's file and line; validation errors don't need one.
60
+ let config_module: unknown;
61
+ try {
62
+ config_module = await import(config_path);
63
+ } catch (err) {
64
+ throw Error(`Failed to import the gitops config at ${config_path}:\n${error_with_stack(err)}`);
65
+ }
154
66
  validate_gitops_config_module(config_module, config_path);
155
- return normalize_gitops_config(
156
- typeof config_module.default === 'function'
157
- ? await config_module.default(create_empty_gitops_config())
158
- : config_module.default,
159
- );
67
+ let raw: unknown;
68
+ try {
69
+ raw =
70
+ typeof config_module.default === 'function'
71
+ ? await config_module.default()
72
+ : config_module.default;
73
+ } catch (err) {
74
+ throw Error(`The gitops config at ${config_path} threw:\n${error_with_stack(err)}`);
75
+ }
76
+ return parse_gitops_config(raw, config_path);
77
+ };
78
+
79
+ const error_with_stack = (err: unknown): string =>
80
+ err instanceof Error ? (err.stack ?? err.message) : String(err);
81
+
82
+ /**
83
+ * Validates a loaded config value: registry keys, each listed once.
84
+ *
85
+ * @throws {Error} naming the config and what's wrong with it
86
+ */
87
+ export const parse_gitops_config = (raw: unknown, config_path: string): GitopsConfig => {
88
+ const parsed = GitopsConfig.safeParse(raw);
89
+ if (!parsed.success) {
90
+ const hint =
91
+ "\n `repos` lists repos.toml registry keys; a repo's url, dir, branch, visibility, ci, and archived come from the registry";
92
+ throw Error(
93
+ `Invalid gitops config at ${config_path}:\n${z.prettifyError(parsed.error)}${hint}`
94
+ );
95
+ }
96
+ const config = parsed.data;
97
+ const problems: Array<string> = [];
98
+ const seen: Set<string> = new Set();
99
+ for (const key of config.repos) {
100
+ if (key.includes('/')) {
101
+ problems.push(`\`${key}\` isn't a registry key — list repos by their repos.toml key`);
102
+ } else if (seen.has(key)) {
103
+ problems.push(`\`${key}\` is listed more than once`);
104
+ }
105
+ seen.add(key);
106
+ }
107
+ if (problems.length) {
108
+ throw Error(`Invalid gitops config at ${config_path}:\n ${problems.join('\n ')}`);
109
+ }
110
+ return config;
160
111
  };
161
112
 
162
113
  export const validate_gitops_config_module: (
163
114
  config_module: any,
164
- config_path: string,
115
+ config_path: string
165
116
  ) => asserts config_module is GitopsConfigModule = (config_module, config_path) => {
166
117
  const config = config_module.default;
167
118
  if (!config) {
168
- throw Error(`Invalid Fuz config module at ${config_path}: expected a default export`);
119
+ throw Error(`Invalid gitops config module at ${config_path}: expected a default export`);
169
120
  } else if (!(typeof config === 'function' || typeof config === 'object')) {
170
121
  throw Error(
171
- `Invalid Fuz config module at ${config_path}: the default export must be a function or object`,
122
+ `Invalid gitops config module at ${config_path}: the default export must be a function or object`
172
123
  );
173
124
  }
174
125
  };
@@ -7,11 +7,13 @@
7
7
  */
8
8
 
9
9
  /**
10
- * Maximum number of iterations for fixed-point iteration during publishing.
11
- * Used in both plan generation and actual publishing to resolve transitive dependency cascades.
10
+ * Maximum number of fixed-point iterations plan generation runs to resolve
11
+ * transitive dependency cascades. Publishing executes the frozen plan in a
12
+ * single pass and doesn't iterate.
12
13
  *
13
- * In practice, most repos converge in 2-3 iterations.
14
- * Deep dependency chains may require more iterations.
14
+ * Each iteration reaches at least one more level of dependents, so a deep dependency
15
+ * chain needs more; a plan that hits the limit still changing warns, naming
16
+ * the packages left.
15
17
  */
16
18
  export const GITOPS_MAX_ITERATIONS_DEFAULT = 10;
17
19