@fuzdev/fuz_gitops 0.67.0 → 0.69.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 (89) hide show
  1. package/dist/ModulesDetail.svelte +14 -14
  2. package/dist/ModulesNav.svelte +2 -2
  3. package/dist/PageFooter.svelte +1 -1
  4. package/dist/ReposTable.svelte +1 -1
  5. package/dist/ReposTree.svelte +6 -6
  6. package/dist/ReposTreeNav.svelte +1 -1
  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/gitops_plan.task.d.ts +3 -3
  29. package/dist/gitops_plan.task.js +3 -3
  30. package/dist/gitops_run.task.js +1 -1
  31. package/dist/gitops_task_helpers.d.ts +5 -5
  32. package/dist/gitops_task_helpers.js +5 -5
  33. package/dist/graph_validation.d.ts +5 -5
  34. package/dist/graph_validation.js +5 -5
  35. package/dist/local_repo.d.ts +6 -6
  36. package/dist/local_repo.js +12 -9
  37. package/dist/multi_repo_publisher.d.ts.map +1 -1
  38. package/dist/multi_repo_publisher.js +4 -4
  39. package/dist/npm_install_helpers.d.ts +3 -3
  40. package/dist/npm_install_helpers.js +3 -3
  41. package/dist/npm_registry.d.ts +4 -4
  42. package/dist/npm_registry.js +5 -6
  43. package/dist/operations.d.ts +19 -17
  44. package/dist/operations.d.ts.map +1 -1
  45. package/dist/operations.js +1 -1
  46. package/dist/operations_defaults.d.ts.map +1 -1
  47. package/dist/operations_defaults.js +49 -14
  48. package/dist/output_helpers.d.ts +2 -2
  49. package/dist/output_helpers.js +2 -2
  50. package/dist/paths.d.ts +1 -1
  51. package/dist/paths.js +1 -1
  52. package/dist/preflight_checks.d.ts +2 -2
  53. package/dist/preflight_checks.js +7 -7
  54. package/dist/publishing_plan.js +4 -4
  55. package/dist/publishing_plan_helpers.d.ts +1 -1
  56. package/dist/publishing_plan_helpers.js +1 -1
  57. package/dist/repo.svelte.d.ts +3 -3
  58. package/dist/repo.svelte.js +2 -2
  59. package/dist/repo_ops.d.ts +6 -6
  60. package/dist/repo_ops.js +7 -7
  61. package/dist/version_utils.d.ts +2 -2
  62. package/dist/version_utils.js +2 -2
  63. package/package.json +18 -16
  64. package/src/lib/changeset_generator.ts +5 -5
  65. package/src/lib/changeset_reader.ts +7 -5
  66. package/src/lib/dependency_graph.ts +3 -3
  67. package/src/lib/dependency_updater.ts +5 -5
  68. package/src/lib/fetch_repo_data.ts +4 -6
  69. package/src/lib/fs_fetch_value_cache.ts +4 -4
  70. package/src/lib/git_operations.ts +32 -18
  71. package/src/lib/github.ts +1 -1
  72. package/src/lib/gitops_plan.task.ts +3 -3
  73. package/src/lib/gitops_run.task.ts +1 -1
  74. package/src/lib/gitops_task_helpers.ts +5 -5
  75. package/src/lib/graph_validation.ts +5 -5
  76. package/src/lib/local_repo.ts +18 -11
  77. package/src/lib/multi_repo_publisher.ts +4 -6
  78. package/src/lib/npm_install_helpers.ts +3 -3
  79. package/src/lib/npm_registry.ts +6 -6
  80. package/src/lib/operations.ts +19 -17
  81. package/src/lib/operations_defaults.ts +47 -16
  82. package/src/lib/output_helpers.ts +2 -2
  83. package/src/lib/paths.ts +1 -1
  84. package/src/lib/preflight_checks.ts +7 -7
  85. package/src/lib/publishing_plan.ts +4 -4
  86. package/src/lib/publishing_plan_helpers.ts +1 -1
  87. package/src/lib/repo.svelte.ts +3 -3
  88. package/src/lib/repo_ops.ts +7 -7
  89. package/src/lib/version_utils.ts +2 -2
@@ -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;
@@ -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
  */
@@ -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>,
@@ -5,7 +5,7 @@ import {existsSync} from 'node:fs';
5
5
  import {join} from 'node:path';
6
6
  import {TaskError} from '@fuzdev/gro';
7
7
  import type {Logger} from '@fuzdev/fuz_util/log.js';
8
- import {spawn} from '@fuzdev/fuz_util/process.js';
8
+ import {spawn_out} from '@fuzdev/fuz_util/process.js';
9
9
  import {map_concurrent_settled} from '@fuzdev/fuz_util/async.js';
