@fuzdev/fuz_gitops 0.71.0 → 0.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +13 -6
  2. package/dist/changeset_generator.d.ts.map +1 -1
  3. package/dist/changeset_generator.js +2 -16
  4. package/dist/changeset_reader.d.ts +1 -1
  5. package/dist/changeset_reader.d.ts.map +1 -1
  6. package/dist/dependency_graph.js +1 -1
  7. package/dist/git_operations.d.ts +5 -0
  8. package/dist/git_operations.d.ts.map +1 -1
  9. package/dist/git_operations.js +5 -0
  10. package/dist/gitops_analyze.task.d.ts +1 -0
  11. package/dist/gitops_analyze.task.d.ts.map +1 -1
  12. package/dist/gitops_analyze.task.js +12 -17
  13. package/dist/gitops_plan.task.d.ts +1 -0
  14. package/dist/gitops_plan.task.d.ts.map +1 -1
  15. package/dist/gitops_plan.task.js +9 -2
  16. package/dist/gitops_publish.task.d.ts +3 -2
  17. package/dist/gitops_publish.task.d.ts.map +1 -1
  18. package/dist/gitops_publish.task.js +90 -26
  19. package/dist/gitops_run.task.d.ts +2 -1
  20. package/dist/gitops_run.task.d.ts.map +1 -1
  21. package/dist/gitops_run.task.js +40 -7
  22. package/dist/gitops_sync.task.d.ts +1 -0
  23. package/dist/gitops_sync.task.d.ts.map +1 -1
  24. package/dist/gitops_sync.task.js +16 -2
  25. package/dist/gitops_task_helpers.d.ts +16 -2
  26. package/dist/gitops_task_helpers.d.ts.map +1 -1
  27. package/dist/gitops_task_helpers.js +11 -3
  28. package/dist/gitops_validate.task.d.ts +1 -0
  29. package/dist/gitops_validate.task.d.ts.map +1 -1
  30. package/dist/gitops_validate.task.js +20 -20
  31. package/dist/graph_validation.d.ts +16 -1
  32. package/dist/graph_validation.d.ts.map +1 -1
  33. package/dist/graph_validation.js +15 -0
  34. package/dist/local_repo.d.ts +22 -10
  35. package/dist/local_repo.d.ts.map +1 -1
  36. package/dist/local_repo.js +92 -75
  37. package/dist/log_helpers.d.ts +0 -5
  38. package/dist/log_helpers.d.ts.map +1 -1
  39. package/dist/log_helpers.js +8 -19
  40. package/dist/multi_repo_publisher.d.ts +29 -2
  41. package/dist/multi_repo_publisher.d.ts.map +1 -1
  42. package/dist/multi_repo_publisher.js +307 -233
  43. package/dist/operations.d.ts +6 -26
  44. package/dist/operations.d.ts.map +1 -1
  45. package/dist/operations_defaults.d.ts.map +1 -1
  46. package/dist/operations_defaults.js +1 -19
  47. package/dist/output_helpers.d.ts.map +1 -1
  48. package/dist/output_helpers.js +7 -6
  49. package/dist/paths.d.ts +0 -4
  50. package/dist/paths.d.ts.map +1 -1
  51. package/dist/paths.js +0 -4
  52. package/dist/publish_gate.d.ts +44 -0
  53. package/dist/publish_gate.d.ts.map +1 -0
  54. package/dist/publish_gate.js +32 -0
  55. package/dist/publish_steps.d.ts +60 -0
  56. package/dist/publish_steps.d.ts.map +1 -0
  57. package/dist/publish_steps.js +113 -0
  58. package/dist/publishing_event.d.ts +123 -0
  59. package/dist/publishing_event.d.ts.map +1 -0
  60. package/dist/publishing_event.js +140 -0
  61. package/dist/publishing_event_handler.d.ts +42 -0
  62. package/dist/publishing_event_handler.d.ts.map +1 -0
  63. package/dist/publishing_event_handler.js +75 -0
  64. package/dist/publishing_plan.d.ts +1 -2
  65. package/dist/publishing_plan.d.ts.map +1 -1
  66. package/dist/publishing_plan.js +18 -1
  67. package/dist/publishing_plan_helpers.d.ts +1 -1
  68. package/dist/publishing_plan_helpers.d.ts.map +1 -1
  69. package/dist/publishing_plan_helpers.js +2 -15
  70. package/dist/publishing_plan_logging.d.ts.map +1 -1
  71. package/dist/publishing_plan_logging.js +15 -23
  72. package/dist/repo_ops.d.ts +1 -1
  73. package/dist/repo_ops.js +1 -1
  74. package/dist/version_utils.d.ts +17 -3
  75. package/dist/version_utils.d.ts.map +1 -1
  76. package/dist/version_utils.js +22 -0
  77. package/package.json +6 -7
  78. package/src/lib/changeset_generator.ts +10 -19
  79. package/src/lib/changeset_reader.ts +1 -2
  80. package/src/lib/dependency_graph.ts +1 -1
  81. package/src/lib/git_operations.ts +5 -0
  82. package/src/lib/gitops_analyze.task.ts +18 -23
  83. package/src/lib/gitops_plan.task.ts +10 -2
  84. package/src/lib/gitops_publish.task.ts +100 -25
  85. package/src/lib/gitops_run.task.ts +39 -7
  86. package/src/lib/gitops_sync.task.ts +18 -3
  87. package/src/lib/gitops_task_helpers.ts +20 -3
  88. package/src/lib/gitops_validate.task.ts +30 -22
  89. package/src/lib/graph_validation.ts +26 -0
  90. package/src/lib/local_repo.ts +114 -83
  91. package/src/lib/log_helpers.ts +8 -26
  92. package/src/lib/multi_repo_publisher.ts +387 -269
  93. package/src/lib/operations.ts +6 -22
  94. package/src/lib/operations_defaults.ts +1 -19
  95. package/src/lib/output_helpers.ts +7 -6
  96. package/src/lib/paths.ts +0 -5
  97. package/src/lib/publish_gate.ts +53 -0
  98. package/src/lib/publish_steps.ts +149 -0
  99. package/src/lib/publishing_event.ts +151 -0
  100. package/src/lib/publishing_event_handler.ts +98 -0
  101. package/src/lib/publishing_plan.ts +23 -3
  102. package/src/lib/publishing_plan_helpers.ts +5 -18
  103. package/src/lib/publishing_plan_logging.ts +19 -39
  104. package/src/lib/repo_ops.ts +1 -1
  105. package/src/lib/version_utils.ts +30 -9
  106. package/dist/npm_install_helpers.d.ts +0 -23
  107. package/dist/npm_install_helpers.d.ts.map +0 -1
  108. package/dist/npm_install_helpers.js +0 -60
  109. package/dist/semver.d.ts +0 -26
  110. package/dist/semver.d.ts.map +0 -1
  111. package/dist/semver.js +0 -137
  112. package/dist/serialization_types.d.ts +0 -59
  113. package/dist/serialization_types.d.ts.map +0 -1
  114. package/dist/serialization_types.js +0 -42
  115. package/src/lib/npm_install_helpers.ts +0 -85
  116. package/src/lib/semver.ts +0 -170
  117. package/src/lib/serialization_types.ts +0 -92
