@fuzdev/fuz_gitops 0.68.0 → 0.70.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 (95) hide show
  1. package/README.md +3 -3
  2. package/dist/ModulesDetail.svelte +14 -14
  3. package/dist/ModulesNav.svelte +2 -2
  4. package/dist/PageFooter.svelte +1 -1
  5. package/dist/ReposTable.svelte +1 -1
  6. package/dist/ReposTree.svelte +7 -7
  7. package/dist/TablePage.svelte +1 -7
  8. package/dist/TreeItemPage.svelte +3 -3
  9. package/dist/TreePage.svelte +3 -3
  10. package/dist/changeset_generator.d.ts +4 -4
  11. package/dist/changeset_generator.js +5 -5
  12. package/dist/changeset_reader.d.ts +6 -4
  13. package/dist/changeset_reader.d.ts.map +1 -1
  14. package/dist/changeset_reader.js +7 -5
  15. package/dist/dependency_graph.d.ts +3 -3
  16. package/dist/dependency_graph.js +3 -3
  17. package/dist/dependency_updater.d.ts +4 -4
  18. package/dist/dependency_updater.js +5 -5
  19. package/dist/fetch_repo_data.d.ts +4 -4
  20. package/dist/fetch_repo_data.d.ts.map +1 -1
  21. package/dist/fetch_repo_data.js +4 -5
  22. package/dist/fs_fetch_value_cache.d.ts +4 -4
  23. package/dist/fs_fetch_value_cache.js +4 -4
  24. package/dist/git_operations.d.ts +5 -5
  25. package/dist/git_operations.d.ts.map +1 -1
  26. package/dist/git_operations.js +18 -18
  27. package/dist/github.d.ts +1 -1
  28. package/dist/github.js +0 -1
  29. package/dist/gitops_analyze.task.d.ts +1 -1
  30. package/dist/gitops_plan.task.d.ts +4 -4
  31. package/dist/gitops_plan.task.js +3 -3
  32. package/dist/gitops_publish.task.d.ts +1 -1
  33. package/dist/gitops_run.task.d.ts +1 -1
  34. package/dist/gitops_run.task.js +2 -2
  35. package/dist/gitops_task_helpers.d.ts +5 -5
  36. package/dist/gitops_task_helpers.js +5 -5
  37. package/dist/graph_validation.d.ts +5 -5
  38. package/dist/graph_validation.js +5 -5
  39. package/dist/local_repo.d.ts +6 -6
  40. package/dist/local_repo.d.ts.map +1 -1
  41. package/dist/local_repo.js +22 -26
  42. package/dist/multi_repo_publisher.d.ts.map +1 -1
  43. package/dist/multi_repo_publisher.js +4 -4
  44. package/dist/npm_install_helpers.d.ts +3 -3
  45. package/dist/npm_install_helpers.js +3 -3
  46. package/dist/npm_registry.d.ts +4 -4
  47. package/dist/npm_registry.js +5 -6
  48. package/dist/operations.d.ts +19 -17
  49. package/dist/operations.d.ts.map +1 -1
  50. package/dist/operations.js +1 -1
  51. package/dist/operations_defaults.d.ts.map +1 -1
  52. package/dist/operations_defaults.js +49 -14
  53. package/dist/output_helpers.d.ts +2 -2
  54. package/dist/output_helpers.js +2 -2
  55. package/dist/paths.d.ts +1 -1
  56. package/dist/paths.js +1 -1
  57. package/dist/preflight_checks.d.ts +2 -2
  58. package/dist/preflight_checks.js +7 -7
  59. package/dist/publishing_plan.js +4 -4
  60. package/dist/publishing_plan_helpers.d.ts +1 -1
  61. package/dist/publishing_plan_helpers.js +1 -1
  62. package/dist/repo.svelte.d.ts +4 -5
  63. package/dist/repo.svelte.d.ts.map +1 -1
  64. package/dist/repo.svelte.js +2 -2
  65. package/dist/repo_ops.d.ts +6 -6
  66. package/dist/repo_ops.js +7 -7
  67. package/dist/version_utils.d.ts +2 -2
  68. package/dist/version_utils.js +2 -2
  69. package/package.json +18 -16
  70. package/src/lib/changeset_generator.ts +5 -5
  71. package/src/lib/changeset_reader.ts +7 -5
  72. package/src/lib/dependency_graph.ts +3 -3
  73. package/src/lib/dependency_updater.ts +5 -5
  74. package/src/lib/fetch_repo_data.ts +4 -6
  75. package/src/lib/fs_fetch_value_cache.ts +4 -4
  76. package/src/lib/git_operations.ts +32 -18
  77. package/src/lib/github.ts +1 -2
  78. package/src/lib/gitops_plan.task.ts +3 -3
  79. package/src/lib/gitops_run.task.ts +2 -2
  80. package/src/lib/gitops_task_helpers.ts +5 -5
  81. package/src/lib/graph_validation.ts +5 -5
  82. package/src/lib/local_repo.ts +28 -30
  83. package/src/lib/multi_repo_publisher.ts +4 -6
  84. package/src/lib/npm_install_helpers.ts +3 -3
  85. package/src/lib/npm_registry.ts +6 -6
  86. package/src/lib/operations.ts +19 -17
  87. package/src/lib/operations_defaults.ts +47 -16
  88. package/src/lib/output_helpers.ts +2 -2
  89. package/src/lib/paths.ts +1 -1
  90. package/src/lib/preflight_checks.ts +7 -7
  91. package/src/lib/publishing_plan.ts +4 -4
  92. package/src/lib/publishing_plan_helpers.ts +1 -1
  93. package/src/lib/repo.svelte.ts +4 -5
  94. package/src/lib/repo_ops.ts +7 -7
  95. package/src/lib/version_utils.ts +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_gitops",
