@fuzdev/fuz_gitops 0.78.0 → 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 (196) hide show
  1. package/README.md +91 -17
  2. package/dist/ModulesDetail.svelte +22 -27
  3. package/dist/ModulesDetail.svelte.d.ts.map +1 -1
  4. package/dist/ModulesNav.svelte +8 -5
  5. package/dist/ModulesNav.svelte.d.ts.map +1 -1
  6. package/dist/ModulesPage.svelte +3 -5
  7. package/dist/ModulesPage.svelte.d.ts.map +1 -1
  8. package/dist/PageFooter.svelte +2 -2
  9. package/dist/PageHeader.svelte +6 -6
  10. package/dist/PageHeader.svelte.d.ts +0 -3
  11. package/dist/PageHeader.svelte.d.ts.map +1 -1
  12. package/dist/PullRequestsDetail.svelte +29 -20
  13. package/dist/PullRequestsDetail.svelte.d.ts.map +1 -1
  14. package/dist/PullRequestsPage.svelte +4 -6
  15. package/dist/PullRequestsPage.svelte.d.ts.map +1 -1
  16. package/dist/ReposTable.svelte +43 -55
  17. package/dist/ReposTable.svelte.d.ts.map +1 -1
  18. package/dist/ReposTree.svelte +9 -21
  19. package/dist/ReposTree.svelte.d.ts.map +1 -1
  20. package/dist/ReposTreeNav.svelte +9 -12
  21. package/dist/ReposTreeNav.svelte.d.ts.map +1 -1
  22. package/dist/TablePage.svelte +3 -5
  23. package/dist/TablePage.svelte.d.ts.map +1 -1
  24. package/dist/TreeItemPage.svelte +3 -5
  25. package/dist/TreeItemPage.svelte.d.ts.map +1 -1
  26. package/dist/TreePage.svelte +3 -5
  27. package/dist/TreePage.svelte.d.ts.map +1 -1
  28. package/dist/changeset_generator.d.ts.map +1 -1
  29. package/dist/changeset_generator.js +2 -2
  30. package/dist/changeset_reader.d.ts.map +1 -1
  31. package/dist/changeset_reader.js +3 -3
  32. package/dist/ci_reconcile.d.ts +6 -10
  33. package/dist/ci_reconcile.d.ts.map +1 -1
  34. package/dist/ci_reconcile.js +6 -8
  35. package/dist/dependency_graph.d.ts +26 -49
  36. package/dist/dependency_graph.d.ts.map +1 -1
  37. package/dist/dependency_graph.js +40 -83
  38. package/dist/dependency_updater.d.ts +1 -1
  39. package/dist/dependency_updater.d.ts.map +1 -1
  40. package/dist/dependency_updater.js +14 -9
  41. package/dist/fetch_repo_data.d.ts +34 -8
  42. package/dist/fetch_repo_data.d.ts.map +1 -1
  43. package/dist/fetch_repo_data.js +44 -22
  44. package/dist/fs_fetch_value_cache.d.ts +1 -1
  45. package/dist/fs_fetch_value_cache.d.ts.map +1 -1
  46. package/dist/fs_fetch_value_cache.js +3 -3
  47. package/dist/git_operations.d.ts +6 -49
  48. package/dist/git_operations.d.ts.map +1 -1
  49. package/dist/git_operations.js +13 -126
  50. package/dist/github.d.ts +17 -2
  51. package/dist/github.d.ts.map +1 -1
  52. package/dist/github.js +17 -8
  53. package/dist/github_helpers.d.ts.map +1 -1
  54. package/dist/github_helpers.js +3 -11
  55. package/dist/gitops_analyze.task.d.ts +24 -2
  56. package/dist/gitops_analyze.task.d.ts.map +1 -1
  57. package/dist/gitops_analyze.task.js +47 -34
  58. package/dist/gitops_config.d.ts +34 -79
  59. package/dist/gitops_config.d.ts.map +1 -1
  60. package/dist/gitops_config.js +76 -53
  61. package/dist/gitops_constants.d.ts +6 -4
  62. package/dist/gitops_constants.d.ts.map +1 -1
  63. package/dist/gitops_constants.js +6 -4
  64. package/dist/gitops_plan.task.d.ts +29 -3
  65. package/dist/gitops_plan.task.d.ts.map +1 -1
  66. package/dist/gitops_plan.task.js +83 -61
  67. package/dist/gitops_publish.task.d.ts +62 -2
  68. package/dist/gitops_publish.task.d.ts.map +1 -1
  69. package/dist/gitops_publish.task.js +193 -105
  70. package/dist/gitops_run.task.d.ts +74 -0
  71. package/dist/gitops_run.task.d.ts.map +1 -1
  72. package/dist/gitops_run.task.js +176 -162
  73. package/dist/gitops_sync.task.d.ts +65 -3
  74. package/dist/gitops_sync.task.d.ts.map +1 -1
  75. package/dist/gitops_sync.task.js +163 -37
  76. package/dist/gitops_task_helpers.d.ts +86 -59
  77. package/dist/gitops_task_helpers.d.ts.map +1 -1
  78. package/dist/gitops_task_helpers.js +120 -79
  79. package/dist/gitops_validate.task.d.ts +1 -2
  80. package/dist/gitops_validate.task.d.ts.map +1 -1
  81. package/dist/gitops_validate.task.js +25 -32
  82. package/dist/graph_validation.d.ts +16 -27
  83. package/dist/graph_validation.d.ts.map +1 -1
  84. package/dist/graph_validation.js +24 -65
  85. package/dist/local_repo.d.ts +50 -62
  86. package/dist/local_repo.d.ts.map +1 -1
  87. package/dist/local_repo.js +83 -196
  88. package/dist/log_helpers.d.ts +8 -8
  89. package/dist/log_helpers.d.ts.map +1 -1
  90. package/dist/multi_repo_publisher.d.ts +10 -0
  91. package/dist/multi_repo_publisher.d.ts.map +1 -1
  92. package/dist/multi_repo_publisher.js +125 -61
  93. package/dist/npm_registry.d.ts +23 -19
  94. package/dist/npm_registry.d.ts.map +1 -1
  95. package/dist/npm_registry.js +21 -48
  96. package/dist/operations.d.ts +75 -180
  97. package/dist/operations.d.ts.map +1 -1
  98. package/dist/operations.js +15 -7
  99. package/dist/operations_defaults.d.ts +17 -2
  100. package/dist/operations_defaults.d.ts.map +1 -1
  101. package/dist/operations_defaults.js +111 -128
  102. package/dist/output_helpers.d.ts +36 -0
  103. package/dist/output_helpers.d.ts.map +1 -1
  104. package/dist/output_helpers.js +37 -3
  105. package/dist/preflight_checks.d.ts +20 -27
  106. package/dist/preflight_checks.d.ts.map +1 -1
  107. package/dist/preflight_checks.js +37 -126
  108. package/dist/publish_gate.d.ts.map +1 -1
  109. package/dist/publish_steps.d.ts +2 -4
  110. package/dist/publish_steps.d.ts.map +1 -1
  111. package/dist/publish_steps.js +4 -8
  112. package/dist/publishing_event.d.ts +2 -8
  113. package/dist/publishing_event.d.ts.map +1 -1
  114. package/dist/publishing_event.js +20 -17
  115. package/dist/publishing_event_handler.d.ts +10 -8
  116. package/dist/publishing_event_handler.d.ts.map +1 -1
  117. package/dist/publishing_event_handler.js +19 -17
  118. package/dist/publishing_plan.d.ts +16 -1
  119. package/dist/publishing_plan.d.ts.map +1 -1
  120. package/dist/publishing_plan.js +332 -254
  121. package/dist/publishing_plan_helpers.d.ts.map +1 -1
  122. package/dist/publishing_plan_helpers.js +3 -3
  123. package/dist/publishing_plan_logging.d.ts +1 -1
  124. package/dist/publishing_plan_logging.d.ts.map +1 -1
  125. package/dist/publishing_plan_logging.js +22 -15
  126. package/dist/repo.svelte.d.ts +7 -0
  127. package/dist/repo.svelte.d.ts.map +1 -1
  128. package/dist/repo.svelte.js +3 -0
  129. package/dist/repo_readiness.d.ts +210 -0
  130. package/dist/repo_readiness.d.ts.map +1 -0
  131. package/dist/repo_readiness.js +516 -0
  132. package/dist/repos_status.d.ts +3340 -0
  133. package/dist/repos_status.d.ts.map +1 -0
  134. package/dist/repos_status.js +555 -0
  135. package/dist/repos_status_load.d.ts +65 -0
  136. package/dist/repos_status_load.d.ts.map +1 -0
  137. package/dist/repos_status_load.js +131 -0
  138. package/dist/version_utils.js +1 -1
  139. package/package.json +15 -34
  140. package/src/lib/cargo_toml.ts +3 -3
  141. package/src/lib/changeset_generator.ts +19 -19
  142. package/src/lib/changeset_reader.ts +18 -18
  143. package/src/lib/ci_reconcile.ts +12 -16
  144. package/src/lib/dependency_graph.ts +61 -113
  145. package/src/lib/dependency_updater.ts +36 -30
  146. package/src/lib/fetch_repo_data.ts +71 -34
  147. package/src/lib/fs_fetch_value_cache.ts +15 -11
  148. package/src/lib/git_operations.ts +22 -194
  149. package/src/lib/github.ts +33 -21
  150. package/src/lib/github_helpers.ts +9 -16
  151. package/src/lib/gitops_analyze.task.ts +104 -66
  152. package/src/lib/gitops_config.ts +90 -139
  153. package/src/lib/gitops_constants.ts +6 -4
  154. package/src/lib/gitops_plan.task.ts +128 -76
  155. package/src/lib/gitops_publish.task.ts +280 -144
  156. package/src/lib/gitops_run.task.ts +265 -193
  157. package/src/lib/gitops_sync.task.ts +236 -52
  158. package/src/lib/gitops_task_helpers.ts +170 -114
  159. package/src/lib/gitops_validate.task.ts +44 -54
  160. package/src/lib/graph_validation.ts +28 -89
  161. package/src/lib/local_repo.ts +128 -335
  162. package/src/lib/log_helpers.ts +18 -28
  163. package/src/lib/multi_repo_publisher.ts +192 -110
  164. package/src/lib/npm_registry.ts +42 -67
  165. package/src/lib/operations.ts +99 -160
  166. package/src/lib/operations_defaults.ts +157 -201
  167. package/src/lib/output_helpers.ts +58 -6
  168. package/src/lib/preflight_checks.ts +52 -188
  169. package/src/lib/publish_gate.ts +7 -9
  170. package/src/lib/publish_steps.ts +27 -25
  171. package/src/lib/publishing_event.ts +26 -23
  172. package/src/lib/publishing_event_handler.ts +24 -22
  173. package/src/lib/publishing_plan.ts +474 -310
  174. package/src/lib/publishing_plan_helpers.ts +16 -12
  175. package/src/lib/publishing_plan_logging.ts +53 -44
  176. package/src/lib/repo.svelte.ts +16 -8
  177. package/src/lib/repo_readiness.ts +647 -0
  178. package/src/lib/repos_status.ts +671 -0
  179. package/src/lib/repos_status_load.ts +173 -0
  180. package/src/lib/version_utils.ts +3 -3
  181. package/dist/config_reconcile.d.ts +0 -46
  182. package/dist/config_reconcile.d.ts.map +0 -1
  183. package/dist/config_reconcile.js +0 -60
  184. package/dist/paths.d.ts +0 -7
  185. package/dist/paths.d.ts.map +0 -1
  186. package/dist/paths.js +0 -6
  187. package/dist/repo_ops.d.ts +0 -59
  188. package/dist/repo_ops.d.ts.map +0 -1
  189. package/dist/repo_ops.js +0 -170
  190. package/dist/resolved_gitops_config.d.ts +0 -9
  191. package/dist/resolved_gitops_config.d.ts.map +0 -1
  192. package/dist/resolved_gitops_config.js +0 -12
  193. package/src/lib/config_reconcile.ts +0 -94
  194. package/src/lib/paths.ts +0 -6
  195. package/src/lib/repo_ops.ts +0 -216
  196. package/src/lib/resolved_gitops_config.ts +0 -27
