@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,227 @@
1
+ import type {Task} from '@ryanatkn/gro';
2
+ import {z} from 'zod';
3
+ import {createInterface} from 'node:readline/promises';
4
+ import {styleText as st} from 'node:util';
5
+
6
+ import {get_gitops_ready} from './gitops_task_helpers.js';
7
+ import {
8
+ publish_repos,
9
+ type PublishingOptions,
10
+ type PublishingResult,
11
+ } from './multi_repo_publisher.js';
12
+ import {generate_publishing_plan, log_publishing_plan} from './publishing_plan.js';
13
+ import {format_and_output, type OutputFormatters} from './output_helpers.js';
14
+
15
+ /** @nodocs */
16
+ export const Args = z.strictObject({
17
+ path: z
18
+ .string()
19
+ .meta({description: 'path to the gitops config file, absolute or relative to the cwd'})
20
+ .default('gitops.config.ts'),
21
+ dir: z
22
+ .string()
23
+ .meta({description: 'path containing the repos, defaults to the parent of the `path` dir'})
24
+ .optional(),
25
+ peer_strategy: z
26
+ .enum(['exact', 'caret', 'tilde'])
27
+ .meta({description: 'version strategy for peer dependencies'})
28
+ .default('caret' as const),
29
+ dry_run: z
30
+ .boolean()
31
+ .meta({description: 'perform a dry run without actually publishing'})
32
+ .default(false),
33
+ format: z
34
+ .enum(['stdout', 'json', 'markdown'])
35
+ .meta({description: 'output format'})
36
+ .default('stdout'),
37
+ deploy: z.boolean().meta({description: 'deploy all repos after publishing'}).default(false),
38
+ plan: z.boolean().meta({description: 'dual of no-plan'}).default(true),
39
+ 'no-plan': z
40
+ .boolean()
41
+ .meta({description: 'skip plan confirmation before publishing'})
42
+ .default(false),
43
+ max_wait: z
44
+ .number()
45
+ .meta({description: 'max time to wait for npm propagation in ms'})
46
+ .default(600000), // 10 minutes
47
+ skip_install: z
48
+ .boolean()
49
+ .meta({description: 'skip npm install after dependency updates'})
50
+ .default(false),
51
+ outfile: z.string().meta({description: 'write output to file instead of logging'}).optional(),
52
+ verbose: z.boolean().meta({description: 'show additional details in plan output'}).default(false),
53
+ });
54
+ export type Args = z.infer<typeof Args>;
55
+
56
+ /** @nodocs */
57
+ export const task: Task<Args> = {
58
+ summary: 'publish all repos in dependency order',
59
+ Args,
60
+ run: async ({args, log}): Promise<void> => {
61
+ const {
62
+ path,
63
+ dir,
64
+ peer_strategy,
65
+ dry_run,
66
+ format,
67
+ deploy,
68
+ plan,
69
+ max_wait,
70
+ skip_install,
71
+ outfile,
72
+ verbose,
73
+ } = args;
74
+
75
+ // Load repos
76
+ const {local_repos: repos} = await get_gitops_ready({
77
+ path,
78
+ dir,
79
+ download: false, // Don't download if missing
80
+ log,
81
+ });
82
+
83
+ // Show plan if requested (skip for dry runs)
84
+ if (plan && !dry_run) {
85
+ log.info(st('cyan', 'Publishing Plan'));
86
+ const plan_result = await generate_publishing_plan(repos, {log, verbose});
87
+ log_publishing_plan(plan_result, log, {verbose});
88
+
89
+ if (plan_result.errors.length > 0) {
90
+ throw new Error('Cannot proceed with publishing due to errors');
91
+ }
92
+
93
+ // Ask for confirmation
94
+ log.info(st('yellow', '⚠️ This will publish the packages shown above.'));
95
+ process.stdout.write('Continue with publishing? (y/n): ');
96
+ const confirmed = await prompt_for_confirmation();
97
+ if (!confirmed) {
98
+ log.info('Publishing cancelled');
99
+ process.exit(0);
100
+ }
101
+ }
102
+
103
+ // Publishing options
104
+ const options: PublishingOptions = {
105
+ dry_run,
106
+ update_deps: true, // Always update dependencies
107
+ version_strategy: peer_strategy,
108
+ deploy,
109
+ max_wait,
110
+ skip_install,
111
+ log,
112
+ };
113
+
114
+ // Execute publishing (may throw on fatal errors like circular dependencies)
115
+ let result: PublishingResult;
116
+ let fatal_error: Error | null = null;
117
+
118
+ try {
119
+ result = await publish_repos(repos, options);
120
+ } catch (error) {
121
+ // Construct a failure result for fatal errors so output can still be generated
122
+ fatal_error = error instanceof Error ? error : new Error(String(error));
123
+ result = {
124
+ ok: false,
125
+ published: [],
126
+ // Note: FATAL_ERROR is a placeholder - only fatal_error.message is displayed in output
127
+ failed: [{name: 'FATAL_ERROR', error: fatal_error}],
128
+ duration: 0,
129
+ };
130
+ }
131
+
132
+ // Format and output result (always runs, even on fatal errors)
133
+ // Note: stdout format is handled by publish_repos function's logging
134
+ if (format !== 'stdout') {
135
+ await format_and_output({result, fatal_error}, create_publish_formatters(), {
136
+ format,
137
+ outfile,
138
+ log,
139
+ });
140
+ }
141
+
142
+ // Exit with error if failed
143
+ if (!result.ok || fatal_error) {
144
+ process.exit(1);
145
+ }
146
+ },
147
+ };
148
+
149
+ interface PublishResultData {
150
+ result: PublishingResult;
151
+ fatal_error: Error | null;
152
+ }
153
+
154
+ const create_publish_formatters = (): OutputFormatters<PublishResultData> => ({
155
+ json: (data) => JSON.stringify(data.result, null, 2),
156
+ markdown: (data) => format_result_markdown(data.result, data.fatal_error),
157
+ stdout: () => {
158
+ // stdout format is handled by publish_repos function's logging
159
+ // This should never be called due to early return in task
160
+ },
161
+ });
162
+
163
+ // Format the publishing result as markdown
164
+ const format_result_markdown = (
165
+ result: PublishingResult,
166
+ fatal_error: Error | null,
167
+ ): Array<string> => {
168
+ const lines: Array<string> = [];
169
+
170
+ lines.push('# Publishing Result');
171
+ lines.push('');
172
+
173
+ // Show fatal error prominently if present
174
+ if (fatal_error) {
175
+ lines.push('## ❌ Fatal Error');
176
+ lines.push('');
177
+ lines.push(`**Error**: ${fatal_error.message}`);
178
+ lines.push('');
179
+ lines.push('Publishing could not proceed due to the error above.');
180
+ lines.push('');
181
+ return lines;
182
+ }
183
+
184
+ lines.push(`**Status**: ${result.ok ? '✅ Success' : '❌ Failed'}`);
185
+ lines.push(`**Duration**: ${(result.duration / 1000).toFixed(1)}s`);
186
+ lines.push(`**Published**: ${result.published.length} packages`);
187
+
188
+ if (result.failed.length > 0) {
189
+ lines.push(`**Failed**: ${result.failed.length} packages`);
190
+ }
191
+
192
+ if (result.published.length > 0) {
193
+ lines.push('');
194
+ lines.push('## Published Packages');
195
+ lines.push('');
196
+ for (const pkg of result.published) {
197
+ lines.push(`- \`${pkg.name}\`: ${pkg.old_version} → ${pkg.new_version}`);
198
+ }
199
+ }
200
+
201
+ if (result.failed.length > 0) {
202
+ lines.push('');
203
+ lines.push('## Failed Packages');
204
+ lines.push('');
205
+ for (const {name, error} of result.failed) {
206
+ lines.push(`- \`${name}\`: ${error.message}`);
207
+ }
208
+ }
209
+
210
+ return lines;
211
+ };
212
+
213
+ /**
214
+ * Prompts user for y/n confirmation.
215
+ * Returns true if user enters 'y', false otherwise.
216
+ */
217
+ const prompt_for_confirmation = async (): Promise<boolean> => {
218
+ const rl = createInterface({
219
+ input: process.stdin,
220
+ output: process.stdout,
221
+ });
222
+
223
+ const answer = await rl.question('');
224
+ rl.close();
225
+
226
+ return answer.toLowerCase() === 'y';
227
+ };
@@ -0,0 +1,109 @@
1
+ import {TaskError, type Task} from '@ryanatkn/gro';
2
+ import {z} from 'zod';
3
+ import {readFile, writeFile} from 'node:fs/promises';
4
+ import {format_file} from '@ryanatkn/gro/format_file.js';
5
+ import {basename, resolve} from 'node:path';
6
+ import {print_path} from '@ryanatkn/gro/paths.js';
7
+ import {load_from_env} from '@ryanatkn/gro/env.js';
8
+ import {load_package_json} from '@ryanatkn/gro/package_json.js';
9
+ import {existsSync} from 'node:fs';
10
+
11
+ import {fetch_repo_data} from './fetch_repo_data.js';
12
+ import {create_fs_fetch_value_cache} from './fs_fetch_value_cache.js';
13
+ import {get_gitops_ready} from './gitops_task_helpers.js';
14
+
15
+ // TODO add flag to ignore or invalidate cache -- no-cache? clean?
16
+
17
+ /** @nodocs */
18
+ export const Args = z.strictObject({
19
+ path: z
20
+ .string()
21
+ .meta({description: 'path to the gitops config file, absolute or relative to the cwd'})
22
+ .default('gitops.config.ts'),
23
+ dir: z
24
+ .string()
25
+ .meta({description: 'path containing the repos, defaults to the parent of the `path` dir'})
26
+ .optional(),
27
+ outdir: z
28
+ .string()
29
+ .meta({description: 'path to the directory for the generated files, defaults to $routes/'})
30
+ .optional(),
31
+ download: z.boolean().meta({description: 'download all missing local repos'}).default(false),
32
+ check: z
33
+ .boolean()
34
+ .meta({description: 'check repos are ready without fetching remote data'})
35
+ .default(false),
36
+ });
37
+ export type Args = z.infer<typeof Args>;
38
+
39
+ /**
40
+ * This is a task not a `.gen.` file because it makes network calls.
41
+ *
42
+ * @nodocs
43
+ */
44
+ export const task: Task<Args> = {
45
+ Args,
46
+ summary: 'syncs local repos and generates UI data from repo metadata',
47
+ run: async ({args, log, svelte_config, invoke_task}) => {
48
+ const {path, dir, outdir = svelte_config.routes_path, download, check} = args;
49
+
50
+ const {local_repos} = await get_gitops_ready({path, dir, download, log});
51
+
52
+ const outfile = resolve(outdir, 'repos.ts');
53
+
54
+ // This searches the parent directory for the env var, so we don't use SvelteKit's $env imports
55
+ const token = load_from_env('SECRET_GITHUB_API_TOKEN');
56
+ if (!token) {
57
+ throw new TaskError('the env var SECRET_GITHUB_API_TOKEN was not found');
58
+ }
59
+
60
+ // Exit early if only checking repo readiness
61
+ if (check) {
62
+ log.info('repos are ready');
63
+ return;
64
+ }
65
+
66
+ const cache = await create_fs_fetch_value_cache('repos');
67
+
68
+ log.info('fetching remote repo data');
69
+ const repos_json = await fetch_repo_data(local_repos, token, cache.data, log);
70
+
71
+ // TODO should package_json be provided in the Gro task/gen contexts? check if it's always loaded
72
+ const package_json = await load_package_json();
73
+ const repo_specifier =
74
+ package_json.name === '@fuzdev/fuz_gitops'
75
+ ? '$lib/repo.svelte.js'
76
+ : '@fuzdev/fuz_gitops/repo.svelte.js';
77
+
78
+ log.info('generating ' + outfile);
79
+
80
+ // TODO the `basename` is used here because we don't have an `origin_id` like with gen,
81
+ // and this file gets re-exported,
82
+ // and we don't want the file to change based on where it's being generated,
83
+ // because for example linking to a local package would change the contents
84
+ const contents = `
85
+ // generated by ${basename(import.meta.filename)} !! do not edit directly or risk lost data
86
+
87
+ import type {RepoJson} from '${repo_specifier}';
88
+
89
+ export const repos_json: Array<RepoJson> = ${JSON.stringify(repos_json, null, '\t')};
90
+ `;
91
+ // TODO think about possibly using the `gen` functionality in this task, not sure what the API design could look like
92
+ const formatted = await format_file(contents, {filepath: outfile});
93
+ const existing = existsSync(outfile) ? await readFile(outfile, 'utf8') : '';
94
+ if (existing === formatted) {
95
+ log.info(`no changes to ${print_path(outfile)}`);
96
+ } else {
97
+ log.info(`writing changes to ${print_path(outfile)}`);
98
+ await writeFile(outfile, formatted);
99
+ await invoke_task('gen');
100
+ }
101
+
102
+ const changed = await cache.save();
103
+ if (changed) {
104
+ log.info('repos cache updated');
105
+ } else {
106
+ log.info('repos cache did not change');
107
+ }
108
+ },
109
+ };
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Shared initialization logic for all gitops tasks.
3
+ *
4
+ * Provides `get_gitops_ready()` which orchestrates:
5
+ * - Config loading and normalization
6
+ * - Repo resolution (local path discovery)
7
+ * - Branch switching and syncing
8
+ * - Dependency installation
9
+ *
10
+ * Used by: `gitops_sync.task.ts`, `gitops_analyze.task.ts`, `gitops_plan.task.ts`,
11
+ * `gitops_publish.task.ts`, and `gitops_validate.task.ts`.
12
+ *
13
+ * Accepts `git_ops` and `npm_ops` parameters to support testing via operations pattern
14
+ * (see `operations.ts` for dependency injection details).
15
+ */
16
+
17
+ import {TaskError} from '@ryanatkn/gro';
18
+ import {styleText as st} from 'node:util';
19
+ import {resolve, dirname} from 'node:path';
20
+ import {print_path} from '@ryanatkn/gro/paths.js';
21
+ import type {Logger} from '@fuzdev/fuz_util/log.js';
22
+
23
+ import {load_gitops_config, type GitopsConfig} from './gitops_config.js';
24
+ import {local_repos_load, local_repos_ensure, type LocalRepo} from './local_repo.js';
25
+ import {resolve_gitops_config} from './resolved_gitops_config.js';
26
+ import {DEFAULT_REPOS_DIR} from './paths.js';
27
+ import type {GitOperations, NpmOperations} from './operations.js';
28
+
29
+ export interface GetGitopsReadyOptions {
30
+ path: string;
31
+ dir?: string;
32
+ download: boolean;
33
+ log?: Logger;
34
+ git_ops?: GitOperations;
35
+ npm_ops?: NpmOperations;
36
+ }
37
+
38
+ /**
39
+ * Central initialization function for all gitops tasks.
40
+ *
41
+ * Initialization sequence:
42
+ * 1. Loads and normalizes config from `gitops.config.ts`
43
+ * 2. Resolves local repo paths (creates missing with `--download`)
44
+ * 3. Switches branches and pulls latest changes
45
+ * 4. Auto-installs deps if package.json changed during pull
46
+ *
47
+ * Priority for path resolution:
48
+ * - `dir` argument (explicit override)
49
+ * - Config `repos_dir` setting
50
+ * - `DEFAULT_REPOS_DIR` constant
51
+ *
52
+ * @param options.git_ops for testing (defaults to real git operations)
53
+ * @param options.npm_ops for testing (defaults to real npm operations)
54
+ * @returns initialized config and fully loaded repos ready for operations
55
+ * @throws {TaskError} if config loading or repo resolution fails
56
+ */
57
+ export const get_gitops_ready = async (
58
+ options: GetGitopsReadyOptions,
59
+ ): Promise<{
60
+ config_path: string;
61
+ repos_dir: string;
62
+ gitops_config: GitopsConfig;
63
+ local_repos: Array<LocalRepo>;
64
+ }> => {
65
+ const {path, dir, download, log, git_ops, npm_ops} = options;
66
+ const config_path = resolve(path);
67
+ const gitops_config = await import_gitops_config(config_path);
68
+
69
+ // Priority: explicit dir arg → config repos_dir → default (two dirs up from config)
70
+ const repos_dir = resolve_gitops_paths({
71
+ path,
72
+ dir,
73
+ config_repos_dir: gitops_config.repos_dir,
74
+ }).repos_dir;
75
+
76
+ log?.info(
77
+ `resolving gitops configs on the filesystem in ${repos_dir}`,
78
+ gitops_config.repos.map((r) => r.repo_url),
79
+ );
80
+ const resolved_config = resolve_gitops_config(gitops_config, repos_dir);
81
+
82
+ const local_repo_paths = await local_repos_ensure({
83
+ resolved_config,
84
+ repos_dir,
85
+ gitops_config,
86
+ download,
87
+ log,
88
+ npm_ops,
89
+ });
90
+
91
+ const local_repos = await local_repos_load({local_repo_paths, log, git_ops, npm_ops});
92
+
93
+ return {config_path, repos_dir, gitops_config, local_repos};
94
+ };
95
+
96
+ export interface ResolveGitopsPathsOptions {
97
+ path: string;
98
+ dir?: string;
99
+ config_repos_dir?: string;
100
+ }
101
+
102
+ export const resolve_gitops_paths = (
103
+ options: ResolveGitopsPathsOptions,
104
+ ): {config_path: string; repos_dir: string} => {
105
+ const {path, dir, config_repos_dir} = options;
106
+ const config_path = resolve(path);
107
+ const config_dir = dirname(config_path);
108
+
109
+ // Priority: explicit dir arg → config repos_dir → default (parent of config dir)
110
+ const repos_dir =
111
+ dir !== undefined
112
+ ? resolve(dir)
113
+ : config_repos_dir !== undefined
114
+ ? resolve(config_dir, config_repos_dir)
115
+ : resolve(config_dir, DEFAULT_REPOS_DIR);
116
+
117
+ return {config_path, repos_dir};
118
+ };
119
+
120
+ export const import_gitops_config = async (config_path: string): Promise<GitopsConfig> => {
121
+ const gitops_config = await load_gitops_config(config_path);
122
+ if (!gitops_config) {
123
+ throw new TaskError(st('red', `No gitops config found at ${print_path(config_path)}`));
124
+ }
125
+ return gitops_config;
126
+ };
@@ -0,0 +1,248 @@
1
+ import type {Task} from '@ryanatkn/gro';
2
+ import {z} from 'zod';
3
+ import {styleText as st} from 'node:util';
4
+
5
+ import {get_gitops_ready} from './gitops_task_helpers.js';
6
+ import {validate_dependency_graph} from './graph_validation.js';
7
+ import {DependencyGraphBuilder} from './dependency_graph.js';
8
+ import {generate_publishing_plan, log_publishing_plan} from './publishing_plan.js';
9
+ import {publish_repos, type PublishingOptions} from './multi_repo_publisher.js';
10
+ import {log_dependency_analysis} from './log_helpers.js';
11
+
12
+ /** @nodocs */
13
+ export const Args = z.strictObject({
14
+ path: z
15
+ .string()
16
+ .meta({description: 'path to the gitops config file, absolute or relative to the cwd'})
17
+ .default('gitops.config.ts'),
18
+ dir: z
19
+ .string()
20
+ .meta({description: 'path containing the repos, defaults to the parent of the `path` dir'})
21
+ .optional(),
22
+ verbose: z.boolean().meta({description: 'show additional details'}).default(false),
23
+ });
24
+ export type Args = z.infer<typeof Args>;
25
+
26
+ /** @nodocs */
27
+ export const task: Task<Args> = {
28
+ Args,
29
+ summary:
30
+ 'validate gitops configuration by running all read-only commands and checking for issues',
31
+ run: async ({args, log}) => {
32
+ const {path, dir, verbose} = args;
33
+
34
+ log.info(st('cyan', 'Running Gitops Validation Suite'));
35
+ log.info(st('dim', 'This runs all read-only commands and checks for consistency.'));
36
+
37
+ const results: Array<{
38
+ command: string;
39
+ success: boolean;
40
+ warnings: number;
41
+ errors: number;
42
+ duration: number;
43
+ warning_details?: Array<string>;
44
+ info_details?: Array<string>;
45
+ analysis?: ReturnType<DependencyGraphBuilder['analyze']>;
46
+ }> = [];
47
+
48
+ const start_time = Date.now();
49
+
50
+ // Load repos once (shared by all commands)
51
+ log.info(st('dim', 'Loading repositories...'));
52
+ const {local_repos} = await get_gitops_ready({path, dir, download: false, log});
53
+ log.info(st('dim', ` Found ${local_repos.length} local repos`));
54
+
55
+ // 1. Run gitops_analyze
56
+ log.info(st('yellow', 'Running gitops_analyze...'));
57
+ const analyze_start = Date.now();
58
+ try {
59
+ // Build dependency graph and validate (but don't throw on cycles for analyze)
60
+ const {graph} = validate_dependency_graph(local_repos, {
61
+ throw_on_prod_cycles: false, // Analyze should report, not throw
62
+ log_cycles: false, // We'll collect our own statistics
63
+ log_order: false,
64
+ });
65
+
66
+ // Perform additional analysis
67
+ const builder = new DependencyGraphBuilder();
68
+ const analysis = builder.analyze(graph);
69
+
70
+ const analyze_duration = Date.now() - analyze_start;
71
+
72
+ // Collect warnings, info, and errors
73
+ const warning_details: Array<string> = [];
74
+ const info_details: Array<string> = [];
75
+ if (analysis.wildcard_deps.length > 0) {
76
+ warning_details.push('wildcard dependencies');
77
+ }
78
+ if (analysis.dev_cycles.length > 0) {
79
+ info_details.push('dev circular dependencies');
80
+ }
81
+ const warnings = warning_details.length;
82
+ const errors = analysis.production_cycles.length > 0 ? 1 : 0;
83
+
84
+ results.push({
85
+ command: 'gitops_analyze',
86
+ success: true,
87
+ warnings,
88
+ errors,
89
+ duration: analyze_duration,
90
+ warning_details,
91
+ info_details,
92
+ analysis,
93
+ });
94
+
95
+ log.info(st('green', ` ✓ gitops_analyze completed in ${analyze_duration}ms`));
96
+
97
+ // Print detailed analysis
98
+ log_dependency_analysis(analysis, log, ' ');
99
+
100
+ if (errors > 0) {
101
+ log.error(st('red', ` ❌ Found ${errors} error(s)`));
102
+ }
103
+ } catch (error) {
104
+ const analyze_duration = Date.now() - analyze_start;
105
+ results.push({
106
+ command: 'gitops_analyze',
107
+ success: false,
108
+ warnings: 0,
109
+ errors: 1,
110
+ duration: analyze_duration,
111
+ });
112
+ log.error(st('red', ` ✗ gitops_analyze failed: ${error}`));
113
+ }
114
+
115
+ // 2. Run gitops_plan
116
+ log.info(st('yellow', 'Running gitops_plan...'));
117
+ const plan_start = Date.now();
118
+ try {
119
+ const plan = await generate_publishing_plan(local_repos, {log: undefined, verbose});
120
+ const plan_duration = Date.now() - plan_start;
121
+
122
+ const warnings = plan.warnings.length;
123
+ const errors = plan.errors.length;
124
+
125
+ results.push({
126
+ command: 'gitops_plan',
127
+ success: true,
128
+ warnings,
129
+ errors,
130
+ duration: plan_duration,
131
+ });
132
+
133
+ log.info(st('green', ` ✓ gitops_plan completed in ${plan_duration}ms`));
134
+ if (verbose) {
135
+ log_publishing_plan(plan, log, {verbose});
136
+ }
137
+ if (warnings > 0) {
138
+ log.warn(st('yellow', ` ⚠️ Found ${warnings} warning(s)`));
139
+ }
140
+ if (errors > 0) {
141
+ log.error(st('red', ` ❌ Found ${errors} error(s)`));
142
+ }
143
+ } catch (error) {
144
+ const plan_duration = Date.now() - plan_start;
145
+ results.push({
146
+ command: 'gitops_plan',
147
+ success: false,
148
+ warnings: 0,
149
+ errors: 1,
150
+ duration: plan_duration,
151
+ });
152
+ log.error(st('red', ` ✗ gitops_plan failed: ${error}`));
153
+ }
154
+
155
+ // 3. Run gitops_publish --dry_run
156
+ log.info(st('yellow', 'Running gitops_publish --dry_run...'));
157
+ const dry_start = Date.now();
158
+ try {
159
+ const options: PublishingOptions = {
160
+ dry_run: true,
161
+ update_deps: true,
162
+ log: undefined, // Silent for validation
163
+ };
164
+
165
+ const result = await publish_repos(local_repos, options);
166
+ const dry_duration = Date.now() - dry_start;
167
+
168
+ // Dry run doesn't have warnings/errors in the same format
169
+ // We'll just check if it succeeded
170
+ const errors = result.ok ? 0 : result.failed.length;
171
+
172
+ results.push({
173
+ command: 'gitops_publish --dry_run',
174
+ success: result.ok,
175
+ warnings: 0,
176
+ errors,
177
+ duration: dry_duration,
178
+ });
179
+
180
+ log.info(st('green', ` ✓ gitops_publish --dry_run completed in ${dry_duration}ms`));
181
+ if (errors > 0) {
182
+ log.error(st('red', ` ❌ Found ${errors} error(s)`));
183
+ }
184
+ } catch (error) {
185
+ const dry_duration = Date.now() - dry_start;
186
+ results.push({
187
+ command: 'gitops_publish --dry_run',
188
+ success: false,
189
+ warnings: 0,
190
+ errors: 1,
191
+ duration: dry_duration,
192
+ });
193
+ log.error(st('red', ` ✗ gitops_publish --dry_run failed: ${error}`));
194
+ }
195
+
196
+ // Summary
197
+ const total_duration = Date.now() - start_time;
198
+ const all_success = results.every((r) => r.success);
199
+ const total_warnings = results.reduce((sum, r) => sum + r.warnings, 0);
200
+ const total_errors = results.reduce((sum, r) => sum + r.errors, 0);
201
+
202
+ log.info(st('cyan', 'Validation Summary'));
203
+ log.info(` Total duration: ${(total_duration / 1000).toFixed(1)}s`);
204
+ log.info(` Commands run: ${results.length}`);
205
+ log.info(` Commands succeeded: ${results.filter((r) => r.success).length}`);
206
+ log.info(` Commands failed: ${results.filter((r) => !r.success).length}`);
207
+ log.info(` Total warnings: ${total_warnings}`);
208
+ log.info(` Total errors: ${total_errors}`);
209
+
210
+ // Individual command results
211
+ log.info(st('cyan', 'Command Results:'));
212
+ for (const result of results) {
213
+ const status_icon = result.success ? '✓' : '✗';
214
+ const status_color = result.success ? 'green' : 'red';
215
+ const duration = (result.duration / 1000).toFixed(1);
216
+
217
+ log.info(st(status_color, ` ${status_icon} ${result.command} (${duration}s)`));
218
+ if (result.warnings > 0) {
219
+ const details = result.warning_details?.length
220
+ ? ` (${result.warning_details.join(', ')})`
221
+ : '';
222
+ log.info(st('yellow', ` ⚠️ ${result.warnings} warning(s)${details}`));
223
+ }
224
+ if (result.info_details && result.info_details.length > 0) {
225
+ log.info(st('dim', ` ℹ️ ${result.info_details.join(', ')}`));
226
+ }
227
+ if (result.errors > 0) {
228
+ log.info(st('red', ` ❌ ${result.errors} error(s)`));
229
+ }
230
+ }
231
+
232
+ // Final verdict
233
+ log.info('');
234
+ if (all_success && total_errors === 0) {
235
+ log.info(st('green', '✓ All validation checks passed'));
236
+ if (total_warnings > 0) {
237
+ log.warn(
238
+ st('yellow', `⚠️ Note: ${total_warnings} warning(s) found - review output above.`),
239
+ );
240
+ }
241
+ } else if (all_success && total_errors > 0) {
242
+ log.warn(st('yellow', '⚠️ Validation completed but found errors - review output above.'));
243
+ } else {
244
+ log.error(st('red', '❌ Validation failed - one or more commands did not complete.'));
245
+ throw new Error('Validation failed');
246
+ }
247
+ },
248
+ };