@@ -3,10 +3,13 @@ import {z} from 'zod';
3
3
  import {styleText as st} from 'node:util';
4
4
 
5
5
  import {get_gitops_ready} from './gitops_task_helpers.js';
6
- import {validate_dependency_graph} from './graph_validation.js';
7
- import {DependencyGraphBuilder} from './dependency_graph.js';
8
- import {generate_publishing_plan, log_publishing_plan} from './publishing_plan.js';
9
- import {publish_repos, type PublishingOptions} from './multi_repo_publisher.js';
6
+ import {analyze_repos, type DependencyAnalysis} from './graph_validation.js';
7
+ import {
8
+ generate_publishing_plan,
9
+ log_publishing_plan,
10
+ type PublishingPlan,
11
+ } from './publishing_plan.js';
12
+ import {execute_publishing_plan, type PublishingOptions} from './multi_repo_publisher.js';
10
13
  import {log_dependency_analysis} from './log_helpers.js';
11
14
  import {GITOPS_CONFIG_PATH_DEFAULT} from './gitops_constants.js';
12
15
 
@@ -21,6 +24,13 @@ export const Args = z.strictObject({
21
24
  .meta({description: 'path containing the repos, defaults to the parent of the config dir'})
22
25
  .optional(),
23
26
  verbose: z.boolean().meta({description: 'show additional details'}).default(false),
27
+ sync: z
28
+ .boolean()
29
+ .meta({
30
+ description:
31
+ 'sync repos (switch branch, pull, install) before validating instead of reading the working tree as-is',
32
+ })
33
+ .default(false),
24
34
  });
