@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
@@ -2,7 +2,8 @@
2
2
  * Operations interfaces for dependency injection.
3
3
  *
4
4
  * This is the core pattern enabling testability without mocks.
5
- * All side effects (git, npm, fs, process) are abstracted into interfaces.
5
+ * All side effects (git, npm, fs, process, build, and the `repos` binary) are
6
+ * abstracted into interfaces.
6
7
  *
7
8
  * **Design principles:**
8
9
  * - All operations accept a single `options` object parameter
@@ -13,23 +14,30 @@
13
14
  *
14
15
  * **Production usage:**
15
16
  * ```typescript
16
- * import {default_gitops_operations} from './operations_defaults.js';
17
- * const result = await ops.git.current_branch_name({cwd: '/path'});
17
+ * import {default_gitops_operations} from './operations_defaults.ts';
18
+ * const ops = default_gitops_operations;
19
+ * const result = await ops.git.current_commit_hash({cwd: '/path'});
18
20
  * if (!result.ok) {
19
21
  * throw new TaskError(result.message);
20
22
  * }
21
- * const branch = result.value;
23
+ * const commit = result.value;
22
24
  * ```
23
25
  *
24
26
  * **Test usage:**
25
27
  * ```typescript
26
- * const mock_ops = create_mock_operations();
27
- * const result = await publish_repos(repos, {...options, ops: mock_ops});
28
+ * import {create_mock_gitops_ops} from './test_helpers.ts';
29
+ * const ops = create_mock_gitops_ops({
30
+ * changeset: {has_changesets: async () => ({ok: true, value: false})}
31
+ * });
32
+ * const result = await publish_repos(repos, {...options, ops});
28
33
  * // Assert on result without any real git/npm calls
29
34
  * ```
30
35
  *
31
- * See `operations_defaults.ts` for real implementations.
32
- * See test files (`*.test.ts`) for mock implementations.
36
+ * See `operations_defaults.ts` for real implementations, and the test-side mock
37
+ * factories: `create_mock_gitops_ops` in `src/test/test_helpers.ts` (plain
38
+ * objects with per-group overrides) and `create_fixture_gitops_ops` in
39
+ * `src/test/fixtures/mock_operations.ts` (a fixture's changesets over
40
+ * `create_mock_gitops_ops`, its fs empty).
33
41
  *
34
42
  * @module
35
43
  */
@@ -40,7 +48,7 @@ import type { Logger } from '@fuzdev/fuz_util/log.ts';
40
48
  import type { LocalRepo } from './local_repo.ts';
41
49
  import type { ChangesetInfo } from './changeset_reader.ts';
42
50
  import type { BumpType } from './version_utils.ts';
43
- import type { PreflightOptions, PreflightResult } from './preflight_checks.ts';
51
+ import type { PreflightResult, RunPreflightChecksOptions } from './preflight_checks.ts';
44
52
  import type { WaitOptions } from './npm_registry.ts';
45
53
 
46
54
  /**
@@ -76,66 +84,19 @@ export interface ChangesetOperations {
76
84
  }
77
85
 
78
86
  /**
79
- * Git operations for branch management, commits, tags, and workspace state.
80
- * All operations return `Result` instead of throwing errors.
87
+ * Git operations the publishing executor authors with: staging and committing
88
+ * dependency updates and auto-changesets, and reading the commit it published.
89
+ * All operations return `Result` instead of throwing errors. Where each repo
90
+ * sits (branch, dirt, relation to origin) is `ReposOperations`'s to report.
81
91
  */
82
92
  export interface GitOperations {
83
- /**
84
- * Gets the current branch name.
85
- */
86
- current_branch_name: (options?: {
87
- cwd?: string;
88
- }) => Promise<Result<{ value: string }, { message: string }>>;
89
-
90
93
  /**
91
94
  * Gets the current commit hash.
92
95
  */
93
96
  current_commit_hash: (options?: {
94
- branch?: string;
95
97
  cwd?: string;
96
98
  }) => Promise<Result<{ value: string }, { message: string }>>;
97
99
 
98
- /**
99
- * Checks if the workspace is clean (no uncommitted changes).
100
- */
101
- check_clean_workspace: (options?: {
102
- cwd?: string;
103
- }) => Promise<Result<{ value: boolean }, { message: string }>>;
104
-
105
- /**
106
- * Checks out a branch.
107
- */
108
- checkout: (options: {
109
- branch: string;
110
- cwd?: string;
111
- }) => Promise<Result<object, { message: string }>>;
112
-
113
- /**
114
- * Pulls changes from remote.
115
- */
116
- pull: (options?: {
117
- origin?: string;
118
- branch?: string;
119
- cwd?: string;
120
- }) => Promise<Result<object, { message: string }>>;
121
-
122
- /**
123
- * Switches to a branch, optionally pulling.
124
- */
125
- switch_branch: (options: {
126
- branch: string;
127
- pull?: boolean;
128
- cwd?: string;
129
- }) => Promise<Result<object, { message: string }>>;
130
-
131
- /**
132
- * Checks if a remote exists.
133
- */
134
- has_remote: (options?: {
135
- remote?: string;
136
- cwd?: string;
137
- }) => Promise<Result<{ value: boolean }, { message: string }>>;
138
-
139
100
  /**
140
101
  * Stages files for commit.
141
102
  */
@@ -145,93 +106,72 @@ export interface GitOperations {
145
106
  }) => Promise<Result<object, { message: string }>>;
