@fuzdev/fuz_gitops 0.76.0 → 0.78.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 (104) hide show
  1. package/README.md +5 -5
  2. package/dist/ModulesDetail.svelte +1 -1
  3. package/dist/PageFooter.svelte +2 -2
  4. package/dist/PageHeader.svelte +1 -1
  5. package/dist/PageHeader.svelte.d.ts +1 -1
  6. package/dist/ReposTable.svelte +1 -1
  7. package/dist/ReposTree.svelte +1 -1
  8. package/dist/cargo_toml.d.ts +33 -0
  9. package/dist/cargo_toml.d.ts.map +1 -0
  10. package/dist/cargo_toml.js +74 -0
  11. package/dist/changeset_generator.d.ts +1 -1
  12. package/dist/changeset_reader.d.ts +1 -1
  13. package/dist/ci_reconcile.d.ts +50 -0
  14. package/dist/ci_reconcile.d.ts.map +1 -0
  15. package/dist/ci_reconcile.js +46 -0
  16. package/dist/config_reconcile.d.ts +46 -0
  17. package/dist/config_reconcile.d.ts.map +1 -0
  18. package/dist/config_reconcile.js +60 -0
  19. package/dist/dependency_graph.d.ts +2 -2
  20. package/dist/dependency_graph.js +4 -4
  21. package/dist/dependency_updater.d.ts +1 -1
  22. package/dist/fetch_repo_data.d.ts +2 -2
  23. package/dist/fetch_repo_data.js +1 -1
  24. package/dist/fs_fetch_value_cache.d.ts +1 -1
  25. package/dist/fs_fetch_value_cache.js +3 -3
  26. package/dist/git_operations.d.ts +1 -1
  27. package/dist/git_operations.js +2 -2
  28. package/dist/github.d.ts +2 -2
  29. package/dist/github.js +1 -1
  30. package/dist/github_helpers.js +1 -1
  31. package/dist/gitops_analyze.task.d.ts.map +1 -1
  32. package/dist/gitops_analyze.task.js +15 -8
  33. package/dist/gitops_config.d.ts +27 -2
  34. package/dist/gitops_config.d.ts.map +1 -1
  35. package/dist/gitops_config.js +13 -2
  36. package/dist/gitops_run.task.js +2 -2
  37. package/dist/gitops_sync.task.js +4 -4
  38. package/dist/gitops_task_helpers.d.ts +1 -1
  39. package/dist/gitops_task_helpers.js +1 -1
  40. package/dist/gitops_validate.task.d.ts.map +1 -1
  41. package/dist/gitops_validate.task.js +54 -6
  42. package/dist/graph_validation.d.ts +2 -2
  43. package/dist/graph_validation.d.ts.map +1 -1
  44. package/dist/graph_validation.js +3 -1
  45. package/dist/local_repo.d.ts +17 -3
  46. package/dist/local_repo.d.ts.map +1 -1
  47. package/dist/local_repo.js +54 -6
  48. package/dist/log_helpers.d.ts +1 -1
  49. package/dist/multi_repo_publisher.d.ts +3 -3
  50. package/dist/multi_repo_publisher.d.ts.map +1 -1
  51. package/dist/multi_repo_publisher.js +5 -1
  52. package/dist/npm_registry.d.ts +1 -1
  53. package/dist/npm_registry.js +2 -2
  54. package/dist/operations.d.ts +4 -4
  55. package/dist/operations_defaults.js +4 -4
  56. package/dist/output_helpers.d.ts +1 -1
  57. package/dist/preflight_checks.d.ts +1 -1
  58. package/dist/preflight_checks.js +1 -1
  59. package/dist/publish_steps.js +1 -1
  60. package/dist/publishing_plan.d.ts +3 -3
  61. package/dist/publishing_plan.d.ts.map +1 -1
  62. package/dist/publishing_plan.js +11 -1
  63. package/dist/publishing_plan_logging.d.ts +1 -1
  64. package/dist/repo.svelte.d.ts +5 -5
  65. package/dist/repo.svelte.js +2 -2
  66. package/package.json +10 -10
  67. package/src/lib/cargo_toml.ts +82 -0
  68. package/src/lib/changeset_generator.ts +6 -6
  69. package/src/lib/changeset_reader.ts +3 -3
  70. package/src/lib/ci_reconcile.ts +73 -0
  71. package/src/lib/config_reconcile.ts +94 -0
  72. package/src/lib/dependency_graph.ts +5 -5
  73. package/src/lib/dependency_updater.ts +7 -7
  74. package/src/lib/fetch_repo_data.ts +6 -6
  75. package/src/lib/fs_fetch_value_cache.ts +3 -3
  76. package/src/lib/git_operations.ts +2 -2
  77. package/src/lib/github.ts +2 -2
  78. package/src/lib/github_helpers.ts +3 -3
  79. package/src/lib/gitops_analyze.task.ts +26 -18
  80. package/src/lib/gitops_config.ts +45 -5
  81. package/src/lib/gitops_plan.task.ts +4 -4
  82. package/src/lib/gitops_publish.task.ts +9 -9
  83. package/src/lib/gitops_run.task.ts +4 -4
  84. package/src/lib/gitops_sync.task.ts +8 -8
  85. package/src/lib/gitops_task_helpers.ts +7 -7
  86. package/src/lib/gitops_validate.task.ts +63 -11
  87. package/src/lib/graph_validation.ts +5 -4
  88. package/src/lib/local_repo.ts +81 -15
  89. package/src/lib/log_helpers.ts +2 -2
  90. package/src/lib/multi_repo_publisher.ts +15 -11
  91. package/src/lib/npm_registry.ts +3 -3
  92. package/src/lib/operations.ts +9 -9
  93. package/src/lib/operations_defaults.ts +9 -9
  94. package/src/lib/output_helpers.ts +1 -1
  95. package/src/lib/preflight_checks.ts +6 -6
  96. package/src/lib/publish_gate.ts +2 -2
  97. package/src/lib/publish_steps.ts +3 -3
  98. package/src/lib/publishing_event_handler.ts +1 -1
  99. package/src/lib/publishing_plan.ts +22 -10
  100. package/src/lib/publishing_plan_helpers.ts +3 -3
  101. package/src/lib/publishing_plan_logging.ts +2 -2
  102. package/src/lib/repo.svelte.ts +7 -7
  103. package/src/lib/repo_ops.ts +3 -3
  104. package/src/lib/resolved_gitops_config.ts +2 -2
