@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,38 +1,22 @@
1
1
  /**
2
2
  * Production implementations of operations interfaces.
3
3
  *
4
- * Provides real git, npm, fs, and build operations for production use.
4
+ * Provides real git, npm, fs, build, and `repos` operations for production use.
5
5
  * For interface definitions and dependency injection pattern, see `operations.ts`.
6
6
  *
7
7
  * @module
8
8
  */
9
9
 
10
- import {spawn_out} from '@fuzdev/fuz_util/process.ts';
11
- import {readFile, writeFile, mkdir, stat} from 'node:fs/promises';
12
- import {git_checkout, type GitBranch, type GitOrigin} from '@fuzdev/fuz_util/git.ts';
13
- import {fs_classify_error} from '@fuzdev/fuz_util/fs.ts';
14
- import {EMPTY_OBJECT} from '@fuzdev/fuz_util/object.ts';
10
+ import { spawn_out, spawn_process, spawn_result_to_message } from '@fuzdev/fuz_util/process.ts';
11
+ import { readFile, writeFile, mkdir, stat } from 'node:fs/promises';
12
+ import { stripVTControlCharacters } from 'node:util';
13
+ import { fs_classify_error } from '@fuzdev/fuz_util/fs.ts';
14
+ import { EMPTY_OBJECT } from '@fuzdev/fuz_util/object.ts';
15
15
 
16
- import {has_changesets, read_changesets, predict_next_version} from './changeset_reader.ts';
17
- import {wait_for_package} from './npm_registry.ts';
18
- import {run_preflight_checks} from './preflight_checks.ts';
19
- import {
20
- git_add,
21
- git_commit,
22
- git_add_and_commit,
23
- git_tag,
24
- git_push_tag,
25
- git_has_changes,
26
- git_list_uncommitted_files,
27
- git_has_file_changed,
28
- git_stash,
29
- git_stash_pop,
30
- git_switch_branch,
31
- git_current_branch_name_required,
32
- git_current_commit_hash_required,
33
- git_check_clean_workspace_as_boolean,
34
- git_has_remote,
35
- } from './git_operations.ts';
16
+ import { has_changesets, read_changesets, predict_next_version } from './changeset_reader.ts';
17
+ import { wait_for_package } from './npm_registry.ts';
18
+ import { run_preflight_checks } from './preflight_checks.ts';
19
+ import { git_add, git_commit, git_current_commit_hash_required } from './git_operations.ts';
36
20
  import type {
37
21
  ChangesetOperations,
38
22
  GitOperations,
@@ -42,199 +26,181 @@ import type {
42
26
  FsOperations,
43
27
  BuildOperations,
44
28
  GitopsOperations,
29
+ ReposOperations
45
30
  } from './operations.ts';
46
31
 
47
32
  /** Wrap an async function that returns a value */
48
33
  const wrap_with_value = async <T>(
49
- fn: () => Promise<T>,
50
- ): Promise<{ok: true; value: T} | {ok: false; message: string}> => {
34
+ fn: () => Promise<T>
35
+ ): Promise<{ ok: true; value: T } | { ok: false; message: string }> => {
51
36
  try {
52
37
  const value = await fn();
53
- return {ok: true, value};
38
+ return { ok: true, value };
54
39
  } catch (error) {
55
- return {ok: false, message: String(error)};
40
+ return { ok: false, message: String(error) };
56
41
  }
57
42
  };
58
43
 
59
44
  /** Wrap an async function, ignoring its return value */
60
45
  const wrap_void = async (
61
- fn: () => Promise<unknown>,
62
- ): Promise<{ok: true} | {ok: false; message: string}> => {
46
+ fn: () => Promise<unknown>
47
+ ): Promise<{ ok: true } | { ok: false; message: string }> => {
63
48
  try {
64
49
  await fn();
65
- return {ok: true};
50
+ return { ok: true };
66
51
  } catch (error) {
67
- return {ok: false, message: String(error)};
52
+ return { ok: false, message: String(error) };
68
53
  }
69
54
  };
70
55
 
71
56
  export const default_changeset_operations: ChangesetOperations = {
72
57
  has_changesets: async (options) => {
73
- const {repo} = options;
58
+ const { repo } = options;
74
59
  return wrap_with_value(() => has_changesets(repo));
75
60
  },
76
61
 
77
62
  read_changesets: async (options) => {
78
- const {repo, log} = options;
63
+ const { repo, log } = options;
79
64
  return wrap_with_value(() => read_changesets(repo, log));
80
65
  },
81
66
 
82
67
  predict_next_version: async (options) => {
83
- const {repo, log} = options;
68
+ const { repo, log } = options;
84
69
  try {
85
70
  const result = await predict_next_version(repo, log);
86
71
  if (result === null) {
87
72
  return null;
88
73
  }
89
- return {ok: true, ...result};
74
+ return { ok: true, ...result };
90
75
  } catch (error) {
91
- return {ok: false, message: String(error)};
76
+ return { ok: false, message: String(error) };
92
77
  }
93
- },
78
+ }
94
79
  };