146
107
 
147
108
  /**
148
- * Creates a commit.
109
+ * Commits `files` alone (`git commit -- <files>`), leaving anything else
110
+ * staged out of the commit; `files` must be non-empty.
149
111
  */
150
112
  commit: (options: {
151
113
  message: string;
114
+ files: Array<string>;
152
115
  cwd?: string;
153
116
  }) => Promise<Result<object, { message: string }>>;
117
+ }
154
118
 
119
+ /**
120
+ * Process operations for the commands the publishing executor runs in a repo
121
+ * (`gro publish`, `gro deploy`).
122
+ */
123
+ export interface ProcessOperations {
155
124
  /**
156
- * Stages files and creates a commit.
157
- */
158
- add_and_commit: (options: {
159
- files: string | Array<string>;
160
- message: string;
161
- cwd?: string;
162
- }) => Promise<Result<object, { message: string }>>;
163
-
164
- /**
165
- * Checks whether the working tree has any changes — staged, unstaged, or
166
- * untracked (`git status --porcelain`). Broader than `list_uncommitted_files`,
167
- * which reports only tracked working-tree changes relative to HEAD.
168
- */
169
- has_changes: (options?: {
170
- cwd?: string;
171
- }) => Promise<Result<{ value: boolean }, { message: string }>>;
172
-
173
- /**
174
- * Lists uncommitted files in the working tree (`git diff --name-only HEAD`),
175
- * i.e. working-tree changes relative to HEAD (not a diff between two refs).
176
- */
177
- list_uncommitted_files: (options?: {
178
- cwd?: string;
179
- }) => Promise<Result<{ value: Array<string> }, { message: string }>>;
180
-
181
- /**
182
- * Creates a git tag.
183
- */
184
- tag: (options: {
185
- tag_name: string;
186
- message?: string;
187
- cwd?: string;
188
- }) => Promise<Result<object, { message: string }>>;
189
-
190
- /**
191
- * Pushes a tag to remote.
192
- */
193
- push_tag: (options: {
194
- tag_name: string;
195
- origin?: string;
196
- cwd?: string;
197
- }) => Promise<Result<object, { message: string }>>;
198
-
199
- /**
200
- * Stashes uncommitted changes.
125
+ * Runs a command in the foreground and waits for it to exit: stdin is the
126
+ * terminal's, so a prompt (npm's 2FA one-time password) can be answered, and
127
+ * the child's output shows live — its stdout on ours, or on our stderr when
128
+ * `stdout` says so, and its stderr on ours. A failure carries the end of
129
+ * what the child wrote to stderr, bounded in lines and characters.
201
130
  */
202
- stash: (options?: {
203
- message?: string;
131
+ run_interactive: (options: {
132
+ cmd: string;
133
+ args: Array<string>;
204
134
  cwd?: string;
205
- }) => Promise<Result<object, { message: string }>>;
206
-
207
- /**
208
- * Pops the most recent stash.
209
- */
210
- stash_pop: (options?: { cwd?: string }) => Promise<Result<object, { message: string }>>;
135
+ /**
136
+ * Where the child's stdout goes: our stdout, or our stderr when our stdout
137
+ * carries a machine-readable stream (JSON-lines events, a JSON or markdown
138
+ * report) the child's output would corrupt.
139
+ *
140
+ * @default 'stdout'
141
+ */
142
+ stdout?: 'stdout' | 'stderr';
143
+ }) => Promise<Result<object, { message: string; stderr_tail?: string }>>;
144
+ }
211
145
 