@@ -1,4 +1,5 @@
1
1
  import { styleText as st } from 'node:util';
2
+ import { repo_is_npm } from './local_repo.js';
2
3
  import { validate_dependency_graph } from './graph_validation.js';
3
4
  import { is_breaking_change, compare_bump_types, calculate_next_version, } from './version_utils.js';
4
5
  import { default_changeset_operations } from './operations_defaults.js';
@@ -11,12 +12,21 @@ export { log_publishing_plan } from './publishing_plan_logging.js';
11
12
  * Shows version changes, dependency updates, and breaking change cascades.
12
13
  * Uses fixed-point iteration to resolve transitive cascades.
13
14
  */
14
- export const generate_publishing_plan = async (repos, options = {}) => {
15
+ export const generate_publishing_plan = async (all_repos, options = {}) => {
15
16
  const { log, ops = default_changeset_operations, verbose } = options;
16
17
  log?.info(st('cyan', 'Generating publishing plan...'));
17
18
  const warnings = [];
18
19
  const info = []; // Informational status (not warnings)
19
20
  const errors = [];
21
+ // Publishing concerns only npm packages. Non-npm repos (e.g. Rust cargo repos) are
22
+ // loaded for the dashboard but have no npm identity in the changeset cascade — drop them
23
+ // before building the graph so they never appear in the plan or publishing order.
24
+ const non_npm_repos = all_repos.filter((r) => !repo_is_npm(r));
25
+ if (non_npm_repos.length > 0) {
26
+ info.push(`Excluded ${non_npm_repos.length} non-npm repo(s) from publishing: ` +
27
+ non_npm_repos.map((r) => r.library.name).join(', '));
28
+ }
29
+ const repos = all_repos.filter(repo_is_npm);
20
30
  // Verbose data collection
21
31
  const verbose_changeset_details = [];
22
32
  const verbose_iterations = [];
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * @module
7
7
  */
8
- import type { Logger } from '@fuzdev/fuz_util/log.js';
8
+ import type { Logger } from '@fuzdev/fuz_util/log.ts';
9
9
  import type { PublishingPlan } from './publishing_plan.js';
10
10
  export interface LogPlanOptions {
11
11
  verbose?: boolean;
@@ -1,8 +1,8 @@
1
- import type { LibraryJson, SourceJson } from '@fuzdev/fuz_util/library_json.js';
2
- import type { PkgJson } from '@fuzdev/fuz_util/pkg_json.js';
3
- import type { PackageJson } from '@fuzdev/fuz_util/package_json.js';
4
- import type { Url } from '@fuzdev/fuz_util/url.js';
5
- import { Library } from '@fuzdev/fuz_ui/library.svelte.js';
1
+ import type { LibraryJson, SourceJson } from '@fuzdev/fuz_util/library_json.ts';
2
+ import type { PkgJson } from '@fuzdev/fuz_util/pkg_json.ts';
3
+ import type { PackageJson } from '@fuzdev/fuz_util/package_json.ts';
4
+ import type { Url } from '@fuzdev/fuz_util/url.ts';
5
+ import { Library } from '@fuzdev/fuz_ui/library.svelte.ts';
6
6
  import { GithubCheckRunsItem, type GithubPullRequest } from './github.js';
7
7
  /**
8
8
  * Serialized repo data as stored in `repos.ts` (JSON).
@@ -1,5 +1,5 @@
1
- import { create_context } from '@fuzdev/fuz_ui/context_helpers.js';
2
- import { Library } from '@fuzdev/fuz_ui/library.svelte.js';
1
+ import { create_context } from '@fuzdev/fuz_ui/context_helpers.ts';
2
+ import { Library } from '@fuzdev/fuz_ui/library.svelte.ts';
3
3
  import { GithubCheckRunsItem } from './github.js';
4
4
  /**
5
5
  * Runtime repo with `Library` composition for package metadata.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_gitops",
3
- "version": "0.76.0",
3
+ "version": "0.78.0",
4
4
  "description": "a tool for managing many repos",
5
5
  "glyph": "🪄",
6
6
  "logo": "logo.svg",
@@ -30,8 +30,8 @@
30
30
  },
31
31
  "peerDependencies": {
32
32
  "@fuzdev/fuz_css": ">=0.62.0",
33
- "@fuzdev/fuz_ui": ">=0.205.0",
34
- "@fuzdev/fuz_util": ">=0.65.1",
33
+ "@fuzdev/fuz_ui": ">=0.205.1",
34
+ "@fuzdev/fuz_util": ">=0.65.2",
35
35
  "@fuzdev/gro": ">=0.204.0",
36
36
  "@sveltejs/kit": "^2",
37
37
  "svelte": "^5",
@@ -40,18 +40,18 @@
40
40
  "devDependencies": {
41
41
  "@changesets/changelog-git": "^0.2.1",
42
42
  "@fuzdev/blake3_wasm": "^0.1.1",
43
- "@fuzdev/fuz_code": "^0.46.0",
44
- "@fuzdev/fuz_css": "^0.63.0",
45
- "@fuzdev/fuz_ui": "^0.205.0",
46
- "@fuzdev/fuz_util": "^0.65.1",
47
- "@fuzdev/gro": "^0.204.0",
43
+ "@fuzdev/fuz_code": "^0.46.1",
44
+ "@fuzdev/fuz_css": "^0.63.2",
45
+ "@fuzdev/fuz_ui": "^0.205.1",
46
+ "@fuzdev/fuz_util": "^0.65.2",
47
+ "@fuzdev/gro": "^0.205.1",
48
48
  "@fuzdev/mdz": "^0.1.0",
49
49
  "@ryanatkn/eslint-config": "^0.12.1",
50
50
  "@sveltejs/acorn-typescript": "^1.0.9",
51
51
  "@sveltejs/adapter-static": "^3.0.10",
52
52
  "@sveltejs/kit": "^2.63.0",
53
53
  "@sveltejs/package": "^2.5.7",
54
- "@sveltejs/vite-plugin-svelte": "^6.2.4",
54
+ "@sveltejs/vite-plugin-svelte": "^7.1.2",
55
55
  "@types/estree": "^1.0.8",
56
56
  "@types/node": "^24.10.1",
57
57
  "@webref/css": "^8.2.0",
@@ -68,7 +68,7 @@
68
68
  "tslib": "^2.8.1",
69
69
  "typescript": "^5.9.3",
70
70
  "typescript-eslint": "^8.48.1",
71
- "vite": "^7.3.1",
71
+ "vite": "^8.0.16",
72
72
  "vitest": "^4.0.15",
73
73
  "zimmerframe": "^1.1.4",
74
74
  "zod": "^4.3.6"
@@ -0,0 +1,82 @@
1
+ import {existsSync} from 'node:fs';
2
+ import {readFile} from 'node:fs/promises';
3
+ import {join} from 'node:path';
4
+
5
+ /**
6
+ * The handful of identity fields gitops reads from a Rust repo's `Cargo.toml`
7
+ * to render it on the dashboard. Everything is optional — a workspace root has
8
+ * no `name`, and any field may be absent or inherited.
9
+ */
10
+ export interface CargoMetadata {
11
+ name?: string;
12
+ version?: string;
13
+ description?: string;
14
+ repository?: string;
15
+ }
16
+
17
+ /** The `Cargo.toml` tables that carry package identity (single-crate vs workspace root). */
18
+ const CARGO_METADATA_SECTIONS: ReadonlySet<string> = new Set(['package', 'workspace.package']);
19
+ const CARGO_METADATA_KEYS = ['name', 'version', 'description', 'repository'] as const;
20
+ type CargoMetadataKey = (typeof CARGO_METADATA_KEYS)[number];
21
+
22
+ /**
23
+ * Best-effort read of a repo's root `Cargo.toml` for the identity fields the
24
+ * gitops dashboard renders. Returns `null` when there's no `Cargo.toml`.
25
+ *
26
+ * @param repo_dir - absolute path to the repo
27
+ */
28
+ export const cargo_toml_load = async (repo_dir: string): Promise<CargoMetadata | null> => {
29
+ const cargo_toml_path = join(repo_dir, 'Cargo.toml');
30
+ if (!existsSync(cargo_toml_path)) return null;
31
+ return cargo_toml_parse(await readFile(cargo_toml_path, 'utf8'));
32
+ };
33
+
34
+ /**
35
+ * Extracts `name`/`version`/`description`/`repository` from the `[package]` and
36
+ * `[workspace.package]` tables of a `Cargo.toml`.
37
+ *
38
+ * Deliberately not a full TOML parser: it scans for simple `key = "value"`
39
+ * string entries in those two tables, which covers both a single-crate manifest
40
+ * and a workspace root. Inline-table values like `version = { workspace = true }`
41
+ * (and the equivalent `version.workspace = true`) are ignored — a member crate
42
+ * inheriting from the workspace has no literal here, and gitops only ever reads a
43
+ * repo's root manifest, where these are concrete. The first non-empty value for a
44
+ * key wins, so a top-level `[package]` takes precedence over `[workspace.package]`
45
+ * when both appear.
46
+ */
47
+ export const cargo_toml_parse = (contents: string): CargoMetadata => {
48
+ const metadata: CargoMetadata = {};
49
+ let in_metadata_section = false;
50
+ for (const raw_line of contents.split('\n')) {
51
+ const line = raw_line.trim();
52
+ if (line === '' || line.startsWith('#')) continue;
53
+ // Section header — only `[package]`/`[workspace.package]` arm value capture.
54
+ if (line.startsWith('[')) {
55
+ const end = line.indexOf(']');
56
+ in_metadata_section = end !== -1 && CARGO_METADATA_SECTIONS.has(line.slice(1, end).trim());
57
+ continue;
58
+ }
59
+ if (!in_metadata_section) continue;
60
+ const eq = line.indexOf('=');
61
+ if (eq === -1) continue;
62
+ const key = line.slice(0, eq).trim();
63
+ if (!CARGO_METADATA_KEYS.includes(key as CargoMetadataKey)) continue;
64
+ if (metadata[key as CargoMetadataKey] !== undefined) continue; // first value wins
65
+ const value = cargo_toml_string_value(line.slice(eq + 1).trim());
66
+ if (value !== null) metadata[key as CargoMetadataKey] = value;
67
+ }
68
+ return metadata;
69
+ };
70
+
71
+ /**
72
+ * Reads a simple single- or double-quoted TOML string, ignoring any trailing
73
+ * inline comment. Returns `null` for non-string values (inline tables, arrays,
74
+ * booleans, numbers), which the caller skips.
75
+ */
76
+ const cargo_toml_string_value = (raw: string): string | null => {
77
+ const quote = raw[0];
78
+ if (quote !== '"' && quote !== "'") return null;
79
+ const end = raw.indexOf(quote, 1);
80
+ if (end === -1) return null;
81
+ return raw.slice(1, end);
82
+ };
@@ -8,16 +8,16 @@
8
8
  */
9
9
 
10
10
  import {join} from 'node:path';
11
- import type {Logger} from '@fuzdev/fuz_util/log.js';
12
- import type {LocalRepo} from './local_repo.js';
13
- import type {PublishedVersion} from './multi_repo_publisher.js';
11
+ import type {Logger} from '@fuzdev/fuz_util/log.ts';
12
+ import type {LocalRepo} from './local_repo.ts';
13
+ import type {PublishedVersion} from './multi_repo_publisher.ts';
14
14
  import {
15
15
  strip_version_prefix,
16
16
  required_bump_for_dependency_update,
17
17
  type BumpType,
18
- } from './version_utils.js';
19
- import type {FsOperations} from './operations.js';
20
- import {default_fs_operations} from './operations_defaults.js';
18
+ } from './version_utils.ts';
19
+ import type {FsOperations} from './operations.ts';
20
+ import {default_fs_operations} from './operations_defaults.ts';
21
21
 
22
22
  export interface DependencyVersionChange {
23
23
  package_name: string;
@@ -7,13 +7,13 @@
7
7
  * @module
8
8
  */
9
9
 
10
- import type {Logger} from '@fuzdev/fuz_util/log.js';
10
+ import type {Logger} from '@fuzdev/fuz_util/log.ts';
11
11
  import {existsSync} from 'node:fs';
12
12
  import {readdir, readFile} from 'node:fs/promises';
13
13
  import {join} from 'node:path';
14
14
 
15
- import type {LocalRepo} from './local_repo.js';
16
- import {compare_bump_types, calculate_next_version, type BumpType} from './version_utils.js';
15
+ import type {LocalRepo} from './local_repo.ts';
16
+ import {compare_bump_types, calculate_next_version, type BumpType} from './version_utils.ts';
17
17
 
18
18
  export interface ChangesetInfo {
19
19
  filename: string;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Reconciles each repo's declared `ci` flag against whether it actually has
3
+ * GitHub Actions workflow files on disk.
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 —
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.
13
+ *
14
+ * @module
15
+ */
16
+
17
+ import {existsSync, readdirSync} from 'node:fs';
18
+ import {join} from 'node:path';
19
+
20
+ /** How a repo's declared `ci` diverges from its workflow files on disk. */
21
+ export type CiDriftKind =
22
+ /** `ci` is `true` but the repo has no workflow files. */
23
+ | 'missing_ci'
24
+ /** `ci` is `false` but the repo has workflow files. */
25
+ | 'stray_ci';
26
+
27
+ export interface CiDrift {
28
+ repo_url: string;
29
+ /** The declared/derived `ci` value. */
30
+ ci: boolean;
31
+ has_workflows: boolean;
32
+ kind: CiDriftKind;
33
+ }
34
+
35
+ export interface CiReconcileInput {
36
+ repo_url: string;
37
+ /** The declared/derived `ci` value from the gitops config. */
38
+ ci: boolean;
39
+ /** Whether the repo has at least one workflow file on disk. */
40
+ has_workflows: boolean;
41
+ /** Whether the repo is checked out locally; uncheckable repos are skipped. */
42
+ checkable: boolean;
43
+ /** Whether the repo is archived (frozen) on its host; archived repos are skipped. */
44
+ archived: boolean;
45
+ }
46
+
47
+ /**
48
+ * Compares each repo's declared `ci` against its actual workflow presence.
49
+ * @returns one `CiDrift` per repo whose declaration and reality disagree
50
+ */
51
+ export const reconcile_ci = (repos: Array<CiReconcileInput>): Array<CiDrift> => {
52
+ const drift: Array<CiDrift> = [];
53
+ for (const repo of repos) {
54
+ if (!repo.checkable || repo.archived) continue;
55
+ const {repo_url, ci, has_workflows} = repo;
56
+ if (ci && !has_workflows) {
57
+ drift.push({repo_url, ci, has_workflows, kind: 'missing_ci'});
58
+ } else if (!ci && has_workflows) {
59
+ drift.push({repo_url, ci, has_workflows, kind: 'stray_ci'});
60
+ }
61
+ }
62
+ return drift;
63
+ };
64
+
65
+ /**
66
+ * Whether a local repo directory contains at least one GitHub Actions workflow.
67
+ * @param repo_dir - absolute or cwd-relative path to the repo's local directory
68
+ */
69
+ export const repo_has_workflows = (repo_dir: string): boolean => {
70
+ const workflows_dir = join(repo_dir, '.github', 'workflows');
71
+ if (!existsSync(workflows_dir)) return false;
72
+ return readdirSync(workflows_dir).some((file) => file.endsWith('.yml') || file.endsWith('.yaml'));
73
+ };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Reconciles repo metadata across multiple gitops configs.
3
+ *
4
+ * The ecosystem keeps several gitops configs — a canonical superset plus
5
+ * narrower per-project subsets. They can't share one source of truth (the
6
+ * superset includes private repos that public configs may not reference), so the
7
+ * same repo is declared independently in each. This check flags when a subset's
8
+ * intrinsic fields drift from the canonical config, or when a subset lists a
9
+ * repo the canonical config doesn't. `repo_dir` is per-config and ignored.
10
+ *
11
+ * @module
12
+ */
13
+
14
+ import type {GitopsRepoConfig} from './gitops_config.ts';
15
+
16
+ /** Repo fields that must agree across every config that lists the repo. */
17
+ const INTRINSIC_FIELDS = ['visibility', 'ci', 'archived', 'branch'] as const;
18
+
19
+ export type IntrinsicField = (typeof INTRINSIC_FIELDS)[number];
20
+
21
+ export type ConfigDriftKind =
22
+ /** The repo is in a subset config but absent from the canonical config. */
23
+ | 'missing_from_canonical'
24
+ /** An intrinsic field disagrees between the subset and the canonical config. */
25
+ | 'field_mismatch';
26
+
27
+ export interface ConfigDrift {
28
+ repo_url: string;
29
+ /** Name of the subset config whose entry diverges. */
30
+ config: string;
31
+ kind: ConfigDriftKind;
32
+ /** The field that disagrees, set when `kind` is `'field_mismatch'`. */
33
+ field?: IntrinsicField;
34
+ /** The canonical config's value, set when `kind` is `'field_mismatch'`. */
35
+ canonical_value?: string;
36
+ /** The subset config's value, set when `kind` is `'field_mismatch'`. */
37
+ config_value?: string;
38
+ }
39
+
40
+ export interface NamedRepos {
41
+ name: string;
42
+ repos: Array<GitopsRepoConfig>;
43
+ }
44
+
45
+ const field_value = (repo: GitopsRepoConfig, field: IntrinsicField): string => {
46
+ switch (field) {
47
+ case 'visibility':
48
+ return repo.visibility;
49
+ case 'ci':
50
+ return String(repo.ci);
51
+ case 'archived':
52
+ return String(repo.archived);
53
+ case 'branch':
54
+ return repo.branch;
55
+ }
56
+ };
57
+
58
+ /**
59
+ * Compares each subset config against the canonical config. Repos that live only
60
+ * in the canonical config are fine — the subsets are intentional partial views.
61
+ * @returns one `ConfigDrift` per subset entry that is missing from the canonical
62
+ * config or whose intrinsic fields disagree with it
63
+ */
64
+ export const reconcile_configs = (
65
+ canonical: NamedRepos,
66
+ subsets: Array<NamedRepos>,
67
+ ): Array<ConfigDrift> => {
68
+ const drift: Array<ConfigDrift> = [];
69
+ const canonical_by_url = new Map(canonical.repos.map((r) => [r.repo_url, r] as const));
70
+ for (const subset of subsets) {
71
+ for (const repo of subset.repos) {
72
+ const canon = canonical_by_url.get(repo.repo_url);
73
+ if (!canon) {
74
+ drift.push({repo_url: repo.repo_url, config: subset.name, kind: 'missing_from_canonical'});
75
+ continue;
76
+ }
77
+ for (const field of INTRINSIC_FIELDS) {
78
+ const canonical_value = field_value(canon, field);
79
+ const config_value = field_value(repo, field);
80
+ if (canonical_value !== config_value) {
81
+ drift.push({
82
+ repo_url: repo.repo_url,
83
+ config: subset.name,
84
+ kind: 'field_mismatch',
85
+ field,
86
+ canonical_value,
87
+ config_value,
88
+ });
89
+ }
90
+ }
91
+ }
92
+ }
93
+ return drift;
94
+ };
@@ -1,17 +1,17 @@
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.js`)
4
+ * Provides `DependencyGraph` class with topological sort (via `@fuzdev/fuz_util/sort.ts`)
5
5
  * and cycle detection by dependency type.
6
6
  * For validation workflow and publishing order computation, see `graph_validation.ts`.
7
7
  *
8
8
  * @module
9
9
  */
10
10
 
11
- import {EMPTY_OBJECT} from '@fuzdev/fuz_util/object.js';
12
- import {topological_sort as topological_sort_generic} from '@fuzdev/fuz_util/sort.js';
11
+ import {EMPTY_OBJECT} from '@fuzdev/fuz_util/object.ts';
12
+ import {topological_sort as topological_sort_generic} from '@fuzdev/fuz_util/sort.ts';
13
13
 
14
- import type {LocalRepo} from './local_repo.js';
14
+ import type {LocalRepo} from './local_repo.ts';
15
15
 
16
16
  export const DEPENDENCY_TYPE = {
17
17
  PROD: 'prod',
@@ -122,7 +122,7 @@ export class DependencyGraph {
122
122
  /**
123
123
  * Computes topological sort order for dependency graph.
124
124
  *
125
- * Delegates to `@fuzdev/fuz_util/sort.js` for the sorting algorithm.
125
+ * Delegates to `@fuzdev/fuz_util/sort.ts` for the sorting algorithm.
126
126
  * Throws if cycles detected.
127
127
  *
128
128
  * @param exclude_dev - if true, excludes dev dependencies to break cycles
@@ -1,15 +1,15 @@
1
- import type {Logger} from '@fuzdev/fuz_util/log.js';
1
+ import type {Logger} from '@fuzdev/fuz_util/log.ts';
2
2
  import {join} from 'node:path';
3
3
 
4
- import type {LocalRepo} from './local_repo.js';
5
- import type {PublishedVersion} from './multi_repo_publisher.js';
4
+ import type {LocalRepo} from './local_repo.ts';
5
+ import type {PublishedVersion} from './multi_repo_publisher.ts';
6
6
  import {
7
7
  create_changeset_for_dependency_updates,
8
8
  create_dependency_updates,
9
- } from './changeset_generator.js';
10
- import {needs_update, get_update_prefix} from './version_utils.js';
11
- import type {GitOperations, FsOperations} from './operations.js';
12
- import {default_git_operations, default_fs_operations} from './operations_defaults.js';
9
+ } from './changeset_generator.ts';
10
+ import {needs_update, get_update_prefix} from './version_utils.ts';
11
+ import type {GitOperations, FsOperations} from './operations.ts';
12
+ import {default_git_operations, default_fs_operations} from './operations_defaults.ts';
13
13
 
14
14
  export type VersionStrategy = 'exact' | 'caret' | 'tilde' | 'gte';
15
15
 
@@ -1,10 +1,10 @@
1
- import type {Logger} from '@fuzdev/fuz_util/log.js';
2
- import {wait} from '@fuzdev/fuz_util/async.js';
3
- import type {FetchValueCache} from '@fuzdev/fuz_util/fetch.js';
1
+ import type {Logger} from '@fuzdev/fuz_util/log.ts';
2
+ import {wait} from '@fuzdev/fuz_util/async.ts';
3
+ import type {FetchValueCache} from '@fuzdev/fuz_util/fetch.ts';
4
4
 
5
- import {fetch_github_check_runs, fetch_github_pull_requests} from './github.js';
6
- import type {RepoJson} from './repo.svelte.js';
7
- import type {LocalRepo} from './local_repo.js';
5
+ import {fetch_github_check_runs, fetch_github_pull_requests} from './github.ts';
6
+ import type {RepoJson} from './repo.svelte.ts';
7
+ import type {LocalRepo} from './local_repo.ts';
8
8
 
9
9
  /**
10
10
  * Fetches GitHub metadata (CI status, PRs) for all repos.
@@ -1,8 +1,8 @@
1
1
  import {mkdir, readFile, rm, writeFile} from 'node:fs/promises';
2
2
  import {dirname, join} from 'node:path';
3
- import {paths} from '@fuzdev/gro/paths.js';
4
- import {format_file} from '@fuzdev/gro/format_file.js';
5
- import {deserialize_cache, serialize_cache, type FetchValueCache} from '@fuzdev/fuz_util/fetch.js';
3
+ import {paths} from '@fuzdev/gro/paths.ts';
4
+ import {format_file} from '@fuzdev/gro/format_file.ts';
5
+ import {deserialize_cache, serialize_cache, type FetchValueCache} from '@fuzdev/fuz_util/fetch.ts';
6
6
  import {existsSync} from 'node:fs';
7
7
 
8
8
  // TODO upstream to Gro probably, and rename/redesign?
@@ -1,4 +1,4 @@
1
- import {spawn_out, spawn_result_to_message} from '@fuzdev/fuz_util/process.js';
1
+ import {spawn_out, spawn_result_to_message} from '@fuzdev/fuz_util/process.ts';
2
2
  import type {SpawnOptions} from 'node:child_process';
3
3
  import {
4
4
  git_check_clean_workspace as gro_git_check_clean_workspace,
@@ -8,7 +8,7 @@ import {
8
8
  git_current_commit_hash as gro_git_current_commit_hash,
9
9
  type GitBranch,
10
10
  type GitOrigin,
11
- } from '@fuzdev/fuz_util/git.js';
11
+ } from '@fuzdev/fuz_util/git.ts';
12
12
 
13
13
  /**
14
14
  * Adds files to git staging area and throws if anything goes wrong.
package/src/lib/github.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type {Logger} from '@fuzdev/fuz_util/log.js';
1
+ import type {Logger} from '@fuzdev/fuz_util/log.ts';
2
2
  import {z} from 'zod';
3
- import {fetch_value, type FetchValueCache} from '@fuzdev/fuz_util/fetch.js';
3
+ import {fetch_value, type FetchValueCache} from '@fuzdev/fuz_util/fetch.ts';
4
4
 
5
5
  /**
6
6
  * Minimal interface for GitHub API calls - works with both `Pkg` and `Repo`.
@@ -1,7 +1,7 @@
1
- import {ensure_end} from '@fuzdev/fuz_util/string.js';
1
+ import {ensure_end} from '@fuzdev/fuz_util/string.ts';
2
2
 
3
- import type {GithubPullRequest} from './github.js';
4
- import type {Repo} from './repo.svelte.js';
3
+ import type {GithubPullRequest} from './github.ts';
4
+ import type {Repo} from './repo.svelte.ts';
5
5
 
6
6
  export type FilterPullRequest = (pull_request: GithubPullRequest, repo: Repo) => boolean;
7
7
 
@@ -1,19 +1,19 @@
1
1
  import type {Task} from '@fuzdev/gro';
2
2
  import {z} from 'zod';
3
3
  import {styleText as st} from 'node:util';
4
- import type {Logger} from '@fuzdev/fuz_util/log.js';
4
+ import type {Logger} from '@fuzdev/fuz_util/log.ts';
5
5
 
6
- import {get_gitops_ready} from './gitops_task_helpers.js';
7
- import type {DependencyGraph} from './dependency_graph.js';
8
- import type {LocalRepo} from './local_repo.js';
9
- import {analyze_repos, type DependencyAnalysis} from './graph_validation.js';
6
+ import {get_gitops_ready} from './gitops_task_helpers.ts';
7
+ import type {DependencyGraph} from './dependency_graph.ts';
8
+ import {repo_is_npm} from './local_repo.ts';
9
+ import {analyze_repos, type DependencyAnalysis} from './graph_validation.ts';
10
10
  import {
11
11
  format_wildcard_dependencies,
12
12
  format_dev_cycles,
13
13
  format_production_cycles,
14
- } from './log_helpers.js';
15
- import {format_and_output, type OutputFormatters} from './output_helpers.js';
16
- import {GITOPS_CONFIG_PATH_DEFAULT} from './gitops_constants.js';
14
+ } from './log_helpers.ts';
15
+ import {format_and_output, type OutputFormatters} from './output_helpers.ts';
16
+ import {GITOPS_CONFIG_PATH_DEFAULT} from './gitops_constants.ts';
17
17
 
18
18
  /** @nodocs */
19
19
  export const Args = z.strictObject({
@@ -50,12 +50,24 @@ export const task: Task<Args> = {
50
50
  // Get repos ready (without downloading); read the working tree as-is unless `--sync`
51
51
  const {local_repos} = await get_gitops_ready({config, dir, download: false, sync, log});
52
52
 
53
+ // Only npm packages form the dependency graph; note any non-npm repos (e.g. cargo)
54
+ // that are excluded so the omission isn't silent.
55
+ const non_npm_repos = local_repos.filter((r) => !repo_is_npm(r));
56
+ if (non_npm_repos.length > 0) {
57
+ log.info(
58
+ st(
59
+ 'dim',
60
+ `excluding ${non_npm_repos.length} non-npm repo(s) from analysis (dashboard-only): ` +
61
+ non_npm_repos.map((r) => r.library.name).join(', '),
62
+ ),
63
+ );
64
+ }
65
+
53
66
  // Build the dependency graph and analyze cycles/wildcards (tolerating cycles)
54
67
  const {graph, analysis, publishing_order} = analyze_repos(local_repos);
55
68
 
56
69
  // Format and output using output_helpers
57
70
  const data = {
58
- repos: local_repos,
59
71
  graph,
60
72
  analysis,
61
73
  publishing_order,
@@ -67,7 +79,6 @@ export const task: Task<Args> = {
67
79
 
68
80
  // Data type for analysis output
69
81
  interface AnalysisData {
70
- repos: Array<LocalRepo>;
71
82
  graph: DependencyGraph;
72
83
  analysis: DependencyAnalysis;
73
84
  publishing_order: Array<string> | null;
@@ -76,9 +87,8 @@ interface AnalysisData {
76
87
  // Create formatters for output_helpers
77
88
  const create_formatters = (): OutputFormatters<AnalysisData> => ({
78
89
  json: (data) => format_json(data.graph, data.analysis, data.publishing_order),
79
- markdown: (data) => format_markdown(data.repos, data.graph, data.analysis, data.publishing_order),
80
- stdout: (data, log) =>
81
- format_stdout(data.repos, data.graph, data.analysis, data.publishing_order, log),
90
+ markdown: (data) => format_markdown(data.graph, data.analysis, data.publishing_order),
91
+ stdout: (data, log) => format_stdout(data.graph, data.analysis, data.publishing_order, log),
82
92
  });
83
93
 
84
94
  // Helper to calculate common statistics
@@ -109,7 +119,6 @@ const format_json = (
109
119
  };
110
120
 
111
121
  const format_markdown = (
112
- repos: Array<LocalRepo>,
113
122
  graph: DependencyGraph,
114
123
  analysis: DependencyAnalysis,
115
124
  publishing_order: Array<string> | null,
@@ -120,7 +129,7 @@ const format_markdown = (
120
129
  const {total_deps, internal_deps} = calculate_stats(graph);
121
130
 
122
131
  lines.push('', '## Summary', '');
123
- lines.push(`- **Total packages**: ${repos.length}`);
132
+ lines.push(`- **Total packages**: ${graph.nodes.size}`);
124
133
  lines.push(`- **Total dependencies**: ${total_deps}`);
125
134
  lines.push(`- **Internal dependencies**: ${internal_deps}`);
126
135
  lines.push(`- **Wildcard dependencies**: ${analysis.wildcard_deps.length}`);
@@ -185,13 +194,12 @@ const format_markdown = (
185
194
  };
186
195
 
187
196
  const format_stdout = (
188
- repos: Array<LocalRepo>,
189
197
  graph: DependencyGraph,
190
198
  analysis: DependencyAnalysis,
191
199
  publishing_order: Array<string> | null,
192
200
  log: Logger,
193
201
  ): void => {
194
- log.info(st('cyan', `📊 Analyzing ${repos.length} repositories...`));
202
+ log.info(st('cyan', `📊 Analyzing ${graph.nodes.size} repositories...`));
195
203
 
196
204
  // Publishing order
197
205
  if (publishing_order) {
@@ -248,7 +256,7 @@ const format_stdout = (
248
256
 
249
257
  log.info('');
250
258
  log.info(st('cyan', 'Summary:'));
251
- log.info(` Total packages: ${repos.length}`);
259
+ log.info(` Total packages: ${graph.nodes.size}`);
252
260
  log.info(` Total dependencies: ${total_deps}`);
253
261
  log.info(` Internal dependencies: ${internal_deps}`);
254
262
  log.info(` Wildcard dependencies: ${analysis.wildcard_deps.length}`);