@fuzdev/fuz_gitops 0.72.0 → 0.73.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 (110) hide show
  1. package/README.md +13 -6
  2. package/dist/changeset_generator.d.ts.map +1 -1
  3. package/dist/changeset_generator.js +2 -16
  4. package/dist/changeset_reader.d.ts +1 -1
  5. package/dist/changeset_reader.d.ts.map +1 -1
  6. package/dist/dependency_graph.js +1 -1
  7. package/dist/git_operations.d.ts +5 -0
  8. package/dist/git_operations.d.ts.map +1 -1
  9. package/dist/git_operations.js +5 -0
  10. package/dist/gitops_analyze.task.d.ts +1 -0
  11. package/dist/gitops_analyze.task.d.ts.map +1 -1
  12. package/dist/gitops_analyze.task.js +12 -17
  13. package/dist/gitops_plan.task.d.ts +1 -0
  14. package/dist/gitops_plan.task.d.ts.map +1 -1
  15. package/dist/gitops_plan.task.js +9 -2
  16. package/dist/gitops_publish.task.d.ts +2 -2
  17. package/dist/gitops_publish.task.d.ts.map +1 -1
  18. package/dist/gitops_publish.task.js +83 -28
  19. package/dist/gitops_run.task.d.ts +2 -1
  20. package/dist/gitops_run.task.d.ts.map +1 -1
  21. package/dist/gitops_run.task.js +40 -7
  22. package/dist/gitops_sync.task.d.ts +1 -0
  23. package/dist/gitops_sync.task.d.ts.map +1 -1
  24. package/dist/gitops_sync.task.js +16 -2
  25. package/dist/gitops_task_helpers.d.ts +16 -2
  26. package/dist/gitops_task_helpers.d.ts.map +1 -1
  27. package/dist/gitops_task_helpers.js +11 -3
  28. package/dist/gitops_validate.task.d.ts +1 -0
  29. package/dist/gitops_validate.task.d.ts.map +1 -1
  30. package/dist/gitops_validate.task.js +20 -20
  31. package/dist/graph_validation.d.ts +16 -1
  32. package/dist/graph_validation.d.ts.map +1 -1
  33. package/dist/graph_validation.js +15 -0
  34. package/dist/local_repo.d.ts +22 -10
  35. package/dist/local_repo.d.ts.map +1 -1
  36. package/dist/local_repo.js +92 -75
  37. package/dist/log_helpers.d.ts +0 -5
  38. package/dist/log_helpers.d.ts.map +1 -1
  39. package/dist/log_helpers.js +8 -19
  40. package/dist/multi_repo_publisher.d.ts +21 -2
  41. package/dist/multi_repo_publisher.d.ts.map +1 -1
  42. package/dist/multi_repo_publisher.js +279 -287
  43. package/dist/operations.d.ts +6 -26
  44. package/dist/operations.d.ts.map +1 -1
  45. package/dist/operations_defaults.d.ts.map +1 -1
  46. package/dist/operations_defaults.js +1 -19
  47. package/dist/output_helpers.d.ts.map +1 -1
  48. package/dist/output_helpers.js +7 -6
  49. package/dist/paths.d.ts +0 -4
  50. package/dist/paths.d.ts.map +1 -1
  51. package/dist/paths.js +0 -4
  52. package/dist/publish_gate.d.ts +44 -0
  53. package/dist/publish_gate.d.ts.map +1 -0
  54. package/dist/publish_gate.js +32 -0
  55. package/dist/publish_steps.d.ts +60 -0
  56. package/dist/publish_steps.d.ts.map +1 -0
  57. package/dist/publish_steps.js +113 -0
  58. package/dist/publishing_event.d.ts +12 -19
  59. package/dist/publishing_event.d.ts.map +1 -1
  60. package/dist/publishing_event.js +16 -25
  61. package/dist/publishing_plan.d.ts +1 -2
  62. package/dist/publishing_plan.d.ts.map +1 -1
  63. package/dist/publishing_plan.js +18 -1
  64. package/dist/publishing_plan_helpers.d.ts +1 -1
  65. package/dist/publishing_plan_helpers.d.ts.map +1 -1
  66. package/dist/publishing_plan_helpers.js +2 -15
  67. package/dist/publishing_plan_logging.d.ts.map +1 -1
  68. package/dist/publishing_plan_logging.js +15 -23
  69. package/dist/version_utils.d.ts +17 -3
  70. package/dist/version_utils.d.ts.map +1 -1
  71. package/dist/version_utils.js +22 -0
  72. package/package.json +5 -6
  73. package/src/lib/changeset_generator.ts +10 -19
  74. package/src/lib/changeset_reader.ts +1 -2
  75. package/src/lib/dependency_graph.ts +1 -1
  76. package/src/lib/git_operations.ts +5 -0
  77. package/src/lib/gitops_analyze.task.ts +18 -23
  78. package/src/lib/gitops_plan.task.ts +10 -2
  79. package/src/lib/gitops_publish.task.ts +92 -27
  80. package/src/lib/gitops_run.task.ts +39 -7
  81. package/src/lib/gitops_sync.task.ts +18 -3
  82. package/src/lib/gitops_task_helpers.ts +20 -3
  83. package/src/lib/gitops_validate.task.ts +30 -22
  84. package/src/lib/graph_validation.ts +26 -0
  85. package/src/lib/local_repo.ts +114 -83
  86. package/src/lib/log_helpers.ts +8 -26
  87. package/src/lib/multi_repo_publisher.ts +336 -353
  88. package/src/lib/operations.ts +6 -22
  89. package/src/lib/operations_defaults.ts +1 -19
  90. package/src/lib/output_helpers.ts +7 -6
  91. package/src/lib/paths.ts +0 -5
  92. package/src/lib/publish_gate.ts +53 -0
  93. package/src/lib/publish_steps.ts +149 -0
  94. package/src/lib/publishing_event.ts +16 -25
  95. package/src/lib/publishing_plan.ts +23 -3
  96. package/src/lib/publishing_plan_helpers.ts +5 -18
  97. package/src/lib/publishing_plan_logging.ts +19 -39
  98. package/src/lib/version_utils.ts +30 -9
  99. package/dist/npm_install_helpers.d.ts +0 -23
  100. package/dist/npm_install_helpers.d.ts.map +0 -1
  101. package/dist/npm_install_helpers.js +0 -60
  102. package/dist/semver.d.ts +0 -26
  103. package/dist/semver.d.ts.map +0 -1
  104. package/dist/semver.js +0 -137
  105. package/dist/serialization_types.d.ts +0 -59
  106. package/dist/serialization_types.d.ts.map +0 -1
  107. package/dist/serialization_types.js +0 -42
  108. package/src/lib/npm_install_helpers.ts +0 -85
  109. package/src/lib/semver.ts +0 -170
  110. package/src/lib/serialization_types.ts +0 -92
