@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
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Reads fleet git state from the Rust `repos` binary: runs
3
+ * `repos status <keys…> --json` through the injected `ReposOperations` and
4
+ * parses what it printed with the `repos_status.ts` mirror.
5
+ *
6
+ * Parsing is pure (`parse_repos_status_output`): the format version is read
7
+ * first, so a binary and npm package out of step fail with both versions and
8
+ * the install command rather than a schema error; then the document is parsed
9
+ * strictly as a report or, when it carries `error`, as an error document,
10
+ * whose kind maps to a message saying what to change.
11
+ *
12
+ * @module
13
+ */
14
+ import { z } from 'zod';
15
+ import { REPOS_STATUS_FORMAT_VERSION, ReposStatusErrorReport, ReposStatusReport } from './repos_status.js';
16
+ /**
17
+ * How to install the `repos` binary, run in a `fuz_repos` checkout. The npm
18
+ * package doesn't carry the binary, so the two are installed separately.
19
+ */
20
+ export const REPOS_INSTALL_COMMAND = 'cargo install --path crates/fuz_repos --locked';
21
+ /**
22
+ * The `repos` invocation a suggested fix names: `repos`, or `repos --registry
23
+ * <path>` when the run passed one, the path POSIX-shell-quoted when it needs it.
24
+ *
25
+ * @param registry - the run's `--registry`, if any
26
+ * @returns the command prefix, ready for a subcommand
27
+ */
28
+ export const to_repos_command = (registry) => registry === undefined ? 'repos' : `repos --registry ${shell_quote(registry)}`;
29
+ const shell_quote = (arg) => /^[\w@%+=:,./-]+$/.test(arg) ? arg : `'${arg.replaceAll("'", `'\\''`)}'`;
30
+ /**
31
+ * Parses what `repos status --json` printed into its report, or a message
32
+ * saying what went wrong and what to change.
33
+ *
34
+ * @param output - the command's stdout, stderr, and exit code
35
+ * @returns the report, or a message naming the error document's kind and hint, a format version mismatch, or output that isn't a status document; an error document rides along as `error`
36
+ */
37
+ export const parse_repos_status_output = (output) => {
38
+ const { stdout, stderr, exit_code } = output;
39
+ let json;
40
+ try {
41
+ json = JSON.parse(stdout);
42
+ }
43
+ catch {
44
+ // exit 1 is a fatal I/O error, which may print no JSON
45
+ const detail = stderr.trim() || stdout.trim() || 'no output';
46
+ return {
47
+ ok: false,
48
+ message: `\`repos status --json\` exited ${exit_code} without a JSON document: ${detail}`
49
+ };
50
+ }
51
+ if (typeof json !== 'object' || json === null || Array.isArray(json)) {
52
+ return { ok: false, message: '`repos status --json` printed JSON that is not a document' };
53
+ }
54
+ const version = 'version' in json ? json.version : undefined;
55
+ if (version !== REPOS_STATUS_FORMAT_VERSION) {
56
+ const fix = typeof version === 'number' && version > REPOS_STATUS_FORMAT_VERSION
57
+ ? "upgrade @fuzdev/fuz_gitops to match the binary, or install the binary from a fuz_repos checkout at this package's version"
58
+ : "install the binary from a fuz_repos checkout at this package's version";
59
+ return {
60
+ ok: false,
61
+ message: `the \`repos\` binary prints status format ${JSON.stringify(version ?? null)}, ` +
62
+ `but this @fuzdev/fuz_gitops parses format ${REPOS_STATUS_FORMAT_VERSION}: ` +
63
+ `${fix} (\`${REPOS_INSTALL_COMMAND}\`) — the npm package and the binary are installed separately`
64
+ };
65
+ }
66
+ if ('error' in json) {
67
+ const parsed = ReposStatusErrorReport.safeParse(json);
68
+ if (!parsed.success) {
69
+ return {
70
+ ok: false,
71
+ message: `\`repos status --json\` printed an error document this package can't parse:\n${z.prettifyError(parsed.error)}`
72
+ };
73
+ }
74
+ return {
75
+ ok: false,
76
+ message: format_repos_status_error(parsed.data.error),
77
+ error: parsed.data.error
78
+ };
79
+ }
80
+ const parsed = ReposStatusReport.safeParse(json);
81
+ if (!parsed.success) {
82
+ return {
83
+ ok: false,
84
+ message: `\`repos status --json\` printed a report this package can't parse:\n${z.prettifyError(parsed.error)}`
85
+ };
86
+ }
87
+ return { ok: true, report: parsed.data };
88
+ };
89
+ const format_repos_status_error = (error) => {
90
+ switch (error.kind) {
91
+ case 'unknown_entry': {
92
+ const suggestions = error.suggestions.length
93
+ ? ` — did you mean ${error.suggestions.map((s) => `\`${s}\``).join(', ')}?`
94
+ : '';
95
+ return `the gitops config lists \`${error.name}\`, which the registry (repos.toml) doesn't name${suggestions}`;
96
+ }
97
+ case 'registry_not_found':
98
+ return (`${error.message}: the gitops tasks read repo state from a repos.toml registry — ` +
99
+ `run inside its workspace, or pass \`--registry <path>\``);
100
+ default: {
101
+ const hint = error.hint ? `\n hint: ${error.hint}` : '';
102
+ return `\`repos status\` failed (${error.kind}): ${error.message}${hint}`;
103
+ }
104
+ }
105
+ };
106
+ /**
107
+ * Runs `repos status <keys…> --json` and parses its report.
108
+ *
109
+ * @param options.keys - the registry keys to report on; must not be empty, since no targets means every entry
110
+ * @param options.registry - a `repos.toml` to use instead of the one found walking up from the cwd
111
+ * @param options.fetch - fetch each entry from origin first (`--fetch`), which writes remote-tracking refs and nothing else
112
+ * @param options.repos_ops - the `repos` runner
113
+ * @returns the report, or a message saying what went wrong and what to change
114
+ */
115
+ export const load_repos_status = async (options) => {
116
+ const { keys, registry, fetch, repos_ops } = options;
117
+ if (keys.length === 0) {
118
+ // `repos status` with no targets reports every entry
119
+ return { ok: false, message: 'no registry keys to report on' };
120
+ }
121
+ const ran = await repos_ops.status(fetch ? { keys, registry, fetch } : { keys, registry });
122
+ if (!ran.ok) {
123
+ return {
124
+ ok: false,
125
+ message: ran.kind === 'not_found'
126
+ ? `the \`repos\` binary was not found on PATH: install it from a fuz_repos checkout with \`${REPOS_INSTALL_COMMAND}\` — the npm package doesn't carry it`
127
+ : `\`repos status\` didn't run: ${ran.message}`
128
+ };
129
+ }
130
+ return parse_repos_status_output(ran.output);
131
+ };
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_gitops",
3
- "version": "0.78.1",
3
+ "version": "0.79.0",
4
4
  "description": "a tool for managing many repos",
