harnery 0.41.0 → 0.42.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 (212) hide show
  1. package/dist/commander.js +3 -0
  2. package/dist/commands/agents.d.ts.map +1 -1
  3. package/dist/commands/agents.js +9 -1
  4. package/dist/commands/artifacts.d.ts.map +1 -1
  5. package/dist/commands/artifacts.js +113 -12
  6. package/dist/commands/browse.d.ts +164 -0
  7. package/dist/commands/browse.d.ts.map +1 -1
  8. package/dist/commands/browse.js +263 -51
  9. package/dist/commands/claude-desktop.d.ts.map +1 -1
  10. package/dist/commands/claude-desktop.js +52 -0
  11. package/dist/commands/disk.d.ts +6 -0
  12. package/dist/commands/disk.d.ts.map +1 -0
  13. package/dist/commands/disk.js +80 -0
  14. package/dist/commands/doctor.d.ts +8 -0
  15. package/dist/commands/doctor.d.ts.map +1 -1
  16. package/dist/commands/doctor.js +48 -1
  17. package/dist/commands/files.d.ts +16 -0
  18. package/dist/commands/files.d.ts.map +1 -1
  19. package/dist/commands/files.js +54 -2
  20. package/dist/commands/init.d.ts +2 -2
  21. package/dist/commands/init.d.ts.map +1 -1
  22. package/dist/commands/init.js +43 -9
  23. package/dist/commands/rm.d.ts +4 -0
  24. package/dist/commands/rm.d.ts.map +1 -0
  25. package/dist/commands/rm.js +87 -0
  26. package/dist/commands/servers.d.ts +7 -0
  27. package/dist/commands/servers.d.ts.map +1 -0
  28. package/dist/commands/servers.js +394 -0
  29. package/dist/commands/tunnel.d.ts.map +1 -1
  30. package/dist/commands/tunnel.js +82 -2
  31. package/dist/core/adapters/profiles.d.ts +9 -0
  32. package/dist/core/adapters/profiles.d.ts.map +1 -1
  33. package/dist/core/adapters/profiles.js +22 -1
  34. package/dist/core/adapters/types.d.ts +9 -0
  35. package/dist/core/adapters/types.d.ts.map +1 -1
  36. package/dist/core/agents/cli.js +13 -6
  37. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  38. package/dist/core/agents/render/session-context.js +2 -0
  39. package/dist/core/agents/state/live-coordination-view.d.ts +20 -0
  40. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  41. package/dist/core/agents/state/live-coordination-view.js +60 -0
  42. package/dist/core/artifacts/delivery-card.js +9 -2
  43. package/dist/core/artifacts/index.d.ts +140 -4
  44. package/dist/core/artifacts/index.d.ts.map +1 -1
  45. package/dist/core/artifacts/index.js +522 -49
  46. package/dist/core/config.d.ts +68 -11
  47. package/dist/core/config.d.ts.map +1 -1
  48. package/dist/core/config.js +128 -3
  49. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  50. package/dist/core/diagnostics/bundle.js +2 -2
  51. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  52. package/dist/core/events/v3/producers/recorder.js +16 -0
  53. package/dist/core/events/v3/projection.js +9 -1
  54. package/dist/core/hooks/adapter/events.d.ts +17 -0
  55. package/dist/core/hooks/adapter/events.d.ts.map +1 -1
  56. package/dist/core/hooks/adapter/events.js +5 -0
  57. package/dist/core/hooks/adapter/runtime-telemetry.js +3 -1
  58. package/dist/core/hooks/adapter/wiring.d.ts +37 -7
  59. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  60. package/dist/core/hooks/adapter/wiring.js +62 -4
  61. package/dist/core/hooks/cli.js +45 -6
  62. package/dist/core/hooks/run-markers.d.ts +37 -0
  63. package/dist/core/hooks/run-markers.d.ts.map +1 -0
  64. package/dist/core/hooks/run-markers.js +109 -0
  65. package/dist/core/servers/index.d.ts +211 -0
  66. package/dist/core/servers/index.d.ts.map +1 -0
  67. package/dist/core/servers/index.js +560 -0
  68. package/dist/core/servers/net.d.ts +58 -0
  69. package/dist/core/servers/net.d.ts.map +1 -0
  70. package/dist/core/servers/net.js +270 -0
  71. package/dist/core/servers/tunnels.d.ts +14 -0
  72. package/dist/core/servers/tunnels.d.ts.map +1 -0
  73. package/dist/core/servers/tunnels.js +33 -0
  74. package/dist/core/storage/builtins.js +1 -0
  75. package/dist/core/workflow/engine.d.ts.map +1 -1
  76. package/dist/core/workflow/engine.js +30 -1
  77. package/dist/core/workflow/sandbox-projection.d.ts +13 -2
  78. package/dist/core/workflow/sandbox-projection.d.ts.map +1 -1
  79. package/dist/core/workflow/sandbox-projection.js +23 -0
  80. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  81. package/dist/core/workflow/spawn-claude.js +6 -1
  82. package/dist/core/workflow/spawn-codex.d.ts +2 -0
  83. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  84. package/dist/core/workflow/spawn-codex.js +20 -1
  85. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  86. package/dist/core/workflow/spawn-cursor.js +6 -1
  87. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  88. package/dist/core/workflow/spawn-opencode.js +4 -1
  89. package/dist/core/workflow/types.d.ts +47 -2
  90. package/dist/core/workflow/types.d.ts.map +1 -1
  91. package/dist/core/workflow/worker-access.d.ts +92 -0
  92. package/dist/core/workflow/worker-access.d.ts.map +1 -0
  93. package/dist/core/workflow/worker-access.js +450 -0
  94. package/dist/lib/browser/batch-steps.d.ts +18 -0
  95. package/dist/lib/browser/batch-steps.d.ts.map +1 -0
  96. package/dist/lib/browser/batch-steps.js +82 -0
  97. package/dist/lib/browser/collect/collector.d.ts +99 -0
  98. package/dist/lib/browser/collect/collector.d.ts.map +1 -0
  99. package/dist/lib/browser/collect/collector.js +202 -0
  100. package/dist/lib/browser/collect/fields.d.ts +31 -0
  101. package/dist/lib/browser/collect/fields.d.ts.map +1 -0
  102. package/dist/lib/browser/collect/fields.js +75 -0
  103. package/dist/lib/browser/collect/index.d.ts +6 -0
  104. package/dist/lib/browser/collect/index.d.ts.map +1 -0
  105. package/dist/lib/browser/collect/index.js +5 -0
  106. package/dist/lib/browser/collect/page-driver.d.ts +14 -0
  107. package/dist/lib/browser/collect/page-driver.d.ts.map +1 -0
  108. package/dist/lib/browser/collect/page-driver.js +306 -0
  109. package/dist/lib/browser/collect/presets.d.ts +52 -0
  110. package/dist/lib/browser/collect/presets.d.ts.map +1 -0
  111. package/dist/lib/browser/collect/presets.js +176 -0
  112. package/dist/lib/browser/collect/scroll-plan.d.ts +85 -0
  113. package/dist/lib/browser/collect/scroll-plan.d.ts.map +1 -0
  114. package/dist/lib/browser/collect/scroll-plan.js +118 -0
  115. package/dist/lib/browser/index.d.ts +1 -1
  116. package/dist/lib/browser/index.d.ts.map +1 -1
  117. package/dist/lib/browser/page-review-judge.d.ts +2 -0
  118. package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
  119. package/dist/lib/browser/page-review-judge.js +13 -0
  120. package/dist/lib/browser/qa-run-contracts.d.ts +10 -0
  121. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  122. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  123. package/dist/lib/browser/qa-run.js +1 -0
  124. package/dist/lib/browser/windows-chrome.d.ts +59 -0
  125. package/dist/lib/browser/windows-chrome.d.ts.map +1 -0
  126. package/dist/lib/browser/windows-chrome.js +179 -0
  127. package/dist/lib/claude-desktop-share.d.ts +136 -0
  128. package/dist/lib/claude-desktop-share.d.ts.map +1 -0
  129. package/dist/lib/claude-desktop-share.js +394 -0
  130. package/dist/lib/disk-usage.d.ts +50 -0
  131. package/dist/lib/disk-usage.d.ts.map +1 -0
  132. package/dist/lib/disk-usage.js +303 -0
  133. package/dist/lib/docs-lint.d.ts +6 -0
  134. package/dist/lib/docs-lint.d.ts.map +1 -1
  135. package/dist/lib/docs-lint.js +8 -3
  136. package/dist/lib/guarded-remove.d.ts +34 -0
  137. package/dist/lib/guarded-remove.d.ts.map +1 -0
  138. package/dist/lib/guarded-remove.js +310 -0
  139. package/dist/lib/instructions/templates.js +8 -8
  140. package/dist/lib/tunnel/error-page.d.ts +1 -1
  141. package/dist/lib/tunnel/error-page.d.ts.map +1 -1
  142. package/dist/lib/tunnel/error-page.js +19 -8
  143. package/dist/lib/tunnel/gate.js +20 -1
  144. package/dist/lib/tunnel/path-scope.d.ts +22 -0
  145. package/dist/lib/tunnel/path-scope.d.ts.map +1 -0
  146. package/dist/lib/tunnel/path-scope.js +62 -0
  147. package/dist/lib/tunnel/state.d.ts +5 -0
  148. package/dist/lib/tunnel/state.d.ts.map +1 -1
  149. package/dist/lib/tunnel/state.js +17 -3
  150. package/package.json +6 -1
  151. package/schemas/config.schema.json +91 -0
  152. package/src/commander.ts +19 -0
  153. package/src/commands/agents.ts +14 -0
  154. package/src/commands/artifacts.ts +171 -19
  155. package/src/commands/browse.ts +400 -57
  156. package/src/commands/claude-desktop.ts +61 -0
  157. package/src/commands/disk.ts +114 -0
  158. package/src/commands/doctor.ts +52 -1
  159. package/src/commands/files.ts +93 -2
  160. package/src/commands/init.ts +55 -5
  161. package/src/commands/rm.ts +115 -0
  162. package/src/commands/servers.ts +506 -0
  163. package/src/commands/tunnel.ts +102 -2
  164. package/src/core/adapters/profiles.ts +22 -1
  165. package/src/core/adapters/types.ts +10 -0
  166. package/src/core/agents/cli.ts +14 -6
  167. package/src/core/agents/render/session-context.ts +1 -0
  168. package/src/core/agents/state/live-coordination-view.ts +73 -0
  169. package/src/core/artifacts/delivery-card.ts +10 -2
  170. package/src/core/artifacts/index.ts +697 -44
  171. package/src/core/config.ts +183 -4
  172. package/src/core/diagnostics/bundle.ts +2 -1
  173. package/src/core/events/v3/producers/recorder.ts +16 -0
  174. package/src/core/events/v3/projection.ts +11 -1
  175. package/src/core/hooks/adapter/events.ts +19 -0
  176. package/src/core/hooks/adapter/runtime-telemetry.ts +3 -1
  177. package/src/core/hooks/adapter/wiring.ts +116 -8
  178. package/src/core/hooks/cli.ts +47 -6
  179. package/src/core/hooks/run-markers.ts +132 -0
  180. package/src/core/servers/index.ts +712 -0
  181. package/src/core/servers/net.ts +283 -0
  182. package/src/core/servers/tunnels.ts +39 -0
  183. package/src/core/storage/builtins.ts +1 -0
  184. package/src/core/workflow/engine.ts +38 -1
  185. package/src/core/workflow/sandbox-projection.ts +38 -2
  186. package/src/core/workflow/spawn-claude.ts +5 -1
  187. package/src/core/workflow/spawn-codex.ts +18 -1
  188. package/src/core/workflow/spawn-cursor.ts +5 -1
  189. package/src/core/workflow/spawn-opencode.ts +4 -1
  190. package/src/core/workflow/types.ts +62 -2
  191. package/src/core/workflow/worker-access.ts +525 -0
  192. package/src/lib/browser/batch-steps.ts +79 -0
  193. package/src/lib/browser/collect/collector.ts +306 -0
  194. package/src/lib/browser/collect/fields.ts +87 -0
  195. package/src/lib/browser/collect/index.ts +47 -0
  196. package/src/lib/browser/collect/page-driver.ts +327 -0
  197. package/src/lib/browser/collect/presets.ts +237 -0
  198. package/src/lib/browser/collect/scroll-plan.ts +179 -0
  199. package/src/lib/browser/index.ts +1 -0
  200. package/src/lib/browser/page-review-judge.ts +16 -0
  201. package/src/lib/browser/qa-run-contracts.ts +11 -0
  202. package/src/lib/browser/qa-run.ts +1 -0
  203. package/src/lib/browser/windows-chrome.ts +227 -0
  204. package/src/lib/claude-desktop-share.ts +511 -0
  205. package/src/lib/disk-usage.ts +347 -0
  206. package/src/lib/docs-lint.ts +8 -3
  207. package/src/lib/guarded-remove.ts +376 -0
  208. package/src/lib/instructions/templates.ts +8 -8
  209. package/src/lib/tunnel/error-page.ts +25 -9
  210. package/src/lib/tunnel/gate.ts +22 -1
  211. package/src/lib/tunnel/path-scope.ts +73 -0
  212. package/src/lib/tunnel/state.ts +20 -3
