@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
@@ -1,22 +1,20 @@
1
- import { strip_end } from '@fuzdev/fuz_util/string.ts';
2
1
  import { to_error_message } from '@fuzdev/fuz_util/error.ts';
3
2
  import { library_json_from_modules, type LibraryJson } from '@fuzdev/fuz_util/library_json.ts';
4
3
  import type { PackageJson } from '@fuzdev/fuz_util/package_json.ts';
4
+ import type { Result } from '@fuzdev/fuz_util/result.ts';
5
5
  import { Library } from '@fuzdev/fuz_ui/library.svelte.ts';
6
6
  import { existsSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
8
  import { TaskError } from '@fuzdev/gro';
9
9
  import { library_load_from_repo } from '@fuzdev/gro/library_load.ts';
10
10
  import type { Logger } from '@fuzdev/fuz_util/log.ts';
11
- import { spawn_out } from '@fuzdev/fuz_util/process.ts';
12
11
  import { map_concurrent_settled } from '@fuzdev/fuz_util/async.ts';
13
- import type { GitOperations, NpmOperations } from './operations.ts';
14
- import { default_git_operations, default_npm_operations } from './operations_defaults.ts';
15
12
 
16
- import type { GitopsConfig, GitopsRepoConfig } from './gitops_config.ts';
17
- import type { ResolvedGitopsConfig } from './resolved_gitops_config.ts';
13
+ import { gitops_config_leaked_private_repos } from './gitops_config.ts';
14
+ import type { ReposEntryStatus, ReposStatusReport } from './repos_status.ts';
18
15
  import { GITOPS_CONCURRENCY_DEFAULT } from './gitops_constants.ts';
19
16
  import { cargo_toml_load } from './cargo_toml.ts';
17
+ import { to_repos_command } from './repos_status_load.ts';
20
18
 
21
19
  /**
22
20
  * Fully loaded local repo with `Library` and extracted dependency data.
@@ -35,190 +33,123 @@ export interface LocalRepo {
35
33
  /** The repo's full `package.json` (with `dependencies`/`devDependencies`). */
36
34
  package_json: PackageJson;
37
35
  repo_dir: string;
38
- repo_git_ssh_url: string;
39
- repo_config: GitopsRepoConfig;
36
+ /**
37
+ * The repo's registry entry as `repos status --json` reported it: its
38
+ * `branch`, `visibility`, `ci`, and `archived`, and its git state.
39
+ */
40
+ entry: ReposEntryStatus;
40
41
  dependencies?: Map<string, string>;
41
42
  dev_dependencies?: Map<string, string>;
42
43
  peer_dependencies?: Map<string, string>;
43
44
  }
44
45
 
45
46
  /**
46
- * A repo that has been located on the filesystem (path exists).
47
- * Used before loading - just filesystem/git concerns.
47
+ * A configured repo resolved through `repos status`: present on disk, before
48
+ * its library is loaded. See `local_repos_resolve`.
48
49
  */
49
50
  export interface LocalRepoPath {
50
- type: 'local_repo_path';
51
- repo_name: string; // from URL parsing (for display/logging before Library loaded)
51
+ /** The repo's registry key (for display/logging before `Library` is loaded). */
52
+ repo_name: string;
53
+ /** The workspace root joined with the entry's `dir`. */
52
54
  repo_dir: string;
55
+ /** The registry's HTTPS URL for the repo. */
53
56
  repo_url: string;
54
- repo_git_ssh_url: string;
55
- repo_config: GitopsRepoConfig;
57
+ /** The repo's registry entry as `repos status --json` reported it. */
58
+ entry: ReposEntryStatus;
56
59
  }
57
60
 
58
61
  /**
59
- * A repo that is missing from the filesystem (needs cloning).
62
+ * Resolves a gitops config's registry keys against a `repos status` report,
63
+ * in config order. Every key must name an owned repo that's present and was
64
+ * probed; each that doesn't is a problem, and any problem fails the whole
65
+ * resolve, naming them all.
66
+ *
67
+ * @param options.keys - the config's registry keys, in config order
68
+ * @param options.report - `repos status <keys…> --json`'s report
69
+ * @param options.host - the package whose generated data the run writes; when public, a private repo is a problem (`gitops_config_leaked_private_repos`)
70
+ * @param options.registry - the `--registry` the report was made with, repeated in the hints
71
+ * @returns the repos in config order, or a message listing every problem
60
72
  */