@@ -55,128 +55,153 @@ export interface LocalRepoMissing {
55
55
  }
56
56
 
57
57
  /**
58
- * Loads repo data with automatic syncing and dependency management.
58
+ * Loads repo data, optionally syncing the working tree first.
59
59
  *
60
- * Workflow:
60
+ * When `sync` is `false` (the default for read-only diagnostics like
61
+ * `gitops_analyze`/`gitops_plan`), the repo is loaded exactly as it sits on
62
+ * disk — no branch switch, pull, install, or clean-workspace check. This makes
63
+ * those commands safe to run on an active workspace with uncommitted changes or
64
+ * feature branches checked out.
65
+ *
66
+ * When `sync` is `true` (used by `gitops_sync`), the working tree is brought in
67
+ * line with the configured branch first:
61
68
  * 1. Records current commit hash (for detecting changes)
62
- * 2. Switches to target branch if needed (requires clean workspace)
69
+ * 2. Switches to target branch if needed (requires clean workspace unless `allow_dirty`)
63
70
  * 3. Pulls latest changes from remote (skipped for local-only repos)
64
- * 4. Validates workspace is clean after pull
71
+ * 4. Validates workspace is clean after pull (skipped if `allow_dirty`)
65
72
  * 5. Auto-installs dependencies if `package.json` changed
73
+ *
74
+ * Either way it then:
66
75
  * 6. Loads `library_json` via `library_load_from_repo` (svelte-docinfo analysis)
67
76
  * 7. Creates `Library` and extracts dependency maps
68
77
  *
69
- * This ensures repos are always in sync with their configured branch
70
- * before being used by gitops commands.
71
- *
72
- * @throws {TaskError} if workspace dirty, branch switch fails, install fails, or analysis fails
78
+ * @param sync - sync the working tree to the configured branch before loading (default `true`)
79
+ * @param allow_dirty - when syncing, tolerate uncommitted changes instead of throwing (default `false`)
80
+ * @throws {TaskError} if syncing fails (dirty workspace, branch switch, install) or analysis fails
73
81
  */