212
- /**
213
- * Checks if a specific file changed between two commits.
214
- */
215
- has_file_changed: (options: {
216
- from_commit: string;
217
- to_commit: string;
218
- file_path: string;
219
- cwd?: string;
220
- }) => Promise<Result<{ value: boolean }, { message: string }>>;
146
+ /**
147
+ * What a `repos` command printed and how it exited, unparsed.
148
+ */
149
+ export interface ReposCommandOutput {
150
+ stdout: string;
151
+ stderr: string;
152
+ /** `0` for a report, `2` for an error document, `1` for a fatal I/O error (maybe no JSON). */
153
+ exit_code: number;
221
154
  }
222
155
 
223
156
  /**
224
- * Process spawning operations for running shell commands.
157
+ * Operations running the Rust `repos` binary, which owns fleet git state.
158
+ * Parsing its output is `repos_status_load.ts`'s, not the runner's.
225
159
  */
226
- export interface ProcessOperations {
160
+ export interface ReposOperations {
227
161
  /**
228
- * Spawns a child process and waits for completion.
162
+ * Runs `repos [--registry <path>] status [--fetch] <keys…> --json` in the
163
+ * process's cwd and returns what it printed, whatever its exit code. With
164
+ * `fetch`, `repos` fetches each entry from origin first, which writes
165
+ * remote-tracking refs and nothing else. Fails only when the binary didn't
166
+ * run to an exit: `not_found` when it isn't on `PATH`.
229
167
  */
230
- spawn: (options: {
231
- cmd: string;
232
- args: Array<string>;
233
- cwd?: string;
234
- }) => Promise<Result<{ stdout?: string; stderr?: string }, { message: string; stderr?: string }>>;
168
+ status: (options: {
169
+ keys: Array<string>;
170
+ registry?: string;
171
+ fetch?: boolean;
172
+ }) => Promise<
173
+ Result<{ output: ReposCommandOutput }, { kind: 'not_found' | 'failed'; message: string }>
174
+ >;
235
175
  }
236
176
 
237
177
  /**
@@ -243,7 +183,6 @@ export interface BuildOperations {
243
183
  */
244
184
  build_package: (options: {
245
185
  repo: LocalRepo;
246
- log?: Logger;
247
186
  }) => Promise<Result<object, { message: string; output?: string }>>;
248
187
  }
249
188
 
@@ -261,7 +200,7 @@ export interface NpmOperations {
261
200
  version: string;
262
201
  wait_options?: WaitOptions;
263
202
  log?: Logger;
264
- }) => Promise<Result<object, { message: string; timeout?: boolean }>>;
203
+ }) => Promise<Result<object, { message: string }>>;
265
204
 
266
205
  /**
267
206
  * Checks npm authentication status.
@@ -272,31 +211,18 @@ export interface NpmOperations {
272
211
  * Checks if npm registry is reachable.
273
212
  */
274
213
  check_registry: () => Promise<Result<object, { message: string }>>;
275
-
276
- /**
277
- * Installs npm dependencies.
278
- */
279
- install: (options?: {
280
- cwd?: string;
281
- }) => Promise<Result<object, { message: string; stderr?: string }>>;
282
214
  }
283
215
 
284
216
  /**
285
- * Preflight validation operations to ensure repos are ready for publishing.
286
- * Validates workspace state, branches, builds, and npm authentication.
217
+ * Preflight validation operations run before publishing: building every
218
+ * package the plan publishes, and npm authentication. Repo git state is the
219
+ * readiness gate's, before preflight (see `repo_readiness.ts`).
287
220
  */
288
221
  export interface PreflightOperations {
289
222
  /**
290
223
  * Runs preflight validation checks before publishing.
291
224
  */
292
- run_preflight_checks: (options: {
293
- repos: Array<LocalRepo>;
294
- preflight_options: PreflightOptions;
295
- git_ops?: GitOperations;
296
- npm_ops?: NpmOperations;
297
- build_ops?: BuildOperations;
298
- changeset_ops?: ChangesetOperations;
299
- }) => Promise<PreflightResult>;
225
+ run_preflight_checks: (options: RunPreflightChecksOptions) => Promise<PreflightResult>;
300
226
  }
301
227
 
302
228
  /**
@@ -343,4 +269,6 @@ export interface GitopsOperations {
343
269
  preflight: PreflightOperations;
344
270
  fs: FsOperations;
345
271
  build: BuildOperations;
272
+ /** `repos status`, for the executor's re-check of each repo right before its publish. */
273
+ repos: ReposOperations;
346
274
  }
@@ -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';
10
+ import { spawn_out, spawn_process, spawn_result_to_message } from '@fuzdev/fuz_util/process.ts';
11
11
  import { readFile, writeFile, mkdir, stat } from 'node:fs/promises';
