@fuzdev/fuz_gitops 0.57.0 → 0.59.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 (93) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +15 -0
  3. package/dist/ModulesDetail.svelte +5 -4
  4. package/dist/ModulesDetail.svelte.d.ts +3 -3
  5. package/dist/ModulesDetail.svelte.d.ts.map +1 -1
  6. package/dist/ModulesNav.svelte +4 -4
  7. package/dist/ModulesNav.svelte.d.ts +3 -3
  8. package/dist/ModulesNav.svelte.d.ts.map +1 -1
  9. package/dist/ModulesPage.svelte +5 -4
  10. package/dist/ModulesPage.svelte.d.ts +3 -3
  11. package/dist/ModulesPage.svelte.d.ts.map +1 -1
  12. package/dist/PageHeader.svelte +8 -4
  13. package/dist/PageHeader.svelte.d.ts +3 -3
  14. package/dist/PageHeader.svelte.d.ts.map +1 -1
  15. package/dist/PullRequestsDetail.svelte +5 -4
  16. package/dist/PullRequestsDetail.svelte.d.ts +3 -3
  17. package/dist/PullRequestsDetail.svelte.d.ts.map +1 -1
  18. package/dist/PullRequestsPage.svelte +9 -10
  19. package/dist/PullRequestsPage.svelte.d.ts +5 -5
  20. package/dist/PullRequestsPage.svelte.d.ts.map +1 -1
  21. package/dist/ReposTable.svelte +5 -4
  22. package/dist/ReposTable.svelte.d.ts +3 -3
  23. package/dist/ReposTable.svelte.d.ts.map +1 -1
  24. package/dist/ReposTree.svelte +6 -4
  25. package/dist/ReposTree.svelte.d.ts +3 -3
  26. package/dist/ReposTree.svelte.d.ts.map +1 -1
  27. package/dist/ReposTreeNav.svelte +6 -4
  28. package/dist/ReposTreeNav.svelte.d.ts +3 -3
  29. package/dist/ReposTreeNav.svelte.d.ts.map +1 -1
  30. package/dist/TablePage.svelte +5 -4
  31. package/dist/TablePage.svelte.d.ts +3 -3
  32. package/dist/TablePage.svelte.d.ts.map +1 -1
  33. package/dist/TreeItemPage.svelte +6 -4
  34. package/dist/TreeItemPage.svelte.d.ts +3 -3
  35. package/dist/TreeItemPage.svelte.d.ts.map +1 -1
  36. package/dist/TreePage.svelte +5 -4
  37. package/dist/TreePage.svelte.d.ts +3 -3
  38. package/dist/TreePage.svelte.d.ts.map +1 -1
  39. package/dist/fetch_repo_data.d.ts +1 -1
  40. package/dist/fetch_repo_data.js +1 -1
  41. package/dist/fs_fetch_value_cache.d.ts +1 -1
  42. package/dist/fs_fetch_value_cache.js +1 -1
  43. package/dist/gitops_analyze.task.d.ts +1 -1
  44. package/dist/gitops_analyze.task.d.ts.map +1 -1
  45. package/dist/gitops_analyze.task.js +6 -5
  46. package/dist/gitops_constants.d.ts +27 -0
  47. package/dist/gitops_constants.d.ts.map +1 -0
  48. package/dist/gitops_constants.js +26 -0
  49. package/dist/gitops_plan.task.d.ts +2 -2
  50. package/dist/gitops_plan.task.d.ts.map +1 -1
  51. package/dist/gitops_plan.task.js +7 -6
  52. package/dist/gitops_publish.task.d.ts +1 -1
  53. package/dist/gitops_publish.task.d.ts.map +1 -1
  54. package/dist/gitops_publish.task.js +7 -6
  55. package/dist/gitops_run.task.d.ts +14 -0
  56. package/dist/gitops_run.task.d.ts.map +1 -0
  57. package/dist/gitops_run.task.js +173 -0
  58. package/dist/gitops_sync.task.d.ts +1 -1
  59. package/dist/gitops_sync.task.d.ts.map +1 -1
  60. package/dist/gitops_sync.task.js +6 -5
  61. package/dist/gitops_task_helpers.d.ts +7 -3
  62. package/dist/gitops_task_helpers.d.ts.map +1 -1
  63. package/dist/gitops_task_helpers.js +16 -7
  64. package/dist/gitops_validate.task.d.ts +1 -1
  65. package/dist/gitops_validate.task.d.ts.map +1 -1
  66. package/dist/gitops_validate.task.js +6 -5
  67. package/dist/local_repo.d.ts +3 -1
  68. package/dist/local_repo.d.ts.map +1 -1
  69. package/dist/local_repo.js +34 -3
  70. package/dist/multi_repo_publisher.d.ts.map +1 -1
  71. package/dist/multi_repo_publisher.js +6 -6
  72. package/dist/publishing_plan.js +4 -4
  73. package/dist/repo_ops.d.ts.map +1 -1
  74. package/dist/repo_ops.js +2 -1
  75. package/package.json +11 -11
  76. package/src/lib/fetch_repo_data.ts +1 -1
  77. package/src/lib/fs_fetch_value_cache.ts +1 -1
  78. package/src/lib/gitops_analyze.task.ts +6 -5
  79. package/src/lib/gitops_constants.ts +30 -0
  80. package/src/lib/gitops_plan.task.ts +7 -6
  81. package/src/lib/gitops_publish.task.ts +7 -6
  82. package/src/lib/gitops_run.task.ts +218 -0
  83. package/src/lib/gitops_sync.task.ts +6 -5
  84. package/src/lib/gitops_task_helpers.ts +20 -9
  85. package/src/lib/gitops_validate.task.ts +6 -5
  86. package/src/lib/local_repo.ts +45 -2
  87. package/src/lib/multi_repo_publisher.ts +11 -6
  88. package/src/lib/publishing_plan.ts +4 -4
  89. package/src/lib/repo_ops.ts +2 -1
  90. package/dist/constants.d.ts +0 -9
  91. package/dist/constants.d.ts.map +0 -1
  92. package/dist/constants.js +0 -8
  93. package/src/lib/constants.ts +0 -8