10
10
  import type {GitOperations, NpmOperations} from './operations.js';
11
11
  import {default_git_operations, default_npm_operations} from './operations_defaults.js';
@@ -15,8 +15,8 @@ import type {ResolvedGitopsConfig} from './resolved_gitops_config.js';
15
15
  import {GITOPS_CONCURRENCY_DEFAULT} from './gitops_constants.js';
16
16
 
17
17
  /**
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.
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.
20
20
  */
21
21
  export interface LocalRepo {
22
22
  library: Library;
@@ -61,14 +61,14 @@ export interface LocalRepoMissing {
61
61
  * 2. Switches to target branch if needed (requires clean workspace)
62
62
  * 3. Pulls latest changes from remote (skipped for local-only repos)
63
63
  * 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
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
67
67
  *
68
68
  * This ensures repos are always in sync with their configured branch
69
69
  * before being used by gitops commands.
70
70
  *
71
- * @throws {TaskError} if workspace dirty, branch switch fails, install fails, or library.ts missing
71
+ * @throws {TaskError} if workspace dirty, branch switch fails, install fails, or `library.ts` missing
72
72
  */
73
73
  export const local_repo_load = async ({
74
74
  local_repo_path,
@@ -296,7 +296,7 @@ export const local_repos_load = async ({
296
296
  // Sequential loading (original behavior)
297
297
  const loaded: Array<LocalRepo> = [];
298
298
  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
299
+ loaded.push(await local_repo_load({local_repo_path, log, git_ops, npm_ops}));
300
300
  }
301
301
  return loaded;
302
302
  }
@@ -383,14 +383,21 @@ const download_repos = async ({
383
383
  const resolved: Array<LocalRepoPath> = [];
384
384
  for (const {repo_config, repo_git_ssh_url} of local_repos_missing) {
385
385
  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
386
+ const clone_result = await spawn_out('git', ['clone', repo_git_ssh_url], {cwd: repos_dir});
387
+ if (!clone_result.result.ok) {
388
+ throw new TaskError(
389
+ `Failed to clone repo ${repo_git_ssh_url} to ${repos_dir}${clone_result.stderr ? ': ' + clone_result.stderr.trim() : ''}`,
390
+ );
391
+ }
387
392
  const local_repo = local_repo_locate({repo_config, repos_dir});
388
393
  if (local_repo.type === 'local_repo_missing') {
389
- throw new TaskError(`Failed to clone repo ${repo_git_ssh_url} to ${repos_dir}`);
394
+ throw new TaskError(
395
+ `Failed to clone repo ${repo_git_ssh_url} to ${repos_dir}: directory not found after clone`,
396
+ );
390
397
  }
391
398
  // Always install dependencies after cloning
392
399
  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
400
+ const install_result = await npm_ops.install({cwd: local_repo.repo_dir});
394
401
  if (!install_result.ok) {
395
402
  throw new TaskError(
396
403
  `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);
@@ -29,14 +29,14 @@
29
29
  * ```
30
30
  *
31
31
  * See `operations_defaults.ts` for real implementations.
32
- * See test files (*.test.ts) for mock implementations.
32
+ * See test files (`*.test.ts`) for mock implementations.
33
33
  *
34
34
  * @module
35
35
  */
36
36
 
37
37
  import type {Result} from '@fuzdev/fuz_util/result.js';
38
+ import type {FsError} from '@fuzdev/fuz_util/fs.js';
38
39
  import type {Logger} from '@fuzdev/fuz_util/log.js';
39
- import type {SpawnOptions} from 'node:child_process';
40
40
  import type {LocalRepo} from './local_repo.js';
41
41
  import type {ChangesetInfo} from './changeset_reader.js';
42
42
  import type {BumpType} from './semver.js';
@@ -67,7 +67,7 @@ export interface ChangesetOperations {
67
67
  /**
68
68
  * Predicts the next version based on changesets.
69
69
  * Returns null if no changesets found (expected, not an error).
70
- * Returns error Result if changesets exist but can't be read/parsed.
70
+ * Returns error `Result` if changesets exist but can't be read/parsed.
71
71
  */
72
72
  predict_next_version: (options: {
73
73
  repo: LocalRepo;
@@ -161,9 +161,13 @@ export interface GitOperations {
161
161
  has_changes: (options?: {cwd?: string}) => Promise<Result<{value: boolean}, {message: string}>>;
162
162
 
163
163
  /**
164
- * Gets a list of changed files.
164
+ * Lists uncommitted files in the working tree (`git diff --name-only HEAD`).
165
+ *
166
+ * Renamed from `get_changed_files` in 2026-04 because "changed files" collided
167
+ * with mageguild's `get_changed_files` which diffs two refs. This one reports
168
+ * uncommitted working-tree changes relative to HEAD.
165
169
  */
166
- get_changed_files: (options?: {
170
+ list_uncommitted_files: (options?: {
167
171
  cwd?: string;
168
172
  }) => Promise<Result<{value: Array<string>}, {message: string}>>;
169
173
 
@@ -216,7 +220,7 @@ export interface ProcessOperations {
216
220
  spawn: (options: {
217
221
  cmd: string;
218
222
  args: Array<string>;
219
- spawn_options?: SpawnOptions;
223
+ cwd?: string;
220
224
  }) => Promise<Result<{stdout?: string; stderr?: string}, {message: string; stderr?: string}>>;
221
225
  }
222
226
 
@@ -225,7 +229,7 @@ export interface ProcessOperations {
225
229
  */
226
230
  export interface BuildOperations {
227
231
  /**
228
- * Builds a package using gro build.
232
+ * Builds a package using `gro build`.
229
233
  */
230
234
  build_package: (options: {
231
235
  repo: LocalRepo;
@@ -302,6 +306,10 @@ export interface PreflightOperations {
302
306
 
303
307
  /**
304
308
  * File system operations for reading and writing files.
309
+ *
310
+ * Errors are typed via `FsError` (`not_found | permission_denied |
311
+ * already_exists | io_error`) so callers can branch on `kind` instead of
312
+ * regex-matching `message`. See `@fuzdev/fuz_util/fs.js`.
305
313
  */
306
314
  export interface FsOperations {
307
315
  /**
@@ -310,28 +318,22 @@ export interface FsOperations {
310
318
  readFile: (options: {
311
319
  path: string;
312
320
  encoding: BufferEncoding;
313
- }) => Promise<Result<{value: string}, {message: string}>>;
321
+ }) => Promise<Result<{value: string}, FsError>>;
314
322
 
315
323
  /**
316
324
  * Writes a file to the file system.
317
325
  */
318
- writeFile: (options: {
319
- path: string;
320
- content: string;
321
- }) => Promise<Result<object, {message: string}>>;
326
+ writeFile: (options: {path: string; content: string}) => Promise<Result<object, FsError>>;
322
327
 
323
328
  /**
324
329
  * Creates a directory, optionally with recursive creation.
325
330
  */
326
- mkdir: (options: {
327
- path: string;
328
- recursive?: boolean;
329
- }) => Promise<Result<object, {message: string}>>;
331
+ mkdir: (options: {path: string; recursive?: boolean}) => Promise<Result<object, FsError>>;
330
332
 
331
333
  /**
332
334
  * Checks if a path exists on the file system.
333
335
  */
334
- exists: (options: {path: string}) => boolean;
336
+ exists: (options: {path: string}) => Promise<boolean>;
335
337
  }
336
338
 
337
339
  /**
@@ -7,10 +7,10 @@
7
7
  * @module
8
8
  */
9
9
 
10
- import {spawn, spawn_out} from '@fuzdev/fuz_util/process.js';
11
- import {readFile, writeFile, mkdir} from 'node:fs/promises';
12
- import {existsSync} from 'node:fs';
10
+ import {spawn_out} from '@fuzdev/fuz_util/process.js';
11
+ import {readFile, writeFile, mkdir, stat} from 'node:fs/promises';
13
12
  import {git_checkout, type GitBranch, type GitOrigin} from '@fuzdev/fuz_util/git.js';
13
+ import {fs_classify_error} from '@fuzdev/fuz_util/fs.js';
14
14
  import {EMPTY_OBJECT} from '@fuzdev/fuz_util/object.js';
15
15
 
16
16
  import {has_changesets, read_changesets, predict_next_version} from './changeset_reader.js';
@@ -23,7 +23,7 @@ import {
23
23
  git_tag,
24
24
  git_push_tag,
25
25
  git_has_changes,
26
- git_get_changed_files,
26
+ git_list_uncommitted_files,
27
27
  git_has_file_changed,
28
28
  git_stash,
29
29
  git_stash_pop,
@@ -118,9 +118,20 @@ export const default_git_operations: GitOperations = {
118
118
 
119
119
  pull: async (options) => {
120
120
  const {origin, branch, cwd} = options ?? EMPTY_OBJECT;
121
- return wrap_void(() =>
122
- spawn('git', ['pull', origin || 'origin', branch || ''], cwd ? {cwd} : undefined),
123
- );
121
+ try {
122
+ const spawned = await spawn_out(
123
+ 'git',
124
+ ['pull', origin || 'origin', branch || ''],
125
+ cwd ? {cwd} : undefined,
126
+ );
127
+ if (spawned.result.ok) {
128
+ return {ok: true};
129
+ } else {
130
+ return {ok: false, message: spawned.stderr || 'Pull failed'};
131
+ }
132
+ } catch (error) {
133
+ return {ok: false, message: String(error)};
134
+ }
124
135
  },
125
136
 
126
137
  switch_branch: async (options) => {
@@ -154,9 +165,9 @@ export const default_git_operations: GitOperations = {
154
165
  return wrap_with_value(() => git_has_changes(cwd ? {cwd} : undefined));
155
166
  },
156
167
 
157
- get_changed_files: async (options) => {
168
+ list_uncommitted_files: async (options) => {
158
169
  const {cwd} = options ?? EMPTY_OBJECT;
159
- return wrap_with_value(() => git_get_changed_files(cwd ? {cwd} : undefined));
170
+ return wrap_with_value(() => git_list_uncommitted_files(cwd ? {cwd} : undefined));
160
171
  },
161
172
 
162
173
  // Tagging
@@ -192,9 +203,9 @@ export const default_git_operations: GitOperations = {
192
203
 
193
204
  export const default_process_operations: ProcessOperations = {
194
205
  spawn: async (options) => {
195
- const {cmd, args, spawn_options} = options;
206
+ const {cmd, args, cwd} = options;
196
207
  try {
197
- const spawned = await spawn_out(cmd, args, spawn_options);
208
+ const spawned = await spawn_out(cmd, args, cwd ? {cwd} : undefined);
198
209
  if (spawned.result.ok) {
199
210
  return {
200
211
  ok: true,
@@ -294,21 +305,41 @@ export const default_preflight_operations: PreflightOperations = {
294
305
  export const default_fs_operations: FsOperations = {
295
306
  readFile: async (options) => {
296
307
  const {path, encoding} = options;
297
- return wrap_with_value(() => readFile(path, encoding));
308
+ try {
309
+ const value = await readFile(path, encoding);
310
+ return {ok: true, value};
311
+ } catch (error) {
312
+ return {ok: false, ...fs_classify_error(error)};
313
+ }
298
314
  },
299
315
 
300
316
  writeFile: async (options) => {
301
317
  const {path, content} = options;
302
- return wrap_void(() => writeFile(path, content));
318
+ try {
319
+ await writeFile(path, content);
320
+ return {ok: true};
321
+ } catch (error) {
322
+ return {ok: false, ...fs_classify_error(error)};
323
+ }
303
324
  },
304
325
 
305
326
  mkdir: async (options) => {
306
327
  const {path, recursive} = options;
307
- return wrap_void(() => mkdir(path, {recursive}));
328
+ try {
329
+ await mkdir(path, {recursive});
330
+ return {ok: true};
331
+ } catch (error) {
332
+ return {ok: false, ...fs_classify_error(error)};
333
+ }
308
334
  },
309
335
 
310
- exists: (options) => {
311
- return existsSync(options.path);
336
+ exists: async (options) => {
337
+ try {
338
+ await stat(options.path);
339
+ return true;
340
+ } catch {
341
+ return false;
342
+ }
312
343
  },
313
344
  };
314
345
 
@@ -22,11 +22,11 @@ export interface OutputFormatters<T> {
22
22
  * Formats data and outputs to file or stdout based on options.
23
23
  *
24
24
  * Supports three formats:
25
- * - stdout: Uses logger for colored/styled output (cannot use with --outfile)
25
+ * - stdout: Uses logger for colored/styled output (cannot use with `--outfile`)
26
26
  * - json: Stringified JSON
27
27
  * - markdown: Formatted markdown text
28
28
  *
29
- * @throws {Error} if stdout format used with outfile, or if logger missing for stdout
29
+ * @throws {Error} if stdout format used with `outfile`, or if logger missing for stdout
30
30
  */
31
31
  export const format_and_output = async <T>(
32
32
  data: T,
package/src/lib/paths.ts CHANGED
@@ -6,6 +6,6 @@ export const GITOPS_OUTPUT_DIR = '.gro/fuz_gitops';
6
6
  /**
7
7
  * Default repos directory relative to gitops config file.
8
8
  * Resolves to the parent of the directory with the config
9
- * (e.g., ~/dev/repo/gitops.config.ts resolves to ~/dev/).
9
+ * (e.g., `~/dev/repo/gitops.config.ts` resolves to `~/dev/`).
10
10
  */
11
11
  export const DEFAULT_REPOS_DIR = '..';