61
- export interface LocalRepoMissing {
62
- type: 'local_repo_missing';
63
- repo_name: string;
64
- repo_url: string;
65
- repo_git_ssh_url: string;
66
- repo_config: GitopsRepoConfig;
67
- }
73
+ export const local_repos_resolve = (options: {
74
+ keys: ReadonlyArray<string>;
75
+ report: ReposStatusReport;
76
+ host?: { name: string; private: boolean };
77
+ registry?: string;
78
+ }): Result<{ value: Array<LocalRepoPath> }, { message: string; problems: Array<string> }> => {
79
+ const { keys, report, host, registry } = options;
80
+ const repos_command = to_repos_command(registry);
81
+ const by_key = new Map(report.entries.map((e) => [e.key, e] as const));
82
+
83
+ const problems: Array<string> = [];
84
+ const resolved: Array<LocalRepoPath> = [];
85
+ for (const key of keys) {
86
+ const entry = by_key.get(key);
87
+ if (!entry) {
88
+ // `repos` also takes a dir name or path as a target, reporting the entry by its key
89
+ const by_dir = report.entries.find((e) => e.dir === key);
90
+ problems.push(
91
+ by_dir
92
+ ? `\`${key}\` is the dir of \`${by_dir.key}\`, not a registry key — list \`${by_dir.key}\``
93
+ : `\`${key}\` isn't in the \`repos status\` report — is it a registry key?`
94
+ );
95
+ continue;
96
+ }
97
+ const repo_dir = join(report.workspace, entry.dir);
98
+ if (entry.kind === 'reference') {
99
+ problems.push(`\`${key}\` is a third-party reference, not an owned repo`);
100
+ } else if (entry.presence.kind === 'missing') {
101
+ problems.push(
102
+ `\`${key}\` is missing at ${repo_dir} — \`${repos_command} sync ${key}\` clones it`
103
+ );
104
+ } else if (entry.presence.kind === 'not_a_repo') {
105
+ problems.push(`\`${key}\`: ${repo_dir} isn't a git repo`);
106
+ } else if (entry.probe_error !== null) {
107
+ problems.push(`\`${key}\`: probing ${repo_dir} failed: ${entry.probe_error.message}`);
108
+ } else {
109
+ resolved.push({ repo_name: key, repo_dir, repo_url: entry.url, entry });
110
+ }
111
+ }
112
+
113
+ if (host) {
114
+ const configured = keys.map((k) => by_key.get(k)).filter((e) => e !== undefined);
115
+ for (const leaked of gitops_config_leaked_private_repos(configured, host.private)) {
116
+ problems.push(
117
+ `\`${leaked.key}\` is private, and ${host.name} is a public package whose generated repos.json would publish its metadata`
118
+ );
119
+ }
120
+ }
121
+
122
+ if (problems.length) {
123
+ return {
124
+ ok: false,
125
+ message: `${problems.length === 1 ? 'a configured repo' : 'configured repos'} can't be loaded:\n ${problems.join('\n ')}`,
126
+ problems
127
+ };
128
+ }
129
+ return { ok: true, value: resolved };
130
+ };
68
131
 
69
132
  /**
70
- * Loads repo data, optionally syncing the working tree first.
133
+ * Loads a resolved repo as its working tree sits (the tasks read where each
134
+ * repo sits from `repos status`, and `repos sync` moves them). Moves no ref;
135
+ * gro caches the library at `.gro/library.json` in the repo, at a clean commit.
71
136
  *
72
- * When `sync` is `false` (the default for read-only diagnostics like
73
- * `gitops_analyze`/`gitops_plan`), the repo is loaded exactly as it sits on
74
- * disk — no branch switch, pull, install, or clean-workspace check. This makes
75
- * those commands safe to run on an active workspace with uncommitted changes or
76
- * feature branches checked out.
137
+ * 1. Loads `library_json` via `library_load_from_repo` (svelte-docinfo analysis)
138
+ * 2. Creates `Library` and extracts dependency maps
77
139
  *
78
- * When `sync` is `true` (used by `gitops_sync`), the working tree is brought in
79
- * line with the configured branch first:
80
- * 1. Records current commit hash (for detecting changes)
81
- * 2. Switches to target branch if needed (requires clean workspace unless `allow_dirty`)
82
- * 3. Pulls latest changes from remote (skipped for local-only repos)
83
- * 4. Validates workspace is clean after pull (skipped if `allow_dirty`)
84
- * 5. Auto-installs dependencies if `package.json` changed
140
+ * A repo with no `package.json` but a Rust `Cargo.toml` loads as a
141
+ * dashboard-only `cargo` repo instead.
85
142
  *
86
- * Either way it then:
87
- * 6. Loads `library_json` via `library_load_from_repo` (svelte-docinfo analysis)
88
- * 7. Creates `Library` and extracts dependency maps
89
- *
90
- * @param sync - sync the working tree to the configured branch before loading (default `true`)
91
- * @param allow_dirty - when syncing, tolerate uncommitted changes instead of throwing (default `false`)
92
- * @throws {TaskError} if syncing fails (dirty workspace, branch switch, install) or analysis fails
143
+ * @throws {TaskError} if the analysis fails
93
144
  */
