@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,22 +1,20 @@
1
- import {strip_end} from '@fuzdev/fuz_util/string.ts';
2
- import {to_error_message} from '@fuzdev/fuz_util/error.ts';
3
- import {library_json_from_modules, type LibraryJson} from '@fuzdev/fuz_util/library_json.ts';
4
- import type {PackageJson} from '@fuzdev/fuz_util/package_json.ts';
5
- import {Library} from '@fuzdev/fuz_ui/library.svelte.ts';
6
- import {existsSync} from 'node:fs';
7
- import {join} from 'node:path';
8
- import {TaskError} from '@fuzdev/gro';
9
- import {library_load_from_repo} from '@fuzdev/gro/library_load.ts';
10
- import type {Logger} from '@fuzdev/fuz_util/log.ts';
11
- import {spawn_out} from '@fuzdev/fuz_util/process.ts';
12
- 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
-
16
- import type {GitopsConfig, GitopsRepoConfig} from './gitops_config.ts';
17
- import type {ResolvedGitopsConfig} from './resolved_gitops_config.ts';
18
- import {GITOPS_CONCURRENCY_DEFAULT} from './gitops_constants.ts';
19
- import {cargo_toml_load} from './cargo_toml.ts';
1
+ import { to_error_message } from '@fuzdev/fuz_util/error.ts';
2
+ import { library_json_from_modules, type LibraryJson } from '@fuzdev/fuz_util/library_json.ts';
3
+ import type { PackageJson } from '@fuzdev/fuz_util/package_json.ts';
4
+ import type { Result } from '@fuzdev/fuz_util/result.ts';
5
+ import { Library } from '@fuzdev/fuz_ui/library.svelte.ts';
6
+ import { existsSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { TaskError } from '@fuzdev/gro';
9
+ import { library_load_from_repo } from '@fuzdev/gro/library_load.ts';
10
+ import type { Logger } from '@fuzdev/fuz_util/log.ts';
11
+ import { map_concurrent_settled } from '@fuzdev/fuz_util/async.ts';
12
+
13
+ import { gitops_config_leaked_private_repos } from './gitops_config.ts';
14
+ import type { ReposEntryStatus, ReposStatusReport } from './repos_status.ts';
15
+ import { GITOPS_CONCURRENCY_DEFAULT } from './gitops_constants.ts';
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,211 +33,140 @@ 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.
71
- *
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.
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.
77
136
  *
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
137
+ * 1. Loads `library_json` via `library_load_from_repo` (svelte-docinfo analysis)
138
+ * 2. Creates `Library` and extracts dependency maps
85
139
  *
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
140
+ * A repo with no `package.json` but a Rust `Cargo.toml` loads as a
141
+ * dashboard-only `cargo` repo instead.
89
142
  *
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
225
156
  // publishing/analysis skips. Anything else falls through to the npm loader below, whose
226
157
  // error covers a genuinely missing or unreadable manifest.
227
158
  if (!existsSync(join(repo_dir, 'package.json')) && existsSync(join(repo_dir, 'Cargo.toml'))) {
228
- return local_repo_load_cargo({local_repo_path});
159
+ return local_repo_load_cargo({ local_repo_path });
229
160
  }
230
161
 
231
162
  // Load library metadata via svelte-docinfo analysis (cached under `.gro/library.json`).
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,28 +204,28 @@ 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`).
285
211
  */
286
212
  const local_repo_load_cargo = async ({
287
- local_repo_path,
213
+ local_repo_path
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',
300
226
  repository: cargo?.repository ?? repo_url,
301
227
  private: true,
302
- ...(cargo?.description ? {description: cargo.description} : null),
228
+ ...(cargo?.description ? { description: cargo.description } : null)
303
229
  };
304
230
 
305
231
  const library = new Library(library_json_from_modules(package_json, []));
@@ -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,13 +264,13 @@ 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});
403
- },
267
+ return local_repo_load({ local_repo_path, log });
268
+ }
404
269
  );
405
270
 
406
271
  // Check for failures and collect successes
407
272
  const loaded: Array<LocalRepo> = [];
408
- const errors: Array<{repo_name: string; error: string}> = [];
273
+ const errors: Array<{ repo_name: string; error: string }> = [];
409
274
 
410
275
  for (let i = 0; i < results.length; i++) {
411
276
  const result = results[i]!;
@@ -415,7 +280,7 @@ export const local_repos_load = async ({
415
280
  const repo_path = local_repo_paths[i]!;
416
281
  errors.push({
417
282
  repo_name: repo_path.repo_name,
418
- error: String(result.reason),
283
+ error: String(result.reason)
419
284
  });
420
285
  }
421
286
  }
@@ -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
- };