5
5
  "glyph": "🪄",
6
6
  "logo": "logo.svg",
7
7
  "logo_alt": "a friendly blue spider facing you",
8
8
  "license": "MIT",
9
9
  "homepage": "https://gitops.fuz.dev/",
10
- "repository": "https://github.com/fuzdev/fuz_gitops",
10
+ "repository": "https://github.com/fuzdev/fuz_repos",
11
11
  "author": {
12
12
  "name": "Ryan Atkinson",
13
13
  "email": "mail@ryanatkn.com",
14
14
  "url": "https://www.ryanatkn.com/"
15
15
  },
16
- "bugs": "https://github.com/fuzdev/fuz_gitops/issues",
16
+ "bugs": "https://github.com/fuzdev/fuz_repos/issues",
17
17
  "funding": "https://www.ryanatkn.com/funding",
18
18
  "scripts": {
19
19
  "start": "gro dev",
@@ -39,12 +39,12 @@
39
39
  },
40
40
  "devDependencies": {
41
41
  "@changesets/changelog-git": "^0.2.1",
42
- "@fuzdev/blake3_wasm": "^0.1.1",
42
+ "@fuzdev/blake3-wasm": "^0.2.0",
43
43
  "@fuzdev/fuz_code": "^0.48.0",
44
- "@fuzdev/fuz_css": "^0.63.3",
44
+ "@fuzdev/fuz_css": "^0.64.0",
45
45
  "@fuzdev/fuz_ui": "^0.208.0",
46
- "@fuzdev/fuz_util": "^0.68.0",
47
- "@fuzdev/gro": "^0.210.1",
46
+ "@fuzdev/fuz_util": "^0.69.0",
47
+ "@fuzdev/gro": "^0.212.0",
48
48
  "@fuzdev/mdz": "^0.3.0",
49
49
  "@ryanatkn/eslint-config": "^0.12.2",
50
50
  "@sveltejs/acorn-typescript": "^1.0.9",
@@ -59,8 +59,6 @@
59
59
  "eslint-plugin-svelte": "^3.13.1",
60
60
  "esm-env": "^1.2.2",
61
61
  "magic-string": "^0.30.21",
62
- "prettier": "^3.9.6",
63
- "prettier-plugin-svelte": "^4.1.1",
64
62
  "svelte": "^5.56.2",
65
63
  "svelte-check": "^4.6.0",
66
64
  "svelte-docinfo": "^0.7.0",
@@ -73,24 +71,6 @@
73
71
  "zimmerframe": "^1.1.4",
74
72
  "zod": "^4.3.6"
75
73
  },