@@ -1,184 +1,317 @@
1
- import type {Task} from '@fuzdev/gro';
2
- import type {Logger} from '@fuzdev/fuz_util/log.ts';
3
- import {z} from 'zod';
4
- import {createInterface} from 'node:readline/promises';
5
- import {styleText as st} from 'node:util';
1
+ import type { Task } from '@fuzdev/gro';
2
+ import type { Logger } from '@fuzdev/fuz_util/log.ts';
3
+ import { z } from 'zod';
4
+ import { createInterface } from 'node:readline/promises';
5
+ import { styleText as st } from 'node:util';
6
6
 
7
- import {get_gitops_ready} from './gitops_task_helpers.ts';
7
+ import {
8
+ gate_publish_readiness,
9
+ get_gitops_ready,
10
+ log_readiness_block,
11
+ type ResolveGitopsReposOptions
12
+ } from './gitops_task_helpers.ts';
13
+ import type { LocalRepo } from './local_repo.ts';
14
+ import type { GitopsOperations, ReposOperations } from './operations.ts';
15
+ import { default_gitops_operations, default_repos_operations } from './operations_defaults.ts';
8
16
  import {
9
17
  execute_publishing_plan,
10
18
  type PublishingOptions,
11
- type PublishingResult,
19
+ type PublishingResult
12
20
  } from './multi_repo_publisher.ts';