95
80
 
96
81
  export const default_git_operations: GitOperations = {
97
- // Core git info
98
- current_branch_name: async (options) => {
99
- const {cwd} = options ?? EMPTY_OBJECT;
100
- return wrap_with_value(() => git_current_branch_name_required(cwd ? {cwd} : undefined));
101
- },
102
-
103
82
  current_commit_hash: async (options) => {
104
- const {branch, cwd} = options ?? EMPTY_OBJECT;
105
- return wrap_with_value(() => git_current_commit_hash_required(branch, cwd ? {cwd} : undefined));
106
- },
107
-
108
- check_clean_workspace: async (options) => {
109
- const {cwd} = options ?? EMPTY_OBJECT;
110
- return wrap_with_value(() => git_check_clean_workspace_as_boolean(cwd ? {cwd} : undefined));
111
- },
112
-
113
- // Branch operations
114
- checkout: async (options) => {
115
- const {branch, cwd} = options;
116
- return wrap_void(() => git_checkout(branch, cwd ? {cwd} : undefined));
83
+ const { cwd } = options ?? EMPTY_OBJECT;
84
+ return wrap_with_value(() => git_current_commit_hash_required(cwd ? { cwd } : undefined));
117
85
  },
118
86
 
119
- pull: async (options) => {
120
- const {origin, branch, cwd} = options ?? EMPTY_OBJECT;
121
- try {
122
- // Omit the branch arg when absent: a trailing `''` makes git pull the remote's
123
- // default branch (origin/HEAD) instead of the current branch's upstream.
124
- const spawned = await spawn_out(
125
- 'git',
126
- ['pull', origin || 'origin', ...(branch ? [branch] : [])],
127
- cwd ? {cwd} : undefined,
128
- );
129
- if (spawned.result.ok) {
130
- return {ok: true};
131
- } else {
132
- return {ok: false, message: spawned.stderr || 'Pull failed'};
133
- }
134
- } catch (error) {
135
- return {ok: false, message: String(error)};
136
- }
137
- },
138
-
139
- switch_branch: async (options) => {
140
- const {branch, pull, cwd} = options;
141
- return wrap_void(() => git_switch_branch(branch as GitBranch, pull, cwd ? {cwd} : undefined));
142
- },
143
-
144
- has_remote: async (options) => {
145
- const {remote, cwd} = options ?? EMPTY_OBJECT;
146
- return wrap_with_value(() => git_has_remote(remote, cwd ? {cwd} : undefined));
147
- },
148
-
149
- // Staging and committing
150
87
  add: async (options) => {
151
- const {files, cwd} = options;
152
- return wrap_void(() => git_add(files, cwd ? {cwd} : undefined));
88
+ const { files, cwd } = options;
89
+ return wrap_void(() => git_add(files, cwd ? { cwd } : undefined));
153
90
  },
154
91
 
155
92
  commit: async (options) => {
156
- const {message, cwd} = options;
157
- return wrap_void(() => git_commit(message, cwd ? {cwd} : undefined));
158
- },
159
-
160
- add_and_commit: async (options) => {
161
- const {files, message, cwd} = options;
162
- return wrap_void(() => git_add_and_commit(files, message, cwd ? {cwd} : undefined));
163
- },
164
-
165
- has_changes: async (options) => {
166
- const {cwd} = options ?? EMPTY_OBJECT;
167
- return wrap_with_value(() => git_has_changes(cwd ? {cwd} : undefined));
168
- },
169
-
170
- list_uncommitted_files: async (options) => {
171
- const {cwd} = options ?? EMPTY_OBJECT;
172
- return wrap_with_value(() => git_list_uncommitted_files(cwd ? {cwd} : undefined));
173
- },
174
-
175
- // Tagging
176
- tag: async (options) => {
177
- const {tag_name, message, cwd} = options;
178
- return wrap_void(() => git_tag(tag_name, message, cwd ? {cwd} : undefined));
179
- },
93
+ const { message, files, cwd } = options;
94
+ return wrap_void(() => git_commit(message, files, cwd ? { cwd } : undefined));
95
+ }
96
+ };
180
97
 