76
- "prettier": {
77
- "plugins": [
78
- "prettier-plugin-svelte"
79
- ],
80
- "useTabs": true,
81
- "printWidth": 100,
82
- "singleQuote": true,
83
- "bracketSpacing": true,
84
- "trailingComma": "none",
85
- "overrides": [
86
- {
87
- "files": "package.json",
88
- "options": {
89
- "useTabs": false
90
- }
91
- }
92
- ]
93
- },
94
74
  "sideEffects": [
95
75
  "**/*.css"
96
76
  ],
@@ -2,14 +2,12 @@
2
2
  * Reconciles each repo's declared `ci` flag against whether it actually has
3
3
  * GitHub Actions workflow files on disk.
4
4
  *
5
- * The gitops config derives `ci` from visibility (on for public repos, off for
6
- * private) with per-repo overrides; this check catches drift between that
7
- * declaration and reality —
5
+ * The registry (`repos.toml`) derives `ci` from visibility (on for public repos,
6
+ * off for private) with per-repo overrides; this check catches drift between
7
+ * that declaration and reality —
8
8
  * a repo that claims CI but ships no workflow, or one that disclaims CI yet
9
- * still carries one. Repos that aren't checked out locally can't be judged, so
10
- * the caller marks them uncheckable and they're skipped. Archived repos are
11
- * frozen on their host, so their CI state is intentionally left alone and they're
12
- * skipped too.
9
+ * still carries one. Archived repos are frozen on their host, so their CI state
10
+ * is intentionally left alone and they're skipped.
13
11
  *
14
12
  * @module
15
13
  */