3
- "version": "0.68.0",
3
+ "version": "0.70.0",
4
4
  "description": "a tool for managing many repos",
5
5
  "glyph": "🪄",
6
6
  "logo": "logo.svg",
@@ -26,29 +26,30 @@
26
26
  },
27
27
  "type": "module",
28
28
  "engines": {
29
- "node": ">=22.15"
29
+ "node": ">=24.14"
30
30
  },
31
31
  "peerDependencies": {
32
- "@fuzdev/fuz_css": ">=0.56.0",
33
- "@fuzdev/fuz_ui": ">=0.190.0",
34
- "@fuzdev/fuz_util": ">=0.54.0",
35
- "@fuzdev/gro": ">=0.197.0",
32
+ "@fuzdev/fuz_css": ">=0.61.0",
33
+ "@fuzdev/fuz_ui": ">=0.196.0",
34
+ "@fuzdev/fuz_util": ">=0.63.0",
35
+ "@fuzdev/gro": ">=0.200.0",
36
36
  "@sveltejs/kit": "^2",
37
37
  "svelte": "^5",
38
38
  "zod": "^4.3.6"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@changesets/changelog-git": "^0.2.1",
42
+ "@fuzdev/blake3_wasm": "^0.1.1",
42
43
  "@fuzdev/fuz_code": "^0.45.1",
43
- "@fuzdev/fuz_css": "^0.56.0",
44
- "@fuzdev/fuz_ui": "^0.190.0",
45
- "@fuzdev/fuz_util": "^0.54.0",
46
- "@fuzdev/gro": "^0.197.0",
44
+ "@fuzdev/fuz_css": "^0.61.0",
45
+ "@fuzdev/fuz_ui": "^0.196.0",
46
+ "@fuzdev/fuz_util": "^0.63.0",
47
+ "@fuzdev/gro": "^0.200.0",
47
48
  "@jridgewell/trace-mapping": "^0.3.31",
48
- "@ryanatkn/eslint-config": "^0.10.1",
49
+ "@ryanatkn/eslint-config": "^0.12.1",
49
50
  "@sveltejs/acorn-typescript": "^1.0.9",
50
51
  "@sveltejs/adapter-static": "^3.0.10",
51
- "@sveltejs/kit": "^2.50.1",
52
+ "@sveltejs/kit": "^2.61.1",
52
53
  "@sveltejs/package": "^2.5.7",
53
54
  "@sveltejs/vite-plugin-svelte": "^6.2.4",
54
55
  "@types/estree": "^1.0.8",
@@ -59,10 +60,11 @@
59
60
  "esm-env": "^1.2.2",
60
61
  "magic-string": "^0.30.21",
61
62
  "prettier": "^3.7.4",
62
- "prettier-plugin-svelte": "^3.4.1",
63
- "svelte": "^5.49.1",
64
- "svelte-check": "^4.3.6",
65
- "svelte2tsx": "^0.7.47",
63
+ "prettier-plugin-svelte": "^3.5.1",
64
+ "svelte": "^5.56.0",
65
+ "svelte-check": "^4.4.6",
66
+ "svelte-docinfo": "^0.2.0",
67
+ "svelte2tsx": "^0.7.52",
66
68
  "tslib": "^2.8.1",
67
69
  "typescript": "^5.9.3",
68
70
  "typescript-eslint": "^8.48.1",
@@ -36,7 +36,7 @@ export const create_changeset_for_dependency_updates = async (
36
36
  const changesets_dir = join(repo.repo_dir, '.changeset');
37
37
 
38
38
  // Ensure .changeset directory exists
39
- if (!fs_ops.exists({path: changesets_dir})) {
39
+ if (!(await fs_ops.exists({path: changesets_dir}))) {
40
40
  const mkdir_result = await fs_ops.mkdir({path: changesets_dir, recursive: true});
41
41
  if (!mkdir_result.ok) {
42
42
  throw new Error(`Failed to create .changeset directory: ${mkdir_result.message}`);
@@ -95,10 +95,10 @@ const calculate_required_bump = (
95
95
  * and categorized list of breaking vs regular updates. Output format
96
96
  * matches changesets CLI for consistency.
97
97
  *
98
- * @param package_name package receiving the dependency updates
99
- * @param updates list of dependency changes with version info
100
- * @param bump_type required bump type (calculated from breaking changes)
101
- * @returns markdown content ready to write to .changeset/*.md file
98
+ * @param package_name - package receiving the dependency updates
99
+ * @param updates - list of dependency changes with version info
100
+ * @param bump_type - required bump type (calculated from breaking changes)
101
+ * @returns markdown content ready to write to `.changeset/*.md` file
102
102
  */
103
103
  export const generate_changeset_content = (
104
104
  package_name: string,
@@ -30,15 +30,17 @@ export interface ChangesetInfo {
30
30
  * Returns null if format is invalid or no packages found.
31
31
  *
32
32
  * Expected format:
33
+ * ```
33
34
  * ---
34
35
  * "package-name": patch
35
36
  * "@scope/package": minor
36
37
  * ---
37
38
  *
38
39
  * Summary of changes
40
+ * ```
39
41
  *
40
- * @param content changeset markdown with YAML frontmatter
41
- * @param filename optional filename for error reporting context
42
+ * @param content - changeset markdown with YAML frontmatter
43
+ * @param filename - optional filename for error reporting context
42
44
  * @returns parsed changeset info or null if invalid format
43
45
  */
44
46
  export const parse_changeset_content = (
@@ -116,7 +118,7 @@ export const read_changesets = async (
116
118
 
117
119
  for (const file of changeset_files) {
118
120
  const filepath = join(changesets_dir, file);
119
- const changeset = await parse_changeset_file(filepath, log); // eslint-disable-line no-await-in-loop
121
+ const changeset = await parse_changeset_file(filepath, log);
120
122
  if (changeset) {
121
123
  changesets.push(changeset);
122
124
  }
@@ -161,8 +163,8 @@ export const determine_bump_from_changesets = (
161
163
  * Checks if a repo has any changeset files (excluding README.md).
162
164
  *
163
165
  * Used by preflight checks and publishing workflow to determine which packages
164
- * need to be published. Returns false if .changeset directory doesn't exist
165
- * or contains only README.md.
166
+ * need to be published. Returns false if `.changeset` directory doesn't exist
167
+ * or contains only `README.md`.
166
168
  *
167
169
  * @returns true if repo has unpublished changesets
168
170
  */
@@ -127,8 +127,8 @@ export class DependencyGraph {
127
127
  * Delegates to `@fuzdev/fuz_util/sort.js` for the sorting algorithm.
128
128
  * Throws if cycles detected.
129
129
  *
130
- * @param exclude_dev if true, excludes dev dependencies to break cycles.
131
- * Publishing uses exclude_dev=true to handle circular dev deps.
130
+ * @param exclude_dev - if true, excludes dev dependencies to break cycles
131
+ * Publishing uses `exclude_dev`=true to handle circular dev deps.
132
132
  * @returns array of package names in dependency order (dependencies before dependents)
133
133
  * @throws {Error} if circular dependencies detected in included dependency types
134
134
  */
@@ -158,7 +158,7 @@ export class DependencyGraph {
158
158
  * Uses DFS traversal with recursion stack to identify back edges.
159
159
  * Deduplicates cycles using sorted cycle keys.
160
160
  *
161
- * @returns object with production_cycles (errors) and dev_cycles (info)
161
+ * @returns object with `production_cycles` (errors) and `dev_cycles` (info)
162
162
  */
163
163
  detect_cycles_by_type(): {
164
164
  production_cycles: Array<Array<string>>;
@@ -22,13 +22,13 @@ export interface UpdatePackageJsonOptions {
22
22
  }
23
23
 
24
24
  /**
25
- * Updates package.json dependencies and creates changeset if needed.
25
+ * Updates `package.json` dependencies and creates changeset if needed.
26
26
  *
27
27
  * Workflow:
28
28
  * 1. Updates all dependency types (dependencies, devDependencies, peerDependencies)
29
- * 2. Writes updated package.json with tabs formatting
30
- * 3. Creates auto-changeset if published_versions provided (for transitive updates)
31
- * 4. Commits both package.json and changeset with standard message
29
+ * 2. Writes updated `package.json` with tabs formatting
30
+ * 3. Creates auto-changeset if `published_versions` provided (for transitive updates)
31
+ * 4. Commits both `package.json` and changeset with standard message
32
32
  *
33
33
  * Uses version strategy to determine prefix (exact, caret, tilde) while preserving
34
34
  * existing prefixes when possible.
@@ -226,7 +226,7 @@ export const update_all_repos = async (
226
226
  if (updates.size === 0) continue;
227
227
 
228
228
  try {
229
- await update_package_json(repo, updates, {strategy, log, git_ops, fs_ops}); // eslint-disable-line no-await-in-loop
229
+ await update_package_json(repo, updates, {strategy, log, git_ops, fs_ops});
230
230
  updated_count++;
231
231
  log?.info(` Updated ${updates.size} dependencies in ${repo.library.name}`);
232
232
  } catch (error) {
@@ -6,8 +6,6 @@ import {fetch_github_check_runs, fetch_github_pull_requests} from './github.js';
6
6
  import type {RepoJson} from './repo.svelte.js';
7
7
  import type {LocalRepo} from './local_repo.js';
8
8
 
9
- /* eslint-disable no-await-in-loop */
10
-
11
9
  /**
12
10
  * Fetches GitHub metadata (CI status, PRs) for all repos.
13
11
  *
@@ -15,11 +13,11 @@ import type {LocalRepo} from './local_repo.js';
15
13
  * Uses `await_in_loop` intentionally to avoid parallel requests overwhelming the API.
16
14
  *
17
15
  * Error handling: Logs fetch failures but continues processing remaining repos.
18
- * Repos with failed fetches will have `null` for check_runs or pull_requests.
16
+ * Repos with failed fetches will have `null` for `check_runs` or `pull_requests`.
19
17
  *
20
- * @param delay milliseconds between API requests (default: 33ms)
21
- * @param cache optional cache from fuz_util's fetch.js for response memoization
22
- * @returns array of Repo objects with GitHub metadata attached
18
+ * @param delay - milliseconds between API requests (default: 33ms)
19
+ * @param cache - optional cache from `fuz_util`'s `fetch.js` for response memoization
20
+ * @returns array of `Repo` objects with GitHub metadata attached
23
21
  */
24
22
  export const fetch_repo_data = async (
25
23
  resolved_repos: Array<LocalRepo>,
@@ -17,7 +17,7 @@ export interface FetchCache {
17
17
  }
18
18
 
19
19
  /**
20
- * Creates file-system backed cache for fuz_util'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.
@@ -25,9 +25,9 @@ export interface FetchCache {
25
25
  * Uses `structuredClone` to track changes - only writes to disk if data modified.
26
26
  * Formatted with Prettier before writing for version control friendliness.
27
27
  *
28
- * @param name cache filename (without .json extension)
29
- * @param dir cache directory (defaults to `.gro/build/fetch/`)
30
- * @returns cache object with Map-based data and save() method
28
+ * @param name - cache filename (without .json extension)
29
+ * @param dir - cache directory (defaults to `.gro/build/fetch/`)
30
+ * @returns cache object with Map-based data and `save()` method
31
31
  */
32
32
  export const create_fs_fetch_value_cache = async (
33
33
  name: string,
@@ -1,4 +1,4 @@
1
- import {spawn, spawn_out} from '@fuzdev/fuz_util/process.js';
1
+ import {spawn_out, spawn_result_to_message} from '@fuzdev/fuz_util/process.js';
2
2
  import type {SpawnOptions} from 'node:child_process';
3
3
  import {
4
4
  git_check_clean_workspace as gro_git_check_clean_workspace,
@@ -18,9 +18,11 @@ export const git_add = async (
18
18
  options?: SpawnOptions,
19
19
  ): Promise<void> => {
20
20
  const file_list = Array.isArray(files) ? files : [files];
21
- const result = await spawn('git', ['add', ...file_list], options);
21
+ const {result, stderr} = await spawn_out('git', ['add', ...file_list], options);
22
22
  if (!result.ok) {
23
- throw Error(`git_add failed with code ${result.code}`);
23
+ throw Error(
24
+ `git_add failed with ${spawn_result_to_message(result)}${stderr ? ': ' + stderr.trim() : ''}`,
25
+ );
24
26
  }
25
27
  };
26
28
 
@@ -28,9 +30,11 @@ export const git_add = async (
28
30
  * Commits staged changes with a message and throws if anything goes wrong.
29
31
  */
30
32
  export const git_commit = async (message: string, options?: SpawnOptions): Promise<void> => {
31
- const result = await spawn('git', ['commit', '-m', message], options);
33
+ const {result, stderr} = await spawn_out('git', ['commit', '-m', message], options);
32
34
  if (!result.ok) {
33
- throw Error(`git_commit failed with code ${result.code}`);
35
+ throw Error(
36
+ `git_commit failed with ${spawn_result_to_message(result)}${stderr ? ': ' + stderr.trim() : ''}`,
37
+ );
34
38
  }
35
39
  };
36
40
 
@@ -56,9 +60,11 @@ export const git_tag = async (
56
60
  ): Promise<void> => {
57
61
  const args = message ? ['tag', '-a', tag_name, '-m', message] : ['tag', tag_name];
58
62
 
59
- const result = await spawn('git', args, options);
63
+ const {result, stderr} = await spawn_out('git', args, options);
60
64
  if (!result.ok) {
61
- throw Error(`git_tag failed for tag '${tag_name}' with code ${result.code}`);
65
+ throw Error(
66
+ `git_tag failed for tag '${tag_name}' with ${spawn_result_to_message(result)}${stderr ? ': ' + stderr.trim() : ''}`,
67
+ );
62
68
  }
63
69
  };
64
70
 
@@ -70,9 +76,11 @@ export const git_push_tag = async (
70
76
  origin: GitOrigin = 'origin' as GitOrigin,
71
77
  options?: SpawnOptions,
72
78
  ): Promise<void> => {
73
- const result = await spawn('git', ['push', origin, tag_name], options);
79
+ const {result, stderr} = await spawn_out('git', ['push', origin, tag_name], options);
74
80
  if (!result.ok) {
75
- throw Error(`git_push_tag failed for tag '${tag_name}' with code ${result.code}`);
81
+ throw Error(
82
+ `git_push_tag failed for tag '${tag_name}' with ${spawn_result_to_message(result)}${stderr ? ': ' + stderr.trim() : ''}`,
83
+ );
76
84
  }
77
85
  };
78
86
 
@@ -82,9 +90,11 @@ export const git_has_changes = async (options?: SpawnOptions): Promise<boolean>
82
90
  };
83
91
 
84
92
  /**
85
- * Returns list of changed files compared to HEAD.
93
+ * Lists uncommitted files in the working tree (`git diff --name-only HEAD`).
86
94
  */
87
- export const git_get_changed_files = async (options?: SpawnOptions): Promise<Array<string>> => {
95
+ export const git_list_uncommitted_files = async (
96
+ options?: SpawnOptions,
97
+ ): Promise<Array<string>> => {
88
98
  const {stdout} = await spawn_out('git', ['diff', '--name-only', 'HEAD'], options);
89
99
  if (!stdout) return [];
90
100
 
@@ -114,9 +124,11 @@ export const git_has_file_changed = async (
114
124
  export const git_stash = async (message?: string, options?: SpawnOptions): Promise<void> => {
115
125
  const args = message ? ['stash', 'push', '-m', message] : ['stash', 'push'];
116
126
 
117
- const result = await spawn('git', args, options);
127
+ const {result, stderr} = await spawn_out('git', args, options);
118
128
  if (!result.ok) {
119
- throw Error(`git_stash failed with code ${result.code}`);
129
+ throw Error(
130
+ `git_stash failed with ${spawn_result_to_message(result)}${stderr ? ': ' + stderr.trim() : ''}`,
131
+ );
120
132
  }
121
133
  };
122
134
 
@@ -124,9 +136,11 @@ export const git_stash = async (message?: string, options?: SpawnOptions): Promi
124
136
  * Applies stashed changes and throws if anything goes wrong.
125
137
  */
126
138
  export const git_stash_pop = async (options?: SpawnOptions): Promise<void> => {
127
- const result = await spawn('git', ['stash', 'pop'], options);
139
+ const {result, stderr} = await spawn_out('git', ['stash', 'pop'], options);
128
140
  if (!result.ok) {
129
- throw Error(`git_stash_pop failed with code ${result.code}`);
141
+ throw Error(
142
+ `git_stash_pop failed with ${spawn_result_to_message(result)}${stderr ? ': ' + stderr.trim() : ''}`,
143
+ );
130
144
  }
131
145
  };
132
146
 
@@ -160,7 +174,7 @@ export const git_switch_branch = async (
160
174
  };
161
175
 
162
176
  /**
163
- * Wrapper for gro's git_current_branch_name that throws if null.
177
+ * Wrapper for gro's `git_current_branch_name` that throws if null.
164
178
  */
165
179
  export const git_current_branch_name_required = async (options?: SpawnOptions): Promise<string> => {
166
180
  const branch = await gro_git_current_branch_name(options);
@@ -171,7 +185,7 @@ export const git_current_branch_name_required = async (options?: SpawnOptions):
171
185
  };
172
186
 
173
187
  /**
174
- * Wrapper for gro's git_current_commit_hash that throws if null.
188
+ * Wrapper for gro's `git_current_commit_hash` that throws if null.
175
189
  */
176
190
  export const git_current_commit_hash_required = async (
177
191
  branch?: string,
@@ -185,7 +199,7 @@ export const git_current_commit_hash_required = async (
185
199
  };
186
200
 
187
201
  /**
188
- * Wrapper for gro's git_check_clean_workspace that returns a boolean.
202
+ * Wrapper for gro's `git_check_clean_workspace` that returns a boolean.
189
203
  */
190
204
  export const git_check_clean_workspace_as_boolean = async (
191
205
  options?: SpawnOptions,
package/src/lib/github.ts CHANGED
@@ -3,7 +3,7 @@ import {z} from 'zod';
3
3
  import {fetch_value, type FetchValueCache} from '@fuzdev/fuz_util/fetch.js';
4
4
 
5
5
  /**
6
- * Minimal interface for GitHub API calls - works with both Pkg and Repo.
6
+ * Minimal interface for GitHub API calls - works with both `Pkg` and `Repo`.
7
7
  */
8
8
  export interface GithubRepoInfo {
9
9
  owner_name: string | null;
@@ -116,7 +116,6 @@ const reduce_check_runs = (check_runs: Array<GithubCheckRunsItem>): GithubCheckR
116
116
  let status!: GithubCheckRunsItem['status'];
117
117
  let conclusion!: GithubCheckRunsItem['conclusion'];
118
118
  for (const check_run of check_runs) {
119
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
120
119
  if (!status || status === 'completed') {
121
120
  status = check_run.status;
122
121
  }
@@ -36,9 +36,9 @@ export type Args = z.infer<typeof Args>;
36
36
  * Shows version changes, dependency updates, and breaking change cascades.
37
37
  *
38
38
  * Usage:
39
- * gro gitops_plan
40
- * gro gitops_plan --dir ../repos
41
- * gro gitops_plan --config ./custom.config.ts
39
+ * `gro gitops_plan`
40
+ * `gro gitops_plan --dir ../repos`
41
+ * `gro gitops_plan --config ./custom.config.ts`
42
42
  *
43
43
  * @nodocs
44
44
  */
@@ -41,7 +41,7 @@ export const task: Task<Args> = {
41
41
  run: async ({args, log}) => {
42
42
  const {command, config, concurrency, format} = args;
43
43
 
44
- // Get repo paths (lightweight, no library.ts loading needed)
44
+ // Get repo paths (lightweight, no library-metadata loading needed)
45
45
  const config_path = resolve(config);
46
46
  const repos = await get_repo_paths(config_path);
47
47
 
@@ -75,7 +75,7 @@ export const task: Task<Args> = {
75
75
  repo_name,
76
76
  repo_dir,
77
77
  status: success ? 'success' : 'failure',
78
- exit_code: spawned.result.code ?? 0,
78
+ exit_code: spawned.result.kind === 'exited' ? spawned.result.code : 0,
79
79
  stdout: spawned.stdout || '',
80
80
  stderr: spawned.stderr || '',
81
81
  duration_ms,
@@ -46,17 +46,17 @@ export interface GetGitopsReadyOptions {
46
46
  * 1. Loads and normalizes config from `gitops.config.ts`
47
47
  * 2. Resolves local repo paths (creates missing with `--download`)
48
48
  * 3. Switches branches and pulls latest changes (in parallel by default)
49
- * 4. Auto-installs deps if package.json changed during pull
49
+ * 4. Auto-installs deps if `package.json` changed during pull
50
50
  *
51
51
  * Priority for path resolution:
52
52
  * - `dir` argument (explicit override)
53
53
  * - Config `repos_dir` setting
54
54
  * - `DEFAULT_REPOS_DIR` constant
55
55
  *
56
- * @param options.git_ops for testing (defaults to real git operations)
57
- * @param options.npm_ops for testing (defaults to real npm operations)
58
- * @param options.parallel whether to load repos in parallel (default: true)
59
- * @param options.concurrency max concurrent repo loads (default: 5)
56
+ * @param options.git_ops - for testing (defaults to real git operations)
57
+ * @param options.npm_ops - for testing (defaults to real npm operations)
58
+ * @param options.parallel - whether to load repos in parallel (default: true)
59
+ * @param options.concurrency - max concurrent repo loads (default: 5)
60
60
  * @returns initialized config and fully loaded repos ready for operations
61
61
  * @throws {TaskError} if config loading or repo resolution fails
62
62
  */
@@ -30,13 +30,13 @@ export interface GraphValidationResult {
30
30
 
31
31
  /**
32
32
  * Shared utility for building dependency graph, detecting cycles, and computing publishing order.
33
- * This centralizes logic that was duplicated across multi_repo_publisher, publishing_plan, and gitops_analyze.
33
+ * This centralizes logic that was duplicated across `multi_repo_publisher`, `publishing_plan`, and `gitops_analyze`.
34
34
  *
35
- * @param options.throw_on_prod_cycles whether to throw an error if production cycles are detected (default: true)
36
- * @param options.log_cycles whether to log cycle information (default: true)
37
- * @param options.log_order whether to log publishing order (default: true)
35
+ * @param options.throw_on_prod_cycles - whether to throw an error if production cycles are detected (default: true)
36
+ * @param options.log_cycles - whether to log cycle information (default: true)
37
+ * @param options.log_order - whether to log publishing order (default: true)
38
38
  * @returns graph validation result with graph, publishing order, and detected cycles
39
- * @throws {TaskError} if production cycles detected and throw_on_prod_cycles is true
39
+ * @throws {TaskError} if production cycles detected and `throw_on_prod_cycles` is true
40
40
  */
41
41
  export const validate_dependency_graph = (
42
42
  repos: Array<LocalRepo>,
@@ -4,8 +4,9 @@ import {Library} from '@fuzdev/fuz_ui/library.svelte.js';
4
4
  import {existsSync} from 'node:fs';
5
5
  import {join} from 'node:path';
6
6
  import {TaskError} from '@fuzdev/gro';
7
+ import {library_load_from_repo} from '@fuzdev/gro/library_load.js';
7
8
  import type {Logger} from '@fuzdev/fuz_util/log.js';
8
- import {spawn} from '@fuzdev/fuz_util/process.js';
9
+ import {spawn_out} from '@fuzdev/fuz_util/process.js';
9
10
  import {map_concurrent_settled} from '@fuzdev/fuz_util/async.js';
10
11
  import type {GitOperations, NpmOperations} from './operations.js';
11
12
  import {default_git_operations, default_npm_operations} from './operations_defaults.js';
@@ -15,8 +16,8 @@ import type {ResolvedGitopsConfig} from './resolved_gitops_config.js';
15
16
  import {GITOPS_CONCURRENCY_DEFAULT} from './gitops_constants.js';
16
17
 
17
18
  /**
18
- * Fully loaded local repo with Library and extracted dependency data.
19
- * Does not extend LocalRepoPath - Library is source of truth for name/repo_url/etc.
19
+ * Fully loaded local repo with `Library` and extracted dependency data.
20
+ * Does not extend `LocalRepoPath` - `Library` is source of truth for name/repo_url/etc.
20
21
  */
21
22
  export interface LocalRepo {
22
23
  library: Library;
@@ -61,14 +62,14 @@ export interface LocalRepoMissing {
61
62
  * 2. Switches to target branch if needed (requires clean workspace)
62
63
  * 3. Pulls latest changes from remote (skipped for local-only repos)
63
64
  * 4. Validates workspace is clean after pull
64
- * 5. Auto-installs dependencies if package.json changed
65
- * 6. Imports library_json from src/routes/library.ts
66
- * 7. Creates Library and extracts dependency maps
65
+ * 5. Auto-installs dependencies if `package.json` changed
66
+ * 6. Loads `library_json` via `library_load_from_repo` (svelte-docinfo analysis)
67
+ * 7. Creates `Library` and extracts dependency maps
67
68
  *
68
69
  * This ensures repos are always in sync with their configured branch
69
70
  * before being used by gitops commands.
70
71
  *
71
- * @throws {TaskError} if workspace dirty, branch switch fails, install fails, or library.ts missing
72
+ * @throws {TaskError} if workspace dirty, branch switch fails, install fails, or analysis fails
72
73
  */
73
74
  export const local_repo_load = async ({
74
75
  local_repo_path,
@@ -179,27 +180,17 @@ export const local_repo_load = async ({
179
180
  }
180
181
  }
181
182
 
182
- // Validate and load library.ts
183
- const library_path = join(repo_dir, 'src/routes/library.ts');
184
- if (!existsSync(library_path)) {
185
- throw new TaskError(
186
- `Repo "${repo_name}" is missing src/routes/library.ts\n` +
187
- `This file is required for fuz_gitops. To fix:\n` +
188
- ` 1. Create src/routes/library.gen.ts with:\n` +
189
- ` import {library_gen} from '@fuzdev/fuz_ui/library_gen.js';\n` +
190
- ` export const gen = library_gen();\n` +
191
- ` 2. Run: cd ${repo_dir} && gro gen`,
183
+ // Load library metadata via svelte-docinfo analysis (cached under `.gro/library.json`).
184
+ let library_json: LibraryJson;
185
+ try {
186
+ library_json = await library_load_from_repo(repo_dir, {log: _log});
187
+ } catch (err) {
188
+ const message = err instanceof Error ? err.message : String(err);
189
+ _log?.warn(
190
+ `Failed to load library metadata for repo "${repo_name}" in ${repo_dir}: ${message}`,
192
191
  );
193
- }
194
-
195
- const library_module = await import(library_path);
196
- const {library_json} = library_module as {library_json: LibraryJson | undefined};
197
- if (!library_json) {
198
192
  throw new TaskError(
199
- `Repo "${repo_name}" has invalid src/routes/library.ts - missing library_json export\n` +
200
- `The file must export a library_json object. To fix:\n` +
201
- ` 1. Ensure src/routes/library.gen.ts uses library_gen from @fuzdev/fuz_ui\n` +
202
- ` 2. Run: cd ${repo_dir} && gro gen`,
193
+ `Failed to load library metadata for repo "${repo_name}" in ${repo_dir}: ${message}`,
203
194
  );
204
195
  }
205
196
  const library = new Library(library_json);
@@ -296,7 +287,7 @@ export const local_repos_load = async ({
296
287
  // Sequential loading (original behavior)
297
288
  const loaded: Array<LocalRepo> = [];
298
289
  for (const local_repo_path of local_repo_paths) {
299
- loaded.push(await local_repo_load({local_repo_path, log, git_ops, npm_ops})); // eslint-disable-line no-await-in-loop
290
+ loaded.push(await local_repo_load({local_repo_path, log, git_ops, npm_ops}));
300
291
  }
301
292
  return loaded;
302
293
  }
@@ -383,14 +374,21 @@ const download_repos = async ({
383
374
  const resolved: Array<LocalRepoPath> = [];
384
375
  for (const {repo_config, repo_git_ssh_url} of local_repos_missing) {
385
376
  log?.info(`cloning repo ${repo_git_ssh_url} to ${repos_dir}`);
386
- await spawn('git', ['clone', repo_git_ssh_url], {cwd: repos_dir}); // eslint-disable-line no-await-in-loop
377
+ const clone_result = await spawn_out('git', ['clone', repo_git_ssh_url], {cwd: repos_dir});
378
+ if (!clone_result.result.ok) {
379
+ throw new TaskError(
380
+ `Failed to clone repo ${repo_git_ssh_url} to ${repos_dir}${clone_result.stderr ? ': ' + clone_result.stderr.trim() : ''}`,
381
+ );
382
+ }
387
383
  const local_repo = local_repo_locate({repo_config, repos_dir});
388
384
  if (local_repo.type === 'local_repo_missing') {
389
- throw new TaskError(`Failed to clone repo ${repo_git_ssh_url} to ${repos_dir}`);
385
+ throw new TaskError(
386
+ `Failed to clone repo ${repo_git_ssh_url} to ${repos_dir}: directory not found after clone`,
387
+ );
390
388
  }
391
389
  // Always install dependencies after cloning
392
390
  log?.info(`installing dependencies for newly cloned repo ${local_repo.repo_dir}`);
393
- const install_result = await npm_ops.install({cwd: local_repo.repo_dir}); // eslint-disable-line no-await-in-loop
391
+ const install_result = await npm_ops.install({cwd: local_repo.repo_dir});
394
392
  if (!install_result.ok) {
395
393
  throw new TaskError(
396
394
  `Failed to install dependencies in ${local_repo.repo_dir}: ${install_result.message}${install_result.stderr ? `\n${install_result.stderr}` : ''}`,
@@ -16,8 +16,6 @@ import {
16
16
  } from './gitops_constants.js';
17
17
  import {install_with_cache_healing} from './npm_install_helpers.js';
18
18
 
19
- /* eslint-disable no-await-in-loop */
20
-
21
19
  export interface PublishingOptions {
22
20
  wetrun: boolean;
23
21
  update_deps: boolean;
@@ -337,7 +335,7 @@ export const publish_repos = async (
337
335
  const deploy_result = await ops.process.spawn({
338
336
  cmd: 'gro',
339
337
  args: ['deploy', '--no-build'],
340
- spawn_options: {cwd: repo.repo_dir},
338
+ cwd: repo.repo_dir,
341
339
  });
342
340
 
343
341
  if (deploy_result.ok) {
@@ -377,11 +375,11 @@ export const publish_repos = async (
377
375
  };
378
376
 
379
377
  /**
380
- * Publishes a single repo using gro publish.
378
+ * Publishes a single repo using `gro publish`.
381
379
  *
382
380
  * Dry run mode: Predicts version from changesets without side effects.
383
381
  * Real mode: Runs `gro publish --no-build` (builds already validated in preflight),
384
- * reads new version from package.json, and returns metadata.
382
+ * reads new version from `package.json`, and returns metadata.
385
383
  *
386
384
  * @throws {Error} if changeset prediction fails (dry run) or publish fails (real)
387
385
  */
@@ -426,7 +424,7 @@ const publish_single_repo = async (
426
424
  const publish_result = await ops.process.spawn({
427
425
  cmd: 'gro',
428
426
  args: ['publish', '--no-build'],
429
- spawn_options: {cwd: repo.repo_dir},
427
+ cwd: repo.repo_dir,
430
428
  });
431
429
 
432
430
  if (!publish_result.ok) {
@@ -34,9 +34,9 @@ const is_etarget_error = (message: string, stderr: string): boolean => {
34
34
  * npm's local cache may still have stale "404" metadata. This healing
35
35
  * strategy clears the cache to force fresh metadata fetch.
36
36
  *
37
- * @param repo - The repository to install dependencies for
38
- * @param ops - Gitops operations (for dependency injection)
39
- * @param log - Optional logger
37
+ * @param repo - the repository to install dependencies for
38
+ * @param ops - gitops operations (for dependency injection)
39
+ * @param log - optional logger
40
40
  * @throws Error if install fails (with details about cache healing attempts)
41
41
  */
42
42
  export const install_with_cache_healing = async (
@@ -48,10 +48,10 @@ export const check_package_available = async (
48
48
  * Critical for multi-repo publishing: ensures published packages are available
49
49
  * before updating dependent packages.
50
50
  *
51
- * @param options.max_attempts max poll attempts (default 30)
52
- * @param options.initial_delay starting delay in ms (default 1000)
53
- * @param options.max_delay max delay between attempts (default 60000)
54
- * @param options.timeout total timeout in ms (default 300000 = 5min)
51
+ * @param options.max_attempts - max poll attempts (default 30)
52
+ * @param options.initial_delay - starting delay in ms (default 1000)
53
+ * @param options.max_delay - max delay between attempts (default 60000)
54
+ * @param options.timeout - total timeout in ms (default 300000 = 5min)
55
55
  * @throws {Error} if timeout reached or max attempts exceeded
56
56
  */
57
57
  export const wait_for_package = async (
@@ -80,7 +80,7 @@ export const wait_for_package = async (
80
80
  }
81
81
 
82
82
  // Check if package is available
83
- // eslint-disable-next-line no-await-in-loop
83
+
84
84
  if (await check_package_available(pkg, version, {log})) {
85
85
  log?.info(st('green', ` ✓ ${pkg}@${version} is now available on NPM`));
86
86
  return;
@@ -94,7 +94,7 @@ export const wait_for_package = async (
94
94
  // Wait with exponential backoff + jitter
95
95
  const jitter = Math.random() * delay * 0.1; // 10% jitter
96
96
  const actual_delay = Math.min(delay + jitter, max_delay);
97
- await wait(actual_delay); // eslint-disable-line no-await-in-loop
97
+ await wait(actual_delay);
98
98
 
99
99
  // Exponential backoff
100
100
  delay = Math.min(delay * 1.5, max_delay);