13
- import {stdout_handler} from './publishing_event_handler.ts';
14
- import {generate_publishing_plan, log_publishing_plan} from './publishing_plan.ts';
15
- import {derive_publish_steps, format_publish_steps, type PublishStep} from './publish_steps.ts';
16
- import {decide_publish_gate, publish_run_failed} from './publish_gate.ts';
17
- import {format_and_output, type OutputFormatters} from './output_helpers.ts';
18
- import {GITOPS_CONFIG_PATH_DEFAULT, GITOPS_NPM_WAIT_TIMEOUT_DEFAULT} from './gitops_constants.ts';
21
+ import {
22
+ mask_secrets,
23
+ masking_handler,
24
+ redact_secrets,
25
+ stdout_handler
26
+ } from './publishing_event_handler.ts';
27
+ import { generate_publishing_plan } from './publishing_plan.ts';
28
+ import { log_publishing_plan } from './publishing_plan_logging.ts';
29
+ import { derive_publish_steps, format_publish_steps, type PublishStep } from './publish_steps.ts';
30
+ import { decide_publish_gate, publish_run_failed } from './publish_gate.ts';
31
+ import {
32
+ format_and_output,
33
+ output_is_machine,
34
+ route_human_output,
35
+ type OutputFormatters
36
+ } from './output_helpers.ts';
37
+ import { GITOPS_CONFIG_PATH_DEFAULT, GITOPS_NPM_WAIT_TIMEOUT_DEFAULT } from './gitops_constants.ts';
19
38
 
