@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,213 @@
1
+ /**
2
+ * Generic repository operations for scripts that work across repos.
3
+ *
4
+ * Provides lightweight utilities for:
5
+ * - Getting repo paths from gitops config (without full git sync)
6
+ * - Walking files in repos with sensible exclusions
7
+ * - Common exclusion patterns for node/svelte projects
8
+ *
9
+ * For full git sync/clone functionality, use `get_gitops_ready()` from gitops_task_helpers.
10
+ */
11
+
12
+ import {existsSync} from 'node:fs';
13
+ import {readdir, stat} from 'node:fs/promises';
14
+ import {join, resolve, dirname} from 'node:path';
15
+
16
+ import {load_gitops_config} from './gitops_config.js';
17
+ import {DEFAULT_REPOS_DIR} from './paths.js';
18
+
19
+ /** Default directories to exclude from file walking */
20
+ export const DEFAULT_EXCLUDE_DIRS = [
21
+ 'node_modules',
22
+ '.git',
23
+ '.gro',
24
+ '.svelte-kit',
25
+ '.deno',
26
+ '.vscode',
27
+ '.idea',
28
+ 'dist',
29
+ 'build',
30
+ 'coverage',
31
+ '.cache',
32
+ '.turbo',
33
+ ] as const;
34
+
35
+ /** Default binary/non-text extensions to exclude from content processing */
36
+ export const DEFAULT_EXCLUDE_EXTENSIONS = [
37
+ '.png',
38
+ '.jpg',
39
+ '.jpeg',
40
+ '.gif',
41
+ '.svg',
42
+ '.ico',
43
+ '.webp',
44
+ '.woff',
45
+ '.woff2',
46
+ '.ttf',
47
+ '.eot',
48
+ '.mp4',
49
+ '.webm',
50
+ '.mp3',
51
+ '.wav',
52
+ '.ogg',
53
+ '.zip',
54
+ '.tar',
55
+ '.gz',
56
+ '.lock',
57
+ '.pdf',
58
+ ] as const;
59
+
60
+ export interface WalkOptions {
61
+ /** Additional directories to exclude (merged with defaults) */
62
+ exclude_dirs?: Array<string>;
63
+ /** Additional extensions to exclude (merged with defaults) */
64
+ exclude_extensions?: Array<string>;
65
+ /** Maximum file size in bytes (default: 10MB) */
66
+ max_file_size?: number;
67
+ /** Include directories in output (default: false) */
68
+ include_dirs?: boolean;
69
+ /** Use only provided exclusions, ignoring defaults */
70
+ no_defaults?: boolean;
71
+ }
72
+
73
+ export interface RepoPath {
74
+ name: string;
75
+ path: string;
76
+ url: string;
77
+ }
78
+
79
+ /**
80
+ * Get repo paths from gitops config without full git sync.
81
+ * Lighter weight than `get_gitops_ready()` - just resolves paths.
82
+ *
83
+ * @param config_path Path to gitops.config.ts (defaults to ./gitops.config.ts)
84
+ * @returns Array of repo info with name, path, and url
85
+ */
86
+ export const get_repo_paths = async (config_path?: string): Promise<Array<RepoPath>> => {
87
+ const resolved_config_path = resolve(config_path ?? 'gitops.config.ts');
88
+ const config = await load_gitops_config(resolved_config_path);
89
+
90
+ if (!config) {
91
+ throw new Error(`No gitops config found at ${resolved_config_path}`);
92
+ }
93
+
94
+ const config_dir = dirname(resolved_config_path);
95
+ const repos_dir = resolve(config_dir, config.repos_dir || DEFAULT_REPOS_DIR);
96
+
97
+ const repos: Array<RepoPath> = [];
98
+
99
+ for (const repo_config of config.repos) {
100
+ const url = repo_config.repo_url;
101
+ const name = url.split('/').at(-1);
102
+ if (!name) continue;
103
+
104
+ const path = repo_config.repo_dir
105
+ ? resolve(config_dir, repo_config.repo_dir)
106
+ : join(repos_dir, name);
107
+
108
+ if (existsSync(path)) {
109
+ repos.push({name, path, url});
110
+ }
111
+ }
112
+
113
+ return repos;
114
+ };
115
+
116
+ /**
117
+ * Check if a path should be excluded based on options.
118
+ */
119
+ export const should_exclude_path = (file_path: string, options?: WalkOptions): boolean => {
120
+ const exclude_dirs = options?.no_defaults
121
+ ? (options.exclude_dirs ?? [])
122
+ : [...DEFAULT_EXCLUDE_DIRS, ...(options?.exclude_dirs ?? [])];
123
+
124
+ const exclude_extensions = options?.no_defaults
125
+ ? (options.exclude_extensions ?? [])
126
+ : [...DEFAULT_EXCLUDE_EXTENSIONS, ...(options?.exclude_extensions ?? [])];
127
+
128
+ const normalized = file_path.toLowerCase();
129
+
130
+ // Check excluded directories
131
+ for (const dir of exclude_dirs) {
132
+ // Must match as a full directory component, not a prefix
133
+ if (normalized.includes(`/${dir}/`) || normalized.endsWith(`/${dir}`)) {
134
+ return true;
135
+ }
136
+ }
137
+
138
+ // Check excluded extensions
139
+ for (const ext of exclude_extensions) {
140
+ if (normalized.endsWith(ext)) {
141
+ return true;
142
+ }
143
+ }
144
+
145
+ return false;
146
+ };
147
+
148
+ /**
149
+ * Walk files in a directory, respecting common exclusions.
150
+ * Yields absolute paths to files (and optionally directories).
151
+ *
152
+ * @param dir Directory to walk
153
+ * @param options Walk options for exclusions and filtering
154
+ */
155
+ export async function* walk_repo_files(
156
+ dir: string,
157
+ options?: WalkOptions,
158
+ ): AsyncGenerator<string, void, undefined> {
159
+ const max_file_size = options?.max_file_size ?? 10 * 1024 * 1024;
160
+ const include_dirs = options?.include_dirs ?? false;
161
+
162
+ async function* walk(current_dir: string): AsyncGenerator<string, void, undefined> {
163
+ let entries;
164
+ try {
165
+ entries = await readdir(current_dir, {withFileTypes: true});
166
+ } catch {
167
+ // Skip directories we can't read
168
+ return;
169
+ }
170
+
171
+ for (const entry of entries) {
172
+ const full_path = join(current_dir, entry.name);
173
+
174
+ if (should_exclude_path(full_path, options)) {
175
+ continue;
176
+ }
177
+
178
+ if (entry.isDirectory()) {
179
+ if (include_dirs) {
180
+ yield full_path;
181
+ }
182
+ yield* walk(full_path);
183
+ } else if (entry.isFile()) {
184
+ // Check file size
185
+ try {
186
+ const file_stat = await stat(full_path); // eslint-disable-line no-await-in-loop
187
+ if (file_stat.size <= max_file_size) {
188
+ yield full_path;
189
+ }
190
+ } catch {
191
+ // Skip files we can't stat
192
+ }
193
+ }
194
+ }
195
+ }
196
+
197
+ yield* walk(dir);
198
+ }
199
+
200
+ /**
201
+ * Collect all files from walk_repo_files into an array.
202
+ * Convenience function for when you need all paths upfront.
203
+ */
204
+ export const collect_repo_files = async (
205
+ dir: string,
206
+ options?: WalkOptions,
207
+ ): Promise<Array<string>> => {
208
+ const files: Array<string> = [];
209
+ for await (const file of walk_repo_files(dir, options)) {
210
+ files.push(file);
211
+ }
212
+ return files;
213
+ };
@@ -0,0 +1,27 @@
1
+ import type {GitopsConfig} from './gitops_config.js';
2
+ import {local_repo_locate, type LocalRepoPath, type LocalRepoMissing} from './local_repo.js';
3
+
4
+ export interface ResolvedGitopsConfig {
5
+ local_repos: Array<LocalRepoPath | LocalRepoMissing> | null;
6
+ local_repo_paths: Array<LocalRepoPath> | null;
7
+ local_repos_missing: Array<LocalRepoMissing> | null;
8
+ }
9
+
10
+ export const resolve_gitops_config = (
11
+ gitops_config: GitopsConfig,
12
+ repos_dir: string,
13
+ ): ResolvedGitopsConfig => {
14
+ const local_repos = gitops_config.repos.map((r) =>
15
+ local_repo_locate({repo_config: r, repos_dir}),
16
+ );
17
+
18
+ const local_repo_paths = local_repos.filter((r) => r.type === 'local_repo_path');
19
+ const local_repos_missing = local_repos.filter((r) => r.type === 'local_repo_missing');
20
+
21
+ const config: ResolvedGitopsConfig = {
22
+ local_repos: local_repos.length ? local_repos : null,
23
+ local_repo_paths: local_repo_paths.length ? local_repo_paths : null,
24
+ local_repos_missing: local_repos_missing.length ? local_repos_missing : null,
25
+ };
26
+ return config;
27
+ };
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Semantic Versioning 2.0.0 utilities
3
+ * @see https://semver.org/
4
+ */
5
+
6
+ export type BumpType = 'major' | 'minor' | 'patch';
7
+
8
+ export interface Semver {
9
+ major: number;
10
+ minor: number;
11
+ patch: number;
12
+ prerelease?: string;
13
+ build?: string;
14
+ }
15
+
16
+ /**
17
+ * SemVer 2.0.0 validation regex.
18
+ * @see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
19
+ */
20
+ const SEMVER_REGEX =
21
+ /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
22
+
23
+ /**
24
+ * Parses a semver version string.
25
+ * Accepts optional 'v' prefix for convenience.
26
+ */
27
+ const semver_parse = (version: string): Semver => {
28
+ // Remove leading 'v' if present (common in git tags)
29
+ const clean = version.replace(/^v/, '');
30
+
31
+ // Validate the cleaned version
32
+ if (!SEMVER_REGEX.test(clean)) {
33
+ throw new Error(`Invalid semver: ${version}`);
34
+ }
35
+
36
+ const match = SEMVER_REGEX.exec(clean)!;
37
+ return {
38
+ major: parseInt(match[1]!, 10),
39
+ minor: parseInt(match[2]!, 10),
40
+ patch: parseInt(match[3]!, 10),
41
+ prerelease: match[4],
42
+ build: match[5],
43
+ };
44
+ };
45
+
46
+ const semver_to_string = (semver: Semver): string => {
47
+ let version = `${semver.major}.${semver.minor}.${semver.patch}`;
48
+ if (semver.prerelease) {
49
+ version += `-${semver.prerelease}`;
50
+ }
51
+ if (semver.build) {
52
+ version += `+${semver.build}`;
53
+ }
54
+ return version;
55
+ };
56
+
57
+ /**
58
+ * Compares two prerelease versions according to SemVer 2.0.0 spec.
59
+ * Returns -1 if a < b, 0 if a === b, 1 if a > b.
60
+ */
61
+ const semver_compare_prerelease = (a: string | undefined, b: string | undefined): number => {
62
+ // Handle missing prereleases
63
+ if (!a && !b) return 0;
64
+ if (!a) return 1; // normal version > prerelease
65
+ if (!b) return -1; // prerelease < normal version
66
+
67
+ // Split into identifiers
68
+ const a_parts = a.split('.');
69
+ const b_parts = b.split('.');
70
+
71
+ // Compare each identifier
72
+ const min_length = Math.min(a_parts.length, b_parts.length);
73
+ for (let i = 0; i < min_length; i++) {
74
+ const a_part = a_parts[i]!;
75
+ const b_part = b_parts[i]!;
76
+
77
+ // Check if numeric
78
+ const a_is_numeric = /^\d+$/.test(a_part);
79
+ const b_is_numeric = /^\d+$/.test(b_part);
80
+
81
+ if (a_is_numeric && b_is_numeric) {
82
+ // Both numeric - compare numerically
83
+ const a_num = parseInt(a_part, 10);
84
+ const b_num = parseInt(b_part, 10);
85
+ if (a_num !== b_num) {
86
+ return a_num < b_num ? -1 : 1;
87
+ }
88
+ } else if (a_is_numeric && !b_is_numeric) {
89
+ // Numeric identifiers always have lower precedence
90
+ return -1;
91
+ } else if (!a_is_numeric && b_is_numeric) {
92
+ // Numeric identifiers always have lower precedence
93
+ return 1;
94
+ } else {
95
+ // Both alphanumeric - compare lexically
96
+ const cmp = a_part.localeCompare(b_part);
97
+ if (cmp !== 0) {
98
+ return cmp < 0 ? -1 : 1;
99
+ }
100
+ }
101
+ }
102
+
103
+ // All identifiers equal - larger set has higher precedence
104
+ if (a_parts.length !== b_parts.length) {
105
+ return a_parts.length < b_parts.length ? -1 : 1;
106
+ }
107
+
108
+ return 0;
109
+ };
110
+
111
+ /**
112
+ * Compares two semver versions according to SemVer 2.0.0 spec.
113
+ * Returns -1 if a < b, 0 if a === b, 1 if a > b.
114
+ * Build metadata is ignored in precedence comparison.
115
+ */
116
+ export const semver_compare_versions = (a: string, b: string): number => {
117
+ const v1 = semver_parse(a);
118
+ const v2 = semver_parse(b);
119
+
120
+ // Compare major
121
+ if (v1.major !== v2.major) {
122
+ return v1.major < v2.major ? -1 : 1;
123
+ }
124
+
125
+ // Compare minor
126
+ if (v1.minor !== v2.minor) {
127
+ return v1.minor < v2.minor ? -1 : 1;
128
+ }
129
+
130
+ // Compare patch
131
+ if (v1.patch !== v2.patch) {
132
+ return v1.patch < v2.patch ? -1 : 1;
133
+ }
134
+
135
+ // Compare prerelease (build metadata is ignored)
136
+ return semver_compare_prerelease(v1.prerelease, v2.prerelease);
137
+ };
138
+
139
+ /**
140
+ * Bumps a version according to the specified type.
141
+ * Resets lower version numbers per SemVer spec.
142
+ */
143
+ export const semver_bump_version = (version: string, type: BumpType): string => {
144
+ const semver = semver_parse(version);
145
+
146
+ switch (type) {
147
+ case 'major':
148
+ semver.major++;
149
+ semver.minor = 0;
150
+ semver.patch = 0;
151
+ break;
152
+ case 'minor':
153
+ semver.minor++;
154
+ semver.patch = 0;
155
+ break;
156
+ case 'patch':
157
+ semver.patch++;
158
+ break;
159
+ }
160
+
161
+ // Remove prerelease and build when bumping
162
+ semver.prerelease = undefined;
163
+ semver.build = undefined;
164
+
165
+ return semver_to_string(semver);
166
+ };
@@ -0,0 +1,90 @@
1
+ /**
2
+ * JSON-serializable types for command output formats.
3
+ *
4
+ * Gitops commands support `--format json` and `--format markdown` output modes
5
+ * in addition to styled terminal output. These types define the JSON schema for:
6
+ * - Dependency graph structures (`SerializedGraph`)
7
+ * - Publishing plan predictions (`SerializedPublishingPlan`)
8
+ *
9
+ * Used by `gitops_analyze`, `gitops_plan`, and `gitops_publish --dry_run` when
10
+ * `--format json` or `--outfile` is specified.
11
+ */
12
+
13
+ import type {DependencyGraph} from './dependency_graph.js';
14
+
15
+ export interface SerializedNode {
16
+ name: string;
17
+ version: string;
18
+ dependencies: Array<{
19
+ name: string;
20
+ type: string;
21
+ version: string;
22
+ }>;
23
+ dependents: Array<string>;
24
+ publishable: boolean;
25
+ }
26
+
27
+ export interface SerializedGraph {
28
+ nodes: Array<SerializedNode>;
29
+ edges: Array<[string, string]>;
30
+ }
31
+
32
+ export interface SerializedPublishingPlan {
33
+ publishing_order: Array<string>;
34
+ version_changes: Array<{
35
+ package_name: string;
36
+ from: string;
37
+ to: string;
38
+ bump_type: string;
39
+ breaking: boolean;
40
+ has_changesets: boolean;
41
+ will_generate_changeset?: boolean;
42
+ needs_bump_escalation?: boolean;
43
+ existing_bump?: string;
44
+ required_bump?: string;
45
+ }>;
46
+ dependency_updates: Array<{
47
+ dependent_package: string;
48
+ updated_dependency: string;
49
+ new_version: string;
50
+ type: string;
51
+ causes_republish: boolean;
52
+ }>;
53
+ breaking_cascades: Record<string, Array<string>>;
54
+ warnings: Array<string>;
55
+ errors: Array<string>;
56
+ }
57
+
58
+ /**
59
+ * Serializes a dependency graph to a JSON-safe format.
60
+ */
61
+ export const serialize_graph = (graph: DependencyGraph): SerializedGraph => {
62
+ const nodes: Array<SerializedNode> = [];
63
+ const edges: Array<[string, string]> = [];
64
+
65
+ // Serialize nodes
66
+ for (const node of graph.nodes.values()) {
67
+ const dependencies = Array.from(node.dependencies, ([dep_name, spec]) => ({
68
+ name: dep_name,
69
+ type: spec.type,
70
+ version: spec.version,
71
+ }));
72
+
73
+ nodes.push({
74
+ name: node.name,
75
+ version: node.version,
76
+ dependencies,
77
+ dependents: Array.from(node.dependents),
78
+ publishable: node.publishable,
79
+ });
80
+ }
81
+
82
+ // Serialize edges
83
+ for (const [from, tos] of graph.edges) {
84
+ for (const to of tos) {
85
+ edges.push([from, to]);
86
+ }
87
+ }
88
+
89
+ return {nodes, edges};
90
+ };
@@ -0,0 +1,150 @@
1
+ import type {BumpType} from './semver.js';
2
+
3
+ export const is_wildcard = (version: string): boolean => {
4
+ return version === '*';
5
+ };
6
+
7
+ /**
8
+ * Strips version prefix (^, ~, >=, <=, etc) from a version string.
9
+ */
10
+ export const strip_version_prefix = (version: string): string => {
11
+ return version.replace(/^(>=|<=|>|<|=|\^|~)/, '');
12
+ };
13
+
14
+ /**
15
+ * Gets the version prefix (^, ~, >=, <=, or empty string).
16
+ */
17
+ export const get_version_prefix = (version: string): string => {
18
+ const match = /^(>=|<=|>|<|=|\^|~)/.exec(version);
19
+ return match ? match[1]! : '';
20
+ };
21
+
22
+ /**
23
+ * Normalizes version string for comparison.
24
+ *
25
+ * Strips prefixes (^, ~, >=) to get bare version number.
26
+ * Handles wildcards as-is. Used by needs_update to compare versions.
27
+ *
28
+ * @example normalize_version_for_comparison('^1.2.3') // '1.2.3'
29
+ * @example normalize_version_for_comparison('>=2.0.0') // '2.0.0'
30
+ * @example normalize_version_for_comparison('*') // '*'
31
+ */
32
+ export const normalize_version_for_comparison = (version: string): string => {
33
+ // Handle wildcards
34
+ if (is_wildcard(version)) return version;
35
+
36
+ // Handle >= ranges - extract just the version number
37
+ if (version.startsWith('>=')) {
38
+ return version.substring(2).trim();
39
+ }
40
+
41
+ // Strip other prefixes
42
+ return strip_version_prefix(version);
43
+ };
44
+
45
+ export const needs_update = (current: string, new_version: string): boolean => {
46
+ // Always update wildcards
47
+ if (is_wildcard(current)) return true;
48
+
49
+ // Compare normalized versions
50
+ const current_normalized = normalize_version_for_comparison(current);
51
+ const new_normalized = normalize_version_for_comparison(new_version);
52
+
53
+ return current_normalized !== new_normalized;
54
+ };
55
+
56
+ /**
57
+ * Determines version prefix to use when updating dependencies.
58
+ *
59
+ * Strategy:
60
+ * - Wildcard (*): Use caret (^) as default
61
+ * - Has existing prefix: Preserve it (^, ~, >=, <=, etc)
62
+ * - No prefix: Use default_strategy
63
+ *
64
+ * This preserves user intent while handling wildcard replacements sensibly.
65
+ *
66
+ * @param default_strategy prefix to use when no existing prefix found
67
+ */
68
+ export const get_update_prefix = (
69
+ current_version: string,
70
+ default_strategy: '^' | '~' | '' | '>=' = '^',
71
+ ): string => {
72
+ // Use caret for wildcard replacements
73
+ if (is_wildcard(current_version)) {
74
+ return '^';
75
+ }
76
+
77
+ // Preserve existing prefix if present
78
+ const existing_prefix = get_version_prefix(current_version);
79
+ if (existing_prefix) {
80
+ return existing_prefix;
81
+ }
82
+
83
+ // Use default strategy
84
+ return default_strategy;
85
+ };
86
+
87
+ /**
88
+ * Determines if a bump is a breaking change based on semver rules.
89
+ * Pre-1.0: minor bumps are breaking
90
+ * 1.0+: major bumps are breaking
91
+ */
92
+ export const is_breaking_change = (
93
+ old_version: string,
94
+ bump_type: 'major' | 'minor' | 'patch',
95
+ ): boolean => {
96
+ const [major] = old_version.split('.').map(Number);
97
+ const is_pre_1_0 = major === 0;
98
+
99
+ if (is_pre_1_0) {
100
+ // In 0.x.x, minor bumps are breaking changes
101
+ return bump_type === 'minor' || bump_type === 'major';
102
+ } else {
103
+ // In 1.x.x+, only major bumps are breaking
104
+ return bump_type === 'major';
105
+ }
106
+ };
107
+
108
+ export const detect_bump_type = (
109
+ old_version: string,
110
+ new_version: string,
111
+ ): 'major' | 'minor' | 'patch' => {
112
+ const old_parts = old_version.split('.').map(Number);
113
+ const new_parts = new_version.split('.').map(Number);
114
+
115
+ if (new_parts[0]! > old_parts[0]!) return 'major';
116
+ if (new_parts[1]! > old_parts[1]!) return 'minor';
117
+ return 'patch';
118
+ };
119
+
120
+ /**
121
+ * Compares bump types. Returns positive if a > b, negative if a < b, 0 if equal.
122
+ */
123
+ export const compare_bump_types = (a: BumpType, b: BumpType): number => {
124
+ const order: Record<BumpType, number> = {
125
+ major: 3,
126
+ minor: 2,
127
+ patch: 1,
128
+ };
129
+ return order[a] - order[b];
130
+ };
131
+
132
+ export const calculate_next_version = (current_version: string, bump_type: BumpType): string => {
133
+ const parts = current_version.split('.').map(Number);
134
+ if (parts.length !== 3 || parts.some((p) => Number.isNaN(p))) {
135
+ throw new Error(`Invalid version format: ${current_version}`);
136
+ }
137
+
138
+ const [major, minor, patch] = parts;
139
+
140
+ switch (bump_type) {
141
+ case 'major':
142
+ return `${major! + 1}.0.0`;
143
+ case 'minor':
144
+ return `${major!}.${minor! + 1}.0`;
145
+ case 'patch':
146
+ return `${major!}.${minor!}.${patch! + 1}`;
147
+ default:
148
+ throw new Error(`Invalid bump type: ${bump_type}`);
149
+ }
150
+ };