@@ -4,7 +4,9 @@ import { existsSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import { TaskError } from '@ryanatkn/gro';
6
6
  import { spawn } from '@fuzdev/fuz_util/process.js';
7
+ import { map_concurrent_settled } from '@fuzdev/fuz_util/async.js';
7
8
  import { default_git_operations, default_npm_operations } from './operations_defaults.js';
9
+ import { GITOPS_CONCURRENCY_DEFAULT } from './gitops_constants.js';
8
10
  /**
9
11
  * Loads repo data with automatic syncing and dependency management.
10
12
  *
@@ -162,10 +164,39 @@ export const local_repos_ensure = async ({ resolved_config, repos_dir, gitops_co
162
164
  }
163
165
  return local_repo_paths;
164
166
  };
165
- export const local_repos_load = async ({ local_repo_paths, log, git_ops = default_git_operations, npm_ops = default_npm_operations, }) => {
167
+ export const local_repos_load = async ({ local_repo_paths, log, git_ops = default_git_operations, npm_ops = default_npm_operations, parallel = true, concurrency = GITOPS_CONCURRENCY_DEFAULT, }) => {
168
+ if (!parallel) {
169
+ // Sequential loading (original behavior)
170
+ const loaded = [];
171
+ for (const local_repo_path of local_repo_paths) {
172
+ loaded.push(await local_repo_load({ local_repo_path, log, git_ops, npm_ops })); // eslint-disable-line no-await-in-loop
173
+ }
174
+ return loaded;
175
+ }
176
+ // Parallel loading with concurrency limit
177
+ const results = await map_concurrent_settled(local_repo_paths, async (local_repo_path) => {
178
+ return local_repo_load({ local_repo_path, log, git_ops, npm_ops });
179
+ }, concurrency);
180
+ // Check for failures and collect successes
166
181
  const loaded = [];
167
- for (const local_repo_path of local_repo_paths) {
168
- loaded.push(await local_repo_load({ local_repo_path, log, git_ops, npm_ops })); // eslint-disable-line no-await-in-loop
182
+ const errors = [];
183
+ for (let i = 0; i < results.length; i++) {
184
+ const result = results[i];
185
+ if (result.status === 'fulfilled') {
186
+ loaded.push(result.value);
187
+ }
188
+ else {
189
+ const repo_path = local_repo_paths[i];
190
+ errors.push({
191
+ repo_name: repo_path.repo_name,
192
+ error: String(result.reason),
193
+ });
194
+ }
195
+ }
196
+ // If any repos failed to load, throw with details
197
+ if (errors.length > 0) {
198
+ const error_details = errors.map((e) => ` ${e.repo_name}: ${e.error}`).join('\n');
199
+ throw new TaskError(`Failed to load ${errors.length} repos:\n${error_details}`);
169
200
  }
170
201
  return loaded;
171
202
  };
@@ -1 +1 @@
1
- {"version":3,"file":"multi_repo_publisher.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/multi_repo_publisher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAKpD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAsB,KAAK,eAAe,EAAC,MAAM,yBAAyB,CAAC;AAIlF,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,iBAAiB,CAAC;AAOtD,MAAM,WAAW,iBAAiB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;IACrB,gBAAgB,CAAC,EAAE,eAAe,CAAC;IACnC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,gBAAgB,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;IACvC,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,gBAAgB;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,SAAS,EAAE,KAAK,CAAC,gBAAgB,CAAC,CAAC;IACnC,MAAM,EAAE,KAAK,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAC,CAAC,CAAC;IAC5C,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,eAAO,MAAM,aAAa,GACzB,OAAO,KAAK,CAAC,SAAS,CAAC,EACvB,SAAS,iBAAiB,KACxB,OAAO,CAAC,gBAAgB,CAmU1B,CAAC"}
1
+ {"version":3,"file":"multi_repo_publisher.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/multi_repo_publisher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAKpD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAsB,KAAK,eAAe,EAAC,MAAM,yBAAyB,CAAC;AAIlF,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,iBAAiB,CAAC;AAUtD,MAAM,WAAW,iBAAiB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;IACrB,gBAAgB,CAAC,EAAE,eAAe,CAAC;IACnC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,gBAAgB,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;IACvC,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,gBAAgB;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,SAAS,EAAE,KAAK,CAAC,gBAAgB,CAAC,CAAC;IACnC,MAAM,EAAE,KAAK,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAC,CAAC,CAAC;IAC5C,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,eAAO,MAAM,aAAa,GACzB,OAAO,KAAK,CAAC,SAAS,CAAC,EACvB,SAAS,iBAAiB,KACxB,OAAO,CAAC,gBAAgB,CAqU1B,CAAC"}
@@ -6,7 +6,7 @@ import { validate_dependency_graph } from './graph_validation.js';
6
6
  import {} from './preflight_checks.js';
7
7
  import { needs_update, is_breaking_change, detect_bump_type } from './version_utils.js';
8
8
  import { default_gitops_operations } from './operations_defaults.js';
9
- import { MAX_ITERATIONS } from './constants.js';
9
+ import { GITOPS_MAX_ITERATIONS_DEFAULT, GITOPS_NPM_WAIT_TIMEOUT_DEFAULT, } from './gitops_constants.js';
10
10
  import { install_with_cache_healing } from './npm_install_helpers.js';
11
11
  export const publish_repos = async (repos, options) => {
12
12
  const start_time = Date.now();
@@ -47,9 +47,9 @@ export const publish_repos = async (repos, options) => {
47
47
  // This handles transitive dependency updates (auto-generated changesets)
48
48
  let iteration = 0;
49
49
  let converged = false;
50
- while (!converged && iteration < MAX_ITERATIONS) {
50
+ while (!converged && iteration < GITOPS_MAX_ITERATIONS_DEFAULT) {
51
51
  iteration++;
52
- log?.info(st('cyan', `\n🚀 Publishing iteration ${iteration}/${MAX_ITERATIONS}...\n`));
52
+ log?.info(st('cyan', `\n🚀 Publishing iteration ${iteration}/${GITOPS_MAX_ITERATIONS_DEFAULT}...\n`));
53
53
  // Track if any packages were published in this iteration
54
54
  let published_in_iteration = false;
55
55
  let published_count = 0;
@@ -108,7 +108,7 @@ export const publish_repos = async (repos, options) => {
108
108
  max_attempts: 30,
109
109
  initial_delay: 1000,
110
110
  max_delay: 60000,
111
- timeout: options.max_wait || 600000, // 10 minutes default
111
+ timeout: options.max_wait ?? GITOPS_NPM_WAIT_TIMEOUT_DEFAULT,
112
112
  },
113
113
  log,
114
114
  });
@@ -186,11 +186,11 @@ export const publish_repos = async (repos, options) => {
186
186
  converged = true;
187
187
  log?.info(st('green', `\n✓ Converged after ${iteration} iteration(s) - no new changesets\n`));
188
188
  }
189
- else if (iteration === MAX_ITERATIONS) {
189
+ else if (iteration === GITOPS_MAX_ITERATIONS_DEFAULT) {
190
190
  // Count packages that still have changesets (not yet published)
191
191
  const pending_count = order.length - published.size;
192
192
  const estimated_iterations = Math.ceil(pending_count / 2); // Rough estimate
193
- log?.warn(st('yellow', `\n⚠️ Reached maximum iterations (${MAX_ITERATIONS}) without full convergence\n` +
193
+ log?.warn(st('yellow', `\n⚠️ Reached maximum iterations (${GITOPS_MAX_ITERATIONS_DEFAULT}) without full convergence\n` +
194
194
  ` ${pending_count} package(s) may still have changesets to process\n` +
195
195
  ` Estimated ${estimated_iterations} more iteration(s) needed - run 'gro gitops_publish' again\n`));
196
196
  }
@@ -2,7 +2,7 @@ import { styleText as st } from 'node:util';
2
2
  import { validate_dependency_graph } from './graph_validation.js';
3
3
  import { is_breaking_change, compare_bump_types, calculate_next_version } from './version_utils.js';
4
4
  import { default_changeset_operations } from './operations_defaults.js';
5
- import { MAX_ITERATIONS } from './constants.js';
5
+ import { GITOPS_MAX_ITERATIONS_DEFAULT } from './gitops_constants.js';
6
6
  import { calculate_dependency_updates, get_required_bump_for_dependencies, } from './publishing_plan_helpers.js';
7
7
  // Re-export logging functions
8
8
  export { log_publishing_plan } from './publishing_plan_logging.js';
@@ -128,7 +128,7 @@ export const generate_publishing_plan = async (repos, options = {}) => {
128
128
  // Loop until no new version changes are discovered
129
129
  let iteration = 0;
130
130
  let changed = true;
131
- while (changed && iteration < MAX_ITERATIONS) {
131
+ while (changed && iteration < GITOPS_MAX_ITERATIONS_DEFAULT) {
132
132
  changed = false;
133
133
  iteration++;
134
134
  // Verbose iteration tracking
@@ -241,7 +241,7 @@ export const generate_publishing_plan = async (repos, options = {}) => {
241
241
  }
242
242
  }
243
243
  // Check if we hit iteration limit without convergence
244
- if (iteration === MAX_ITERATIONS && changed) {
244
+ if (iteration === GITOPS_MAX_ITERATIONS_DEFAULT && changed) {
245
245
  // Calculate how many packages still need processing
246
246
  const pending_packages = [];
247
247
  // Recalculate one more time to see what's pending
@@ -262,7 +262,7 @@ export const generate_publishing_plan = async (repos, options = {}) => {
262
262
  // Add warning with diagnostics
263
263
  const pending_count = pending_packages.length;
264
264
  const estimated_iterations = Math.ceil(pending_count / 2); // Rough estimate
265
- warnings.push(`Reached maximum iterations (${MAX_ITERATIONS}) without full convergence - ` +
265
+ warnings.push(`Reached maximum iterations (${GITOPS_MAX_ITERATIONS_DEFAULT}) without full convergence - ` +
266
266
  `${pending_count} package(s) may still need processing: ${pending_packages.join(', ')}. ` +
267
267
  `Estimated ${estimated_iterations} more iteration(s) needed.`);
268
268
  }
@@ -1 +1 @@
1
- {"version":3,"file":"repo_ops.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/repo_ops.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AASH,uDAAuD;AACvD,eAAO,MAAM,oBAAoB,wIAavB,CAAC;AAEX,4EAA4E;AAC5E,eAAO,MAAM,0BAA0B,yLAsB7B,CAAC;AAEX,MAAM,WAAW,WAAW;IAC3B,+DAA+D;IAC/D,YAAY,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7B,8DAA8D;IAC9D,kBAAkB,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACnC,iDAAiD;IACjD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,qDAAqD;IACrD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,sDAAsD;IACtD,WAAW,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,QAAQ;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GAAU,cAAc,MAAM,KAAG,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CA4BlF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,mBAAmB,GAAI,WAAW,MAAM,EAAE,UAAU,WAAW,KAAG,OA2B9E,CAAC;AAEF;;;;;;GAMG;AACH,wBAAuB,eAAe,CACrC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,WAAW,GACnB,cAAc,CAAC,MAAM,EAAE,IAAI,EAAE,SAAS,CAAC,CAwCzC;AAED;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAC9B,KAAK,MAAM,EACX,UAAU,WAAW,KACnB,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAMvB,CAAC"}
1
+ {"version":3,"file":"repo_ops.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/repo_ops.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAUH,uDAAuD;AACvD,eAAO,MAAM,oBAAoB,wIAavB,CAAC;AAEX,4EAA4E;AAC5E,eAAO,MAAM,0BAA0B,yLAsB7B,CAAC;AAEX,MAAM,WAAW,WAAW;IAC3B,+DAA+D;IAC/D,YAAY,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7B,8DAA8D;IAC9D,kBAAkB,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACnC,iDAAiD;IACjD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,qDAAqD;IACrD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,sDAAsD;IACtD,WAAW,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,QAAQ;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GAAU,cAAc,MAAM,KAAG,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CA4BlF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,mBAAmB,GAAI,WAAW,MAAM,EAAE,UAAU,WAAW,KAAG,OA2B9E,CAAC;AAEF;;;;;;GAMG;AACH,wBAAuB,eAAe,CACrC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,WAAW,GACnB,cAAc,CAAC,MAAM,EAAE,IAAI,EAAE,SAAS,CAAC,CAwCzC;AAED;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAC9B,KAAK,MAAM,EACX,UAAU,WAAW,KACnB,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAMvB,CAAC"}
package/dist/repo_ops.js CHANGED
@@ -13,6 +13,7 @@ import { readdir, stat } from 'node:fs/promises';
13
13
  import { join, resolve, dirname } from 'node:path';
14
14
  import { load_gitops_config } from './gitops_config.js';
15
15
  import { DEFAULT_REPOS_DIR } from './paths.js';
16
+ import { GITOPS_CONFIG_PATH_DEFAULT } from './gitops_constants.js';
16
17
  /** Default directories to exclude from file walking */
17
18
  export const DEFAULT_EXCLUDE_DIRS = [
18
19
  'node_modules',
@@ -60,7 +61,7 @@ export const DEFAULT_EXCLUDE_EXTENSIONS = [
60
61
  * @returns Array of repo info with name, path, and url
61
62
  */
62
63
  export const get_repo_paths = async (config_path) => {
63
- const resolved_config_path = resolve(config_path ?? 'gitops.config.ts');
64
+ const resolved_config_path = resolve(config_path ?? GITOPS_CONFIG_PATH_DEFAULT);
64
65
  const config = await load_gitops_config(resolved_config_path);
65
66
  if (!config) {
66
67
  throw new Error(`No gitops config found at ${resolved_config_path}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_gitops",
3
- "version": "0.57.0",
3
+ "version": "0.59.0",
4
4
  "description": "a tool for managing many repos",
5
5
  "glyph": "🪄",
6
6
  "logo": "logo.svg",
@@ -40,27 +40,27 @@
40
40
  },
41
41
  "devDependencies": {
42
42
  "@changesets/changelog-git": "^0.2.1",
43
- "@fuzdev/fuz_code": "^0.37.0",
44
- "@fuzdev/fuz_css": "^0.40.0",
45
- "@fuzdev/fuz_ui": "^0.169.0",
46
- "@fuzdev/fuz_util": "^0.42.0",
43
+ "@fuzdev/fuz_code": "^0.38.0",
44
+ "@fuzdev/fuz_css": "^0.42.1",
45
+ "@fuzdev/fuz_ui": "^0.172.0",
46
+ "@fuzdev/fuz_util": "^0.44.1",
47
47
  "@ryanatkn/eslint-config": "^0.9.0",
48
- "@ryanatkn/gro": "^0.181.0",
48
+ "@ryanatkn/gro": "^0.182.0",
49
49
  "@sveltejs/adapter-static": "^3.0.10",
50
- "@sveltejs/kit": "^2.49.0",
50
+ "@sveltejs/kit": "^2.49.1",
51
51
  "@sveltejs/package": "^2.5.7",
52
52
  "@sveltejs/vite-plugin-svelte": "^6.2.1",
53
53
  "@types/node": "^24.10.1",
54
54
  "eslint": "^9.39.1",
55
- "eslint-plugin-svelte": "^3.13.0",
55
+ "eslint-plugin-svelte": "^3.13.1",
56
56
  "prettier": "^3.6.2",
57
57
  "prettier-plugin-svelte": "^3.4.0",
58
- "svelte": "^5.45.2",
58
+ "svelte": "^5.45.6",
59
59
  "svelte-check": "^4.3.4",
60
60
  "tslib": "^2.8.1",
61
61
  "typescript": "^5.9.3",
62
- "typescript-eslint": "^8.48.0",
63
- "vitest": "^4.0.14"
62
+ "typescript-eslint": "^8.48.1",
63
+ "vitest": "^4.0.15"
64
64
  },
65
65
  "prettier": {
66
66
  "plugins": [
@@ -18,7 +18,7 @@ import type {LocalRepo} from './local_repo.js';
18
18
  * Repos with failed fetches will have `null` for check_runs or pull_requests.
19
19
  *
20
20
  * @param delay milliseconds between API requests (default: 33ms)
21
- * @param cache optional cache from belt's fetch.js for response memoization
21
+ * @param cache optional cache from fuz_util's fetch.js for response memoization
22
22
  * @returns array of Repo objects with GitHub metadata attached
23
23
  */
24
24
  export const fetch_repo_data = async (
@@ -17,7 +17,7 @@ export interface FetchCache {
17
17
  }
18
18
 
19
19
  /**
20
- * Creates file-system backed cache for belt's fetch.js API responses.
20
+ * Creates file-system backed cache for fuz_util's fetch.js API responses.
21
21
  *
22
22
  * Cache invalidation strategy: If cache file can't be read or parsed, entire
23
23
  * cache is cleared (delete file) and starts fresh. This handles format changes.
@@ -13,16 +13,17 @@ import {
13
13
  format_production_cycles,
14
14
  } from './log_helpers.js';
15
15
  import {format_and_output, type OutputFormatters} from './output_helpers.js';
16
+ import {GITOPS_CONFIG_PATH_DEFAULT} from './gitops_constants.js';
16
17
 
17
18
  /** @nodocs */
18
19
  export const Args = z.strictObject({
19
- path: z
20
+ config: z
20
21
  .string()
21
22
  .meta({description: 'path to the gitops config file, absolute or relative to the cwd'})
22
- .default('gitops.config.ts'),
23
+ .default(GITOPS_CONFIG_PATH_DEFAULT),
23
24
  dir: z
24
25
  .string()
25
- .meta({description: 'path containing the repos, defaults to the parent of the `path` dir'})
26
+ .meta({description: 'path containing the repos, defaults to the parent of the config dir'})
26
27
  .optional(),
27
28
  format: z
28
29
  .enum(['stdout', 'json', 'markdown'])
@@ -37,10 +38,10 @@ export const task: Task<Args> = {
37
38
  Args,
38
39
  summary: 'analyze dependency structure and relationships across repos',
39
40
  run: async ({args, log}) => {
40
- const {path, dir, format, outfile} = args;
41
+ const {config, dir, format, outfile} = args;
41
42
 
42
43
  // Get repos ready (without downloading)
43
- const {local_repos} = await get_gitops_ready({path, dir, download: false, log});
44
+ const {local_repos} = await get_gitops_ready({config, dir, download: false, log});
44
45
 
45
46
  // Build dependency graph and validate (but don't throw on cycles for analyze)
46
47
  const {graph, publishing_order: order} = validate_dependency_graph(local_repos, {
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Shared constants for gitops tasks and operations.
3
+ *
4
+ * Naming convention: GITOPS_{NAME}_DEFAULT for user-facing defaults.
5
+ */
6
+
7
+ /**
8
+ * Maximum number of iterations for fixed-point iteration during publishing.
9
+ * Used in both plan generation and actual publishing to resolve transitive dependency cascades.
10
+ *
11
+ * In practice, most repos converge in 2-3 iterations.
12
+ * Deep dependency chains may require more iterations.
13
+ */
14
+ export const GITOPS_MAX_ITERATIONS_DEFAULT = 10;
15
+
16
+ /**
17
+ * Default path to the gitops configuration file.
18
+ */
19
+ export const GITOPS_CONFIG_PATH_DEFAULT = 'gitops.config.ts';
20
+
21
+ /**
22
+ * Default number of repos to process concurrently during parallel operations.
23
+ */
24
+ export const GITOPS_CONCURRENCY_DEFAULT = 5;
25
+
26
+ /**
27
+ * Default timeout in milliseconds for waiting on NPM package propagation (10 minutes).
28
+ * NPM's CDN uses eventual consistency, so published packages may not be immediately available.
29
+ */
30
+ export const GITOPS_NPM_WAIT_TIMEOUT_DEFAULT = 600_000; // 10 minutes
@@ -10,16 +10,17 @@ import {
10
10
  type LogPlanOptions,
11
11
  } from './publishing_plan.js';
12
12
  import {format_and_output, type OutputFormatters} from './output_helpers.js';
13
+ import {GITOPS_CONFIG_PATH_DEFAULT} from './gitops_constants.js';
13
14
 
14
15
  /** @nodocs */
15
16
  export const Args = z.strictObject({
16
- path: z
17
+ config: z
17
18
  .string()
18
19
  .meta({description: 'path to the gitops config file, absolute or relative to the cwd'})
19
- .default('gitops.config.ts'),
20
+ .default(GITOPS_CONFIG_PATH_DEFAULT),
20
21
  dir: z
21
22
  .string()
22
- .meta({description: 'path containing the repos, defaults to the parent of the `path` dir'})
23
+ .meta({description: 'path containing the repos, defaults to the parent of the config dir'})
23
24
  .optional(),
24
25
  format: z
25
26
  .enum(['stdout', 'json', 'markdown'])
@@ -37,7 +38,7 @@ export type Args = z.infer<typeof Args>;
37
38
  * Usage:
38
39
  * gro gitops_plan
39
40
  * gro gitops_plan --dir ../repos
40
- * gro gitops_plan --path ./custom.config.ts
41
+ * gro gitops_plan --config ./custom.config.ts
41
42
  *
42
43
  * @nodocs
43
44
  */
@@ -45,13 +46,13 @@ export const task: Task<Args> = {
45
46
  summary: 'generate a publishing plan based on changesets',
46
47
  Args,
47
48
  run: async ({args, log}): Promise<void> => {
48
- const {dir, path, format, outfile, verbose} = args;
49
+ const {dir, config, format, outfile, verbose} = args;
49
50
 
50
51
  log.info(st('cyan', 'Generating multi-repo publishing plan...'));
51
52
 
52
53
  // Load local repos
53
54
  const {local_repos} = await get_gitops_ready({
54
- path,
55
+ config,
55
56
  dir,
56
57
  download: false, // Don't download if missing
57
58
  log,
@@ -11,16 +11,17 @@ import {
11
11
  } from './multi_repo_publisher.js';
12
12
  import {generate_publishing_plan, log_publishing_plan} from './publishing_plan.js';
13
13
  import {format_and_output, type OutputFormatters} from './output_helpers.js';
14
+ import {GITOPS_CONFIG_PATH_DEFAULT, GITOPS_NPM_WAIT_TIMEOUT_DEFAULT} from './gitops_constants.js';
14
15
 
15
16
  /** @nodocs */
16
17
  export const Args = z.strictObject({
17
- path: z
18
+ config: z
18
19
  .string()
19
20
  .meta({description: 'path to the gitops config file, absolute or relative to the cwd'})
20
- .default('gitops.config.ts'),
21
+ .default(GITOPS_CONFIG_PATH_DEFAULT),
21
22
  dir: z
22
23
  .string()
23
- .meta({description: 'path containing the repos, defaults to the parent of the `path` dir'})
24
+ .meta({description: 'path containing the repos, defaults to the parent of the config dir'})
24
25
  .optional(),
25
26
  peer_strategy: z
26
27
  .enum(['exact', 'caret', 'tilde'])
@@ -43,7 +44,7 @@ export const Args = z.strictObject({
43
44
  max_wait: z
44
45
  .number()
45
46
  .meta({description: 'max time to wait for npm propagation in ms'})
46
- .default(600000), // 10 minutes
47
+ .default(GITOPS_NPM_WAIT_TIMEOUT_DEFAULT),
47
48
  skip_install: z
48
49
  .boolean()
49
50
  .meta({description: 'skip npm install after dependency updates'})
@@ -59,7 +60,7 @@ export const task: Task<Args> = {
59
60
  Args,
60
61
  run: async ({args, log}): Promise<void> => {
61
62
  const {
62
- path,
63
+ config,
63
64
  dir,
64
65
  peer_strategy,
65
66
  dry_run,
@@ -74,7 +75,7 @@ export const task: Task<Args> = {
74
75
 
75
76
  // Load repos
76
77
  const {local_repos: repos} = await get_gitops_ready({
77
- path,
78
+ config,
78
79
  dir,
79
80
  download: false, // Don't download if missing
80
81
  log,
@@ -0,0 +1,218 @@
1
+ import {TaskError, type Task} from '@ryanatkn/gro';
2
+ import {z} from 'zod';
3
+ import {map_concurrent_settled} from '@fuzdev/fuz_util/async.js';
4
+ import {spawn_out} from '@fuzdev/fuz_util/process.js';
5
+ import {styleText as st} from 'node:util';
6
+ import {resolve} from 'node:path';
7
+
8
+ import {get_repo_paths} from './repo_ops.js';
9
+ import {GITOPS_CONCURRENCY_DEFAULT, GITOPS_CONFIG_PATH_DEFAULT} from './gitops_constants.js';
10
+
11
+ export const Args = z.strictObject({
12
+ command: z.string().meta({description: 'shell command to run in each repo'}),
13
+ config: z
14
+ .string()
15
+ .meta({description: 'path to the gitops config file'})
16
+ .default(GITOPS_CONFIG_PATH_DEFAULT),
17
+ concurrency: z
18
+ .number()
19
+ .int()
20
+ .min(1)
21
+ .meta({description: 'maximum number of repos to run in parallel'})
22
+ .default(GITOPS_CONCURRENCY_DEFAULT),
23
+ format: z.enum(['text', 'json']).meta({description: 'output format'}).default('text'),
24
+ });
25
+ export type Args = z.infer<typeof Args>;
26
+
27
+ interface RunResult {
28
+ repo_name: string;
29
+ repo_dir: string;
30
+ status: 'success' | 'failure';
31
+ exit_code: number;
32
+ stdout: string;
33
+ stderr: string;
34
+ duration_ms: number;
35
+ error?: string;
36
+ }
37
+
38
+ export const task: Task<Args> = {
39
+ Args,
40
+ summary: 'run a shell command across all repos in parallel',
41
+ run: async ({args, log}) => {
42
+ const {command, config, concurrency, format} = args;
43
+
44
+ // Get repo paths (lightweight, no library.ts loading needed)
45
+ const config_path = resolve(config);
46
+ const repos = await get_repo_paths(config_path);
47
+
48
+ if (repos.length === 0) {
49
+ throw new TaskError('No repos found in config');
50
+ }
51
+
52
+ log.info(
53
+ `Running ${st('cyan', command)} across ${repos.length} repos (concurrency: ${concurrency})`,
54
+ );
55
+
56
+ const start_time = performance.now();
57
+
58
+ // Run command in parallel across all repos
59
+ const results = await map_concurrent_settled(
60
+ repos,
61
+ async (repo) => {
62
+ const repo_start = performance.now();
63
+ const repo_name = repo.name;
64
+ const repo_dir = repo.path;
65
+
66
+ try {
67
+ // Parse command into cmd + args for spawn
68
+ // For now, we use shell mode to support pipes/redirects/etc
69
+ const spawned = await spawn_out('sh', ['-c', command], {
70
+ cwd: repo_dir,
71
+ });
72
+
73
+ const duration_ms = performance.now() - repo_start;
74
+ const success = spawned.result.ok;
75
+
76
+ const result: RunResult = {
77
+ repo_name,
78
+ repo_dir,
79
+ status: success ? 'success' : 'failure',
80
+ exit_code: spawned.result.code ?? 0,
81
+ stdout: spawned.stdout || '',
82
+ stderr: spawned.stderr || '',
83
+ duration_ms,
84
+ };
85
+
86
+ return result;
87
+ } catch (error) {
88
+ const duration_ms = performance.now() - repo_start;
89
+ return {
90
+ repo_name,
91
+ repo_dir,
92
+ status: 'failure' as const,
93
+ exit_code: -1,
94
+ stdout: '',
95
+ stderr: '',
96
+ duration_ms,
97
+ error: String(error),
98
+ };
99
+ }
100
+ },
101
+ concurrency,
102
+ );
103
+
104
+ const total_duration_ms = performance.now() - start_time;
105
+
106
+ // Process results
107
+ const successes: Array<RunResult> = [];
108
+ const failures: Array<RunResult> = [];
109
+
110
+ for (const result of results) {
111
+ if (result.status === 'fulfilled') {
112
+ const run_result = result.value;
113
+ if (run_result.status === 'success') {
114
+ successes.push(run_result);
115
+ } else {
116
+ failures.push(run_result);
117
+ }
118
+ } else {
119
+ // This shouldn't happen since we catch errors in the task fn
120
+ // but handle it anyway
121
+ failures.push({
122
+ repo_name: 'unknown',
123
+ repo_dir: 'unknown',
124
+ status: 'failure',
125
+ exit_code: -1,
126
+ stdout: '',
127
+ stderr: '',
128
+ duration_ms: 0,
129
+ error: String(result.reason),
130
+ });
131
+ }
132
+ }
133
+
134
+ // Output results based on format
135
+ if (format === 'json') {
136
+ const json_output = {
137
+ command,
138
+ concurrency,
139
+ repos: [...successes, ...failures],
140
+ summary: {
141
+ total: repos.length,
142
+ success: successes.length,
143
+ failure: failures.length,
144
+ duration_ms: Math.round(total_duration_ms),
145
+ },
146
+ };
147
+ // eslint-disable-next-line no-console
148
+ console.log(JSON.stringify(json_output, null, 2));
149
+ } else {
150
+ // Text format
151
+ log.info(''); // blank line
152
+
153
+ // Show successes
154
+ if (successes.length > 0) {
155
+ log.info(st('green', `✓ ${successes.length} succeeded:`));
156
+ for (const result of successes) {
157
+ const duration = `${Math.round(result.duration_ms)}ms`;
158
+ log.info(st('gray', ` ${result.repo_name} ${st('blue', `(${duration})`)}`));
159
+ }
160
+ }
161
+
162
+ // Show failures with details
163
+ if (failures.length > 0) {
164
+ log.info(''); // blank line
165
+ log.error(st('red', `✗ ${failures.length} failed:`));
166
+ for (const result of failures) {
167
+ const duration = `${Math.round(result.duration_ms)}ms`;
168
+ log.error(st('gray', ` ${result.repo_name} ${st('blue', `(${duration})`)}`));
169
+
170
+ if (result.error) {
171
+ log.error(st('gray', ` Error: ${result.error}`));
172
+ } else if (result.exit_code !== 0) {
173
+ log.error(st('gray', ` Exit code: ${result.exit_code}`));
174
+ }
175
+
176
+ if (result.stderr) {
177
+ // Show first few lines of stderr
178
+ const stderr_lines = result.stderr.trim().split('\n');
179
+ const preview_lines = stderr_lines.slice(0, 3);
180
+ for (const line of preview_lines) {
181
+ log.error(st('gray', ` ${line}`));
182
+ }
183
+ if (stderr_lines.length > 3) {
184
+ log.error(st('gray', ` ... (${stderr_lines.length - 3} more lines)`));
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ // Summary
191
+ log.info(''); // blank line
192
+ const total = repos.length;
193
+ const success_rate = ((successes.length / total) * 100).toFixed(0);
194
+ const duration = `${Math.round(total_duration_ms)}ms`;
195
+
196
+ if (failures.length === 0) {
197
+ log.info(
198
+ st(
199
+ 'green',
200
+ `✓ All ${total} repos succeeded in ${duration} (${success_rate}% success rate)`,
201
+ ),
202
+ );
203
+ } else {
204
+ log.info(
205
+ st(
206
+ 'yellow',
207
+ `⚠ ${successes.length}/${total} repos succeeded in ${duration} (${success_rate}% success rate)`,
208
+ ),
209
+ );
210
+ }
211
+ }
212
+
213
+ // Exit with error if any failures (so CI fails)
214
+ if (failures.length > 0) {
215
+ throw new TaskError(`${failures.length} repos failed`);
216
+ }
217
+ },
218
+ };
@@ -11,18 +11,19 @@ import {existsSync} from 'node:fs';
11
11
  import {fetch_repo_data} from './fetch_repo_data.js';
12
12
  import {create_fs_fetch_value_cache} from './fs_fetch_value_cache.js';
13
13
  import {get_gitops_ready} from './gitops_task_helpers.js';
14
+ import {GITOPS_CONFIG_PATH_DEFAULT} from './gitops_constants.js';
14
15
 
15
16
  // TODO add flag to ignore or invalidate cache -- no-cache? clean?
16
17
 
17
18
  /** @nodocs */
18
19
  export const Args = z.strictObject({
19
- path: z
20
+ config: z
20
21
  .string()
21
22
  .meta({description: 'path to the gitops config file, absolute or relative to the cwd'})
22
- .default('gitops.config.ts'),
23
+ .default(GITOPS_CONFIG_PATH_DEFAULT),
23
24
  dir: z
24
25
  .string()
25
- .meta({description: 'path containing the repos, defaults to the parent of the `path` dir'})
26
+ .meta({description: 'path containing the repos, defaults to the parent of the config dir'})
26
27
  .optional(),
27
28
  outdir: z
28
29
  .string()
@@ -45,9 +46,9 @@ export const task: Task<Args> = {
45
46
  Args,
46
47
  summary: 'syncs local repos and generates UI data from repo metadata',
47
48
  run: async ({args, log, svelte_config, invoke_task}) => {
48
- const {path, dir, outdir = svelte_config.routes_path, download, check} = args;
49
+ const {config, dir, outdir = svelte_config.routes_path, download, check} = args;
49
50
 
50
- const {local_repos} = await get_gitops_ready({path, dir, download, log});
51
+ const {local_repos} = await get_gitops_ready({config, dir, download, log});
51
52
 
52
53
  const outfile = resolve(outdir, 'repos.ts');
53
54