20
39
  /** @nodocs */
21
40
  export const Args = z.strictObject({
22
41
  config: z
23
42
  .string()
24
- .meta({description: 'path to the gitops config file, absolute or relative to the cwd'})
43
+ .meta({ description: 'path to the gitops config file, absolute or relative to the cwd' })
25
44
  .default(GITOPS_CONFIG_PATH_DEFAULT),
26
- dir: z
45
+ registry: z
27
46
  .string()
28
- .meta({description: 'path containing the repos, defaults to the parent of the config dir'})
47
+ .meta({
48
+ description:
49
+ 'path to the repos.toml registry, when `repos` would not find it walking up from the cwd'
50
+ })
29
51
  .optional(),
30
52
  peer_strategy: z
31
- .enum(['exact', 'caret', 'tilde'])
32
- .meta({description: 'version strategy for peer dependencies'})
53
+ .enum(['exact', 'caret', 'tilde', 'gte'])
54
+ .meta({
55
+ description:
56
+ 'range prefix for a rewritten dependency of any type whose range has none (an existing prefix is kept; a wildcard becomes ^)'
57
+ })
33
58
  .default('caret' as const),
34
- wetrun: z.boolean().meta({description: 'actually publish (default is dry run)'}).default(false),
59
+ wetrun: z.boolean().meta({ description: 'actually publish (default is dry run)' }).default(false),
35
60
  format: z
36
61
  .enum(['stdout', 'json', 'markdown'])
37
- .meta({description: 'output format'})
62
+ .meta({ description: 'output format' })
38
63
  .default('stdout'),
39
- deploy: z.boolean().meta({description: 'deploy all repos after publishing'}).default(false),
64
+ deploy: z
65
+ .boolean()
66
+ .meta({
67
+ description:
68
+ 'deploy each repo the run changed (published, or any dependency updated) after publishing'
69
+ })
70
+ .default(false),
40
71
  plan: z
41
72
  .boolean()
42
- .meta({description: 'show the plan and confirm before publishing; --no-plan to skip'})
73
+ .meta({ description: 'show the plan and confirm before publishing; --no-plan to skip' })
43
74
  .default(true),
44
75
  max_wait: z
45
76
  .number()
46
- .meta({description: 'max time to wait for npm propagation in ms'})
77
+ .meta({ description: 'max time to wait for npm propagation in ms' })
47
78
  .default(GITOPS_NPM_WAIT_TIMEOUT_DEFAULT),
48
79
  emit_json: z
49
80
  .boolean()
50
- .meta({description: 'stream structured publishing events as JSON-lines to stdout'})
81
+ .meta({ description: 'stream structured publishing events as JSON-lines to stdout' })
51
82
  .default(false),
52
- outfile: z.string().meta({description: 'write output to file instead of logging'}).optional(),
53
- verbose: z.boolean().meta({description: 'show additional details in plan output'}).default(false),
54
- sync: z
83
+ outfile: z.string().meta({ description: 'write output to file instead of logging' }).optional(),
84
+ verbose: z
55
85
  .boolean()
56
- .meta({
57
- description:
58
- 'sync repos (switch branch, pull, install) before the dry run instead of reading the working tree as-is; always on for --wetrun',
59
- })
86
+ .meta({ description: 'show additional details in plan output' })
60
87
  .default(false),
61
88
  preview: z
62
89
  .boolean()
63
- .meta({description: 'show the ordered side-effects a --wetrun would perform'})
64
- .default(false),
90
+ .meta({ description: 'show the ordered side-effects a --wetrun would perform' })
91
+ .default(false)
65
92
  });
