@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,156 +1,212 @@
1
1
  /**
2
2
  * Shared initialization logic for all gitops tasks.
3
3
  *
4
- * Provides `get_gitops_ready()` which orchestrates:
5
- * - Config loading and normalization
6
- * - Repo resolution (local path discovery)
7
- * - Branch switching and syncing
8
- * - Dependency installation
4
+ * `resolve_gitops_repos()` loads the config's registry keys, runs
5
+ * `repos status <keys…> --json`, and resolves each key to its checkout.
6
+ * `get_gitops_ready()` then loads each repo's library as its working tree
7
+ * sits. `gate_publish_readiness()` is the read-only gate a real publish runs
8
+ * before its prompt, and `log_readiness_block()` the diagnostics' report of
9
+ * repos not at rest (both over `repo_readiness.ts`).
9
10
  *
10
11
  * Used by: `gitops_sync.task.ts`, `gitops_analyze.task.ts`, `gitops_plan.task.ts`,
11
- * `gitops_publish.task.ts`, and `gitops_validate.task.ts`.
12
+ * `gitops_publish.task.ts`, `gitops_validate.task.ts`, and `gitops_run.task.ts`.
12
13
  *
13
- * Accepts `git_ops` and `npm_ops` parameters to support testing via operations pattern
14
- * (see `operations.ts` for dependency injection details).
14
+ * Accepts `repos_ops` to support testing via the operations pattern (see
15
+ * `operations.ts` for dependency injection details).
15
16
  *
16
17
  * @module
17
18
  */
18
19
 
19
- import {TaskError} from '@fuzdev/gro';
20
- import {styleText as st} from 'node:util';
21
- import {resolve, dirname} from 'node:path';
22
- import {print_path} from '@fuzdev/gro/paths.ts';
23
- import type {Logger} from '@fuzdev/fuz_util/log.ts';
20
+ import { TaskError } from '@fuzdev/gro';
21
+ import { styleText as st } from 'node:util';
22
+ import { resolve } from 'node:path';
23
+ import type { Logger } from '@fuzdev/fuz_util/log.ts';
24
+ import { to_error_message } from '@fuzdev/fuz_util/error.ts';
24
25
 
25
- import {load_gitops_config, type GitopsConfig} from './gitops_config.ts';
26
- import {local_repos_load, local_repos_ensure, type LocalRepo} from './local_repo.ts';
27
- import {resolve_gitops_config} from './resolved_gitops_config.ts';
28
- import {DEFAULT_REPOS_DIR} from './paths.ts';
29
- import type {GitOperations, NpmOperations} from './operations.ts';
26
+ import { load_gitops_config, type GitopsConfig } from './gitops_config.ts';
27
+ import {
28
+ local_repos_load,
29
+ local_repos_resolve,
30
+ repo_is_npm,
31
+ type LocalRepo,
32
+ type LocalRepoPath
33
+ } from './local_repo.ts';
34
+ import { load_repos_status, to_repos_command } from './repos_status_load.ts';
35
+ import {
36
+ check_publish_readiness,
37
+ format_readiness_ahead,
38
+ format_readiness_block,
39
+ repos_not_at_rest
40
+ } from './repo_readiness.ts';
41
+ import type { ReposStatusReport } from './repos_status.ts';
42
+ import type { ReposOperations } from './operations.ts';
43
+ import { default_repos_operations } from './operations_defaults.ts';
30
44
 
