@fuzdev/fuz_gitops 0.78.1 → 0.79.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 (179) hide show
  1. package/README.md +78 -24
  2. package/dist/ModulesDetail.svelte +15 -22
  3. package/dist/ModulesDetail.svelte.d.ts.map +1 -1
  4. package/dist/ModulesNav.svelte +4 -1
  5. package/dist/ModulesPage.svelte +1 -3
  6. package/dist/ModulesPage.svelte.d.ts.map +1 -1
  7. package/dist/PageHeader.svelte +2 -2
  8. package/dist/PageHeader.svelte.d.ts +0 -3
  9. package/dist/PageHeader.svelte.d.ts.map +1 -1
  10. package/dist/PullRequestsDetail.svelte +10 -9
  11. package/dist/PullRequestsDetail.svelte.d.ts.map +1 -1
  12. package/dist/PullRequestsPage.svelte +1 -3
  13. package/dist/PullRequestsPage.svelte.d.ts.map +1 -1
  14. package/dist/ReposTable.svelte +27 -48
  15. package/dist/ReposTable.svelte.d.ts.map +1 -1
  16. package/dist/ReposTree.svelte +5 -19
  17. package/dist/ReposTree.svelte.d.ts.map +1 -1
  18. package/dist/ReposTreeNav.svelte +5 -7
  19. package/dist/ReposTreeNav.svelte.d.ts.map +1 -1
  20. package/dist/TablePage.svelte +1 -3
  21. package/dist/TablePage.svelte.d.ts.map +1 -1
  22. package/dist/TreeItemPage.svelte +1 -3
  23. package/dist/TreeItemPage.svelte.d.ts.map +1 -1
  24. package/dist/TreePage.svelte +1 -3
  25. package/dist/TreePage.svelte.d.ts.map +1 -1
  26. package/dist/ci_reconcile.d.ts +6 -10
  27. package/dist/ci_reconcile.d.ts.map +1 -1
  28. package/dist/ci_reconcile.js +6 -8
  29. package/dist/dependency_graph.d.ts +26 -49
  30. package/dist/dependency_graph.d.ts.map +1 -1
  31. package/dist/dependency_graph.js +37 -80
  32. package/dist/dependency_updater.d.ts +1 -1
  33. package/dist/dependency_updater.d.ts.map +1 -1
  34. package/dist/dependency_updater.js +6 -1
  35. package/dist/fetch_repo_data.d.ts +34 -8
  36. package/dist/fetch_repo_data.d.ts.map +1 -1
  37. package/dist/fetch_repo_data.js +44 -22
  38. package/dist/fs_fetch_value_cache.d.ts +1 -1
  39. package/dist/fs_fetch_value_cache.js +1 -1
  40. package/dist/git_operations.d.ts +6 -49
  41. package/dist/git_operations.d.ts.map +1 -1
  42. package/dist/git_operations.js +13 -126
  43. package/dist/github.d.ts +17 -2
  44. package/dist/github.d.ts.map +1 -1
  45. package/dist/github.js +15 -6
  46. package/dist/github_helpers.d.ts.map +1 -1
  47. package/dist/github_helpers.js +3 -11
  48. package/dist/gitops_analyze.task.d.ts +24 -2
  49. package/dist/gitops_analyze.task.d.ts.map +1 -1
  50. package/dist/gitops_analyze.task.js +43 -30
  51. package/dist/gitops_config.d.ts +34 -79
  52. package/dist/gitops_config.d.ts.map +1 -1
  53. package/dist/gitops_config.js +76 -53
  54. package/dist/gitops_constants.d.ts +6 -4
  55. package/dist/gitops_constants.d.ts.map +1 -1
  56. package/dist/gitops_constants.js +6 -4
  57. package/dist/gitops_plan.task.d.ts +29 -3
  58. package/dist/gitops_plan.task.d.ts.map +1 -1
  59. package/dist/gitops_plan.task.js +80 -58
  60. package/dist/gitops_publish.task.d.ts +62 -2
  61. package/dist/gitops_publish.task.d.ts.map +1 -1
  62. package/dist/gitops_publish.task.js +187 -102
  63. package/dist/gitops_run.task.d.ts +74 -0
  64. package/dist/gitops_run.task.d.ts.map +1 -1
  65. package/dist/gitops_run.task.js +174 -160
  66. package/dist/gitops_sync.task.d.ts +65 -3
  67. package/dist/gitops_sync.task.d.ts.map +1 -1
  68. package/dist/gitops_sync.task.js +154 -30
  69. package/dist/gitops_task_helpers.d.ts +86 -59
  70. package/dist/gitops_task_helpers.d.ts.map +1 -1
  71. package/dist/gitops_task_helpers.js +120 -79
  72. package/dist/gitops_validate.task.d.ts +1 -2
  73. package/dist/gitops_validate.task.d.ts.map +1 -1
  74. package/dist/gitops_validate.task.js +15 -22
  75. package/dist/graph_validation.d.ts +16 -27
  76. package/dist/graph_validation.d.ts.map +1 -1
  77. package/dist/graph_validation.js +23 -64
  78. package/dist/local_repo.d.ts +50 -62
  79. package/dist/local_repo.d.ts.map +1 -1
  80. package/dist/local_repo.js +80 -193
  81. package/dist/log_helpers.d.ts +8 -8
  82. package/dist/log_helpers.d.ts.map +1 -1
  83. package/dist/multi_repo_publisher.d.ts +10 -0
  84. package/dist/multi_repo_publisher.d.ts.map +1 -1
  85. package/dist/multi_repo_publisher.js +115 -51
  86. package/dist/npm_registry.d.ts +23 -19
  87. package/dist/npm_registry.d.ts.map +1 -1
  88. package/dist/npm_registry.js +20 -47
  89. package/dist/operations.d.ts +75 -180
  90. package/dist/operations.d.ts.map +1 -1
  91. package/dist/operations.js +15 -7
  92. package/dist/operations_defaults.d.ts +17 -2
  93. package/dist/operations_defaults.d.ts.map +1 -1
  94. package/dist/operations_defaults.js +104 -121
  95. package/dist/output_helpers.d.ts +36 -0
  96. package/dist/output_helpers.d.ts.map +1 -1
  97. package/dist/output_helpers.js +37 -3
  98. package/dist/preflight_checks.d.ts +20 -27
  99. package/dist/preflight_checks.d.ts.map +1 -1
  100. package/dist/preflight_checks.js +37 -126
  101. package/dist/publish_steps.d.ts +2 -4
  102. package/dist/publish_steps.d.ts.map +1 -1
  103. package/dist/publish_steps.js +2 -6
  104. package/dist/publishing_event.d.ts +2 -8
  105. package/dist/publishing_event.d.ts.map +1 -1
  106. package/dist/publishing_event.js +5 -4
  107. package/dist/publishing_event_handler.d.ts +10 -8
  108. package/dist/publishing_event_handler.d.ts.map +1 -1
  109. package/dist/publishing_event_handler.js +14 -12
  110. package/dist/publishing_plan.d.ts +16 -1
  111. package/dist/publishing_plan.d.ts.map +1 -1
  112. package/dist/publishing_plan.js +328 -250
  113. package/dist/publishing_plan_logging.d.ts +1 -1
  114. package/dist/publishing_plan_logging.d.ts.map +1 -1
  115. package/dist/publishing_plan_logging.js +21 -14
  116. package/dist/repo.svelte.d.ts +7 -0
  117. package/dist/repo.svelte.d.ts.map +1 -1
  118. package/dist/repo.svelte.js +3 -0
  119. package/dist/repo_readiness.d.ts +210 -0
  120. package/dist/repo_readiness.d.ts.map +1 -0
  121. package/dist/repo_readiness.js +516 -0
  122. package/dist/repos_status.d.ts +3340 -0
  123. package/dist/repos_status.d.ts.map +1 -0
  124. package/dist/repos_status.js +555 -0
  125. package/dist/repos_status_load.d.ts +65 -0
  126. package/dist/repos_status_load.d.ts.map +1 -0
  127. package/dist/repos_status_load.js +131 -0
  128. package/package.json +7 -27
  129. package/src/lib/ci_reconcile.ts +7 -11
  130. package/src/lib/dependency_graph.ts +45 -97
  131. package/src/lib/dependency_updater.ts +7 -1
  132. package/src/lib/fetch_repo_data.ts +66 -29
  133. package/src/lib/fs_fetch_value_cache.ts +1 -1
  134. package/src/lib/git_operations.ts +15 -187
  135. package/src/lib/github.ts +20 -8
  136. package/src/lib/github_helpers.ts +5 -12
  137. package/src/lib/gitops_analyze.task.ts +79 -41
  138. package/src/lib/gitops_config.ts +88 -137
  139. package/src/lib/gitops_constants.ts +6 -4
  140. package/src/lib/gitops_plan.task.ts +113 -61
  141. package/src/lib/gitops_publish.task.ts +255 -122
  142. package/src/lib/gitops_run.task.ts +250 -178
  143. package/src/lib/gitops_sync.task.ts +208 -33
  144. package/src/lib/gitops_task_helpers.ts +166 -110
  145. package/src/lib/gitops_validate.task.ts +17 -27
  146. package/src/lib/graph_validation.ts +25 -86
  147. package/src/lib/local_repo.ts +109 -316
  148. package/src/lib/log_helpers.ts +8 -18
  149. package/src/lib/multi_repo_publisher.ts +138 -53
  150. package/src/lib/npm_registry.ts +37 -62
  151. package/src/lib/operations.ts +78 -150
  152. package/src/lib/operations_defaults.ts +108 -154
  153. package/src/lib/output_helpers.ts +55 -3
  154. package/src/lib/preflight_checks.ts +47 -183
  155. package/src/lib/publish_steps.ts +15 -13
  156. package/src/lib/publishing_event.ts +5 -4
  157. package/src/lib/publishing_event_handler.ts +18 -16
  158. package/src/lib/publishing_plan.ts +451 -287
  159. package/src/lib/publishing_plan_logging.ts +32 -23
  160. package/src/lib/repo.svelte.ts +8 -0
  161. package/src/lib/repo_readiness.ts +647 -0
  162. package/src/lib/repos_status.ts +671 -0
  163. package/src/lib/repos_status_load.ts +173 -0
  164. package/dist/config_reconcile.d.ts +0 -46
  165. package/dist/config_reconcile.d.ts.map +0 -1
  166. package/dist/config_reconcile.js +0 -64
  167. package/dist/paths.d.ts +0 -7
  168. package/dist/paths.d.ts.map +0 -1
  169. package/dist/paths.js +0 -6
  170. package/dist/repo_ops.d.ts +0 -59
  171. package/dist/repo_ops.d.ts.map +0 -1
  172. package/dist/repo_ops.js +0 -170
  173. package/dist/resolved_gitops_config.d.ts +0 -9
  174. package/dist/resolved_gitops_config.d.ts.map +0 -1
  175. package/dist/resolved_gitops_config.js +0 -12
  176. package/src/lib/config_reconcile.ts +0 -98
  177. package/src/lib/paths.ts +0 -6
  178. package/src/lib/repo_ops.ts +0 -216
  179. package/src/lib/resolved_gitops_config.ts +0 -27
