@oxy.so/doctor 1.1.1 → 1.2.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.
package/README.md CHANGED
@@ -15,9 +15,14 @@ bunx @oxy.so/doctor --ci --grace-days=7
15
15
  ```
16
16
 
17
17
  It checks direct dependencies under the canonical `@oxy.so/*` scope against
18
- npm, reports duplicate
19
- versions in `bun.lock`, and fails
20
- clearly when the lockfile is missing.
18
+ npm, reports duplicate versions in `bun.lock`, and fails clearly when the
19
+ lockfile is missing.
20
+
21
+ It reads the manifests Bun installs from: the root `package.json` and the
22
+ workspaces its `workspaces` patterns match. A `catalog:` dependency is checked
23
+ where its range lives, the root's `workspaces.catalog` (or `catalogs.<name>`).
24
+ A `package.json` outside the workspaces, such as a frozen copy kept as audit
25
+ evidence, is not this repository's dependency state and is not read.
21
26
 
22
27
  A range that doesn't include the newest published `@oxy.so/*` release is a
23
28
  **warning** for 14 days after that release was published (`--grace-days=N`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxy.so/doctor",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Read-only health checks for Oxy ecosystem repositories",
5
5
  "repository": {
6
6
  "type": "git",
package/src/checks.mjs CHANGED
@@ -74,18 +74,85 @@ export async function findRepositoryRoot(start = process.cwd()) {
74
74
  }
75
75
  }
76
76
 
77
- async function collectManifests(directory, depth = 0) {
78
- if (depth > 4) return [];
79
- const ignored = new Set(['.git', '.expo', '.turbo', '.worktrees', 'dist', 'lib', 'node_modules']);
80
- const entries = await readdir(directory, { withFileTypes: true });
81
- const manifests = [];
82
- for (const entry of entries) {
83
- if (entry.isFile() && entry.name === 'package.json')
84
- manifests.push(join(directory, entry.name));
85
- if (entry.isDirectory() && !ignored.has(entry.name))
86
- manifests.push(...(await collectManifests(join(directory, entry.name), depth + 1)));
77
+ const IGNORED_DIRECTORIES = new Set([
78
+ '.git',
79
+ '.expo',
80
+ '.turbo',
81
+ '.worktrees',
82
+ 'dist',
83
+ 'lib',
84
+ 'node_modules',
85
+ ]);
86
+
87
+ async function childDirectories(directory) {
88
+ const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
89
+ return entries
90
+ .filter((entry) => entry.isDirectory() && !IGNORED_DIRECTORIES.has(entry.name))
91
+ .map((entry) => join(directory, entry.name));
92
+ }
93
+
94
+ /** Directories matching one Bun workspace pattern (`dir`, `dir/*`, `dir/**`). */
95
+ async function expandWorkspacePattern(directory, segments) {
96
+ if (segments.length === 0) return [directory];
97
+ const [segment, ...rest] = segments;
98
+ if (segment === '**') {
99
+ const here = await expandWorkspacePattern(directory, rest);
100
+ const deeper = await Promise.all(
101
+ (await childDirectories(directory)).map((child) => expandWorkspacePattern(child, segments)),
102
+ );
103
+ return [...here, ...deeper.flat()];
104
+ }
105
+ if (segment.includes('*')) {
106
+ const matcher = new RegExp(
107
+ `^${segment.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*')}$`,
108
+ );
109
+ const children = (await childDirectories(directory)).filter((child) =>
110
+ matcher.test(child.slice(directory.length + 1)),
111
+ );
112
+ return (await Promise.all(children.map((child) => expandWorkspacePattern(child, rest)))).flat();
113
+ }
114
+ return expandWorkspacePattern(join(directory, segment), rest);
115
+ }
116
+
117
+ /**
118
+ * The manifests Bun installs from: the root and its workspaces. A package.json
119
+ * outside them (a frozen copy kept as audit evidence, a fixture) is not
120
+ * installed from this lockfile, so it is not this repository's dependency state.
121
+ */
122
+ async function collectManifests(root, rootManifest) {
123
+ const workspaces = rootManifest.workspaces;
124
+ const patterns = Array.isArray(workspaces) ? workspaces : (workspaces?.packages ?? []);
125
+ const directories = new Set([root]);
126
+ const excluded = new Set();
127
+ for (const pattern of patterns) {
128
+ const negated = pattern.startsWith('!');
129
+ const segments = (negated ? pattern.slice(1) : pattern)
130
+ .replace(/^\.\//, '')
131
+ .split('/')
132
+ .filter(Boolean);
133
+ for (const directory of await expandWorkspacePattern(root, segments))
134
+ (negated ? excluded : directories).add(directory);
87
135
  }
88
- return manifests;
136
+ return [...directories]
137
+ .filter((directory) => !excluded.has(directory))
138
+ .map((directory) => join(directory, 'package.json'))
139
+ .filter((path) => existsSync(path))
140
+ .sort();
141
+ }
142
+
143
+ /** `catalog:` / `catalog:<name>` resolved against the root manifest's catalogs. */
144
+ function catalogEntries(rootManifest) {
145
+ const workspaces = Array.isArray(rootManifest.workspaces) ? {} : (rootManifest.workspaces ?? {});
146
+ const rows = [];
147
+ const add = (section, catalog) => {
148
+ for (const [name, range] of Object.entries(catalog ?? {})) rows.push({ section, name, range });
149
+ };
150
+ add('catalog', workspaces.catalog ?? rootManifest.catalog);
151
+ for (const [catalogName, catalog] of Object.entries(
152
+ workspaces.catalogs ?? rootManifest.catalogs ?? {},
153
+ ))
154
+ add(`catalogs.${catalogName}`, catalog);
155
+ return rows;
89
156
  }
90
157
 
91
158
  /**
@@ -98,8 +165,15 @@ export async function inspectRepository(
98
165
  fetchLatest,
99
166
  { graceDays = DEFAULT_GRACE_DAYS, now = new Date() } = {},
100
167
  ) {
101
- const manifestPaths = await collectManifests(root);
168
+ const rootManifest = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
169
+ const manifestPaths = await collectManifests(root, rootManifest);
102
170
  const usages = new Map();
171
+ for (const { section, name, range } of catalogEntries(rootManifest)) {
172
+ if (!isFirstPartyPackage(name)) continue;
173
+ const rows = usages.get(name) ?? [];
174
+ rows.push({ manifest: 'package.json', section, range });
175
+ usages.set(name, rows);
176
+ }
103
177
  for (const path of manifestPaths) {
104
178
  const manifest = JSON.parse(await readFile(path, 'utf8'));
105
179
  for (const section of [
@@ -109,7 +183,12 @@ export async function inspectRepository(
109
183
  'optionalDependencies',
110
184
  ]) {
111
185
  for (const [name, range] of Object.entries(manifest[section] ?? {})) {
112
- if (!isFirstPartyPackage(name) || range === 'workspace:*' || range === 'catalog:') continue;
186
+ if (
187
+ !isFirstPartyPackage(name) ||
188
+ range.startsWith('workspace:') ||
189
+ range.startsWith('catalog:')
190
+ )
191
+ continue;
113
192
  const rows = usages.get(name) ?? [];
114
193
  rows.push({ manifest: path.slice(root.length + 1), section, range });
115
194
  usages.set(name, rows);