181
- push_tag: async (options) => {
182
- const {tag_name, origin, cwd} = options;
183
- return wrap_void(() => git_push_tag(tag_name, origin as GitOrigin, cwd ? {cwd} : undefined));
184
- },
98
+ /** The most lines of a child's stderr a failure carries. */
99
+ export const OUTPUT_TAIL_MAX_LINES = 20;
185
100
 
186
- // Stashing
187
- stash: async (options) => {
188
- const {message, cwd} = options ?? EMPTY_OBJECT;
189
- return wrap_void(() => git_stash(message, cwd ? {cwd} : undefined));
190
- },
101
+ /** The most characters of a child's stderr a failure carries. */
102
+ export const OUTPUT_TAIL_MAX_CHARS = 4096;
191
103
 
192
- stash_pop: async (options) => {
193
- const {cwd} = options ?? EMPTY_OBJECT;
194
- return wrap_void(() => git_stash_pop(cwd ? {cwd} : undefined));
195
- },
196
-
197
- // File change detection
198
- has_file_changed: async (options) => {
199
- const {from_commit, to_commit, file_path, cwd} = options;
200
- return wrap_with_value(() =>
201
- git_has_file_changed(from_commit, to_commit, file_path, cwd ? {cwd} : undefined),
202
- );
203
- },
104
+ /**
105
+ * The end of a process's output, for a failure message: its last lines, then
106
+ * its last characters, with terminal escape sequences stripped and trailing
107
+ * whitespace trimmed.
108
+ *
109
+ * @param text - the output, or as much of its end as was kept
110
+ * @param max_lines - the most lines to keep
111
+ * @param max_chars - the most characters to keep, applied after `max_lines`
112
+ */
113
+ export const output_tail = (
114
+ text: string,
115
+ max_lines: number = OUTPUT_TAIL_MAX_LINES,
116
+ max_chars: number = OUTPUT_TAIL_MAX_CHARS
117
+ ): string => {
118
+ const lines = stripVTControlCharacters(text).replace(/\r\n?/g, '\n').trimEnd().split('\n');
119
+ const tail = lines.slice(-max_lines).join('\n');
120
+ if (tail.length <= max_chars) return drop_leading_blank_lines(tail);
121
+ // a character cut can split a line, and with it a secret masking would no longer
122
+ // recognize, so the partial first line goes
123
+ const cut = tail.slice(-max_chars);
124
+ if (tail[tail.length - max_chars - 1] === '\n') return drop_leading_blank_lines(cut);
125
+ const nl = cut.indexOf('\n');
126
+ return nl === -1 ? '' : drop_leading_blank_lines(cut.slice(nl + 1));
204
127
  };
205
128
 