12
- import { git_checkout, type GitBranch, type GitOrigin } from '@fuzdev/fuz_util/git.ts';
12
+ import { stripVTControlCharacters } from 'node:util';
13
13
  import { fs_classify_error } from '@fuzdev/fuz_util/fs.ts';
14
14
  import { EMPTY_OBJECT } from '@fuzdev/fuz_util/object.ts';
15
15
 
16
16
  import { has_changesets, read_changesets, predict_next_version } from './changeset_reader.ts';
17
17
  import { wait_for_package } from './npm_registry.ts';
18
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';
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,
@@ -41,7 +25,8 @@ import type {
41
25
  PreflightOperations,
42
26
  FsOperations,
43
27
  BuildOperations,
44
- GitopsOperations
28
+ GitopsOperations,
29
+ ReposOperations
45
30
  } from './operations.ts';
46
31
 
47
32
  /** Wrap an async function that returns a value */
@@ -94,141 +79,120 @@ export const default_changeset_operations: ChangesetOperations = {
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(() =>
106
- git_current_commit_hash_required(branch, cwd ? { cwd } : undefined)
107
- );
108
- },
109
-
110
- check_clean_workspace: async (options) => {
111
83
  const { cwd } = options ?? EMPTY_OBJECT;
112
- return wrap_with_value(() => git_check_clean_workspace_as_boolean(cwd ? { cwd } : undefined));
113
- },
114
-
115
- // Branch operations
116
- checkout: async (options) => {
117
- const { branch, cwd } = options;
118
- return wrap_void(() => git_checkout(branch, cwd ? { cwd } : undefined));
119
- },
120
-
121
- pull: async (options) => {
122
- const { origin, branch, cwd } = options ?? EMPTY_OBJECT;
123
- try {
124
- // Omit the branch arg when absent: a trailing `''` makes git pull the remote's
125
- // default branch (origin/HEAD) instead of the current branch's upstream.
126
- const spawned = await spawn_out(
127
- 'git',
128
- ['pull', origin || 'origin', ...(branch ? [branch] : [])],
129
- cwd ? { cwd } : undefined
130
- );
131
- if (spawned.result.ok) {
132
- return { ok: true };
133
- } else {
134
- return { ok: false, message: spawned.stderr || 'Pull failed' };
135
- }
136
- } catch (error) {
137
- return { ok: false, message: String(error) };
138
- }
139
- },
140
-
141
- switch_branch: async (options) => {
142
- const { branch, pull, cwd } = options;
143
- return wrap_void(() => git_switch_branch(branch as GitBranch, pull, cwd ? { cwd } : undefined));
84
+ return wrap_with_value(() => git_current_commit_hash_required(cwd ? { cwd } : undefined));
144
85
  },
145
86
 
146
- has_remote: async (options) => {
147
- const { remote, cwd } = options ?? EMPTY_OBJECT;
148
- return wrap_with_value(() => git_has_remote(remote, cwd ? { cwd } : undefined));
149
- },
150
-
151
- // Staging and committing
152
87
  add: async (options) => {
153
88
  const { files, cwd } = options;
154
89
  return wrap_void(() => git_add(files, cwd ? { cwd } : undefined));
155
90
  },
156
91
 
157
92
  commit: async (options) => {
158
- const { message, cwd } = options;
159
- return wrap_void(() => git_commit(message, cwd ? { cwd } : undefined));
160
- },
161
-
162
- add_and_commit: async (options) => {
163
- const { files, message, cwd } = options;
164
- return wrap_void(() => git_add_and_commit(files, message, cwd ? { cwd } : undefined));
165
- },
166
-
167
- has_changes: async (options) => {
168
- const { cwd } = options ?? EMPTY_OBJECT;
169
- return wrap_with_value(() => git_has_changes(cwd ? { cwd } : undefined));
170
- },
171
-
172
- list_uncommitted_files: async (options) => {
173
- const { cwd } = options ?? EMPTY_OBJECT;
174
- return wrap_with_value(() => git_list_uncommitted_files(cwd ? { cwd } : undefined));
175
- },
176
-
177
- // Tagging
178
- tag: async (options) => {
179
- const { tag_name, message, cwd } = options;
180
- return wrap_void(() => git_tag(tag_name, message, cwd ? { cwd } : undefined));
181
- },
93
+ const { message, files, cwd } = options;
94
+ return wrap_void(() => git_commit(message, files, cwd ? { cwd } : undefined));
95
+ }
96
+ };
182
97
 
183
- push_tag: async (options) => {
184
- const { tag_name, origin, cwd } = options;
185
- return wrap_void(() => git_push_tag(tag_name, origin as GitOrigin, cwd ? { cwd } : undefined));
186
- },
98
+ /** The most lines of a child's stderr a failure carries. */
99
+ export const OUTPUT_TAIL_MAX_LINES = 20;
187
100
 