25
35
  export type Args = z.infer<typeof Args>;
26
36
 
@@ -30,7 +40,7 @@ export const task: Task<Args> = {
30
40
  summary:
31
41
  'validate gitops configuration by running all read-only commands and checking for issues',
32
42
  run: async ({args, log}) => {
33
- const {config, dir, verbose} = args;
43
+ const {config, dir, verbose, sync} = args;
34
44
 
35
45
  log.info(st('cyan', 'Running Gitops Validation Suite'));
36
46
  log.info(st('dim', 'This runs all read-only commands and checks for consistency.'));
@@ -43,30 +53,22 @@ export const task: Task<Args> = {
43
53
  duration: number;
44
54
  warning_details?: Array<string>;
45
55
  info_details?: Array<string>;
46
- analysis?: ReturnType<DependencyGraphBuilder['analyze']>;
56
+ analysis?: DependencyAnalysis;
47
57
  }> = [];
48
58
 
49
59
  const start_time = Date.now();
50
60
 
51
- // Load repos once (shared by all commands)
61
+ // Load repos once (shared by all commands); read the working tree as-is unless `--sync`
52
62
  log.info(st('dim', 'Loading repositories...'));
53
- const {local_repos} = await get_gitops_ready({config, dir, download: false, log});
63
+ const {local_repos} = await get_gitops_ready({config, dir, download: false, sync, log});
54
64
  log.info(st('dim', ` Found ${local_repos.length} local repos`));
55
65
 
56
66
  // 1. Run gitops_analyze
57
67
  log.info(st('yellow', 'Running gitops_analyze...'));
58
68
  const analyze_start = Date.now();
59
69
  try {
60
- // Build dependency graph and validate (but don't throw on cycles for analyze)
61
- const {graph} = validate_dependency_graph(local_repos, {
62
- throw_on_prod_cycles: false, // Analyze should report, not throw
63
- log_cycles: false, // We'll collect our own statistics
64
- log_order: false,
65
- });
66
-
67
- // Perform additional analysis
68
- const builder = new DependencyGraphBuilder();
69
- const analysis = builder.analyze(graph);
70
+ // Build dependency graph and analyze cycles/wildcards (tolerating cycles)
71
+ const {analysis} = analyze_repos(local_repos);
70
72
 
71
73
  const analyze_duration = Date.now() - analyze_start;
72
74
 
@@ -113,11 +115,13 @@ export const task: Task<Args> = {
113
115
  log.error(st('red', ` ✗ gitops_analyze failed: ${error}`));
114
116
  }
115
117
 
116
- // 2. Run gitops_plan
118
+ // 2. Run gitops_plan (generated once here and reused by the dry run below)
119
+ let publishing_plan: PublishingPlan | undefined;
117
120
  log.info(st('yellow', 'Running gitops_plan...'));
118
121
  const plan_start = Date.now();
119
122
  try {
120
- const plan = await generate_publishing_plan(local_repos, {log: undefined, verbose});
123
+ const plan = await generate_publishing_plan(local_repos, {verbose});
124
+ publishing_plan = plan;
121
125
  const plan_duration = Date.now() - plan_start;
122
126
 
123
127
  const warnings = plan.warnings.length;
@@ -159,11 +163,15 @@ export const task: Task<Args> = {
159
163
  try {
160
164
  const options: PublishingOptions = {
161
165
  wetrun: false,
162
- update_deps: true,
163
166
  log: undefined, // Silent for validation
164
167
  };
165
168
 
166
- const result = await publish_repos(local_repos, options);
169
+ // Reuse the plan from step 2 (regenerate only if that step failed to produce one).
170
+ const result = await execute_publishing_plan(
171
+ local_repos,
172
+ publishing_plan ?? (await generate_publishing_plan(local_repos, {verbose})),
173
+ options,
174
+ );
167
175
  const dry_duration = Date.now() - dry_start;
168
176
 
169
177
  // Dry run doesn't have warnings/errors in the same format
@@ -109,3 +109,29 @@ export const validate_dependency_graph = (
109
109
  sort_error,
110
110
  };
111
111
  };
112
+
113
+ /** Cycles, wildcard deps, and missing peers produced by `DependencyGraphBuilder.analyze`. */
114
+ export type DependencyAnalysis = ReturnType<DependencyGraphBuilder['analyze']>;
115
+
116
+ export interface RepoAnalysis {
117
+ graph: DependencyGraph;
118
+ analysis: DependencyAnalysis;
119
+ /** Topological publishing order, or `null` when prod/peer cycles prevent ordering. */
120
+ publishing_order: Array<string> | null;
121
+ }
122
+
123
+ /**
124
+ * Builds the dependency graph and runs cycle/wildcard analysis, tolerating cycles
125
+ * (reports rather than throws). The shared core of `gitops_analyze` and
126
+ * `gitops_validate`, which format the result themselves — so this stays silent
127
+ * (no `log`) to avoid emitting a redundant "Analyzing dependencies" line.
128
+ */
129
+ export const analyze_repos = (repos: Array<LocalRepo>): RepoAnalysis => {
130
+ const {graph, publishing_order: order} = validate_dependency_graph(repos, {
131
+ throw_on_prod_cycles: false, // report, don't throw
132
+ log_cycles: false, // callers format their own cycle output
133
+ log_order: false,
134
+ });
135
+ const analysis = new DependencyGraphBuilder().analyze(graph);
136
+ return {graph, analysis, publishing_order: order.length > 0 ? order : null};
137
+ };
@@ -55,128 +55,153 @@ export interface LocalRepoMissing {
55
55
  }
56
56
 
57
57
  /**
58
- * Loads repo data with automatic syncing and dependency management.
58
+ * Loads repo data, optionally syncing the working tree first.
59
59
  *
60
- * Workflow:
60
+ * When `sync` is `false` (the default for read-only diagnostics like
61
+ * `gitops_analyze`/`gitops_plan`), the repo is loaded exactly as it sits on
62
+ * disk — no branch switch, pull, install, or clean-workspace check. This makes
63
+ * those commands safe to run on an active workspace with uncommitted changes or
64
+ * feature branches checked out.
65
+ *
66
+ * When `sync` is `true` (used by `gitops_sync`), the working tree is brought in
67
+ * line with the configured branch first:
61
68
  * 1. Records current commit hash (for detecting changes)
62
- * 2. Switches to target branch if needed (requires clean workspace)
69
+ * 2. Switches to target branch if needed (requires clean workspace unless `allow_dirty`)
63
70
  * 3. Pulls latest changes from remote (skipped for local-only repos)
64
- * 4. Validates workspace is clean after pull
71
+ * 4. Validates workspace is clean after pull (skipped if `allow_dirty`)
65
72
  * 5. Auto-installs dependencies if `package.json` changed
73
+ *
74
+ * Either way it then:
66
75
  * 6. Loads `library_json` via `library_load_from_repo` (svelte-docinfo analysis)
67
76
  * 7. Creates `Library` and extracts dependency maps
68
77
  *
69
- * This ensures repos are always in sync with their configured branch
70
- * before being used by gitops commands.
71
- *
72
- * @throws {TaskError} if workspace dirty, branch switch fails, install fails, or analysis fails
78
+ * @param sync - sync the working tree to the configured branch before loading (default `true`)
79
+ * @param allow_dirty - when syncing, tolerate uncommitted changes instead of throwing (default `false`)
80
+ * @throws {TaskError} if syncing fails (dirty workspace, branch switch, install) or analysis fails
73
81
  */
74
82
  export const local_repo_load = async ({
75
83
  local_repo_path,
76
84
  log: _log,
77
85
  git_ops = default_git_operations,
78
86
  npm_ops = default_npm_operations,
87
+ sync = true,
88
+ allow_dirty = false,
79
89
  }: {
80
90
  local_repo_path: LocalRepoPath;
81
91
  log?: Logger;
82
92
  git_ops?: GitOperations;
83
93
  npm_ops?: NpmOperations;
94
+ sync?: boolean;
95
+ allow_dirty?: boolean;
84
96
  }): Promise<LocalRepo> => {
85
97
  const {repo_config, repo_dir, repo_name, repo_git_ssh_url} = local_repo_path;
86
98
 
87
- // Record commit hash before any changes
88
- const commit_before_result = await git_ops.current_commit_hash({cwd: repo_dir});
89
- if (!commit_before_result.ok) {
90
- throw new TaskError(
91
- `Failed to get commit hash in ${repo_dir}: ${commit_before_result.message}`,
92
- );
93
- }
94
- const commit_before = commit_before_result.value;
95
-
96
- // Switch to target branch if needed
97
- const branch_result = await git_ops.current_branch_name({cwd: repo_dir});
98
- if (!branch_result.ok) {
99
- throw new TaskError(`Failed to get current branch in ${repo_dir}: ${branch_result.message}`);
100
- }
101
-
102
- const switched_branches = branch_result.value !== repo_config.branch;
103
- if (switched_branches) {
104
- const clean_result = await git_ops.check_clean_workspace({cwd: repo_dir});
105
- if (!clean_result.ok) {
106
- throw new TaskError(`Failed to check workspace in ${repo_dir}: ${clean_result.message}`);
107
- }
108
-
109
- if (!clean_result.value) {
99
+ if (sync) {
100
+ // Record commit hash before any changes
101
+ const commit_before_result = await git_ops.current_commit_hash({cwd: repo_dir});
102
+ if (!commit_before_result.ok) {
110
103
  throw new TaskError(
111
- `Repo ${repo_dir} is not on branch "${repo_config.branch}" and the workspace is unclean, blocking switch`,
104
+ `Failed to get commit hash in ${repo_dir}: ${commit_before_result.message}`,
112
105
  );
113
106
  }
107
+ const commit_before = commit_before_result.value;
114
108
 
115
- const checkout_result = await git_ops.checkout({branch: repo_config.branch, cwd: repo_dir});
116
- if (!checkout_result.ok) {
117
- throw new TaskError(
118
- `Failed to checkout branch "${repo_config.branch}" in ${repo_dir}: ${checkout_result.message}`,
119
- );
109
+ // Switch to target branch if needed
110
+ const branch_result = await git_ops.current_branch_name({cwd: repo_dir});
111
+ if (!branch_result.ok) {
112
+ throw new TaskError(`Failed to get current branch in ${repo_dir}: ${branch_result.message}`);
120
113
  }
121
- }
122
114
 
123
- // Only pull if remote exists (skip for local-only repos, test fixtures)
124
- const origin_result = await git_ops.has_remote({remote: 'origin', cwd: repo_dir});
125
- if (!origin_result.ok) {
126
- throw new TaskError(`Failed to check for remote in ${repo_dir}: ${origin_result.message}`);
127
- }
115
+ const switched_branches = branch_result.value !== repo_config.branch;
116
+ if (switched_branches) {
117
+ // Guard the switch on a clean workspace unless the caller opts into `allow_dirty`,
118
+ // in which case we let `git checkout` itself fail loudly if it can't proceed.
119
+ if (!allow_dirty) {
120
+ const clean_result = await git_ops.check_clean_workspace({cwd: repo_dir});
121
+ if (!clean_result.ok) {
122
+ throw new TaskError(`Failed to check workspace in ${repo_dir}: ${clean_result.message}`);
123
+ }
124
+
125
+ if (!clean_result.value) {
126
+ throw new TaskError(
127
+ `Repo ${repo_dir} is not on branch "${repo_config.branch}" and the workspace is unclean, blocking switch`,
128
+ );
129
+ }
130
+ }
128
131
 
129
- if (origin_result.value) {
130
- const pull_result = await git_ops.pull({cwd: repo_dir});
131
- if (!pull_result.ok) {
132
- throw new TaskError(`Failed to pull in ${repo_dir}: ${pull_result.message}`);
132
+ const checkout_result = await git_ops.checkout({branch: repo_config.branch, cwd: repo_dir});
133
+ if (!checkout_result.ok) {
134
+ throw new TaskError(
135
+ `Failed to checkout branch "${repo_config.branch}" in ${repo_dir}: ${checkout_result.message}`,
136
+ );
137
+ }
133
138
  }
134
- }
135
139
 
136
- // Check clean workspace after pull to ensure we're in a good state
137
- const clean_after_result = await git_ops.check_clean_workspace({cwd: repo_dir});
138
- if (!clean_after_result.ok) {
139
- throw new TaskError(`Failed to check workspace in ${repo_dir}: ${clean_after_result.message}`);
140
- }
141
-
142
- if (!clean_after_result.value) {
143
- throw new TaskError(
144
- `Workspace ${repo_dir} is unclean after pulling branch "${repo_config.branch}"`,
145
- );
146
- }
140
+ // Only pull if remote exists (skip for local-only repos, test fixtures)
141
+ const origin_result = await git_ops.has_remote({remote: 'origin', cwd: repo_dir});
142
+ if (!origin_result.ok) {
143
+ throw new TaskError(`Failed to check for remote in ${repo_dir}: ${origin_result.message}`);
144
+ }
147
145
 
148
- // Record commit hash after pull
149
- const commit_after_result = await git_ops.current_commit_hash({cwd: repo_dir});
150
- if (!commit_after_result.ok) {
151
- throw new TaskError(`Failed to get commit hash in ${repo_dir}: ${commit_after_result.message}`);
152
- }
153
- const commit_after = commit_after_result.value;
146
+ if (origin_result.value) {
147
+ const pull_result = await git_ops.pull({cwd: repo_dir});
148
+ if (!pull_result.ok) {
149
+ throw new TaskError(`Failed to pull in ${repo_dir}: ${pull_result.message}`);
150
+ }
151
+ }
154
152
 
155
- // Track if we got new commits
156
- const got_new_commits = commit_before !== commit_after;
153
+ // Check clean workspace after pull to ensure we're in a good state
154
+ // (skipped when `allow_dirty`, since uncommitted changes are expected then)
155
+ if (!allow_dirty) {
156
+ const clean_after_result = await git_ops.check_clean_workspace({cwd: repo_dir});
157
+ if (!clean_after_result.ok) {
158
+ throw new TaskError(
159
+ `Failed to check workspace in ${repo_dir}: ${clean_after_result.message}`,
160
+ );
161
+ }
157
162
 
158
- // Only install if package.json changed
159
- if (got_new_commits) {
160
- const changed_result = await git_ops.has_file_changed({
161
- from_commit: commit_before,
162
- to_commit: commit_after,
163
- file_path: 'package.json',
164
- cwd: repo_dir,
165
- });
163
+ if (!clean_after_result.value) {
164
+ throw new TaskError(
165
+ `Workspace ${repo_dir} is unclean after pulling branch "${repo_config.branch}"`,
166
+ );
167
+ }
168
+ }
166
169
 
167
- if (!changed_result.ok) {
170
+ // Record commit hash after pull
171
+ const commit_after_result = await git_ops.current_commit_hash({cwd: repo_dir});
172
+ if (!commit_after_result.ok) {
168
173
  throw new TaskError(
169
- `Failed to check if package.json changed in ${repo_dir}: ${changed_result.message}`,
174
+ `Failed to get commit hash in ${repo_dir}: ${commit_after_result.message}`,
170
175
  );
171
176
  }
177
+ const commit_after = commit_after_result.value;
178
+
179
+ // Track if we got new commits
180
+ const got_new_commits = commit_before !== commit_after;
181
+
182
+ // Only install if package.json changed
183
+ if (got_new_commits) {
184
+ const changed_result = await git_ops.has_file_changed({
185
+ from_commit: commit_before,
186
+ to_commit: commit_after,
187
+ file_path: 'package.json',
188
+ cwd: repo_dir,
189
+ });
172
190
 
173
- if (changed_result.value) {
174
- const install_result = await npm_ops.install({cwd: repo_dir});
175
- if (!install_result.ok) {
191
+ if (!changed_result.ok) {
176
192
  throw new TaskError(
177
- `Failed to install dependencies in ${repo_dir}: ${install_result.message}${install_result.stderr ? `\n${install_result.stderr}` : ''}`,
193
+ `Failed to check if package.json changed in ${repo_dir}: ${changed_result.message}`,
178
194
  );
179
195
  }
196
+
197
+ if (changed_result.value) {
198
+ const install_result = await npm_ops.install({cwd: repo_dir});
199
+ if (!install_result.ok) {
200
+ throw new TaskError(
201
+ `Failed to install dependencies in ${repo_dir}: ${install_result.message}${install_result.stderr ? `\n${install_result.stderr}` : ''}`,
202
+ );
203
+ }
204
+ }
180
205
  }
181
206
  }
182
207
 
@@ -275,6 +300,8 @@ export const local_repos_load = async ({
275
300
  npm_ops = default_npm_operations,
276
301
  parallel = true,
277
302
  concurrency = GITOPS_CONCURRENCY_DEFAULT,
303
+ sync = true,
304
+ allow_dirty = false,
278
305
  }: {
279
306
  local_repo_paths: Array<LocalRepoPath>;
280
307
  log?: Logger;
@@ -282,12 +309,16 @@ export const local_repos_load = async ({
282
309
  npm_ops?: NpmOperations;
283
310
  parallel?: boolean;
284
311
  concurrency?: number;
312
+ sync?: boolean;
313
+ allow_dirty?: boolean;
285
314
  }): Promise<Array<LocalRepo>> => {
286
315
  if (!parallel) {
287
316
  // Sequential loading (original behavior)
288
317
  const loaded: Array<LocalRepo> = [];
289
318
  for (const local_repo_path of local_repo_paths) {
290
- loaded.push(await local_repo_load({local_repo_path, log, git_ops, npm_ops}));
319
+ loaded.push(
320
+ await local_repo_load({local_repo_path, log, git_ops, npm_ops, sync, allow_dirty}),
321
+ );
291
322
  }
292
323
  return loaded;
293
324
  }
@@ -297,7 +328,7 @@ export const local_repos_load = async ({
297
328
  local_repo_paths,
298
329
  concurrency,
299
330
  async (local_repo_path) => {
300
- return local_repo_load({local_repo_path, log, git_ops, npm_ops});
331
+ return local_repo_load({local_repo_path, log, git_ops, npm_ops, sync, allow_dirty});
301
332
  },
302
333
  );
303
334
 
@@ -13,7 +13,10 @@ export const format_wildcard_dependencies = (
13
13
  if (analysis.wildcard_deps.length === 0) return [];
14
14
 
15
15
  const lines: Array<string> = [];
16
- lines.push(st('yellow', `\n⚠️ Found ${analysis.wildcard_deps.length} wildcard dependencies:`));
16
+ // Blank separator as its own line so the logger prefixes it correctly; embedding
17
+ // a leading `\n` in the header would leave the header line unprefixed.
18
+ lines.push('');
19
+ lines.push(st('yellow', `⚠️ Found ${analysis.wildcard_deps.length} wildcard dependencies:`));
17
20
  for (const {pkg, dep, version} of analysis.wildcard_deps) {
18
21
  lines.push(` ${pkg} → ${dep} ${st('red', version)}`);
19
22
  }
@@ -30,10 +33,11 @@ export const format_dev_cycles = (
30
33
  if (analysis.dev_cycles.length === 0) return [];
31
34
 
32
35
  const lines: Array<string> = [];
36
+ lines.push('');
33
37
  lines.push(
34
38
  st(
35
39
  'dim',
36
- `\nℹ️ Found ${analysis.dev_cycles.length} dev circular dependencies (normal, non-blocking):`,
40
+ `ℹ️ Found ${analysis.dev_cycles.length} dev circular dependencies (normal, non-blocking):`,
37
41
  ),
38
42
  );
39
43
  for (const cycle of analysis.dev_cycles) {
@@ -52,10 +56,11 @@ export const format_production_cycles = (
52
56
  if (analysis.production_cycles.length === 0) return [];
53
57
 
54
58
  const lines: Array<string> = [];
59
+ lines.push('');
55
60
  lines.push(
56
61
  st(
57
62
  'red',
58
- `\n❌ Found ${analysis.production_cycles.length} production/peer circular dependencies (blocks publishing):`,
63
+ `❌ Found ${analysis.production_cycles.length} production/peer circular dependencies (blocks publishing):`,
59
64
  ),
60
65
  );
61
66
  for (const cycle of analysis.production_cycles) {
@@ -122,26 +127,3 @@ export const log_dependency_analysis = (
122
127
  log_production_cycles(analysis, log, indent);
123
128
  log_dev_cycles(analysis, log, indent);
124
129
  };
125
-
126
- /**
127
- * Logs a simple bulleted list with a header.
128
- * Common pattern for warnings, info messages, and other lists.
129
- */
130
- export const log_list = (
131
- items: Array<string>,
132
- header: string,
133
- color: 'cyan' | 'yellow' | 'red' | 'dim',
134
- log: Logger,
135
- log_method: 'info' | 'warn' | 'error' = 'info',
136
- ): void => {
137
- if (items.length === 0) return;
138
-
139
- // Only add newline prefix if header is non-empty
140
- const formatted_header = header ? `\n${header}` : '';
141
- if (formatted_header) {
142
- log[log_method](st(color, formatted_header));
143
- }
144
- for (const item of items) {
145
- log[log_method](st(color, ` • ${item}`));
146
- }
147
- };