@@ -3,10 +3,24 @@ import { writeFile } from 'node:fs/promises';
3
3
 
4
4
  export type OutputFormat = 'stdout' | 'json' | 'markdown';
5
5
 
6
+ /**
7
+ * Writes one machine-readable document, or one line of a stream, to stdout,
8
+ * ending it with a newline.
9
+ */
10
+ export type WriteStdout = (content: string) => void;
11
+
6
12
  export interface OutputOptions {
7
13
  format: OutputFormat;
8
14
  outfile?: string;
9
15
  log?: Logger;
16
+ /**
17
+ * Where a `json` or `markdown` document goes without `outfile`; the one
18
+ * `route_human_output` returns, so the document reaches stdout while the
19
+ * logger's lines go to stderr.
20
+ *
21
+ * @default `console.log`
22
+ */
23
+ write_stdout?: WriteStdout;
10
24
  }
11
25
 
12
26
  export interface OutputFormatters<T> {
@@ -18,6 +32,42 @@ export interface OutputFormatters<T> {
18
32
  stdout: (data: T, log: Logger) => void;
19
33
  }
20
34
 
35
+ /**
36
+ * Whether a task's stdout carries a machine-readable document: a `json` or
37
+ * `markdown` report not sent to `--outfile`.
38
+ *
39
+ * @param format - the task's `--format`
40
+ * @param outfile - the task's `--outfile`, if any
41
+ */
42
+ export const output_is_machine = (format: string, outfile: string | undefined): boolean =>
43
+ (format === 'json' || format === 'markdown') && !outfile;
44
+
45
+ /**
46
+ * Keeps a task's stdout for its machine-readable document or stream when
47
+ * `machine` is set: the logger's `info` and `debug` lines, which it writes with
48
+ * `console.log`, go where its errors go — stderr — and child loggers inherit
49
+ * that. Gro hands a task the logger its own lines go through, so gro's lines
50
+ * after the task runs (`✓`, the timings) follow to stderr; the two it prints
51
+ * before the task runs (`invoking`, `→ <task>`) stay on stdout, out of the
52
+ * task's reach.
53
+ *
54
+ * @param log - the task's logger
55
+ * @param machine - whether stdout carries a machine-readable document or stream
56
+ * @returns writes to the stdout the logger wrote to before, for the document
57
+ * @mutates log - overrides its `console` when `machine` is set
58
+ */
59
+ export const route_human_output = (log: Logger, machine: boolean): WriteStdout => {
60
+ const stdout = log.console;
61
+ if (machine) {
62
+ log.console = {
63
+ log: (...args) => stdout.error(...args),
64
+ warn: (...args) => stdout.warn(...args),
65
+ error: (...args) => stdout.error(...args)
66
+ };
67
+ }
68
+ return (content) => stdout.log(content);
69
+ };
70
+
21
71
  /**
22
72
  * Formats data and outputs to file or stdout based on options.
23
73
  *
@@ -33,7 +83,7 @@ export const format_and_output = async <T>(
33
83
  formatters: OutputFormatters<T>,
34
84
  options: OutputOptions
35
85
  ): Promise<void> => {
36
- const { format, outfile, log } = options;
86
+ const { format, outfile, log, write_stdout = write_console_log } = options;
37
87
 
38
88
  // Handle stdout format (special case - uses logger directly)
39
89
  if (format === 'stdout') {
@@ -59,7 +109,9 @@ export const format_and_output = async <T>(
59
109
  // line with `[taskname]`, corrupting JSON and cluttering markdown — keep the
60
110
  // machine-readable formats unprefixed and pipeable (use `--outfile` for output
61
111
  // fully free of Gro's own preamble).
62
- // eslint-disable-next-line no-console
63
- console.log(content);
112
+ write_stdout(content);
64
113
  }
65
114
  };
115
+
116
+ // eslint-disable-next-line no-console
117
+ const write_console_log: WriteStdout = (content) => console.log(content);
@@ -1,228 +1,113 @@
1
1
  import type { Logger } from '@fuzdev/fuz_util/log.ts';
2
- import type { Result } from '@fuzdev/fuz_util/result.ts';
3
- import { spawn_out } from '@fuzdev/fuz_util/process.ts';
4
2
  import { styleText as st } from 'node:util';
5
3
 
6
4
  import type { LocalRepo } from './local_repo.ts';
7
- import type {
8
- GitOperations,
9
- NpmOperations,
10
- BuildOperations,
11
- ChangesetOperations
12
- } from './operations.ts';
13
- import {
14
- default_git_operations,
15
- default_npm_operations,
16
- default_build_operations,
17
- default_changeset_operations
18
- } from './operations_defaults.ts';
19
-
20
- export interface PreflightOptions {
21
- skip_changesets?: boolean;
22
- skip_build_validation?: boolean; // Skip build validation (useful for tests)
23
- required_branch?: string;
24
- check_remote?: boolean; // Check if git remote is reachable
25
- estimate_time?: boolean; // Estimate total publish time
26
- log?: Logger;
27
- }
5
+ import type { VersionChange } from './publishing_plan.ts';
6
+ import type { NpmOperations, BuildOperations } from './operations.ts';
7
+ import { default_npm_operations, default_build_operations } from './operations_defaults.ts';
28
8
 
29
9
  export interface PreflightResult {
30
10
  ok: boolean;
31
11
  warnings: Array<string>;
32
12
  errors: Array<string>;
33
- repos_with_changesets: Set<string>;
34
- repos_without_changesets: Set<string>;
35
- estimated_duration?: number; // In seconds
36
- npm_username?: string;
37
13
  }
38
14
 
39
15
  export interface RunPreflightChecksOptions {
40
16
  repos: Array<LocalRepo>;
41
- preflight_options?: PreflightOptions;
42
- git_ops?: GitOperations;
17
+ /**
18
+ * The plan's version changes — every package the run publishes, explicit,
19
+ * escalated, and auto-generated alike. Preflight builds exactly these.
20
+ */
21
+ version_changes: Array<VersionChange>;
22
+ log?: Logger;
43
23
  npm_ops?: NpmOperations;
44
24
  build_ops?: BuildOperations;
45
- changeset_ops?: ChangesetOperations;
46
25
  }