@@ -34,12 +32,10 @@ export interface CiDrift {
34
32
 
35
33
  export interface CiReconcileInput {
36
34
  repo_url: string;
37
- /** The declared/derived `ci` value from the gitops config. */
35
+ /** The declared/derived `ci` value from the registry. */
38
36
  ci: boolean;
39
37
  /** Whether the repo has at least one workflow file on disk. */
40
38
  has_workflows: boolean;
41
- /** Whether the repo is checked out locally; uncheckable repos are skipped. */
42
- checkable: boolean;
43
39
  /** Whether the repo is archived (frozen) on its host; archived repos are skipped. */
44
40
  archived: boolean;
45
41
  }
@@ -51,7 +47,7 @@ export interface CiReconcileInput {
51
47
  export const reconcile_ci = (repos: Array<CiReconcileInput>): Array<CiDrift> => {
52
48
  const drift: Array<CiDrift> = [];
53
49
  for (const repo of repos) {
54
- if (!repo.checkable || repo.archived) continue;
50
+ if (repo.archived) continue;
55
51
  const { repo_url, ci, has_workflows } = repo;
56
52
  if (ci && !has_workflows) {
57
53
  drift.push({ repo_url, ci, has_workflows, kind: 'missing_ci' });
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Dependency graph data structure and algorithms for multi-repo publishing.
3
3
  *
4
- * Provides `DependencyGraph` class with topological sort (via `@fuzdev/fuz_util/sort.ts`)
5
- * and cycle detection by dependency type.
6
- * For validation workflow and publishing order computation, see `graph_validation.ts`.
4
+ * Provides the `DependencyGraph` class, built from local repos, with topological sort
5
+ * (via `@fuzdev/fuz_util/sort.ts`), cycle detection by dependency type, and
6
+ * wildcard-range analysis.
7
+ * For the publishing order and the analysis workflow, see `graph_validation.ts`.
7
8
  *
8
9
  * @module
9
10
  */
@@ -24,7 +25,6 @@ export type DependencyType = (typeof DEPENDENCY_TYPE)[keyof typeof DEPENDENCY_TY
24
25
  export interface DependencySpec {
25
26
  type: DependencyType;
26
27
  version: string;
27
- resolved?: string;
28
28
  }
29
29
 
30
30
  export interface DependencyGraphJson {
@@ -33,7 +33,6 @@ export interface DependencyGraphJson {
33
33
  version: string;
34
34
  dependencies: Array<{ name: string; spec: DependencySpec }>;
35
35
  dependents: Array<string>;
36
- publishable: boolean;
37
36
  }>;
38
37
  edges: Array<{ from: string; to: string }>;
39
38
  }
@@ -41,41 +40,43 @@ export interface DependencyGraphJson {
41
40
  export interface DependencyNode {
42
41
  name: string;
43
42
  version: string;
44
- repo?: LocalRepo;
45
43
  dependencies: Map<string, DependencySpec>;
46
44
  dependents: Set<string>;
47
- publishable: boolean;
48
45
  }
49
46
 
50
- export class DependencyGraph {
51
- nodes: Map<string, DependencyNode>;
52
- edges: Map<string, Set<string>>; // pkg -> dependents
47
+ /** Cycles and wildcard dependencies found by `DependencyGraph.analyze`. */
48
+ export interface DependencyAnalysis {
49
+ production_cycles: Array<Array<string>>;
50
+ dev_cycles: Array<Array<string>>;
51
+ wildcard_deps: Array<{ pkg: string; dep: string; version: string }>;
52
+ }
53
53
 
54
- constructor() {
55
- this.nodes = new Map();
56
- this.edges = new Map();
57
- }
54
+ export class DependencyGraph {
55
+ nodes: Map<string, DependencyNode> = new Map();
56
+ edges: Map<string, Set<string>> = new Map(); // pkg -> dependents
58
57
 
59
- public init_from_repos(repos: Array<LocalRepo>): void {
60
- // First pass: create nodes
58
+ /**
59
+ * Builds the graph from local repos.
60
+ *
61
+ * Two passes: first creates nodes, then builds edges (dependents).
62
+ * Prioritizes prod/peer deps over dev deps when the same package appears in
63
+ * multiple dependency types (the stronger constraint wins).
64
+ */
65
+ constructor(repos: Array<LocalRepo>) {
66
+ // first pass: create nodes
61
67
  for (const repo of repos) {
62
68
  const { library, package_json } = repo;
63
69
  const node: DependencyNode = {
64
70
  name: library.name,
65
71
  version: package_json.version || '0.0.0',
66
- repo,
67
72
  dependencies: new Map(),
68
- dependents: new Set(),
69
- publishable: !package_json.private
73
+ dependents: new Set()
70
74
  };
71
75
 
72
- // Extract dependencies
73
76
  const deps = package_json.dependencies || (EMPTY_OBJECT as Record<string, string>);
74
77
  const dev_deps = package_json.devDependencies || (EMPTY_OBJECT as Record<string, string>);
75
78
  const peer_deps = package_json.peerDependencies || (EMPTY_OBJECT as Record<string, string>);
76
79
 
77
- // Add dependencies, prioritizing prod/peer over dev
78
- // (if a package appears in multiple dep types, use the stronger constraint)
79
80
  for (const [name, version] of Object.entries(deps)) {
80
81
  node.dependencies.set(name, { type: DEPENDENCY_TYPE.PROD, version });
81
82
  }
@@ -83,7 +84,7 @@ export class DependencyGraph {
83
84
  node.dependencies.set(name, { type: DEPENDENCY_TYPE.PEER, version });
84
85
  }
85
86
  for (const [name, version] of Object.entries(dev_deps)) {
86
- // Only add dev deps if not already present as prod/peer
87
+ // only add dev deps if not already present as prod/peer
87
88
  if (!node.dependencies.has(name)) {
88
89
  node.dependencies.set(name, { type: DEPENDENCY_TYPE.DEV, version });
89
90
  }
@@ -93,12 +94,11 @@ export class DependencyGraph {
93
94
  this.edges.set(library.name, new Set());
94
95
  }
95
96
 
96
- // Second pass: build edges (dependents)
97
+ // second pass: build edges (dependents) for internal dependencies
97
98
  for (const node of this.nodes.values()) {
98
99
  for (const [dep_name] of node.dependencies) {
99
- if (this.nodes.has(dep_name)) {
100
- // Internal dependency
101
- const dep_node = this.nodes.get(dep_name)!;
100
+ const dep_node = this.nodes.get(dep_name);
101
+ if (dep_node) {
102
102
  dep_node.dependents.add(node.name);
103
103
  this.edges.get(dep_name)!.add(node.name);
104
104
  }
@@ -110,15 +110,6 @@ export class DependencyGraph {
110
110
  return this.nodes.get(name);
111
111
  }
112
112
 
113
- get_dependents(name: string): Set<string> {
114
- return this.edges.get(name) || new Set();
115
- }
116
-
117
- get_dependencies(name: string): Map<string, DependencySpec> {
118
- const node = this.nodes.get(name);
119
- return node ? node.dependencies : new Map();
120
- }
121
-
122
113
  /**
123
114
  * Computes topological sort order for dependency graph.
124
115
  *
@@ -167,6 +158,23 @@ export class DependencyGraph {
167
158
  return { production_cycles, dev_cycles };
168
159
  }
169
160
 
161
+ /**
162
+ * Reports cycles by type and wildcard (`*`) dependency ranges.
163
+ * Tolerates cycles: it reports them rather than throwing.
164
+ */
165
+ analyze(): DependencyAnalysis {
166
+ const { production_cycles, dev_cycles } = this.detect_cycles_by_type();
167
+ const wildcard_deps: DependencyAnalysis['wildcard_deps'] = [];
168
+ for (const node of this.nodes.values()) {
169
+ for (const [dep_name, spec] of node.dependencies) {
170
+ if (spec.version === '*') {
171
+ wildcard_deps.push({ pkg: node.name, dep: dep_name, version: spec.version });
172
+ }
173
+ }
174
+ }
175
+ return { production_cycles, dev_cycles, wildcard_deps };
176
+ }
177
+
170
178
  /** DFS cycle detection following only edges that match the filter. */
171
179
  #find_cycles(include: (spec: DependencySpec) => boolean): Array<Array<string>> {
172
180
  const cycles: Array<Array<string>> = [];
@@ -219,8 +227,7 @@ export class DependencyGraph {
219
227
  name,
220
228
  spec
221
229
  })),
222
- dependents: Array.from(node.dependents),
223
- publishable: node.publishable
230
+ dependents: Array.from(node.dependents)
224
231
  }));
225
232
 
226
233
  const edges: Array<{ from: string; to: string }> = [];
@@ -233,62 +240,3 @@ export class DependencyGraph {
233
240
  return { nodes, edges };
234
241
  }
235
242
  }
236
-
237
- /**
238
- * Builder for creating and analyzing dependency graphs.
239
- */
240
- export class DependencyGraphBuilder {
241
- /**
242
- * Constructs dependency graph from local repos.
243
- *
244
- * Two-pass algorithm: first creates nodes, then builds edges (dependents).
245
- * Prioritizes prod/peer deps over dev deps when same package appears in
246
- * multiple dependency types (stronger constraint wins).
247
- *
248
- * @returns fully initialized dependency graph with all nodes and edges
249
- */
250
- build_from_repos(repos: Array<LocalRepo>): DependencyGraph {
251
- const graph = new DependencyGraph();
252
- graph.init_from_repos(repos);
253
- return graph;
254
- }
255
-
256
- /**
257
- * Computes publishing order using topological sort with dev deps excluded.
258
- *
259
- * Excludes dev dependencies to break circular dev dependency cycles while
260
- * preserving production/peer dependency ordering. This allows patterns like
261
- * shared test utilities that depend on each other for development.
262
- *
263
- * @returns package names in safe publishing order (dependencies before dependents)
264
- * @throws {Error} if production/peer cycles detected (cannot be resolved by exclusion)
265
- */
266
- compute_publishing_order(graph: DependencyGraph): Array<string> {
267
- return graph.topological_sort(true); // Exclude dev dependencies
268
- }
269
-
270
- analyze(graph: DependencyGraph): {
271
- production_cycles: Array<Array<string>>;
272
- dev_cycles: Array<Array<string>>;
273
- wildcard_deps: Array<{ pkg: string; dep: string; version: string }>;
274
- missing_peers: Array<{ pkg: string; dep: string }>;
275
- } {
276
- const { production_cycles, dev_cycles } = graph.detect_cycles_by_type();
277
- const wildcard_deps: Array<{ pkg: string; dep: string; version: string }> = [];
278
- const missing_peers: Array<{ pkg: string; dep: string }> = [];
279
-
280
- for (const node of graph.nodes.values()) {
281
- for (const [dep_name, spec] of node.dependencies) {
282
- if (spec.version === '*') {
283
- wildcard_deps.push({ pkg: node.name, dep: dep_name, version: spec.version });
284
- }
285
- if (spec.type === DEPENDENCY_TYPE.PEER && !graph.nodes.has(dep_name)) {
286
- // External peer dependency
287
- missing_peers.push({ pkg: node.name, dep: dep_name });
288
- }
289
- }
290
- }
291
-
292
- return { production_cycles, dev_cycles, wildcard_deps, missing_peers };
293
- }
294
- }
@@ -30,7 +30,7 @@ export interface UpdatePackageJsonOptions {
30
30
  * 3. Creates auto-changeset if `published_versions` provided (for transitive updates)
31
31
  * 4. Commits both `package.json` and changeset with standard message
32
32
  *
33
- * Uses version strategy to determine prefix (exact, caret, tilde) while preserving
33
+ * Uses version strategy to determine prefix (exact, caret, tilde, gte) while preserving
34
34
  * existing prefixes when possible.
35
35
  *
36
36
  * @throws {Error} if file operations or git operations fail
@@ -111,6 +111,8 @@ export const update_package_json = async (
111
111
  throw new Error(`Failed to write package.json: ${write_result.message}`);
112
112
  }
113
113
 
114
+ const staged: Array<string> = [];
115
+
114
116
  // Create changeset if we have published version info
115
117
  if (published_versions && published_versions.size > 0) {
116
118
  // Build dependency updates info for changeset
@@ -153,6 +155,7 @@ export const update_package_json = async (
153
155
  if (!add_result.ok) {
154
156
  throw new Error(`Failed to stage changeset: ${add_result.message}`);
155
157
  }
158
+ staged.push(changeset_path);
156
159
  }
157
160
  }
158
161
 
@@ -161,9 +164,12 @@ export const update_package_json = async (
161
164
  if (!add_pkg_result.ok) {
162
165
  throw new Error(`Failed to stage package.json: ${add_pkg_result.message}`);
163
166
  }
167
+ staged.push('package.json');
164
168
 
169
+ // commit only what this staged, so nothing else in the index rides along
165
170
  const commit_result = await git_ops.commit({
166
171
  message: `update dependencies after publishing`,
172
+ files: staged,
167
173
  cwd: repo.repo_dir
168
174
  });
169
175
  if (!commit_result.ok) {
@@ -2,45 +2,80 @@ import type { Logger } from '@fuzdev/fuz_util/log.ts';
2
2
  import { wait } from '@fuzdev/fuz_util/async.ts';
3
3
  import type { FetchValueCache } from '@fuzdev/fuz_util/fetch.ts';
4
4
 
5
- import { fetch_github_check_runs, fetch_github_pull_requests } from './github.ts';
5
+ import {
6
+ fetch_github_check_runs,
7
+ fetch_github_pull_requests,
8
+ type GithubCheckRunsItem
9
+ } from './github.ts';
6
10
  import type { RepoJson } from './repo.svelte.ts';
7
11
  import type { LocalRepo } from './local_repo.ts';
8
12
 
13
+ /**
14
+ * Options for `fetch_repo_data`.
15
+ */
16
+ export interface FetchRepoDataOptions {
17
+ /** The repos to fetch GitHub metadata for, in order. */
18
+ local_repos: Array<LocalRepo>;
19
+ /** GitHub API token, sent with each request. */
20
+ token?: string;
21
+ /** Response memoization, from `fuz_util`'s `fetch.ts`. */
22
+ cache?: FetchValueCache;
23
+ log?: Logger;
24
+ /** Milliseconds to wait before each API request. Defaults to `33`. */
25
+ delay?: number;
26
+ /** Sent as the `x-github-api-version` header when set. */
27
+ github_api_version?: string;
28
+ /** The `fetch` the API requests go through. Defaults to `globalThis.fetch`. */
29
+ fetch?: typeof globalThis.fetch;
30
+ }
31
+
9
32
  /**
10
33
  * Fetches GitHub metadata (CI status, PRs) for all repos.
11
34
  *
12
- * Fetches sequentially with delay between requests to respect GitHub API rate limits.
13
- * Uses `await_in_loop` intentionally to avoid parallel requests overwhelming the API.
35
+ * Fetches sequentially with a delay before each request to respect GitHub API
36
+ * rate limits. Uses `await_in_loop` intentionally to avoid parallel requests
37
+ * overwhelming the API. CI status is read for the branch each repo's registry
38
+ * entry follows (`main` when it names none), and that branch is written to
39
+ * `RepoJson.branch`.
14
40
  *
15
- * Error handling: Logs fetch failures but continues processing remaining repos.
16
- * Repos with failed fetches will have `null` for `check_runs` or `pull_requests`.
41
+ * A repo whose registry entry declares no CI (`ci: false`) isn't asked for
42
+ * check runs, and one with none on its branch isn't a failure: both leave
43
+ * `check_runs` `null` without logging. A failed fetch is logged as an error
44
+ * and leaves `null` for that repo's `check_runs` or `pull_requests`, and the
45
+ * remaining repos still fetch — except a 401 response or a repo URL without a
46
+ * GitHub owner, which throw.
17
47
  *
18
- * @param delay - milliseconds between API requests (default: 33ms)
19
- * @param cache - optional cache from `fuz_util`'s `fetch.js` for response memoization
20
- * @returns array of `Repo` objects with GitHub metadata attached
48
+ * @param options - the repos, credentials, cache, and pacing
49
+ * @returns a `RepoJson` for each repo, in the order given
50
+ * @throws {Error} on a 401 response (check `SECRET_GITHUB_API_TOKEN`) or a
51
+ * repo whose URL has no GitHub owner
21
52
  */
22
- export const fetch_repo_data = async (
23
- resolved_repos: Array<LocalRepo>,
24
- token?: string,
25
- cache?: FetchValueCache,
26
- log?: Logger,
27
- delay = 33,
28
- github_api_version?: string
29
- ): Promise<Array<RepoJson>> => {
53
+ export const fetch_repo_data = async (options: FetchRepoDataOptions): Promise<Array<RepoJson>> => {
54
+ const { local_repos, token, cache, log, delay = 33, github_api_version, fetch } = options;
30
55
  const repos: Array<RepoJson> = [];
31
- for (const { library, package_json, repo_config } of resolved_repos) {
56
+ for (const { library, package_json, entry } of local_repos) {
32
57
  const repo_url = library.repo_url;
58
+ const branch = entry.branch ?? 'main';
33
59
 
34
- // CI status
35
- await wait(delay);
36
- const check_runs = await fetch_github_check_runs(library, {
37
- cache,
38
- log,
39
- token,
40
- api_version: github_api_version,
41
- ref: repo_config.branch
42
- });
43
- if (!check_runs) log?.error('failed to fetch CI status: ' + repo_url);
60
+ // CI status, for repos the registry says run CI
61
+ let check_runs: GithubCheckRunsItem | null = null;
62
+ if (entry.ci) {
63
+ await wait(delay);
64
+ const fetched = await fetch_github_check_runs(library, {
65
+ cache,
66
+ log,
67
+ token,
68
+ api_version: github_api_version,
69
+ ref: branch,
70
+ fetch
71
+ });
72
+ if (fetched.ok) {
73
+ check_runs = fetched.value;
74
+ if (!check_runs) log?.debug(`no check runs on \`${branch}\`: ${repo_url}`);
75
+ } else {
76
+ log?.error(`failed to fetch CI status (${fetched.status}): ${repo_url}`);
77
+ }
78
+ }
44
79
 
45
80
  // pull requests
46
81
  await wait(delay);
@@ -48,13 +83,15 @@ export const fetch_repo_data = async (
48
83
  cache,
49
84
  log,
50
85
  token,
51
- api_version: github_api_version
86
+ api_version: github_api_version,
87
+ fetch
52
88
  });
53
- if (!pull_requests) log?.error('failed to fetch issues: ' + repo_url);
89
+ if (!pull_requests) log?.error('failed to fetch pull requests: ' + repo_url);
54
90
 
55
91
  repos.push({
56
92
  library_json: library.library_json,
57
93
  package_json,
94
+ branch,
58
95
  check_runs,
59
96
  pull_requests
60
97
  });
@@ -27,7 +27,7 @@ export interface FetchCache {
27
27
  * cache is cleared (delete file) and starts fresh. This handles format changes.
28
28
  *
29
29
  * Uses `structuredClone` to track changes - only writes to disk if data modified.
30
- * Formatted with Prettier before writing for version control friendliness.
30
+ * Formatted with gro's `format_file` before writing for version control friendliness.
31
31
  *
32
32
  * @param name - cache filename (without .json extension)
33
33
  * @param dir - cache directory (defaults to `.gro/build/fetch/`)