94
145
  export const local_repo_load = async ({
95
146
  local_repo_path,
96
- log: _log,
97
- git_ops = default_git_operations,
98
- npm_ops = default_npm_operations,
99
- sync = true,
100
- allow_dirty = false
147
+ log
101
148
  }: {
102
149
  local_repo_path: LocalRepoPath;
103
150
  log?: Logger;
104
- git_ops?: GitOperations;
105
- npm_ops?: NpmOperations;
106
- sync?: boolean;
107
- allow_dirty?: boolean;
108
151
  }): Promise<LocalRepo> => {
109
- const { repo_config, repo_dir, repo_name, repo_git_ssh_url } = local_repo_path;
110
-
111
- if (sync) {
112
- // Record commit hash before any changes
113
- const commit_before_result = await git_ops.current_commit_hash({ cwd: repo_dir });
114
- if (!commit_before_result.ok) {
115
- throw new TaskError(
116
- `Failed to get commit hash in ${repo_dir}: ${commit_before_result.message}`
117
- );
118
- }
119
- const commit_before = commit_before_result.value;
120
-
121
- // Switch to target branch if needed
122
- const branch_result = await git_ops.current_branch_name({ cwd: repo_dir });
123
- if (!branch_result.ok) {
124
- throw new TaskError(`Failed to get current branch in ${repo_dir}: ${branch_result.message}`);
125
- }
126
-
127
- const switched_branches = branch_result.value !== repo_config.branch;
128
- if (switched_branches) {
129
- // Guard the switch on a clean workspace unless the caller opts into `allow_dirty`,
130
- // in which case we let `git checkout` itself fail loudly if it can't proceed.
131
- if (!allow_dirty) {
132
- const clean_result = await git_ops.check_clean_workspace({ cwd: repo_dir });
133
- if (!clean_result.ok) {
134
- throw new TaskError(`Failed to check workspace in ${repo_dir}: ${clean_result.message}`);
135
- }
136
-
137
- if (!clean_result.value) {
138
- throw new TaskError(
139
- `Repo ${repo_dir} is not on branch "${repo_config.branch}" and the workspace is unclean, blocking switch`
140
- );
141
- }
142
- }
143
-
144
- const checkout_result = await git_ops.checkout({ branch: repo_config.branch, cwd: repo_dir });
145
- if (!checkout_result.ok) {
146
- throw new TaskError(
147
- `Failed to checkout branch "${repo_config.branch}" in ${repo_dir}: ${checkout_result.message}`
148
- );
149
- }
150
- }
151
-
152
- // Only pull if remote exists (skip for local-only repos, test fixtures)
153
- const origin_result = await git_ops.has_remote({ remote: 'origin', cwd: repo_dir });
154
- if (!origin_result.ok) {
155
- throw new TaskError(`Failed to check for remote in ${repo_dir}: ${origin_result.message}`);
156
- }
157
-
158
- if (origin_result.value) {
159
- // Pull the configured branch explicitly. Without a branch, `git pull origin`
160
- // targets the remote's default branch (origin/HEAD), which for a repo checked
161
- // out on a non-default branch rebases the wrong branch onto it.
162
- const pull_result = await git_ops.pull({ branch: repo_config.branch, cwd: repo_dir });
163
- if (!pull_result.ok) {
164
- throw new TaskError(`Failed to pull in ${repo_dir}: ${pull_result.message}`);
165
- }
166
- }
167
-
168
- // Check clean workspace after pull to ensure we're in a good state
169
- // (skipped when `allow_dirty`, since uncommitted changes are expected then)
170
- if (!allow_dirty) {
171
- const clean_after_result = await git_ops.check_clean_workspace({ cwd: repo_dir });
172
- if (!clean_after_result.ok) {
173
- throw new TaskError(
174
- `Failed to check workspace in ${repo_dir}: ${clean_after_result.message}`
175
- );
176
- }
177
-
178
- if (!clean_after_result.value) {
179
- throw new TaskError(
180
- `Workspace ${repo_dir} is unclean after pulling branch "${repo_config.branch}"`
181
- );
182
- }
183
- }
184
-
185
- // Record commit hash after pull
186
- const commit_after_result = await git_ops.current_commit_hash({ cwd: repo_dir });
187
- if (!commit_after_result.ok) {
188
- throw new TaskError(
189
- `Failed to get commit hash in ${repo_dir}: ${commit_after_result.message}`
190
- );
191
- }
192
- const commit_after = commit_after_result.value;
193
-
194
- // Track if we got new commits
195
- const got_new_commits = commit_before !== commit_after;
196
-
197
- // Only install if package.json changed
198
- if (got_new_commits) {
199
- const changed_result = await git_ops.has_file_changed({
200
- from_commit: commit_before,
201
- to_commit: commit_after,
202
- file_path: 'package.json',
203
- cwd: repo_dir
204
- });
205
-
206
- if (!changed_result.ok) {
207
- throw new TaskError(
208
- `Failed to check if package.json changed in ${repo_dir}: ${changed_result.message}`
209
- );
210
- }
211
-
212
- if (changed_result.value) {
213
- const install_result = await npm_ops.install({ cwd: repo_dir });
214
- if (!install_result.ok) {
215
- throw new TaskError(
216
- `Failed to install dependencies in ${repo_dir}: ${install_result.message}${install_result.stderr ? `\n${install_result.stderr}` : ''}`
217
- );
218
- }
219
- }
220
- }
221
- }
152
+ const { entry, repo_dir, repo_name } = local_repo_path;
222
153
 
223
154
  // A repo with no `package.json` but a Rust `Cargo.toml` isn't an npm package and can't be
224
155
  // analyzed as a library. Load it as a dashboard-only `cargo` repo (CI, PRs, identity) that
@@ -232,14 +163,10 @@ export const local_repo_load = async ({
232
163
  let library_json: LibraryJson;
233
164
  let package_json: PackageJson;
234
165
  try {
235
- ({ library_json, package_json } = await library_load_from_repo(repo_dir, { log: _log }));
166
+ ({ library_json, package_json } = await library_load_from_repo(repo_dir, { log }));
236
167
  } catch (err) {
237
- const message = to_error_message(err);
238
- _log?.warn(
239
- `Failed to load library metadata for repo "${repo_name}" in ${repo_dir}: ${message}`
240
- );
241
168
  throw new TaskError(
242
- `Failed to load library metadata for repo "${repo_name}" in ${repo_dir}: ${message}`
169
+ `Failed to load library metadata for repo "${repo_name}" in ${repo_dir}: ${to_error_message(err)}`
243
170
  );
244
171
  }
245
172
  const library = new Library(library_json);
@@ -249,8 +176,7 @@ export const local_repo_load = async ({
249
176
  library,
250
177
  package_json,
251
178
  repo_dir,
252
- repo_git_ssh_url,
253
- repo_config
179
+ entry
254
180
  };
255
181
 
256
182
  // Extract dependencies from the full package_json
@@ -269,8 +195,8 @@ export const local_repo_load = async ({
269
195
 
270
196
  /**
271
197
  * Whether a repo is an npm package and so participates in publishing and
272
- * dependency analysis. Non-npm repos (e.g. Rust `cargo` repos) are still synced
273
- * and rendered on the dashboard, but excluded from the changeset cascade.
198
+ * dependency analysis. Non-npm repos (e.g. Rust `cargo` repos) are still
199
+ * rendered on the dashboard, but excluded from the changeset cascade.
274
200
  */
275
201
  export const repo_is_npm = (repo: LocalRepo): boolean => repo.kind === 'npm';
276
202
 
@@ -278,7 +204,7 @@ export const repo_is_npm = (repo: LocalRepo): boolean => repo.kind === 'npm';
278
204
  * Loads a non-npm Rust repo as a dashboard-only `LocalRepo`. It has no
279
205
  * `package.json`, so there's no `svelte-docinfo` analysis and no npm dependency
280
206
  * graph — a lightweight `Library` is synthesized from the repo's `Cargo.toml`
281
- * (best-effort name/version/description) and its configured repo URL, which is
207
+ * (best-effort name/version/description) and its registry URL, which is
282
208
  * what the dashboard renders (CI status, PRs, identity). Marked `private` so it
283
209
  * never reads as an npm publish target, and tagged `cargo` so the publishing and
284
210
  * analysis paths skip it (see `repo_is_npm`).
@@ -288,12 +214,12 @@ const local_repo_load_cargo = async ({
288
214
  }: {
289
215
  local_repo_path: LocalRepoPath;
290
216
  }): Promise<LocalRepo> => {
291
- const { repo_config, repo_dir, repo_name, repo_url, repo_git_ssh_url } = local_repo_path;
217
+ const { entry, repo_dir, repo_name, repo_url } = local_repo_path;
292
218
 
293
219
  const cargo = await cargo_toml_load(repo_dir);
294
220
 
295
- // A Cargo workspace root has no `name`, so fall back to the repo name parsed from
296
- // the URL; the config's `repo_url` is the authoritative `repository`.
221
+ // A Cargo workspace root has no `name` or `repository`, so fall back to the
222
+ // registry key and URL.
297
223
  const package_json: PackageJson = {
298
224
  name: cargo?.name ?? repo_name,
299
225
  version: cargo?.version ?? '0.0.0',
@@ -309,87 +235,26 @@ const local_repo_load_cargo = async ({
309
235
  library,
310
236
  package_json,
311
237
  repo_dir,
312
- repo_git_ssh_url,
313
- repo_config
238
+ entry
314
239
  };
315
240
  };
316
241
 
317
- export const local_repos_ensure = async ({
318
- resolved_config,
319
- repos_dir,
320
- gitops_config,
321
- download,
322
- log,
323
- npm_ops = default_npm_operations
324
- }: {
325
- resolved_config: ResolvedGitopsConfig;
326
- repos_dir: string;
327
- gitops_config: GitopsConfig;
328
- download: boolean;
329
- log?: Logger;
330
- npm_ops?: NpmOperations;
331
- }): Promise<Array<LocalRepoPath>> => {
332
- let local_repo_paths: Array<LocalRepoPath> | null = null;
333
-
334
- if (!resolved_config.local_repos_missing) {
335
- local_repo_paths = resolved_config.local_repo_paths;
336
- } else {
337
- if (download) {
338
- const downloaded = await download_repos({
339
- repos_dir,
340
- local_repos_missing: resolved_config.local_repos_missing,
341
- log,
342
- npm_ops
343
- });
344
- local_repo_paths = (resolved_config.local_repo_paths ?? [])
345
- .concat(downloaded)
346
- .sort(
347
- (a, b) =>
348
- gitops_config.repos.findIndex((r) => r.repo_url === a.repo_url) -
349
- gitops_config.repos.findIndex((r) => r.repo_url === b.repo_url)
350
- );
351
- } else {
352
- log?.error(
353
- `Failed to resolve local repos in ${repos_dir} - do you need to pass \`--download\` or configure the directory?`, // TODO leaking task impl details
354
- resolved_config.local_repos_missing.map((r) => r.repo_url)
355
- );
356
- throw new TaskError('Failed to resolve local configs');
357
- }
358
- }
359
-
360
- if (!local_repo_paths) {
361
- throw new TaskError('No repos are configured in `gitops_config.ts`');
362
- }
363
-
364
- return local_repo_paths;
365
- };
366
-
367
242
  export const local_repos_load = async ({
368
243
  local_repo_paths,
369
244
  log,
370
- git_ops = default_git_operations,
371
- npm_ops = default_npm_operations,
372
245
  parallel = true,
373
- concurrency = GITOPS_CONCURRENCY_DEFAULT,
374
- sync = true,
375
- allow_dirty = false
246
+ concurrency = GITOPS_CONCURRENCY_DEFAULT
376
247
  }: {
377
248
  local_repo_paths: Array<LocalRepoPath>;
378
249
  log?: Logger;
379
- git_ops?: GitOperations;
380
- npm_ops?: NpmOperations;
381
250
  parallel?: boolean;
382
251
  concurrency?: number;
383
- sync?: boolean;
384
- allow_dirty?: boolean;
385
252
  }): Promise<Array<LocalRepo>> => {
386
253
  if (!parallel) {
387
- // Sequential loading (original behavior)
254
+ // sequential loading
388
255
  const loaded: Array<LocalRepo> = [];
389
256
  for (const local_repo_path of local_repo_paths) {
390
- loaded.push(
391
- await local_repo_load({ local_repo_path, log, git_ops, npm_ops, sync, allow_dirty })
392
- );
257
+ loaded.push(await local_repo_load({ local_repo_path, log }));
393
258
  }
394
259
  return loaded;
395
260
  }
@@ -399,7 +264,7 @@ export const local_repos_load = async ({
399
264
  local_repo_paths,
400
265
  concurrency,
401
266
  async (local_repo_path) => {
402
- return local_repo_load({ local_repo_path, log, git_ops, npm_ops, sync, allow_dirty });
267
+ return local_repo_load({ local_repo_path, log });
403
268
  }
404
269
  );
405
270
 
@@ -428,75 +293,3 @@ export const local_repos_load = async ({
428
293
 
429
294
  return loaded;
430
295
  };
431
-
432
- export const local_repo_locate = ({
433
- repo_config,
434
- repos_dir
435
- }: {
436
- repo_config: GitopsRepoConfig;
437
- repos_dir: string;
438
- }): LocalRepoPath | LocalRepoMissing => {
439
- const { repo_url } = repo_config;
440
- const repo_name = strip_end(repo_url, '/').split('/').at(-1);
441
- if (!repo_name) throw Error('Invalid `repo_config.repo_url` ' + repo_url);
442
-
443
- const repo_git_ssh_url = to_repo_git_ssh_url(repo_url);
444
-
445
- const repo_dir = repo_config.repo_dir ?? join(repos_dir, repo_name);
446
- if (!existsSync(repo_dir)) {
447
- return { type: 'local_repo_missing', repo_name, repo_url, repo_git_ssh_url, repo_config };
448
- }
449
-
450
- return {
451
- type: 'local_repo_path',
452
- repo_name,
453
- repo_dir,
454
- repo_url,
455
- repo_git_ssh_url,
456
- repo_config
457
- };
458
- };
459
-
460
- const to_repo_git_ssh_url = (repo_url: string): string => {
461
- const url = new URL(repo_url);
462
- return `git@${url.hostname}:${url.pathname.substring(1)}`;
463
- };
464
-
465
- const download_repos = async ({
466
- repos_dir,
467
- local_repos_missing,
468
- log,
469
- npm_ops = default_npm_operations
470
- }: {
471
- repos_dir: string;
472
- local_repos_missing: Array<LocalRepoMissing>;
473
- log?: Logger;
474
- npm_ops?: NpmOperations;
475
- }): Promise<Array<LocalRepoPath>> => {
476
- const resolved: Array<LocalRepoPath> = [];
477
- for (const { repo_config, repo_git_ssh_url } of local_repos_missing) {
478
- log?.info(`cloning repo ${repo_git_ssh_url} to ${repos_dir}`);
479
- const clone_result = await spawn_out('git', ['clone', repo_git_ssh_url], { cwd: repos_dir });
480
- if (!clone_result.result.ok) {
481
- throw new TaskError(
482
- `Failed to clone repo ${repo_git_ssh_url} to ${repos_dir}${clone_result.stderr ? ': ' + clone_result.stderr.trim() : ''}`
483
- );
484
- }
485
- const local_repo = local_repo_locate({ repo_config, repos_dir });
486
- if (local_repo.type === 'local_repo_missing') {
487
- throw new TaskError(
488
- `Failed to clone repo ${repo_git_ssh_url} to ${repos_dir}: directory not found after clone`
489
- );
490
- }
491
- // Always install dependencies after cloning
492
- log?.info(`installing dependencies for newly cloned repo ${local_repo.repo_dir}`);
493
- const install_result = await npm_ops.install({ cwd: local_repo.repo_dir });
494
- if (!install_result.ok) {
495
- throw new TaskError(
496
- `Failed to install dependencies in ${local_repo.repo_dir}: ${install_result.message}${install_result.stderr ? `\n${install_result.stderr}` : ''}`
497
- );
498
- }
499
- resolved.push(local_repo);
500
- }
501
- return resolved;
502
- };
@@ -1,15 +1,13 @@
1
1
  import type { Logger } from '@fuzdev/fuz_util/log.ts';
2
2
  import { styleText as st } from 'node:util';
3
3
 
4
- import type { DependencyGraphBuilder } from './dependency_graph.ts';
4
+ import type { DependencyAnalysis } from './dependency_graph.ts';
5
5
 
6
6
  /**
7
7
  * Formats wildcard dependencies as styled strings.
8
8
  * Returns array of lines for inclusion in output.
9
9
  */
10
- export const format_wildcard_dependencies = (
11
- analysis: ReturnType<DependencyGraphBuilder['analyze']>
12
- ): Array<string> => {
10
+ export const format_wildcard_dependencies = (analysis: DependencyAnalysis): Array<string> => {
13
11
  if (analysis.wildcard_deps.length === 0) return [];
14
12
 
15
13
  const lines: Array<string> = [];
@@ -27,9 +25,7 @@ export const format_wildcard_dependencies = (
27
25
  * Formats dev circular dependencies as styled strings.
28
26
  * Returns array of lines for inclusion in output.
29
27
  */
30
- export const format_dev_cycles = (
31
- analysis: ReturnType<DependencyGraphBuilder['analyze']>
32
- ): Array<string> => {
28
+ export const format_dev_cycles = (analysis: DependencyAnalysis): Array<string> => {
33
29
  if (analysis.dev_cycles.length === 0) return [];
34
30
 
35
31
  const lines: Array<string> = [];
@@ -50,9 +46,7 @@ export const format_dev_cycles = (
50
46
  * Formats production/peer circular dependencies as styled strings.
51
47
  * Returns array of lines for inclusion in output.
52
48
  */
53
- export const format_production_cycles = (
54
- analysis: ReturnType<DependencyGraphBuilder['analyze']>
55
- ): Array<string> => {
49
+ export const format_production_cycles = (analysis: DependencyAnalysis): Array<string> => {
56
50
  if (analysis.production_cycles.length === 0) return [];
57
51
 
58
52
  const lines: Array<string> = [];
@@ -74,7 +68,7 @@ export const format_production_cycles = (
74
68
  * Wildcard dependencies require attention and should be reviewed.
75
69
  */
76
70
  export const log_wildcard_dependencies = (
77
- analysis: ReturnType<DependencyGraphBuilder['analyze']>,
71
+ analysis: DependencyAnalysis,
78
72
  log: Logger,
79
73
  indent = ''
80
74
  ): void => {
@@ -88,11 +82,7 @@ export const log_wildcard_dependencies = (
88
82
  * Logs dev circular dependencies as info.
89
83
  * Dev cycles are normal and non-blocking, so they're informational, not warnings.
90
84
  */
91
- export const log_dev_cycles = (
92
- analysis: ReturnType<DependencyGraphBuilder['analyze']>,
93
- log: Logger,
94
- indent = ''
95
- ): void => {
85
+ export const log_dev_cycles = (analysis: DependencyAnalysis, log: Logger, indent = ''): void => {
96
86
  const lines = format_dev_cycles(analysis);
97
87
  for (const line of lines) {
98
88
  log.info(indent + line);
@@ -104,7 +94,7 @@ export const log_dev_cycles = (
104
94
  * Production cycles block publishing and must be resolved.
105
95
  */
106
96
  export const log_production_cycles = (
107
- analysis: ReturnType<DependencyGraphBuilder['analyze']>,
97
+ analysis: DependencyAnalysis,
108
98
  log: Logger,
109
99
  indent = ''
110
100
  ): void => {
@@ -119,7 +109,7 @@ export const log_production_cycles = (
119
109
  * Convenience function that calls all three logging functions in order.
120
110
  */
121
111
  export const log_dependency_analysis = (
122
- analysis: ReturnType<DependencyGraphBuilder['analyze']>,
112
+ analysis: DependencyAnalysis,
123
113
  log: Logger,
124
114
  indent = ''
125
115
  ): void => {