129
+ // not `trimStart`, which would strip the first line's indentation
130
+ const drop_leading_blank_lines = (text: string): string => text.replace(/^\n+/, '');
131
+
206
132
  export const default_process_operations: ProcessOperations = {
207
- spawn: async (options) => {
208
- const {cmd, args, cwd} = options;
133
+ run_interactive: async (options) => {
134
+ const { cmd, args, cwd, stdout = 'stdout' } = options;
209
135
  try {
210
- const spawned = await spawn_out(cmd, args, cwd ? {cwd} : undefined);
211
- if (spawned.result.ok) {
212
- return {
213
- ok: true,
214
- stdout: spawned.stdout || undefined,
215
- stderr: spawned.stderr || undefined,
216
- };
217
- } else {
218
- return {
219
- ok: false,
220
- message: 'Command failed',
221
- stderr: spawned.stderr || undefined,
222
- };
223
- }
136
+ // stdin and stdout go straight to the child (fd 2 for stdout routed to our stderr), so
137
+ // they stay a TTY when ours are — npm prompts for a one-time password only on a TTY.
138
+ // Only stderr is piped, to keep its end for the failure message while echoing it live.
139
+ const { child, closed } = spawn_process(cmd, args, {
140
+ cwd,
141
+ stdio: ['inherit', stdout === 'stderr' ? 2 : 'inherit', 'pipe']
142
+ });
143
+ // a rolling window over stderr's end, bounded however much the child writes
144
+ let kept = '';
145
+ child.stderr?.setEncoding('utf8');
146
+ child.stderr?.on('data', (chunk: string) => {
147
+ process.stderr.write(chunk);
148
+ kept += chunk;
149
+ if (kept.length > OUTPUT_TAIL_MAX_CHARS * 4) kept = kept.slice(-OUTPUT_TAIL_MAX_CHARS * 2);
150
+ });
151
+ const result = await closed;
152
+ if (result.ok) return { ok: true };
153
+ const stderr_tail = output_tail(kept);
154
+ return {
155
+ ok: false,
156
+ message: `\`${[cmd, ...args].join(' ')}\` failed (${spawn_result_to_message(result)})`,
157
+ stderr_tail: stderr_tail || undefined
158
+ };
224
159
  } catch (error) {
225
- return {ok: false, message: String(error)};
160
+ return { ok: false, message: String(error) };
226
161
  }
227
- },
162
+ }
163
+ };
164
+
165
+ export const default_repos_operations: ReposOperations = {
166
+ status: async (options) => {
167
+ const { keys, registry, fetch } = options;
168
+ const args = [...(registry === undefined ? [] : ['--registry', registry]), 'status'];
169
+ if (fetch) args.push('--fetch');
170
+ // `--` so a key can never read as a flag
171
+ args.push('--json', '--', ...keys);
172
+ const spawned = await spawn_out('repos', args);
173
+ const { result } = spawned;
174
+ if (result.kind === 'error') {
175
+ const not_found = (result.error as NodeJS.ErrnoException).code === 'ENOENT';
176
+ return {
177
+ ok: false,
178
+ kind: not_found ? 'not_found' : 'failed',
179
+ message: result.error.message
180
+ };
181
+ }
182
+ if (result.kind === 'signaled') {
183
+ return { ok: false, kind: 'failed', message: `repos was killed by ${result.signal}` };
184
+ }
185
+ return {
186
+ ok: true,
187
+ output: {
188
+ stdout: spawned.stdout ?? '',
189
+ stderr: spawned.stderr ?? '',
190
+ exit_code: result.code
191
+ }
192
+ };
193
+ }
228
194
  };
229
195
 
230
196
  export const default_npm_operations: NpmOperations = {
231
197
  wait_for_package: async (options) => {
232
- const {pkg, version, wait_options, log} = options;
198
+ const { pkg, version, wait_options, log } = options;
233
199
  try {
234
- await wait_for_package(pkg, version, {...wait_options, log});
235
- return {ok: true};
200
+ await wait_for_package(pkg, version, { ...wait_options, log });
201
+ return { ok: true };
236
202
  } catch (error) {
237
- return {ok: false, message: String(error), timeout: true};
203
+ return { ok: false, message: String(error) };
238
204
  }
239
205
  },
240
206
 
@@ -244,76 +210,66 @@ export const default_npm_operations: NpmOperations = {
244
210
  if (result.stdout) {
245
211
  const username = result.stdout.trim();
246
212
  if (username) {
247
- return {ok: true, username};
213
+ return { ok: true, username };
248
214
  }
249
215
  }
250
- return {ok: false, message: 'Not logged in to npm'};
216
+ return { ok: false, message: 'Not logged in to npm' };
251
217
  } catch (error) {
252
- return {ok: false, message: String(error)};
218
+ return { ok: false, message: String(error) };
253
219
  }
254
220
  },
255
221
 
256
222
  check_registry: async () => {
257
223
  try {
258
- const result = await spawn_out('npm', ['ping']);
259
- if (result.stdout) {
260
- return {ok: true};
261
- }
262
- return {ok: false, message: 'Failed to ping npm registry'};
263
- } catch (error) {
264
- return {ok: false, message: String(error)};
265
- }
266
- },
267
-
268
- install: async (options) => {
269
- const {cwd} = options ?? EMPTY_OBJECT;
270
- try {
271
- const spawned = await spawn_out('npm', ['install'], cwd ? {cwd} : undefined);
272
- if (spawned.result.ok) {
273
- return {ok: true};
274
- } else {
275
- return {ok: false, message: 'Install failed', stderr: spawned.stderr || undefined};
224
+ // the exit status alone: `npm ping` reports on stderr, writing nothing to stdout
225
+ const { result } = await spawn_out('npm', ['ping']);
226
+ if (result.ok) {
227
+ return { ok: true };
276
228
  }
229
+ return {
230
+ ok: false,
231
+ message: `Failed to ping npm registry (${spawn_result_to_message(result)})`
232
+ };
277
233
  } catch (error) {
278
- return {ok: false, message: String(error)};
234
+ return { ok: false, message: String(error) };
279
235
  }
280
- },
236
+ }
281
237
  };