@@ -0,0 +1,347 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { type Dirent, lstatSync, readdirSync, type Stats } from "node:fs";
3
+ import { dirname, extname, isAbsolute, relative, resolve, sep } from "node:path";
4
+
5
+ export const DISK_GIT_STATES = [
6
+ "tracked",
7
+ "untracked",
8
+ "ignored",
9
+ "git-metadata",
10
+ "non-repository",
11
+ "unknown",
12
+ ] as const;
13
+ export type DiskGitState = (typeof DISK_GIT_STATES)[number];
14
+ export type DiskGroup = "directory" | "repository" | "extension" | "git" | "files";
15
+
16
+ export interface DiskOptions {
17
+ git?: string;
18
+ type?: string;
19
+ exclude?: string[];
20
+ group?: DiskGroup;
21
+ depth?: number;
22
+ top?: number;
23
+ minSize?: number;
24
+ apparent?: boolean;
25
+ }
26
+
27
+ export interface DiskTotals {
28
+ files: number;
29
+ apparent_bytes: number;
30
+ allocated_bytes: number | null;
31
+ }
32
+ export interface DiskRow extends DiskTotals {
33
+ path: string;
34
+ }
35
+ export interface DiskReport {
36
+ schema: "harnery.disk-usage/v1";
37
+ root: string;
38
+ measurement: "allocated" | "apparent" | "apparent-fallback";
39
+ complete: boolean;
40
+ filters: DiskOptions;
41
+ totals: DiskTotals;
42
+ git: Record<DiskGitState, DiskTotals>;
43
+ groups: DiskRow[];
44
+ group_count: number;
45
+ largest_files: (DiskRow & { git: DiskGitState; repository: string | null })[];
46
+ repositories: string[];
47
+ skipped: { symlinks: number; special_files: number; excluded: number };
48
+ issues: { path: string; message: string }[];
49
+ }
50
+
51
+ const TYPES: Record<string, Set<string>> = {
52
+ video: new Set(["mp4", "mov", "mkv", "webm", "avi", "m4v", "mxf"]),
53
+ audio: new Set(["mp3", "wav", "m4a", "aac", "flac", "ogg", "aiff"]),
54
+ image: new Set([
55
+ "png",
56
+ "jpg",
57
+ "jpeg",
58
+ "webp",
59
+ "gif",
60
+ "tif",
61
+ "tiff",
62
+ "svg",
63
+ "avif",
64
+ "heic",
65
+ "bmp",
66
+ "psd",
67
+ ]),
68
+ archive: new Set(["zip", "gz", "tgz", "tar", "bz2", "xz", "7z", "rar"]),
69
+ };
70
+ TYPES.media = new Set([...TYPES.video!, ...TYPES.audio!, ...TYPES.image!]);
71
+
72
+ interface Repository {
73
+ root: string;
74
+ tracked: Set<string>;
75
+ untracked: Set<string>;
76
+ valid: boolean;
77
+ }
78
+ const portable = (path: string): string => path.split(sep).join("/");
79
+ const empty = (allocated: boolean): DiskTotals => ({
80
+ files: 0,
81
+ apparent_bytes: 0,
82
+ allocated_bytes: allocated ? 0 : null,
83
+ });
84
+
85
+ export function parseDiskSize(value: string): number {
86
+ const match = /^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB|KiB|MiB|GiB|TiB)?$/i.exec(value);
87
+ if (!match) throw new Error("--min-size requires bytes or a size such as 10MB or 1GiB");
88
+ const units: Record<string, number> = {
89
+ b: 1,
90
+ kb: 1e3,
91
+ mb: 1e6,
92
+ gb: 1e9,
93
+ tb: 1e12,
94
+ kib: 1024,
95
+ mib: 1024 ** 2,
96
+ gib: 1024 ** 3,
97
+ tib: 1024 ** 4,
98
+ };
99
+ const bytes = Number(match[1]) * units[(match[2] ?? "b").toLowerCase()]!;
100
+ if (!Number.isSafeInteger(bytes) || bytes < 0)
101
+ throw new Error("--min-size must resolve to a non-negative whole byte count");
102
+ return bytes;
103
+ }
104
+
105
+ /** Inspect regular files only. No bodies are read and no symlinks are followed. */
106
+ export function diskUsage(input = ".", options: DiskOptions = {}): DiskReport {
107
+ const root = resolve(input);
108
+ const rootStat = lstatSync(root);
109
+ if (rootStat.isSymbolicLink() || (!rootStat.isDirectory() && !rootStat.isFile())) {
110
+ throw new Error("disk requires a regular file or directory, not a symlink or special file");
111
+ }
112
+ const group = options.group ?? "directory";
113
+ if (!["directory", "repository", "extension", "git", "files"].includes(group))
114
+ throw new Error("invalid --group; use directory, repository, extension, git, or files");
115
+ const depth = options.depth ?? 1;
116
+ const top = options.top ?? 20;
117
+ if (!Number.isSafeInteger(depth) || depth < 1)
118
+ throw new Error("--depth must be a positive integer");
119
+ if (!Number.isSafeInteger(top) || top < 1) throw new Error("--top must be a positive integer");
120
+ if (
121
+ options.minSize !== undefined &&
122
+ (!Number.isSafeInteger(options.minSize) || options.minSize < 0)
123
+ )
124
+ throw new Error("invalid --min-size");
125
+ const states = options.git ? new Set(options.git.split(",")) : null;
126
+ if (states && [...states].some((s) => !DISK_GIT_STATES.includes(s as DiskGitState)))
127
+ throw new Error(`invalid --git; use ${DISK_GIT_STATES.join(", ")}`);
128
+ const extensions = options.type
129
+ ? new Set(
130
+ options.type
131
+ .toLowerCase()
132
+ .split(",")
133
+ .flatMap((type) => [...(TYPES[type] ?? new Set([type.replace(/^\./, "")]))]),
134
+ )
135
+ : null;
136
+ const excludes = (options.exclude ?? [])
137
+ .flatMap((value) => value.split(","))
138
+ .map((value) => value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, ""));
139
+ if (excludes.some((value) => !value || isAbsolute(value) || value.split("/").includes("..")))
140
+ throw new Error("--exclude requires directory names or paths relative to the scan root");
141
+ let allocatedAvailable = process.platform !== "win32" && Number.isFinite(rootStat.blocks);
142
+ const report: DiskReport = {
143
+ schema: "harnery.disk-usage/v1",
144
+ root,
145
+ measurement: options.apparent
146
+ ? "apparent"
147
+ : allocatedAvailable
148
+ ? "allocated"
149
+ : "apparent-fallback",
150
+ complete: true,
151
+ filters: { ...options, group, depth, top },
152
+ totals: empty(allocatedAvailable),
153
+ git: Object.fromEntries(
154
+ DISK_GIT_STATES.map((state) => [state, empty(allocatedAvailable)]),
155
+ ) as Record<DiskGitState, DiskTotals>,
156
+ groups: [],
157
+ group_count: 0,
158
+ largest_files: [],
159
+ repositories: [],
160
+ skipped: { symlinks: 0, special_files: 0, excluded: 0 },
161
+ issues: [],
162
+ };
163
+ const groups = new Map<string, DiskRow>();
164
+ const repositories = new Map<string, Repository>();
165
+ const seen = new Set<string>();
166
+ const issue = (path: string, error: unknown): void => {
167
+ report.complete = false;
168
+ report.issues.push({ path, message: error instanceof Error ? error.message : String(error) });
169
+ };
170
+ const git = (cwd: string, args: string[]) =>
171
+ spawnSync("git", ["-C", cwd, "-c", "core.fsmonitor=false", ...args], {
172
+ encoding: "utf8",
173
+ maxBuffer: 256 * 1024 * 1024,
174
+ timeout: 60_000,
175
+ windowsHide: true,
176
+ });
177
+ const loadRepo = (repoRoot: string): Repository => {
178
+ const cached = repositories.get(repoRoot);
179
+ if (cached) return cached;
180
+ const repo: Repository = {
181
+ root: repoRoot,
182
+ tracked: new Set(),
183
+ untracked: new Set(),
184
+ valid: false,
185
+ };
186
+ repositories.set(repoRoot, repo);
187
+ for (const [args, target] of [
188
+ [["ls-files", "--cached", "-z"], repo.tracked],
189
+ [["ls-files", "--others", "--exclude-standard", "-z"], repo.untracked],
190
+ ] as const) {
191
+ const result = git(repoRoot, [...args]);
192
+ if (result.status !== 0) {
193
+ issue(
194
+ portable(relative(root, repoRoot)) || ".",
195
+ result.error ?? new Error(`Git classification failed: ${result.stderr.trim()}`),
196
+ );
197
+ return repo;
198
+ }
199
+ for (const path of result.stdout.split("\0")) if (path) target.add(path);
200
+ }
201
+ repo.valid = true;
202
+ return repo;
203
+ };
204
+ const startDir = rootStat.isDirectory() ? root : dirname(root);
205
+ const detected = git(startDir, ["rev-parse", "--show-toplevel"]);
206
+ if (detected.error) issue(".", detected.error);
207
+ const initialRepo = detected.status === 0 ? loadRepo(resolve(detected.stdout.trim())) : null;
208
+ const repoLabel = (repo: Repository | null): string | null => {
209
+ if (!repo) return null;
210
+ const label = portable(relative(root, repo.root));
211
+ return !label || label === ".." || label.startsWith("../") ? "." : label;
212
+ };
213
+ const metric = (row: DiskTotals): number =>
214
+ options.apparent || row.allocated_bytes === null ? row.apparent_bytes : row.allocated_bytes;
215
+ const compare = (a: DiskRow, b: DiskRow): number =>
216
+ metric(b) - metric(a) || a.path.localeCompare(b.path);
217
+ const add = (totals: DiskTotals, size: number, allocated: number): void => {
218
+ totals.files++;
219
+ totals.apparent_bytes += size;
220
+ if (totals.allocated_bytes !== null) totals.allocated_bytes += allocated;
221
+ };
222
+ const excluded = (path: string): boolean =>
223
+ excludes.some((value) =>
224
+ value.includes("/")
225
+ ? path === value || path.startsWith(`${value}/`)
226
+ : path.split("/").includes(value),
227
+ );
228
+ const file = (path: string, metadata: Stats, repo: Repository | null, inGit: boolean): void => {
229
+ const rel = portable(relative(root, path)) || ".";
230
+ const extension = extname(path).slice(1).toLowerCase();
231
+ const repoPath = repo ? portable(relative(repo.root, path)) : "";
232
+ const state: DiskGitState = inGit
233
+ ? "git-metadata"
234
+ : !repo
235
+ ? "non-repository"
236
+ : !repo.valid
237
+ ? "unknown"
238
+ : repo.tracked.has(repoPath)
239
+ ? "tracked"
240
+ : repo.untracked.has(repoPath)
241
+ ? "untracked"
242
+ : "ignored";
243
+ if ((states && !states.has(state)) || (extensions && !extensions.has(extension))) return;
244
+ const blocksAvailable = process.platform !== "win32" && Number.isFinite(metadata.blocks);
245
+ if (!blocksAvailable) allocatedAvailable = false;
246
+ const allocation = blocksAvailable ? metadata.blocks * 512 : metadata.size;
247
+ if (
248
+ (options.apparent || !blocksAvailable ? metadata.size : allocation) < (options.minSize ?? 0)
249
+ )
250
+ return;
251
+ const identity = metadata.ino ? `${metadata.dev}:${metadata.ino}` : path;
252
+ const allocated = seen.has(identity) ? 0 : allocation;
253
+ seen.add(identity);
254
+ add(report.totals, metadata.size, allocated);
255
+ add(report.git[state], metadata.size, allocated);
256
+ const row: DiskReport["largest_files"][number] = {
257
+ path: rel,
258
+ files: 1,
259
+ apparent_bytes: metadata.size,
260
+ allocated_bytes: blocksAvailable ? allocation : null,
261
+ git: state,
262
+ repository: repoLabel(repo),
263
+ };
264
+ report.largest_files.push(row);
265
+ report.largest_files.sort(compare);
266
+ if (report.largest_files.length > top) report.largest_files.pop();
267
+ if (group === "files") return;
268
+ const parts = rel.split("/");
269
+ const label =
270
+ group === "git"
271
+ ? state
272
+ : group === "repository"
273
+ ? (repoLabel(repo) ?? "(non-repository)")
274
+ : group === "extension"
275
+ ? extension
276
+ ? `.${extension}`
277
+ : "(no extension)"
278
+ : parts.length > 1
279
+ ? parts.slice(0, Math.min(depth, parts.length - 1)).join("/")
280
+ : ".";
281
+ let bucket = groups.get(label);
282
+ if (!bucket) {
283
+ bucket = { path: label, ...empty(allocatedAvailable) };
284
+ groups.set(label, bucket);
285
+ }
286
+ add(bucket, metadata.size, allocated);
287
+ };
288
+ const walk = (dir: string, parentRepo: Repository | null, inGit: boolean): void => {
289
+ let entries: Dirent[];
290
+ try {
291
+ // A directory can turn into a link between reading its parent and visiting it.
292
+ if (lstatSync(dir).isSymbolicLink()) {
293
+ report.skipped.symlinks++;
294
+ return;
295
+ }
296
+ entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) =>
297
+ a.name.localeCompare(b.name),
298
+ );
299
+ } catch (error) {
300
+ issue(portable(relative(root, dir)) || ".", error);
301
+ return;
302
+ }
303
+ const marker = entries.find((entry) => entry.name === ".git");
304
+ const repo = !inGit && marker && !marker.isSymbolicLink() ? loadRepo(dir) : parentRepo;
305
+ for (const entry of entries) {
306
+ const path = resolve(dir, entry.name);
307
+ const rel = portable(relative(root, path));
308
+ if (excluded(rel)) {
309
+ report.skipped.excluded++;
310
+ continue;
311
+ }
312
+ if (entry.isSymbolicLink()) {
313
+ report.skipped.symlinks++;
314
+ continue;
315
+ }
316
+ const gitMetadata = inGit || entry.name === ".git";
317
+ if (entry.isDirectory()) {
318
+ if (gitMetadata && states && !states.has("git-metadata")) continue;
319
+ walk(path, repo, gitMetadata);
320
+ } else if (entry.isFile()) {
321
+ try {
322
+ const metadata = lstatSync(path);
323
+ if (metadata.isSymbolicLink()) report.skipped.symlinks++;
324
+ else if (metadata.isFile()) file(path, metadata, repo, gitMetadata);
325
+ else report.skipped.special_files++;
326
+ } catch (error) {
327
+ issue(rel, error);
328
+ }
329
+ } else report.skipped.special_files++;
330
+ }
331
+ };
332
+ const inMetadata = startDir.split(sep).includes(".git");
333
+ if (rootStat.isDirectory()) walk(root, initialRepo, inMetadata);
334
+ else if (!excluded("."))
335
+ file(root, rootStat, initialRepo, inMetadata || root.endsWith(`${sep}.git`));
336
+ if (!allocatedAvailable) {
337
+ report.totals.allocated_bytes = null;
338
+ for (const totals of Object.values(report.git)) totals.allocated_bytes = null;
339
+ for (const row of groups.values()) row.allocated_bytes = null;
340
+ report.measurement = options.apparent ? "apparent" : "apparent-fallback";
341
+ }
342
+ report.repositories = [...repositories.values()].map((repo) => repoLabel(repo)!).sort();
343
+ report.group_count = group === "files" ? report.totals.files : groups.size;
344
+ report.groups =
345
+ group === "files" ? report.largest_files : [...groups.values()].sort(compare).slice(0, top);
346
+ return report;
347
+ }
@@ -166,9 +166,14 @@ function findMarkdownFiles(view: DocsRepositoryView): string[] {
166
166
  );
167
167
  }
168
168
 
169
- /** Detect whether a file declares itself an intentional monolith */
170
- function isDeclaredMonolith(content: string): boolean {
171
- const head = content.split("\n").slice(0, 10).join("\n");
169
+ /**
170
+ * Detect whether a file declares itself an intentional monolith. The banner
171
+ * must sit in the first 10 lines of the body; leading YAML frontmatter does
172
+ * not count toward those lines, since a v2 header alone can fill them.
173
+ */
174
+ export function isDeclaredMonolith(content: string): boolean {
175
+ const body = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "");
176
+ const head = body.split("\n").slice(0, 10).join("\n");
172
177
  return /INTENTIONAL-MONOLITH/i.test(head);
173
178
  }
174
179