188
- // Stashing
189
- stash: async (options) => {
190
- const { message, cwd } = options ?? EMPTY_OBJECT;
191
- return wrap_void(() => git_stash(message, cwd ? { cwd } : undefined));
192
- },
101
+ /** The most characters of a child's stderr a failure carries. */
102
+ export const OUTPUT_TAIL_MAX_CHARS = 4096;
193
103
 
194
- stash_pop: async (options) => {
195
- const { cwd } = options ?? EMPTY_OBJECT;
196
- return wrap_void(() => git_stash_pop(cwd ? { cwd } : undefined));
197
- },
198
-
199
- // File change detection
200
- has_file_changed: async (options) => {
201
- const { from_commit, to_commit, file_path, cwd } = options;
202
- return wrap_with_value(() =>
203
- git_has_file_changed(from_commit, to_commit, file_path, cwd ? { cwd } : undefined)
204
- );
205
- }
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));
206
127
  };
207
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
+
208
132
  export const default_process_operations: ProcessOperations = {
209
- spawn: async (options) => {
210
- const { cmd, args, cwd } = options;
133
+ run_interactive: async (options) => {
134
+ const { cmd, args, cwd, stdout = 'stdout' } = options;
211
135
  try {
212
- const spawned = await spawn_out(cmd, args, cwd ? { cwd } : undefined);
213
- if (spawned.result.ok) {
214
- return {
215
- ok: true,
216
- stdout: spawned.stdout || undefined,
217
- stderr: spawned.stderr || undefined
218
- };
219
- } else {
220
- return {
221
- ok: false,
222
- message: 'Command failed',
223
- stderr: spawned.stderr || undefined
224
- };
225
- }
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
+ };
226
159
  } catch (error) {
227
160
  return { ok: false, message: String(error) };
228
161
  }
229
162
  }
230
163
  };
231
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
+ }
194
+ };
195
+
232
196
  export const default_npm_operations: NpmOperations = {
233
197
  wait_for_package: async (options) => {
234
198
  const { pkg, version, wait_options, log } = options;
@@ -236,7 +200,7 @@ export const default_npm_operations: NpmOperations = {
236
200
  await wait_for_package(pkg, version, { ...wait_options, log });
237
201
  return { ok: true };
238
202
  } catch (error) {
239
- return { ok: false, message: String(error), timeout: true };
203
+ return { ok: false, message: String(error) };
240
204
  }
241
205
  },
242
206
 
@@ -257,25 +221,15 @@ export const default_npm_operations: NpmOperations = {
257
221
 
258
222
  check_registry: async () => {
259
223
  try {
260
- const result = await spawn_out('npm', ['ping']);
261
- if (result.stdout) {
262
- return { ok: true };
263
- }
264
- return { ok: false, message: 'Failed to ping npm registry' };
265
- } catch (error) {
266
- return { ok: false, message: String(error) };
267
- }
268
- },
269
-
270
- install: async (options) => {
271
- const { cwd } = options ?? EMPTY_OBJECT;
272
- try {
273
- const spawned = await spawn_out('npm', ['install'], cwd ? { cwd } : undefined);
274
- if (spawned.result.ok) {
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) {
275
227
  return { ok: true };
276
- } else {
277
- return { ok: false, message: 'Install failed', stderr: spawned.stderr || undefined };
278
228
  }
229
+ return {
230
+ ok: false,
231
+ message: `Failed to ping npm registry (${spawn_result_to_message(result)})`
232
+ };
279
233
  } catch (error) {
280
234
  return { ok: false, message: String(error) };
281
235
  }
@@ -331,9 +285,8 @@ export const default_fs_operations: FsOperations = {
331
285
 
332
286
  export const default_build_operations: BuildOperations = {
333
287
  build_package: async (options) => {
334
- const { repo, log } = options;
288
+ const { repo } = options;
335
289
  try {
336
- log?.info(` Building ${repo.library.name}...`);
337
290
  const spawned = await spawn_out('gro', ['build'], { cwd: repo.repo_dir });
338
291
  if (spawned.result.ok) {
339
292
  return { ok: true };
@@ -360,5 +313,6 @@ export const default_gitops_operations: GitopsOperations = {
360
313
  npm: default_npm_operations,
361
314
  preflight: default_preflight_operations,
362
315
  fs: default_fs_operations,
363
- build: default_build_operations
316
+ build: default_build_operations,
317
+ repos: default_repos_operations
364
318
  };