282
238
 
283
239
  export const default_preflight_operations: PreflightOperations = {
284
240
  run_preflight_checks: async (options) => {
285
241
  return run_preflight_checks(options);
286
- },
242
+ }
287
243
  };
288
244
 
289
245
  export const default_fs_operations: FsOperations = {
290
246
  readFile: async (options) => {
291
- const {path, encoding} = options;
247
+ const { path, encoding } = options;
292
248
  try {
293
249
  const value = await readFile(path, encoding);
294
- return {ok: true, value};
250
+ return { ok: true, value };
295
251
  } catch (error) {
296
- return {ok: false, ...fs_classify_error(error)};
252
+ return { ok: false, ...fs_classify_error(error) };
297
253
  }
298
254
  },
299
255
 
300
256
  writeFile: async (options) => {
301
- const {path, content} = options;
257
+ const { path, content } = options;
302
258
  try {
303
259
  await writeFile(path, content);
304
- return {ok: true};
260
+ return { ok: true };
305
261
  } catch (error) {
306
- return {ok: false, ...fs_classify_error(error)};
262
+ return { ok: false, ...fs_classify_error(error) };
307
263
  }
308
264
  },
309
265
 
310
266
  mkdir: async (options) => {
311
- const {path, recursive} = options;
267
+ const { path, recursive } = options;
312
268
  try {
313
- await mkdir(path, {recursive});
314
- return {ok: true};
269
+ await mkdir(path, { recursive });
270
+ return { ok: true };
315
271
  } catch (error) {
316
- return {ok: false, ...fs_classify_error(error)};
272
+ return { ok: false, ...fs_classify_error(error) };
317
273
  }
318
274
  },
319
275
 
@@ -324,28 +280,27 @@ export const default_fs_operations: FsOperations = {
324
280
  } catch {
325
281
  return false;
326
282
  }
327
- },
283
+ }
328
284
  };
329
285
 
330
286
  export const default_build_operations: BuildOperations = {
331
287
  build_package: async (options) => {
332
- const {repo, log} = options;
288
+ const { repo } = options;
333
289
  try {
334
- log?.info(` Building ${repo.library.name}...`);
335
- const spawned = await spawn_out('gro', ['build'], {cwd: repo.repo_dir});
290
+ const spawned = await spawn_out('gro', ['build'], { cwd: repo.repo_dir });
336
291
  if (spawned.result.ok) {
337
- return {ok: true};
292
+ return { ok: true };
338
293
  } else {
339
294
  return {
340
295
  ok: false,
341
296
  message: 'Build failed',
342
- output: spawned.stderr || spawned.stdout || 'Build failed',
297
+ output: spawned.stderr || spawned.stdout || 'Build failed'
343
298
  };
344
299
  }
345
300
  } catch (error) {
346
- return {ok: false, message: String(error)};
301
+ return { ok: false, message: String(error) };
347
302
  }
348
- },
303
+ }
349
304
  };
350
305
 
351
306
  /**
@@ -359,4 +314,5 @@ export const default_gitops_operations: GitopsOperations = {
359
314
  preflight: default_preflight_operations,
360
315
  fs: default_fs_operations,
361
316
  build: default_build_operations,
317
+ repos: default_repos_operations
362
318
  };
@@ -1,12 +1,26 @@
1
- import type {Logger} from '@fuzdev/fuz_util/log.ts';
2
- import {writeFile} from 'node:fs/promises';
1
+ import type { Logger } from '@fuzdev/fuz_util/log.ts';
2
+ 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
  *
@@ -31,9 +81,9 @@ export interface OutputFormatters<T> {
31
81
  export const format_and_output = async <T>(
32
82
  data: T,
33
83
  formatters: OutputFormatters<T>,
34
- options: OutputOptions,
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);