31
- export interface GetGitopsReadyOptions {
45
+ export interface ResolveGitopsReposOptions {
46
+ /** Path to the gitops config, absolute or relative to the cwd. */
32
47
  config: string;
33
- dir?: string;
34
- download: boolean;
35
- log?: Logger;
36
- git_ops?: GitOperations;
37
- npm_ops?: NpmOperations;
38
- parallel?: boolean;
39
- concurrency?: number;
48
+ /** A `repos.toml` to use instead of the one `repos` finds walking up from the cwd. */
49
+ registry?: string;
40
50
  /**
41
- * Sync each repo's working tree to its configured branch before loading
42
- * (switch branch, pull, install). When `false`, repos load exactly as they
43
- * sit on disk — the safe default for read-only diagnostics. Defaults to `true`.
51
+ * The package whose generated data the run writes; when it's public, a
52
+ * private repo in the config fails the resolve.
44
53
  */
45
- sync?: boolean;
46
- /** When syncing, tolerate uncommitted changes instead of throwing. Defaults to `false`. */
47
- allow_dirty?: boolean;
54
+ host?: { name: string; private: boolean };
55
+ log?: Logger;
56
+ repos_ops?: ReposOperations;
48
57
  }
49
58
 
50
59
  /**
51
- * Central initialization function for all gitops tasks.
52
- *
53
- * Initialization sequence:
54
- * 1. Loads and normalizes config from `gitops.config.ts`
55
- * 2. Resolves local repo paths (creates missing with `--download`)
56
- * 3. If `sync`, switches branches and pulls latest changes (in parallel by default)
57
- * 4. If `sync`, auto-installs deps if `package.json` changed during pull
58
- *
59
- * With `sync: false` (the default for read-only diagnostics), steps 3-4 are
60
- * skipped and repos are loaded exactly as checked out — no branch switch, pull,
61
- * install, or clean-workspace check.
62
- *
63
- * Priority for path resolution:
64
- * - `dir` argument (explicit override)
65
- * - Config `repos_dir` setting
66
- * - `DEFAULT_REPOS_DIR` constant
60
+ * Resolves the gitops config's repos through `repos status`: loads the
61
+ * config's registry keys, reports on them, and resolves each to its checkout,
62
+ * in config order. Reads nothing but git state and writes nothing.
67
63
  *
68
- * @param options.git_ops - for testing (defaults to real git operations)
69
- * @param options.npm_ops - for testing (defaults to real npm operations)
70
- * @param options.parallel - whether to load repos in parallel (default: true)
71
- * @param options.concurrency - max concurrent repo loads (default: 5)
72
- * @param options.sync - sync working trees before loading (default: true)
73
- * @param options.allow_dirty - when syncing, tolerate uncommitted changes (default: false)
74
- * @returns initialized config and fully loaded repos ready for operations
75
- * @throws {TaskError} if config loading or repo resolution fails
64
+ * @returns the config, the `repos status` report, and each repo's path and entry
65
+ * @throws {TaskError} if the config is missing, invalid, or lists no repos, `repos status` fails, or any configured repo is unknown, a reference, missing, not a repo, unprobed, or private under a public `host`
76
66
  */
77
- export const get_gitops_ready = async (
78
- options: GetGitopsReadyOptions,
67
+ export const resolve_gitops_repos = async (
68
+ options: ResolveGitopsReposOptions
79
69
  ): Promise<{
80
70
  config_path: string;
81
- repos_dir: string;
82
71
  gitops_config: GitopsConfig;
83
- local_repos: Array<LocalRepo>;
72
+ report: ReposStatusReport;
73
+ local_repo_paths: Array<LocalRepoPath>;
84
74
  }> => {
85
- const {config, dir, download, log, git_ops, npm_ops, parallel, concurrency, sync, allow_dirty} =
86
- options;
75
+ const { config, registry, host, log, repos_ops = default_repos_operations } = options;
87
76
  const config_path = resolve(config);
88
77
  const gitops_config = await import_gitops_config(config_path);
78
+ const keys = gitops_config.repos;
79
+ if (keys.length === 0) {
80
+ throw new TaskError(`No repos are configured in ${config_path}`);
81
+ }
89
82
 
90
- // Priority: explicit dir arg → config repos_dir → default (two dirs up from config)
91
- const repos_dir = resolve_gitops_paths({
92
- config,
93
- dir,
94
- config_repos_dir: gitops_config.repos_dir,
95
- }).repos_dir;
96
-
97
- log?.info(
98
- `resolving gitops configs on the filesystem in ${repos_dir}`,
99
- gitops_config.repos.map((r) => r.repo_url),
100
- );
101
- const resolved_config = resolve_gitops_config(gitops_config, repos_dir);
83
+ log?.info(`reading the state of ${keys.length} repos from \`repos status\``);
84
+ log?.debug('repos status targets', keys);
85
+ const loaded = await load_repos_status({ keys, registry, repos_ops });
86
+ if (!loaded.ok) {
87
+ // an unknown key is the config's to fix, so name the config
88
+ throw new TaskError(
89
+ loaded.error?.kind === 'unknown_entry' ? `${config_path}: ${loaded.message}` : loaded.message
90
+ );
91
+ }
92
+ const { report } = loaded;
102
93
 
103
- const local_repo_paths = await local_repos_ensure({
104
- resolved_config,
105
- repos_dir,
106
- gitops_config,
107
- download,
108
- log,
109
- npm_ops,
110
- });
94
+ const resolved = local_repos_resolve({ keys, report, host, registry });
95
+ if (!resolved.ok) {
96
+ throw new TaskError(`${config_path}: ${resolved.message}`);
97
+ }
111
98
 
112
- const local_repos = await local_repos_load({
113
- local_repo_paths,
114
- log,
115
- git_ops,
116
- npm_ops,
117
- parallel,
118
- concurrency,
119
- sync,
120
- allow_dirty,
121
- });
99
+ return { config_path, gitops_config, report, local_repo_paths: resolved.value };
100
+ };
122
101
 
123
- return {config_path, repos_dir, gitops_config, local_repos};
102
+ /**
103
+ * Central initialization function for the gitops tasks that load libraries:
104
+ * resolves the config's repos through `repos status` (`resolve_gitops_repos`),
105
+ * then loads each repo's library as its working tree sits (`local_repos_load`).
106
+ * Moves no ref; gro caches each library at `.gro/library.json` in its repo, at
107
+ * a clean commit.
108
+ *
109
+ * @returns the loaded repos, in config order
110
+ * @throws {TaskError} if resolving the repos or loading them fails
111
+ */
112
+ export const get_gitops_ready = async (
113
+ options: ResolveGitopsReposOptions
114
+ ): Promise<{ local_repos: Array<LocalRepo> }> => {
115
+ const { local_repo_paths } = await resolve_gitops_repos(options);
116
+ const local_repos = await local_repos_load({ local_repo_paths, log: options.log });
117
+ return { local_repos };
124
118
  };
125
119
 
126
- export interface ResolveGitopsPathsOptions {
127
- config: string;
128
- dir?: string;
129
- config_repos_dir?: string;
120
+ export interface GatePublishReadinessOptions {
121
+ /** The loaded repos; the npm ones are gated. */
122
+ local_repos: ReadonlyArray<LocalRepo>;
123
+ /** A `repos.toml` to use instead of the one `repos` finds walking up from the cwd. */
124
+ registry?: string;
125
+ /**
126
+ * The package names the plan publishes, to say whether a repo's commits ahead
127
+ * of origin go out with its release or stay unpushed.
128
+ */
129
+ publishing?: ReadonlySet<string>;
130
+ log?: Logger;
131
+ repos_ops?: ReposOperations;
130
132
  }
131
133
 
132
- export const resolve_gitops_paths = (
133
- options: ResolveGitopsPathsOptions,
134
- ): {config_path: string; repos_dir: string} => {
135
- const {config, dir, config_repos_dir} = options;
136
- const config_path = resolve(config);
137
- const config_dir = dirname(config_path);
134
+ /**
135
+ * The readiness gate `gitops_publish --wetrun` runs before its confirmation
136
+ * prompt: fetches every npm repo from origin (`repos status <keys…> --fetch
137
+ * --json`, which writes remote-tracking refs and nothing else) and refuses
138
+ * unless each is ready (`check_publish_readiness`) — on its registry branch,
139
+ * clean, idle, in sync with origin or ahead of it, fetched without error, no
140
+ * other live session in its checkout, and nothing left to a person. Each
141
+ * ready repo ahead of origin is logged, saying whether its release push
142
+ * carries those commits or they stay unpushed.
143
+ *
144
+ * Every npm repo, not just those the plan publishes or rewrites: the plan
145
+ * reads each one's working tree (changesets, versions, dependency ranges), so
146
+ * a repo off its branch or behind origin can hide a changeset and leave the
147
+ * plan wrong about what to publish. Changes nothing.
148
+ *
149
+ * @param options - the loaded repos, the `--registry` if any, the package names the plan publishes, a logger, and the `repos` runner
150
+ * @throws {TaskError} if `repos status` fails or any npm repo isn't ready, naming each problem and its fix
151
+ */
152
+ export const gate_publish_readiness = async (
153
+ options: GatePublishReadinessOptions
154
+ ): Promise<void> => {
155
+ const { local_repos, registry, publishing, log, repos_ops = default_repos_operations } = options;
156
+ const npm_repos = local_repos.filter(repo_is_npm);
157
+ const keys = npm_repos.map((r) => r.entry.key);
158
+ if (keys.length === 0) return;
138
159
 
139
- // Priority: explicit dir arg → config repos_dir → default (parent of config dir)
140
- const repos_dir =
141
- dir !== undefined
142
- ? resolve(dir)
143
- : config_repos_dir !== undefined
144
- ? resolve(config_dir, config_repos_dir)
145
- : resolve(config_dir, DEFAULT_REPOS_DIR);
160
+ log?.info(`fetching ${keys.length} npm repos to check they're ready to publish`);
161
+ const loaded = await load_repos_status({ keys, registry, fetch: true, repos_ops });
162
+ if (!loaded.ok) {
163
+ throw new TaskError(`the readiness check failed: ${loaded.message}`);
164
+ }
165
+ const repos_command = to_repos_command(registry);
166
+ const checked = check_publish_readiness({ report: loaded.report, keys, repos_command });
167
+ if (!checked.ok) {
168
+ throw new TaskError(checked.message);
169
+ }
170
+ log?.info(st('green', `all ${keys.length} npm repos are ready to publish`));
171
+ const name_by_key = new Map(npm_repos.map((r) => [r.entry.key, r.library.name] as const));
172
+ for (const ahead of checked.ahead) {
173
+ const name = name_by_key.get(ahead.key);
174
+ const publishes = name !== undefined && publishing?.has(name) === true;
175
+ log?.info(st('yellow', format_readiness_ahead(ahead, publishes, { repos_command })));
176
+ }
177
+ };
146
178
 
147
- return {config_path, repos_dir};
179
+ /**
180
+ * Logs the diagnostics' readiness block as warnings (stderr, so a `--format
181
+ * json` or `markdown` document on stdout stays clean): each npm repo not at
182
+ * rest, and how. Logs nothing when every one is at rest. Reads the entries
183
+ * `repos status` already reported, from local refs.
184
+ *
185
+ * @param local_repos - the loaded repos; the npm ones are reported
186
+ * @param log - where the warnings go
187
+ * @param now - the current time in unix seconds (defaults to the clock)
188
+ */
189
+ export const log_readiness_block = (
190
+ local_repos: ReadonlyArray<LocalRepo>,
191
+ log: Logger,
192
+ now: number = Math.floor(Date.now() / 1000)
193
+ ): void => {
194
+ const not_ready = repos_not_at_rest(local_repos.filter(repo_is_npm).map((r) => r.entry));
195
+ for (const line of format_readiness_block(not_ready, now)) {
196
+ log.warn(st('yellow', line));
197
+ }
148
198
  };
149
199
 
150
- export const import_gitops_config = async (config_path: string): Promise<GitopsConfig> => {
151
- const gitops_config = await load_gitops_config(config_path);
200
+ const import_gitops_config = async (config_path: string): Promise<GitopsConfig> => {
201
+ let gitops_config: GitopsConfig | null;
202
+ try {
203
+ gitops_config = await load_gitops_config(config_path);
204
+ } catch (err) {
205
+ // an invalid config is the user's to fix, not an unexpected task failure
206
+ throw new TaskError(to_error_message(err));
207
+ }
152
208
  if (!gitops_config) {
153
- throw new TaskError(st('red', `No gitops config found at ${print_path(config_path)}`));
209
+ throw new TaskError(st('red', `No gitops config found at ${config_path}`));
154
210
  }
155
211
  return gitops_config;
156
212
  };
@@ -1,37 +1,31 @@
1
- import type {Task} from '@fuzdev/gro';
2
- import {z} from 'zod';
3
- import {styleText as st} from 'node:util';
1
+ import type { Task } from '@fuzdev/gro';
2
+ import { z } from 'zod';
3
+ import { styleText as st } from 'node:util';
4
4
 
5
- import {get_gitops_ready} from './gitops_task_helpers.ts';
6
- import {analyze_repos, type DependencyAnalysis} from './graph_validation.ts';
7
- import {
8
- generate_publishing_plan,
9
- log_publishing_plan,
10
- type PublishingPlan,
11
- } from './publishing_plan.ts';
12
- import {execute_publishing_plan, type PublishingOptions} from './multi_repo_publisher.ts';
13
- import {log_dependency_analysis} from './log_helpers.ts';
14
- import {GITOPS_CONFIG_PATH_DEFAULT} from './gitops_constants.ts';
15
- import {reconcile_ci, repo_has_workflows} from './ci_reconcile.ts';
5
+ import { get_gitops_ready, log_readiness_block } from './gitops_task_helpers.ts';
6
+ import { analyze_repos } from './graph_validation.ts';
7
+ import type { DependencyAnalysis } from './dependency_graph.ts';
8
+ import { generate_publishing_plan, type PublishingPlan } from './publishing_plan.ts';
9
+ import { log_publishing_plan } from './publishing_plan_logging.ts';
10
+ import { execute_publishing_plan, type PublishingOptions } from './multi_repo_publisher.ts';
11
+ import { log_dependency_analysis } from './log_helpers.ts';
12
+ import { GITOPS_CONFIG_PATH_DEFAULT } from './gitops_constants.ts';
13
+ import { reconcile_ci, repo_has_workflows } from './ci_reconcile.ts';
16
14
 
17
15
  /** @nodocs */
18
16
  export const Args = z.strictObject({
19
17
  config: z
20
18
  .string()
21
- .meta({description: 'path to the gitops config file, absolute or relative to the cwd'})
19
+ .meta({ description: 'path to the gitops config file, absolute or relative to the cwd' })
22
20
  .default(GITOPS_CONFIG_PATH_DEFAULT),
23
- dir: z
21
+ registry: z
24
22
  .string()
25
- .meta({description: 'path containing the repos, defaults to the parent of the config dir'})
26
- .optional(),
27
- verbose: z.boolean().meta({description: 'show additional details'}).default(false),
28
- sync: z
29
- .boolean()
30
23
  .meta({
31
24
  description:
32
- 'sync repos (switch branch, pull, install) before validating instead of reading the working tree as-is',
25
+ 'path to the repos.toml registry, when `repos` would not find it walking up from the cwd'
33
26
  })
34
- .default(false),
27
+ .optional(),
28
+ verbose: z.boolean().meta({ description: 'show additional details' }).default(false)
35
29
  });
36
30
  export type Args = z.infer<typeof Args>;
37
31
 
@@ -40,8 +34,8 @@ export const task: Task<Args> = {
40
34
  Args,
41
35
  summary:
42
36
  'validate gitops configuration by running all read-only commands and checking for issues',
43
- run: async ({args, log}) => {
44
- const {config, dir, verbose, sync} = args;
37
+ run: async ({ args, log }) => {
38
+ const { config, registry, verbose } = args;
45
39
 
46
40
  log.info(st('cyan', 'Running Gitops Validation Suite'));
47
41
  log.info(st('dim', 'This runs all read-only commands and checks for consistency.'));
@@ -59,17 +53,18 @@ export const task: Task<Args> = {
59
53
 
60
54
  const start_time = Date.now();
61
55
 
62
- // Load repos once (shared by all commands); read the working tree as-is unless `--sync`
56
+ // Load repos once (shared by all commands), as they sit, and say which aren't at rest
63
57
  log.info(st('dim', 'Loading repositories...'));
64
- const {local_repos} = await get_gitops_ready({config, dir, download: false, sync, log});
58
+ const { local_repos } = await get_gitops_ready({ config, registry, log });
65
59
  log.info(st('dim', ` Found ${local_repos.length} local repos`));
60
+ log_readiness_block(local_repos, log);
66
61
 
67
62
  // 1. Run gitops_analyze
68
63
  log.info(st('yellow', 'Running gitops_analyze...'));
69
64
  const analyze_start = Date.now();
70
65
  try {
71
66
  // Build dependency graph and analyze cycles/wildcards (tolerating cycles)
72
- const {analysis} = analyze_repos(local_repos);
67
+ const { analysis } = analyze_repos(local_repos);
73
68
 
74
69
  const analyze_duration = Date.now() - analyze_start;
75
70
 
@@ -93,7 +88,7 @@ export const task: Task<Args> = {
93
88
  duration: analyze_duration,
94
89
  warning_details,
95
90
  info_details,
96
- analysis,
91
+ analysis
97
92
  });
98
93
 
99
94
  log.info(st('green', ` ✓ gitops_analyze completed in ${analyze_duration}ms`));
@@ -111,7 +106,7 @@ export const task: Task<Args> = {
111
106
  success: false,
112
107
  warnings: 0,
113
108
  errors: 1,
114
- duration: analyze_duration,
109
+ duration: analyze_duration
115
110
  });
116
111
  log.error(st('red', ` ✗ gitops_analyze failed: ${error}`));
117
112
  }
@@ -121,7 +116,7 @@ export const task: Task<Args> = {
121
116
  log.info(st('yellow', 'Running gitops_plan...'));
122
117
  const plan_start = Date.now();
123
118
  try {
124
- const plan = await generate_publishing_plan(local_repos, {verbose});
119
+ const plan = await generate_publishing_plan(local_repos, { verbose });
125
120
  publishing_plan = plan;
126
121
  const plan_duration = Date.now() - plan_start;
127
122
 
@@ -133,12 +128,12 @@ export const task: Task<Args> = {
133
128
  success: errors === 0,
134
129
  warnings,
135
130
  errors,
136
- duration: plan_duration,
131
+ duration: plan_duration
137
132
  });
138
133
 
139
134
  log.info(st('green', ` ✓ gitops_plan completed in ${plan_duration}ms`));
140
135
  if (verbose) {
141
- log_publishing_plan(plan, log, {verbose});
136
+ log_publishing_plan(plan, log, { verbose });
142
137
  }
143
138
  if (warnings > 0) {
144
139
  log.warn(st('yellow', ` ⚠️ Found ${warnings} warning(s)`));
@@ -153,7 +148,7 @@ export const task: Task<Args> = {
153
148
  success: false,
154
149
  warnings: 0,
155
150
  errors: 1,
156
- duration: plan_duration,
151
+ duration: plan_duration
157
152
  });
158
153
  log.error(st('red', ` ✗ gitops_plan failed: ${error}`));
159
154
  }
@@ -164,14 +159,14 @@ export const task: Task<Args> = {
164
159
  try {
165
160
  const options: PublishingOptions = {
166
161
  wetrun: false,
167
- log: undefined, // Silent for validation
162
+ log: undefined // Silent for validation
168
163
  };
169
164
 
170
165
  // Reuse the plan from step 2 (regenerate only if that step failed to produce one).
171
166
  const result = await execute_publishing_plan(
172
167
  local_repos,
173
- publishing_plan ?? (await generate_publishing_plan(local_repos, {verbose})),
174
- options,
168
+ publishing_plan ?? (await generate_publishing_plan(local_repos, { verbose })),
169
+ options
175
170
  );
176
171
  const dry_duration = Date.now() - dry_start;
177
172
 
@@ -184,7 +179,7 @@ export const task: Task<Args> = {
184
179
  success: result.ok,
185
180
  warnings: 0,
186
181
  errors,
187
- duration: dry_duration,
182
+ duration: dry_duration
188
183
  });
189
184
 
190
185
  log.info(st('green', ` ✓ gitops_publish (dry run) completed in ${dry_duration}ms`));
@@ -198,40 +193,35 @@ export const task: Task<Args> = {
198
193
  success: false,
199
194
  warnings: 0,
200
195
  errors: 1,
201
- duration: dry_duration,
196
+ duration: dry_duration
202
197
  });
203
198
  log.error(st('red', ` ✗ gitops_publish (dry run) failed: ${error}`));
204
199
  }
205
200
 
206
- // 4. Reconcile each repo's declared `ci` against actual workflow files on disk.
201
+ // 4. Reconcile each repo's registry-declared `ci` against actual workflow files on disk.
207
202
  log.info(st('yellow', 'Running ci_reconcile...'));
208
203
  const ci_start = Date.now();
209
204
  try {
210
205
  const ci_drift = reconcile_ci(
211
206
  local_repos.map((r) => ({
212
- repo_url: r.repo_config.repo_url,
213
- ci: r.repo_config.ci,
207
+ repo_url: r.entry.url,
208
+ ci: r.entry.ci,
214
209
  has_workflows: repo_has_workflows(r.repo_dir),
215
- // TODO: `local_repos` only ever holds checked-out repos — a missing repo
216
- // throws in `local_repos_ensure` before we reach here — so `checkable` is
217
- // always `true` today. The gate exists for a future caller that loads a
218
- // partial set; until then the skip path is inert and untested.
219
- checkable: true,
220
- archived: r.repo_config.archived,
221
- })),
210
+ archived: r.entry.archived
211
+ }))
222
212
  );
223
213
  const ci_duration = Date.now() - ci_start;
224
214
  const drift_details = ci_drift.map((d) =>
225
215
  d.kind === 'missing_ci'
226
216
  ? `${d.repo_url}: ci=true but no workflow files`
227
- : `${d.repo_url}: ci=false but workflow files present`,
217
+ : `${d.repo_url}: ci=false but workflow files present`
228
218
  );
229
219
  results.push({
230
220
  command: 'ci_reconcile',
231
221
  success: ci_drift.length === 0,
232
222
  warnings: 0,
233
223
  errors: ci_drift.length,
234
- duration: ci_duration,
224
+ duration: ci_duration
235
225
  });
236
226
  if (ci_drift.length === 0) {
237
227
  log.info(st('green', ` ✓ ci_reconcile completed in ${ci_duration}ms`));
@@ -248,7 +238,7 @@ export const task: Task<Args> = {
248
238
  success: false,
249
239
  warnings: 0,
250
240
  errors: 1,
251
- duration: ci_duration,
241
+ duration: ci_duration
252
242
  });
253
243
  log.error(st('red', ` ✗ ci_reconcile failed: ${error}`));
254
244
  }
@@ -295,7 +285,7 @@ export const task: Task<Args> = {
295
285
  log.info(st('green', '✓ All validation checks passed'));
296
286
  if (total_warnings > 0) {
297
287
  log.warn(
298
- st('yellow', `⚠️ Note: ${total_warnings} warning(s) found - review output above.`),
288
+ st('yellow', `⚠️ Note: ${total_warnings} warning(s) found - review output above.`)
299
289
  );
300
290
  }
301
291
  } else {
@@ -305,5 +295,5 @@ export const task: Task<Args> = {
305
295
  log.error(st('red', '❌ Validation failed - review the errors above.'));
306
296
  throw new Error('Validation failed');
307
297
  }
308
- },
298
+ }
309
299
  };