66
93
  export type Args = z.infer<typeof Args>;
67
94
 
68
95
  /** @nodocs */
69
96
  export const task: Task<Args> = {
70
- summary: 'publish all repos in dependency order',
97
+ summary:
98
+ 'publish the packages the plan publishes, in dependency order (a dry run unless --wetrun)',
71
99
  Args,
72
- run: async ({args, log}): Promise<void> => {
73
- const {
74
- config,
75
- dir,
76
- peer_strategy,
77
- wetrun,
78
- format,
79
- deploy,
80
- plan,
81
- max_wait,
82
- emit_json,
83
- outfile,
84
- verbose,
85
- sync,
86
- preview,
87
- } = args;
88
-
89
- // Load repos. A dry run reads the working tree as-is unless `--sync`;
90
- // a real publish (`--wetrun`) always syncs so preflight sees the canonical branches.
91
- const {local_repos: repos} = await get_gitops_ready({
92
- config,
93
- dir,
94
- download: false, // Don't download if missing
95
- sync: sync || wetrun,
96
- log,
97
- });
100
+ run: async ({ args, log }): Promise<void> => {
101
+ const outcome = await run_gitops_publish(args, log);
102
+ if (outcome === 'cancelled') {
103
+ process.exit(0);
104
+ } else if (outcome === 'failed') {
105
+ process.exit(1);
106
+ }
107
+ }
108
+ };
98
109
 
99
- // Generate the plan once; the executor consumes this exact plan (no second pass).
100
- const publishing_plan = await generate_publishing_plan(repos, {verbose});
101
- const preview_steps = preview ? derive_publish_steps(publishing_plan, {deploy}) : null;
110
+ /**
111
+ * The side effects `run_gitops_publish` reaches through, injectable so the
112
+ * order of its steps is testable.
113
+ *
114
+ * @nodocs
115
+ */
116
+ export interface GitopsPublishDeps {
117
+ /** Loads the configured repos as they sit (`get_gitops_ready`). */
118
+ load_repos: (options: ResolveGitopsReposOptions) => Promise<{ local_repos: Array<LocalRepo> }>;
119
+ /** Runs `repos status` for the readiness gate. */
120
+ repos_ops: ReposOperations;
121
+ /** The executor's operations. */
122
+ ops: GitopsOperations;
123
+ /**
124
+ * Asks the operator to confirm the plan, on stderr, so a machine-readable
125
+ * stdout carries nothing but its document or events.
126
+ */
127
+ confirm: () => Promise<boolean>;
128
+ }
102
129
 
103
- // Decide whether to show the plan + confirm, block, or proceed (the decision table lives
104
- // in `decide_publish_gate`; readline + exit stay here at the edge).
105
- const gate = decide_publish_gate({wetrun, show_plan: plan, plan: publishing_plan});
130
+ const default_gitops_publish_deps: GitopsPublishDeps = {
131
+ load_repos: get_gitops_ready,
132
+ repos_ops: default_repos_operations,
133
+ ops: default_gitops_operations,
134
+ confirm: async () => {
135
+ process.stderr.write('Continue with publishing? (y/n): ');
136
+ return prompt_for_confirmation();
137
+ }
138
+ };
106
139
 
107
- // A real publish that shows its plan prints it first — including before a `blocked` throw,
108
- // so the operator sees the errors that blocked it.
109
- if (gate.action !== 'proceed') {
110
- log.info(st('cyan', 'Publishing Plan'));
111
- log_publishing_plan(publishing_plan, log, {verbose});
112
- }
140
+ /**
141
+ * Runs `gro gitops_publish`, in order: load the repos as they sit,
142
+ * generate the plan, and — for `--wetrun` — run the readiness gate
143
+ * (`gate_publish_readiness`, read-only: `repos status --fetch`) before
144
+ * printing the plan and asking to confirm, so a refusal or a "no" changes
145
+ * nothing. Then preflight and the executor. A dry run skips the gate and
146
+ * prints the diagnostics' readiness block instead. Under `--emit_json`, or
147
+ * `--format json` or `markdown` without `--outfile`, the log goes to stderr
148
+ * and stdout carries the events or the report alone (`route_human_output`).
149
+ *
150
+ * @returns `cancelled` when the operator declines, `failed` when the run failed (its output already written), else `done`
151
+ * @throws {TaskError} when the readiness gate refuses or loading fails
152
+ * @nodocs
153
+ */
154
+ export const run_gitops_publish = async (
155
+ args: Args,
156
+ log: Logger,
157
+ deps: Partial<GitopsPublishDeps> = {}
158
+ ): Promise<'done' | 'cancelled' | 'failed'> => {
159
+ const { load_repos, repos_ops, ops, confirm } = { ...default_gitops_publish_deps, ...deps };
160
+ const {
161
+ config,
162
+ registry,
163
+ peer_strategy,
164
+ wetrun,
165
+ format,
166
+ deploy,
167
+ plan,
168
+ max_wait,
169
+ emit_json,
170
+ outfile,
171
+ verbose,
172
+ preview
173
+ } = args;
113
174
 
114
- if (gate.action === 'blocked') {
115
- throw new Error(gate.message);
116
- } else if (gate.action === 'confirm') {
117
- if (preview_steps) log_preview(preview_steps, log);
118
-
119
- // Ask for confirmation
120
- log.info(st('yellow', '⚠️ This will publish the packages shown above.'));
121
- process.stdout.write('Continue with publishing? (y/n): ');
122
- const confirmed = await prompt_for_confirmation();
123
- if (!confirmed) {
124
- log.info('Publishing cancelled');
125
- process.exit(0);
126
- }
127
- } else if (preview_steps && format === 'stdout') {
128
- // proceed (dry run or --no-plan): only render to stdout for the human format;
129
- // json/markdown carry the preview in their structured output, so logging here too
130
- // would corrupt that stream.
131
- log_preview(preview_steps, log);
132
- }
175
+ // With a machine-readable stdout, every human line goes to stderr: the log (the plan, the
176
+ // readiness block and gate, the executor's progress) and, in `confirm`, the prompt
177
+ const write_stdout = route_human_output(log, to_child_stdout(args) === 'stderr');
133
178
 
134
- // Publishing options
135
- const options: PublishingOptions = {
136
- wetrun,
137
- version_strategy: peer_strategy,
138
- deploy,
139
- max_wait,
140
- log,
141
- // Live JSON-lines stream when requested; events also surface on the result.
142
- events: emit_json ? stdout_handler() : undefined,
143
- };
179
+ // Load repos as they sit; a real publish gates on their state below rather than moving them
180
+ const { local_repos: repos } = await load_repos({ config, registry, log });
144
181
 
145
- // Execute publishing (may throw on fatal errors like circular dependencies)
146
- let result: PublishingResult;
147
- let fatal_error: Error | null = null;
148
-
149
- try {
150
- result = await execute_publishing_plan(repos, publishing_plan, options);
151
- } catch (error) {
152
- // Construct a failure result for fatal errors so output can still be generated
153
- fatal_error = error instanceof Error ? error : new Error(String(error));
154
- result = {
155
- ok: false,
156
- published: [],
157
- // Note: FATAL_ERROR is a placeholder - only fatal_error.message is displayed in output
158
- failed: [{name: 'FATAL_ERROR', error: fatal_error}],
159
- duration: 0,
160
- events: [],
161
- summary: {total: 0, published: 0, failed: 1, skipped: 0, duration: 0},
162
- plan_errors: publishing_plan.errors,
163
- plan_warnings: publishing_plan.warnings,
164
- };
165
- }
182
+ // Generate the plan once; the executor consumes this exact plan (no second pass).
183
+ const publishing_plan = await generate_publishing_plan(repos, { verbose, ops: ops.changeset });
184
+ const preview_steps = preview ? derive_publish_steps(publishing_plan, { deploy }) : null;
166
185
 
167
- // Format and output result (always runs, even on fatal errors)
168
- // Note: stdout format is handled by the executor's logging
169
- if (format !== 'stdout') {
170
- await format_and_output({result, fatal_error, preview_steps}, create_publish_formatters(), {
171
- format,
172
- outfile,
186
+ // Decide whether to show the plan + confirm, block, or proceed (the decision table lives
187
+ // in `decide_publish_gate`; readline + exit stay at the edge).
188
+ const gate = decide_publish_gate({ wetrun, show_plan: plan, plan: publishing_plan });
189
+
190
+ if (wetrun) {
191
+ // The readiness gate: fetch, then refuse unless every npm repo is ready. Read-only, and
192
+ // before the prompt and any side effect. A plan with errors is refused without fetching:
193
+ // below when the plan is shown, else by the executor before anything else it does.
194
+ if (publishing_plan.errors.length === 0) {
195
+ await gate_publish_readiness({
196
+ local_repos: repos,
197
+ registry,
198
+ publishing: new Set(publishing_plan.version_changes.map((vc) => vc.package_name)),
173
199
  log,
200
+ repos_ops
174
201
  });
175
202
  }
203
+ } else {
204
+ log_readiness_block(repos, log);
205
+ }
176
206
 
177
- // Exit with error if failed
178
- if (publish_run_failed(result, fatal_error)) {
179
- process.exit(1);
207
+ // A real publish that shows its plan prints it first — including before a `blocked` throw,
208
+ // so the operator sees the errors that blocked it.
209
+ if (gate.action !== 'proceed') {
210
+ log.info(st('cyan', 'Publishing Plan'));
211
+ log_publishing_plan(publishing_plan, log, { verbose });
212
+ }
213
+
214
+ if (gate.action === 'blocked') {
215
+ throw new Error(gate.message);
216
+ } else if (gate.action === 'confirm') {
217
+ if (preview_steps) log_preview(preview_steps, log);
218
+
219
+ // Ask for confirmation
220
+ log.info(st('yellow', '⚠️ This will publish the packages shown above.'));
221
+ const confirmed = await confirm();
222
+ if (!confirmed) {
223
+ log.info('Publishing cancelled');
224
+ return 'cancelled';
180
225
  }
181
- },
226
+ } else if (preview_steps && format === 'stdout') {
227
+ // proceed (dry run or --no-plan): only render to stdout for the human format;
228
+ // json/markdown carry the preview in their structured output, so logging here too
229
+ // would corrupt that stream.
230
+ log_preview(preview_steps, log);
231
+ }
232
+
233
+ // Publishing options
234
+ const options: PublishingOptions = {
235
+ wetrun,
236
+ version_strategy: peer_strategy,
237
+ deploy,
238
+ max_wait,
239
+ log,
240
+ ops,
241
+ registry,
242
+ // Live JSON-lines stream when requested, secrets masked since failure messages carry
243
+ // npm's stderr; events also surface on the result.
244
+ events: emit_json ? masking_handler(stdout_handler(write_stdout)) : undefined,
245
+ child_stdout: to_child_stdout(args)
246
+ };
247
+
248
+ // Execute publishing (may throw on fatal errors like circular dependencies)
249
+ let result: PublishingResult;
250
+ let fatal_error: Error | null = null;
251
+
252
+ try {
253
+ result = await execute_publishing_plan(repos, publishing_plan, options);
254
+ } catch (error) {
255
+ // Construct a failure result for fatal errors so output can still be generated
256
+ fatal_error = error instanceof Error ? error : new Error(String(error));
257
+ result = {
258
+ ok: false,
259
+ published: [],
260
+ // Note: FATAL_ERROR is a placeholder - only fatal_error.message is displayed in output
261
+ failed: [{ name: 'FATAL_ERROR', error: fatal_error }],
262
+ duration: 0,
263
+ events: [],
264
+ summary: { total: 0, published: 0, failed: 1, skipped: 0, duration: 0 },
265
+ plan_errors: publishing_plan.errors,
266
+ plan_warnings: publishing_plan.warnings
267
+ };
268
+ }
269
+
270
+ // Format and output result (always runs, even on fatal errors)
271
+ // Note: stdout format is handled by the executor's logging
272
+ if (format !== 'stdout') {
273
+ await format_and_output({ result, fatal_error, preview_steps }, create_publish_formatters(), {
274
+ format,
275
+ outfile,
276
+ log,
277
+ write_stdout
278
+ });
279
+ }
280
+
281
+ return publish_run_failed(result, fatal_error) ? 'failed' : 'done';
282
+ };
283
+
284
+ /**
285
+ * Where `gro publish` and `gro deploy`, streaming live, show their stdout: on
286
+ * our stderr when our stdout carries a machine-readable stream — the
287
+ * `--emit_json` events, or a JSON or markdown report not sent to `--outfile` —
288
+ * so their output can't corrupt it, else on our stdout. The same test sends
289
+ * the task's own log and prompt to stderr.
290
+ *
291
+ * @nodocs
292
+ */
293
+ export const to_child_stdout = (
294
+ args: Pick<Args, 'emit_json' | 'format' | 'outfile'>
295
+ ): 'stdout' | 'stderr' =>
296
+ args.emit_json || output_is_machine(args.format, args.outfile) ? 'stderr' : 'stdout';
297
+
298
+ /**
299
+ * A failed package's markdown: the message's first line on the bullet, and the
300
+ * rest — the end of a command's stderr — in an indented fenced block, its fence
301
+ * longer than any backtick run inside so stderr can't close it.
302
+ *
303
+ * @param name - the package name
304
+ * @param message - the failure message
305
+ * @nodocs
306
+ */
307
+ export const format_failure_markdown = (name: string, message: string): Array<string> => {
308
+ const [head, ...rest] = message.split('\n');
309
+ const lines = [`- \`${name}\`: ${head}`];
310
+ if (rest.length === 0) return lines;
311
+ const longest_run = Math.max(0, ...(rest.join('\n').match(/`+/g) ?? []).map((r) => r.length));
312
+ const fence = '`'.repeat(Math.max(3, longest_run + 1));
313
+ lines.push('', ` ${fence}`, ...rest.map((l) => ` ${l}`), ` ${fence}`);
314
+ return lines;
182
315
  };
183
316
 
184
317
  interface PublishResultData {
@@ -188,20 +321,23 @@ interface PublishResultData {
188
321
  }
189
322
 
190
323
  const create_publish_formatters = (): OutputFormatters<PublishResultData> => ({
191
- json: (data) =>
192
- JSON.stringify(
193
- data.preview_steps ? {...data.result, preview: data.preview_steps} : data.result,
324
+ json: (data) => {
325
+ // the result's events are the executor's unmasked capture; mask them as the live stream is
326
+ const result = { ...data.result, events: data.result.events.map(mask_secrets) };
327
+ return JSON.stringify(
328
+ data.preview_steps ? { ...result, preview: data.preview_steps } : result,
194
329
  null,
195
- 2,
196
- ),
330
+ 2
331
+ );
332
+ },
197
333
  markdown: (data) => format_result_markdown(data.result, data.fatal_error, data.preview_steps),
198
334
  stdout: () => {
199
335
  // stdout format is handled by the executor's logging
200
336
  // This should never be called due to early return in task
201
- },
337
+ }
202
338
  });
203
339
 
204
- /** Logs the ordered side-effect preview to stdout. */
340
+ /** Logs the ordered side-effect preview. */
205
341
  const log_preview = (steps: Array<PublishStep>, log: Logger): void => {
206
342
  log.info(st('cyan', '\nSide-effect preview (what a real publish would perform):'));
207
343
  for (const line of format_publish_steps(steps)) {
@@ -213,7 +349,7 @@ const log_preview = (steps: Array<PublishStep>, log: Logger): void => {
213
349
  const format_result_markdown = (
214
350
  result: PublishingResult,
215
351
  fatal_error: Error | null,
216
- preview_steps: Array<PublishStep> | null,
352
+ preview_steps: Array<PublishStep> | null
217
353
  ): Array<string> => {
218
354
  const lines: Array<string> = [];
219
355
 
@@ -224,7 +360,7 @@ const format_result_markdown = (
224
360
  if (fatal_error) {
225
361
  lines.push('## ❌ Fatal Error');
226
362
  lines.push('');
227
- lines.push(`**Error**: ${fatal_error.message}`);
363
+ lines.push(`**Error**: ${redact_secrets(fatal_error.message)}`);
228
364
  lines.push('');
229
365
  lines.push('Publishing could not proceed due to the error above.');
230
366
  lines.push('');
@@ -252,8 +388,8 @@ const format_result_markdown = (
252
388
  lines.push('');
253
389
  lines.push('## Failed Packages');
254
390
  lines.push('');
255
- for (const {name, error} of result.failed) {
256
- lines.push(`- \`${name}\`: ${error.message}`);
391
+ for (const { name, error } of result.failed) {
392
+ lines.push(...format_failure_markdown(name, redact_secrets(error.message)));
257
393
  }
258
394
  }
259
395
 
@@ -290,7 +426,7 @@ const format_result_markdown = (
290
426
  const prompt_for_confirmation = async (): Promise<boolean> => {
291
427
  const rl = createInterface({
292
428
  input: process.stdin,
293
- output: process.stdout,
429
+ output: process.stderr
294
430
  });
295
431
 
296
432
  const answer = await rl.question('');