47
26
 
48
27
  /**
49
- * Validates all requirements before publishing can proceed.
28
+ * Validates the publish-time requirements beyond repo git state:
29
+ * - every package the plan publishes builds (fail-fast to prevent broken state)
30
+ * - npm authentication
31
+ * - npm registry connectivity
50
32
  *
51
- * Performs comprehensive pre-flight validation:
52
- * - Clean workspaces (100% clean required - no uncommitted changes)
53
- * - Correct branch (usually main)
54
- * - Changesets present (unless `skip_changesets`=true)
55
- * - Builds successful (fail-fast to prevent broken state)
56
- * - Git remote reachability
57
- * - NPM authentication with username
58
- * - NPM registry connectivity
33
+ * What publishes is the plan's to decide, so preflight reads no changesets: it
34
+ * builds each package in `version_changes`. Git state — each repo on its
35
+ * registry branch, clean, idle, in sync with origin or ahead of it, and no live
36
+ * session in its checkout — is the readiness gate's, which `gitops_publish
37
+ * --wetrun` runs before the plan's confirmation prompt (`repo_readiness.ts`), so
38
+ * preflight reads no git.
59
39
  *
60
40
  * Build validation runs BEFORE any publishing to prevent the scenario where
61
41
  * version is bumped but build fails, leaving repo in broken state.
62
42
  *
63
- * @returns result with `ok`=false if any errors, plus warnings and detailed status
43
+ * @returns result with `ok`=false if any errors, plus warnings
64
44
  */