74
82
  export const local_repo_load = async ({
75
83
  local_repo_path,
76
84
  log: _log,
77
85
  git_ops = default_git_operations,
78
86
  npm_ops = default_npm_operations,
87
+ sync = true,
88
+ allow_dirty = false,
79
89
  }: {
80
90
  local_repo_path: LocalRepoPath;
81
91
  log?: Logger;
82
92
  git_ops?: GitOperations;
83
93
  npm_ops?: NpmOperations;
94
+ sync?: boolean;
95
+ allow_dirty?: boolean;
84
96
  }): Promise<LocalRepo> => {
85
97
  const {repo_config, repo_dir, repo_name, repo_git_ssh_url} = local_repo_path;
86
98
 
87
- // Record commit hash before any changes
88
- const commit_before_result = await git_ops.current_commit_hash({cwd: repo_dir});
89
- if (!commit_before_result.ok) {
90
- throw new TaskError(
91
- `Failed to get commit hash in ${repo_dir}: ${commit_before_result.message}`,
92
- );
93
- }
94
- const commit_before = commit_before_result.value;
95
-
96
- // Switch to target branch if needed
97
- const branch_result = await git_ops.current_branch_name({cwd: repo_dir});
98
- if (!branch_result.ok) {
99
- throw new TaskError(`Failed to get current branch in ${repo_dir}: ${branch_result.message}`);
100
- }
101
-
102
- const switched_branches = branch_result.value !== repo_config.branch;
103
- if (switched_branches) {
104
- const clean_result = await git_ops.check_clean_workspace({cwd: repo_dir});
105
- if (!clean_result.ok) {
106
- throw new TaskError(`Failed to check workspace in ${repo_dir}: ${clean_result.message}`);
107
- }
108
-
109
- if (!clean_result.value) {
99
+ if (sync) {
100
+ // Record commit hash before any changes
101
+ const commit_before_result = await git_ops.current_commit_hash({cwd: repo_dir});
102
+ if (!commit_before_result.ok) {
110
103
  throw new TaskError(
111
- `Repo ${repo_dir} is not on branch "${repo_config.branch}" and the workspace is unclean, blocking switch`,
104
+ `Failed to get commit hash in ${repo_dir}: ${commit_before_result.message}`,
112
105
  );
113
106
  }
107
+ const commit_before = commit_before_result.value;
114
108
 
115
- const checkout_result = await git_ops.checkout({branch: repo_config.branch, cwd: repo_dir});
116
- if (!checkout_result.ok) {
117
- throw new TaskError(
118
- `Failed to checkout branch "${repo_config.branch}" in ${repo_dir}: ${checkout_result.message}`,
119
- );
109
+ // Switch to target branch if needed
110
+ const branch_result = await git_ops.current_branch_name({cwd: repo_dir});
111
+ if (!branch_result.ok) {
112
+ throw new TaskError(`Failed to get current branch in ${repo_dir}: ${branch_result.message}`);
120
113
  }
121
- }
122
114
 
123
- // Only pull if remote exists (skip for local-only repos, test fixtures)
124
- const origin_result = await git_ops.has_remote({remote: 'origin', cwd: repo_dir});
125
- if (!origin_result.ok) {
126
- throw new TaskError(`Failed to check for remote in ${repo_dir}: ${origin_result.message}`);
127
- }
115
+ const switched_branches = branch_result.value !== repo_config.branch;
116
+ if (switched_branches) {
117
+ // Guard the switch on a clean workspace unless the caller opts into `allow_dirty`,
118
+ // in which case we let `git checkout` itself fail loudly if it can't proceed.
119
+ if (!allow_dirty) {
120
+ const clean_result = await git_ops.check_clean_workspace({cwd: repo_dir});
121
+ if (!clean_result.ok) {
122
+ throw new TaskError(`Failed to check workspace in ${repo_dir}: ${clean_result.message}`);
123
+ }
124
+
125
+ if (!clean_result.value) {
126
+ throw new TaskError(
127
+ `Repo ${repo_dir} is not on branch "${repo_config.branch}" and the workspace is unclean, blocking switch`,
128
+ );
129
+ }
130
+ }
128
131
 
129
- if (origin_result.value) {
130
- const pull_result = await git_ops.pull({cwd: repo_dir});
131
- if (!pull_result.ok) {
132
- throw new TaskError(`Failed to pull in ${repo_dir}: ${pull_result.message}`);
132
+ const checkout_result = await git_ops.checkout({branch: repo_config.branch, cwd: repo_dir});
133
+ if (!checkout_result.ok) {
134
+ throw new TaskError(
135
+ `Failed to checkout branch "${repo_config.branch}" in ${repo_dir}: ${checkout_result.message}`,
136
+ );
137
+ }
133
138
  }
134
- }
135
139
 
136
- // Check clean workspace after pull to ensure we're in a good state
137
- const clean_after_result = await git_ops.check_clean_workspace({cwd: repo_dir});
138
- if (!clean_after_result.ok) {
139
- throw new TaskError(`Failed to check workspace in ${repo_dir}: ${clean_after_result.message}`);
140
- }
141
-
142
- if (!clean_after_result.value) {
143
- throw new TaskError(
144
- `Workspace ${repo_dir} is unclean after pulling branch "${repo_config.branch}"`,
145
- );
146
- }
140
+ // Only pull if remote exists (skip for local-only repos, test fixtures)
141
+ const origin_result = await git_ops.has_remote({remote: 'origin', cwd: repo_dir});
142
+ if (!origin_result.ok) {
143
+ throw new TaskError(`Failed to check for remote in ${repo_dir}: ${origin_result.message}`);
144
+ }
147
145
 
148
- // Record commit hash after pull
149
- const commit_after_result = await git_ops.current_commit_hash({cwd: repo_dir});
150
- if (!commit_after_result.ok) {
151
- throw new TaskError(`Failed to get commit hash in ${repo_dir}: ${commit_after_result.message}`);
152
- }
153
- const commit_after = commit_after_result.value;
146
+ if (origin_result.value) {
147
+ const pull_result = await git_ops.pull({cwd: repo_dir});
148
+ if (!pull_result.ok) {
149
+ throw new TaskError(`Failed to pull in ${repo_dir}: ${pull_result.message}`);
150
+ }
151
+ }
154
152
 
155
- // Track if we got new commits
156
- const got_new_commits = commit_before !== commit_after;
153
+ // Check clean workspace after pull to ensure we're in a good state
154
+ // (skipped when `allow_dirty`, since uncommitted changes are expected then)
155
+ if (!allow_dirty) {
156
+ const clean_after_result = await git_ops.check_clean_workspace({cwd: repo_dir});
157
+ if (!clean_after_result.ok) {
158
+ throw new TaskError(
159
+ `Failed to check workspace in ${repo_dir}: ${clean_after_result.message}`,
160
+ );
161
+ }
157
162
 
158
- // Only install if package.json changed
159
- if (got_new_commits) {
160
- const changed_result = await git_ops.has_file_changed({
161
- from_commit: commit_before,
162
- to_commit: commit_after,
163
- file_path: 'package.json',
164
- cwd: repo_dir,
165
- });
163
+ if (!clean_after_result.value) {
164
+ throw new TaskError(
165
+ `Workspace ${repo_dir} is unclean after pulling branch "${repo_config.branch}"`,
166
+ );
167
+ }
168
+ }
166
169
 
167
- if (!changed_result.ok) {
170
+ // Record commit hash after pull
171
+ const commit_after_result = await git_ops.current_commit_hash({cwd: repo_dir});
172
+ if (!commit_after_result.ok) {
168
173
  throw new TaskError(
169
- `Failed to check if package.json changed in ${repo_dir}: ${changed_result.message}`,
174
+ `Failed to get commit hash in ${repo_dir}: ${commit_after_result.message}`,
170
175
  );
171
176
  }
177
+ const commit_after = commit_after_result.value;
178
+
179
+ // Track if we got new commits
180
+ const got_new_commits = commit_before !== commit_after;
181
+
182
+ // Only install if package.json changed
183
+ if (got_new_commits) {
184
+ const changed_result = await git_ops.has_file_changed({
185
+ from_commit: commit_before,
186
+ to_commit: commit_after,
187
+ file_path: 'package.json',
188
+ cwd: repo_dir,
189
+ });
172
190
 
173
- if (changed_result.value) {
174
- const install_result = await npm_ops.install({cwd: repo_dir});
175
- if (!install_result.ok) {
191
+ if (!changed_result.ok) {
176
192
  throw new TaskError(
177
- `Failed to install dependencies in ${repo_dir}: ${install_result.message}${install_result.stderr ? `\n${install_result.stderr}` : ''}`,
193
+ `Failed to check if package.json changed in ${repo_dir}: ${changed_result.message}`,
178
194
  );
179
195
  }
196
+
197
+ if (changed_result.value) {
198
+ const install_result = await npm_ops.install({cwd: repo_dir});
199
+ if (!install_result.ok) {
200
+ throw new TaskError(
201
+ `Failed to install dependencies in ${repo_dir}: ${install_result.message}${install_result.stderr ? `\n${install_result.stderr}` : ''}`,
202
+ );
203
+ }
204
+ }
180
205
  }
181
206
  }
182
207
 
@@ -275,6 +300,8 @@ export const local_repos_load = async ({
275
300
  npm_ops = default_npm_operations,
276
301
  parallel = true,
277
302
  concurrency = GITOPS_CONCURRENCY_DEFAULT,
303
+ sync = true,
304
+ allow_dirty = false,
278
305
  }: {
279
306
  local_repo_paths: Array<LocalRepoPath>;
280
307
  log?: Logger;
@@ -282,12 +309,16 @@ export const local_repos_load = async ({
282
309
  npm_ops?: NpmOperations;
283
310
  parallel?: boolean;
284
311
  concurrency?: number;
312
+ sync?: boolean;
313
+ allow_dirty?: boolean;
285
314
  }): Promise<Array<LocalRepo>> => {
286
315
  if (!parallel) {
287
316
  // Sequential loading (original behavior)
288
317
  const loaded: Array<LocalRepo> = [];
289
318
  for (const local_repo_path of local_repo_paths) {
290
- loaded.push(await local_repo_load({local_repo_path, log, git_ops, npm_ops}));
319
+ loaded.push(
320
+ await local_repo_load({local_repo_path, log, git_ops, npm_ops, sync, allow_dirty}),
321
+ );
291
322
  }
292
323
  return loaded;
293
324
  }
@@ -297,7 +328,7 @@ export const local_repos_load = async ({
297
328
  local_repo_paths,
298
329
  concurrency,
299
330
  async (local_repo_path) => {
300
- return local_repo_load({local_repo_path, log, git_ops, npm_ops});
331
+ return local_repo_load({local_repo_path, log, git_ops, npm_ops, sync, allow_dirty});
301
332
  },
302
333
  );
303
334
 
@@ -13,7 +13,10 @@ export const format_wildcard_dependencies = (
13
13
  if (analysis.wildcard_deps.length === 0) return [];
14
14
 
15
15
  const lines: Array<string> = [];
16
- lines.push(st('yellow', `\n⚠️ Found ${analysis.wildcard_deps.length} wildcard dependencies:`));
16
+ // Blank separator as its own line so the logger prefixes it correctly; embedding
17
+ // a leading `\n` in the header would leave the header line unprefixed.
18
+ lines.push('');
19
+ lines.push(st('yellow', `⚠️ Found ${analysis.wildcard_deps.length} wildcard dependencies:`));
17
20
  for (const {pkg, dep, version} of analysis.wildcard_deps) {
18
21
  lines.push(` ${pkg} → ${dep} ${st('red', version)}`);
19
22
  }
@@ -30,10 +33,11 @@ export const format_dev_cycles = (
30
33
  if (analysis.dev_cycles.length === 0) return [];
31
34
 
32
35
  const lines: Array<string> = [];
36
+ lines.push('');
33
37
  lines.push(
34
38
  st(
35
39
  'dim',
36
- `\nℹ️ Found ${analysis.dev_cycles.length} dev circular dependencies (normal, non-blocking):`,
40
+ `ℹ️ Found ${analysis.dev_cycles.length} dev circular dependencies (normal, non-blocking):`,
37
41
  ),
38
42
  );
39
43
  for (const cycle of analysis.dev_cycles) {
@@ -52,10 +56,11 @@ export const format_production_cycles = (
52
56
  if (analysis.production_cycles.length === 0) return [];
53
57
 
54
58
  const lines: Array<string> = [];
59
+ lines.push('');
55
60
  lines.push(
56
61
  st(
57
62
  'red',
58
- `\n❌ Found ${analysis.production_cycles.length} production/peer circular dependencies (blocks publishing):`,
63
+ `❌ Found ${analysis.production_cycles.length} production/peer circular dependencies (blocks publishing):`,
59
64
  ),
60
65
  );
61
66
  for (const cycle of analysis.production_cycles) {
@@ -122,26 +127,3 @@ export const log_dependency_analysis = (
122
127
  log_production_cycles(analysis, log, indent);
123
128
  log_dev_cycles(analysis, log, indent);
124
129
  };
125
-
126
- /**
127
- * Logs a simple bulleted list with a header.
128
- * Common pattern for warnings, info messages, and other lists.
129
- */
130
- export const log_list = (
131
- items: Array<string>,
132
- header: string,
133
- color: 'cyan' | 'yellow' | 'red' | 'dim',
134
- log: Logger,
135
- log_method: 'info' | 'warn' | 'error' = 'info',
136
- ): void => {
137
- if (items.length === 0) return;
138
-
139
- // Only add newline prefix if header is non-empty
140
- const formatted_header = header ? `\n${header}` : '';
141
- if (formatted_header) {
142
- log[log_method](st(color, formatted_header));
143
- }
144
- for (const item of items) {
145
- log[log_method](st(color, ` • ${item}`));
146
- }
147
- };