65
45
  export const run_preflight_checks = async ({
66
46
  repos,
67
- preflight_options = {},
68
- git_ops = default_git_operations,
47
+ version_changes,
48
+ log,
69
49
  npm_ops = default_npm_operations,
70
- build_ops = default_build_operations,
71
- changeset_ops = default_changeset_operations
50
+ build_ops = default_build_operations
72
51
  }: RunPreflightChecksOptions): Promise<PreflightResult> => {
73
- const {
74
- skip_changesets = false,
75
- skip_build_validation = false,
76
- required_branch = 'main',
77
- check_remote = true,
78
- estimate_time = true,
79
- log
80
- } = preflight_options;
81
-
82
52
  const warnings: Array<string> = [];
83
53
  const errors: Array<string> = [];
84
- const repos_with_changesets: Set<string> = new Set();
85
- const repos_without_changesets: Set<string> = new Set();
86
- let npm_username: string | undefined;
87
- let estimated_duration: number | undefined;
88
54
 
89
55
  log?.info(st('cyan', '✅ Running preflight checks...'));
90
56
 
91
- // 1. Check clean workspaces - must be 100% clean before publishing
92
- log?.info(' Checking workspace cleanliness...');
93
- for (const repo of repos) {
94
- const clean_result = await git_ops.check_clean_workspace({ cwd: repo.repo_dir });
95
- if (!clean_result.ok) {
96
- errors.push(`${repo.library.name} failed workspace check: ${clean_result.message}`);
97
- continue;
98
- }
99
-
100
- if (!clean_result.value) {
101
- // Get list of changed files for better error message
102
- const files_result = await git_ops.list_uncommitted_files({ cwd: repo.repo_dir });
103
- if (files_result.ok) {
104
- // No filtering - workspace must be 100% clean
105
- const unexpected_files = files_result.value;
106
-
107
- if (unexpected_files.length > 0) {
108
- errors.push(
109
- `${repo.library.name} has uncommitted changes in: ${unexpected_files.slice(0, 3).join(', ')}${unexpected_files.length > 3 ? ` and ${unexpected_files.length - 3} more` : ''}`
110
- );
111
- }
112
- } else {
113
- errors.push(`${repo.library.name} has uncommitted changes`);
114
- }
115
- }
116
- }
117
-
118
- // 2. Check correct branch
119
- log?.info(` Checking branches (expecting ${required_branch})...`);
120
- for (const repo of repos) {
121
- const branch_result = await git_ops.current_branch_name({ cwd: repo.repo_dir });
122
- if (!branch_result.ok) {
123
- errors.push(`${repo.library.name} failed branch check: ${branch_result.message}`);
124
- continue;
125
- }
126
-
127
- if (branch_result.value !== required_branch) {
128
- errors.push(
129
- `${repo.library.name} is on branch '${branch_result.value}', expected '${required_branch}'`
130
- );
131
- }
132
- }
133
-
134
- // 3. Check changesets (unless skipped)
135
- if (!skip_changesets) {
136
- log?.info(' Checking for changesets...');
137
- for (const repo of repos) {
138
- const has_result = await changeset_ops.has_changesets({ repo });
139
- if (!has_result.ok) {
140
- errors.push(`${repo.library.name} failed changeset check: ${has_result.message}`);
141
- continue;
142
- }
143
-
144
- if (has_result.value) {
145
- repos_with_changesets.add(repo.library.name);
146
- } else {
147
- repos_without_changesets.add(repo.library.name);
148
- warnings.push(`${repo.library.name} has no changesets`);
149
- }
150
- }
151
-
152
- if (repos_without_changesets.size > 0) {
153
- log?.warn(st('yellow', ` ⚠️ ${repos_without_changesets.size} packages have no changesets`));
57
+ // 1. Build every package the plan publishes
58
+ const repo_by_name: Map<string, LocalRepo> = new Map(repos.map((r) => [r.library.name, r]));
59
+ const repos_to_build: Array<LocalRepo> = [];
60
+ for (const change of version_changes) {
61
+ const repo = repo_by_name.get(change.package_name);
62
+ if (repo) {
63
+ repos_to_build.push(repo);
64
+ } else {
65
+ errors.push(`${change.package_name} is in the plan but not among the repos`);
154
66
  }
155
67
  }
156
68
 
157
- // 4. Validate builds for packages with changesets
158
- if (!skip_build_validation && repos_with_changesets.size > 0) {
159
- log?.info(st('cyan', ` Validating builds for ${repos_with_changesets.size} package(s)...`));
160
- const repos_to_build = repos.filter((repo) => repos_with_changesets.has(repo.library.name));
161
-
69
+ if (repos_to_build.length > 0) {
70
+ log?.info(st('cyan', ` Validating builds for ${repos_to_build.length} package(s)...`));
71
+ let build_failures = 0;
162
72
  for (let i = 0; i < repos_to_build.length; i++) {
163
73
  const repo = repos_to_build[i]!;
164
74
  log?.info(
165
75
  st('dim', ` [${i + 1}/${repos_to_build.length}] Building ${repo.library.name}...`)
166
76
  );
167
- const build_result = await build_ops.build_package({ repo, log });
168
- if (!build_result.ok) {
77
+ const build_result = await build_ops.build_package({ repo });
78
+ if (build_result.ok) {
79
+ log?.info(st('dim', ` ✓ ${repo.library.name} built successfully`));
80
+ } else {
81
+ build_failures++;
169
82
  errors.push(
170
83
  `${repo.library.name} failed to build: ${build_result.output || build_result.message || 'unknown error'}`
171
84
  );
172
- } else {
173
- log?.info(st('dim', ` ✓ ${repo.library.name} built successfully`));
174
85
  }
175
86
  }
176
87
 
177
- if (errors.some((err) => err.includes('failed to build'))) {
88
+ if (build_failures > 0) {
178
89
  log?.error(st('red', ' ❌ Build validation failed - fix build errors before publishing'));
179
90
  } else {
180
91
  log?.info(st('green', ' ✓ All builds validated successfully'));
181
92
  }
182
93
  }
183
94
 
184
- // 5. Check git remote reachability (skip in tests when check_remote is false)
185
- if (check_remote && repos.length > 0) {
186
- log?.info(' Checking git remote connectivity...');
187
- // Only check first repo to avoid slowing down tests with multiple remote checks
188
- const remote_result = await check_git_remote(repos[0]!.repo_dir);
189
- if (!remote_result.ok) {
190
- warnings.push(`git remote may not be reachable - ${remote_result.message}`);
191
- }
192
- }
193
-
194
- // 6. Check npm authentication with username
95
+ // 2. Check npm authentication
195
96
  log?.info(' Checking npm authentication...');
196
97
  const npm_auth_result = await npm_ops.check_auth();
197
- if (!npm_auth_result.ok) {
198
- errors.push(`npm authentication failed: ${npm_auth_result.message || 'not logged in'}`);
98
+ if (npm_auth_result.ok) {
99
+ log?.info(st('dim', ` Logged in as: ${npm_auth_result.username}`));
199
100
  } else {
200
- npm_username = npm_auth_result.username;
201
- log?.info(st('dim', ` Logged in as: ${npm_username}`));
101
+ errors.push(`npm authentication failed: ${npm_auth_result.message || 'not logged in'}`);
202
102
  }
203
103
 
204
- // 7. Check network connectivity (npm registry)
104
+ // 3. Check network connectivity (npm registry)
205
105
  log?.info(' Checking npm registry connectivity...');
206
106
  const registry_result = await npm_ops.check_registry();
207
107
  if (!registry_result.ok) {
208
108
  warnings.push(`npm registry check failed: ${registry_result.message}`);
209
109
  }
210
110
 
211
- // 8. Estimate total publish time
212
- if (estimate_time) {
213
- const packages_to_publish = repos_with_changesets.size;
214
- if (packages_to_publish > 0) {
215
- // Rough estimate: 30s per package + 10s per package for NPM propagation
216
- estimated_duration = packages_to_publish * 40;
217
- log?.info(
218
- st(
219
- 'dim',
220
- ` Estimated publish time: ~${Math.ceil(estimated_duration / 60)} minutes for ${packages_to_publish} package(s)`
221
- )
222
- );
223
- }
224
- }
225
-
226
111
  // Report results
227
112
  const ok = errors.length === 0;
228
113
 
@@ -244,26 +129,5 @@ export const run_preflight_checks = async ({
244
129
  log?.info(st('green', '\n✨ All preflight checks passed!'));
245
130
  }
246
131
 
247
- return {
248
- ok,
249
- warnings,
250
- errors,
251
- repos_with_changesets,
252
- repos_without_changesets,
253
- estimated_duration,
254
- npm_username
255
- };
256
- };
257
-
258
- const check_git_remote = async (cwd: string): Promise<Result<object, { message: string }>> => {
259
- try {
260
- // Try to fetch refs from remote without downloading objects
261
- const result = await spawn_out('git', ['ls-remote', '--heads', 'origin'], { cwd });
262
- if (result.stdout || result.stderr) {
263
- return { ok: true };
264
- }
265
- return { ok: false, message: 'No response from git remote' };
266
- } catch (error) {
267
- return { ok: false, message: String(error) };
268
- }
132
+ return { ok, warnings, errors };
269
133
  };
@@ -9,15 +9,24 @@
9
9
  */
10
10
 
11
11
  import type { BumpType } from './version_utils.ts';
12
- import type { PublishingPlan, VersionChange } from './publishing_plan.ts';
12
+ import {
13
+ version_change_kind,
14
+ type PublishingPlan,
15
+ type VersionChange,
16
+ type VersionChangeKind
17
+ } from './publishing_plan.ts';
13
18
  import { UnreachableError } from '@fuzdev/fuz_util/error.ts';
14
19
 
15
- /** How a package's version bump arises in the plan. */
16
- export type PublishStepVia = 'changeset' | 'auto_changeset' | 'escalation';
17
-
18
20
  /** One ordered side-effect a wetrun would perform. */
19
21
  export type PublishStep =
20
- | { kind: 'publish'; repo: string; from: string; to: string; bump: BumpType; via: PublishStepVia }
22
+ | {
23
+ kind: 'publish';
24
+ repo: string;
25
+ from: string;
26
+ to: string;
27
+ bump: BumpType;
28
+ via: VersionChangeKind;
29
+ }
21
30
  | { kind: 'npm_wait'; repo: string; version: string }
22
31
  | {
23
32
  kind: 'dependency_update';
@@ -35,13 +44,6 @@ export interface DerivePublishStepsOptions {
35
44
  deploy?: boolean;
36
45
  }
37
46
 
38
- const step_via = (change: VersionChange): PublishStepVia =>
39
- change.needs_bump_escalation
40
- ? 'escalation'
41
- : change.has_changesets
42
- ? 'changeset'
43
- : 'auto_changeset';
44
-
45
47
  /**
46
48
  * Derives the ordered side-effects a wetrun would perform from a frozen plan.
47
49
  *
@@ -72,7 +74,7 @@ export const derive_publish_steps = (
72
74
  from: change.from,
73
75
  to: change.to,
74
76
  bump: change.bump_type,
75
- via: step_via(change)
77
+ via: version_change_kind(change)
76
78
  });
77
79
  steps.push({ kind: 'npm_wait', repo, version: change.to });
78
80
  changed.add(repo);
@@ -21,16 +21,17 @@ import { z } from 'zod';
21
21
  * failure kind without parsing the message.
22
22
  */
23
23
  export const PublishingErrorCode = z.enum([
24
+ // the publish step failed — its cause is in the message, not yet classified further
24
25
  'publish',
26
+ // the published version never appeared on the npm registry within the wait
25
27
  'network',
26
- 'auth',
27
- 'dependency',
28
- 'build',
29
28
  // the real published version diverged from the frozen plan's prediction — an
30
29
  // invariant violation, distinct from an ordinary publish failure (see fail-loud
31
30
  // drift detection in `multi_repo_publisher.ts`)
32
31
  'drift',
33
- 'other'
32
+ // the repo wasn't ready when re-checked right before its `gro publish` (origin moved,
33
+ // a tracked edit, a live session, …) — aborted before any npm side effect
34
+ 'not_ready'
34
35
  ]);
35
36
  export type PublishingErrorCode = z.infer<typeof PublishingErrorCode>;
36
37
 
@@ -4,12 +4,18 @@
4
4
  * A `PublishingEventHandler` is anything that can receive a `PublishingEvent`. Handlers
5
5
  * compose: `multi_handler` fans out, `masking_handler` redacts secrets then forwards.
6
6
  * Emission is best-effort and synchronous — an observability sink must never fail or
7
- * slow a run. The default sink is `null_handler` (drops everything).
7
+ * slow a run. The executor always captures events for its result (`capture_handler`)
8
+ * and forwards them to the caller's sink when one is supplied.
8
9
  *
9
10
  * @module
10
11
  */
11
12
 
12
13
  import type { PublishingEvent } from './publishing_event.ts';
14
+ import type { WriteStdout } from './output_helpers.ts';
15
+
16
+ const write_process_stdout_line: WriteStdout = (line) => {
17
+ process.stdout.write(line + '\n');
18
+ };
13
19
 
14
20
  /** A sink for publishing events. */
15
21
  export interface PublishingEventHandler {
@@ -21,11 +27,6 @@ export interface CapturingEventHandler extends PublishingEventHandler {
21
27
  readonly events: Array<PublishingEvent>;
22
28
  }
23
29
 
24
- /** Drops every event. The default when no handler is supplied. */
25
- export const null_handler = (): PublishingEventHandler => ({
26
- emit: () => {}
27
- });
28
-
29
30
  /** Collects events in memory. Used to build the run report and in tests. */
30
31
  export const capture_handler = (): CapturingEventHandler => {
31
32
  const events: Array<PublishingEvent> = [];
@@ -38,13 +39,17 @@ export const capture_handler = (): CapturingEventHandler => {
38
39
  };
39
40
 
40
41
  /**
41
- * Writes each event as one JSON object per line (JSON-lines) to `process.stdout`.
42
+ * Writes each event as one JSON object per line (JSON-lines) to stdout.
42
43
  * Write failures are swallowed — the stream is observability, not control flow.
44
+ *
45
+ * @param write_line - writes one line and its newline; defaults to `process.stdout`
43
46
  */
44
- export const stdout_handler = (): PublishingEventHandler => ({
47
+ export const stdout_handler = (
48
+ write_line: WriteStdout = write_process_stdout_line
49
+ ): PublishingEventHandler => ({
45
50
  emit: (event) => {
46
51
  try {
47
- process.stdout.write(JSON.stringify(event) + '\n');
52
+ write_line(JSON.stringify(event));
48
53
  } catch {
49
54
  // best-effort: a logging sink must never fail a run
50
55
  }
@@ -61,17 +66,14 @@ export const multi_handler = (handlers: Array<PublishingEventHandler>): Publishi
61
66
  });
62
67
 
63
68
  /**
64
- * Wraps a handler, masking secrets in each event's string fields before forwarding.
69
+ * Wraps a handler, masking secrets in each event's string fields (`mask_secrets`)
70
+ * before forwarding.
65
71
  *
66
72
  * @param inner - the handler to forward masked events to
67
- * @param mask - the masking function, defaults to `mask_secrets`
68
73
  */
69
- export const masking_handler = (
70
- inner: PublishingEventHandler,
71
- mask: (event: PublishingEvent) => PublishingEvent = mask_secrets
72
- ): PublishingEventHandler => ({
74
+ export const masking_handler = (inner: PublishingEventHandler): PublishingEventHandler => ({
73
75
  emit: (event) => {
74
- inner.emit(mask(event));
76
+ inner.emit(mask_secrets(event));
75
77
  }
76
78
  });
77
79