@pruddiman/hem 0.0.1-beta-5671db0
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/LICENSE +21 -0
- package/dist/agents/arbiter-agent.d.ts +72 -0
- package/dist/agents/arbiter-agent.js +149 -0
- package/dist/agents/architecture-agent.d.ts +148 -0
- package/dist/agents/architecture-agent.js +459 -0
- package/dist/agents/base-agent.d.ts +44 -0
- package/dist/agents/base-agent.js +57 -0
- package/dist/agents/crossref-agent.d.ts +140 -0
- package/dist/agents/crossref-agent.js +560 -0
- package/dist/agents/crossref-arbiter-agent.d.ts +72 -0
- package/dist/agents/crossref-arbiter-agent.js +147 -0
- package/dist/agents/documentation-agent.d.ts +55 -0
- package/dist/agents/documentation-agent.js +159 -0
- package/dist/agents/exploration-agent.d.ts +58 -0
- package/dist/agents/exploration-agent.js +102 -0
- package/dist/agents/grouping-agent.d.ts +167 -0
- package/dist/agents/grouping-agent.js +557 -0
- package/dist/agents/index-agent.d.ts +86 -0
- package/dist/agents/index-agent.js +360 -0
- package/dist/agents/organization-agent.d.ts +144 -0
- package/dist/agents/organization-agent.js +607 -0
- package/dist/auth.d.ts +372 -0
- package/dist/auth.js +1072 -0
- package/dist/broadcast-mcp.d.ts +21 -0
- package/dist/broadcast-mcp.js +59 -0
- package/dist/changelog.d.ts +85 -0
- package/dist/changelog.js +223 -0
- package/dist/decision-queue.d.ts +173 -0
- package/dist/decision-queue.js +265 -0
- package/dist/diff-scope.d.ts +24 -0
- package/dist/diff-scope.js +28 -0
- package/dist/discovery.d.ts +54 -0
- package/dist/discovery.js +405 -0
- package/dist/grouping.d.ts +37 -0
- package/dist/grouping.js +343 -0
- package/dist/helpers/format.d.ts +5 -0
- package/dist/helpers/format.js +13 -0
- package/dist/helpers/index.d.ts +11 -0
- package/dist/helpers/index.js +11 -0
- package/dist/helpers/parsing.d.ts +52 -0
- package/dist/helpers/parsing.js +128 -0
- package/dist/helpers/paths.d.ts +41 -0
- package/dist/helpers/paths.js +67 -0
- package/dist/helpers/strings.d.ts +45 -0
- package/dist/helpers/strings.js +97 -0
- package/dist/index.d.ts +135 -0
- package/dist/index.js +1087 -0
- package/dist/merge-utils.d.ts +22 -0
- package/dist/merge-utils.js +34 -0
- package/dist/orchestrator.d.ts +194 -0
- package/dist/orchestrator.js +1169 -0
- package/dist/output.d.ts +106 -0
- package/dist/output.js +243 -0
- package/dist/progress.d.ts +228 -0
- package/dist/progress.js +644 -0
- package/dist/providers/copilot.d.ts +247 -0
- package/dist/providers/copilot.js +598 -0
- package/dist/providers/index.d.ts +15 -0
- package/dist/providers/index.js +12 -0
- package/dist/providers/opencode.d.ts +156 -0
- package/dist/providers/opencode.js +416 -0
- package/dist/providers/types.d.ts +156 -0
- package/dist/providers/types.js +16 -0
- package/dist/resources.d.ts +76 -0
- package/dist/resources.js +151 -0
- package/dist/search-index.d.ts +71 -0
- package/dist/search-index.js +187 -0
- package/dist/search-mcp.d.ts +25 -0
- package/dist/search-mcp.js +100 -0
- package/dist/server-utils.d.ts +56 -0
- package/dist/server-utils.js +135 -0
- package/dist/session.d.ts +227 -0
- package/dist/session.js +370 -0
- package/dist/types.d.ts +272 -0
- package/dist/types.js +5 -0
- package/dist/worktree.d.ts +82 -0
- package/dist/worktree.js +187 -0
- package/package.json +45 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File discovery module for Hem.
|
|
3
|
+
*
|
|
4
|
+
* Scans a source directory for files matching a glob pattern, classifies
|
|
5
|
+
* each file as text or binary, and returns a `FileInfo[]` array.
|
|
6
|
+
*
|
|
7
|
+
* Also provides `detectProjectName()` for inferring the project name
|
|
8
|
+
* from manifest files (package.json, Cargo.toml, etc.) in ancestor
|
|
9
|
+
* directories of the source path.
|
|
10
|
+
*
|
|
11
|
+
* Reference: FR-002 (file scanning), FR-014 (binary detection), FR-015 (destination exclusion).
|
|
12
|
+
*/
|
|
13
|
+
import { resolve, relative, extname, basename, dirname, join } from "node:path";
|
|
14
|
+
import { stat, open, readFile, access } from "node:fs/promises";
|
|
15
|
+
import fg from "fast-glob";
|
|
16
|
+
import { isPathWithin } from "./helpers/paths.js";
|
|
17
|
+
/**
|
|
18
|
+
* Directories that are almost always irrelevant to documentation and
|
|
19
|
+
* should be excluded from file discovery regardless of .gitignore.
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_IGNORE_PATTERNS = [
|
|
22
|
+
"**/node_modules/**",
|
|
23
|
+
"**/.git/**",
|
|
24
|
+
"**/dist/**",
|
|
25
|
+
"**/build/**",
|
|
26
|
+
"**/coverage/**",
|
|
27
|
+
"**/.cache/**",
|
|
28
|
+
"**/.tmp/**",
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* Known binary file extensions.
|
|
32
|
+
*
|
|
33
|
+
* Files with any of these extensions are classified as binary without
|
|
34
|
+
* reading their contents. The set covers images, fonts, audio/video,
|
|
35
|
+
* archives, executables, and compiled artifacts.
|
|
36
|
+
*/
|
|
37
|
+
const BINARY_EXTENSIONS = new Set([
|
|
38
|
+
".png",
|
|
39
|
+
".jpg",
|
|
40
|
+
".jpeg",
|
|
41
|
+
".gif",
|
|
42
|
+
".bmp",
|
|
43
|
+
".ico",
|
|
44
|
+
".svg",
|
|
45
|
+
".woff",
|
|
46
|
+
".woff2",
|
|
47
|
+
".ttf",
|
|
48
|
+
".eot",
|
|
49
|
+
".mp3",
|
|
50
|
+
".mp4",
|
|
51
|
+
".avi",
|
|
52
|
+
".mov",
|
|
53
|
+
".zip",
|
|
54
|
+
".tar",
|
|
55
|
+
".gz",
|
|
56
|
+
".pdf",
|
|
57
|
+
".exe",
|
|
58
|
+
".dll",
|
|
59
|
+
".so",
|
|
60
|
+
".dylib",
|
|
61
|
+
".o",
|
|
62
|
+
".class",
|
|
63
|
+
".pyc",
|
|
64
|
+
".wasm",
|
|
65
|
+
]);
|
|
66
|
+
/**
|
|
67
|
+
* Detects whether a file is binary by checking the first 512 bytes for
|
|
68
|
+
* null bytes (`0x00`). This heuristic is used only for files whose
|
|
69
|
+
* extension is not in the known binary list.
|
|
70
|
+
*
|
|
71
|
+
* @param absolutePath - Absolute path to the file.
|
|
72
|
+
* @returns `true` if null bytes are found (likely binary).
|
|
73
|
+
*/
|
|
74
|
+
async function detectBinaryByContent(absolutePath) {
|
|
75
|
+
let fh;
|
|
76
|
+
try {
|
|
77
|
+
fh = await open(absolutePath, "r");
|
|
78
|
+
const buffer = Buffer.alloc(512);
|
|
79
|
+
const { bytesRead } = await fh.read(buffer, 0, 512, 0);
|
|
80
|
+
for (let i = 0; i < bytesRead; i++) {
|
|
81
|
+
if (buffer[i] === 0x00) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// If we can't read the file, treat it as non-binary so it still
|
|
89
|
+
// shows up in results (the caller can handle read errors later).
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
try {
|
|
94
|
+
await fh?.close();
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Best-effort close; ignore failure to avoid masking the main return value
|
|
98
|
+
// or (in the catch branch above) the original read error.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Determines whether a file is binary based on its extension and,
|
|
104
|
+
* when the extension is not in the known list, by inspecting the
|
|
105
|
+
* first 512 bytes for null bytes.
|
|
106
|
+
*
|
|
107
|
+
* @param absolutePath - Absolute path to the file.
|
|
108
|
+
* @param extension - File extension including the leading dot.
|
|
109
|
+
* @returns `true` if the file is binary.
|
|
110
|
+
*/
|
|
111
|
+
async function isBinaryFile(absolutePath, extension) {
|
|
112
|
+
if (BINARY_EXTENSIONS.has(extension.toLowerCase())) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
// For files with no extension or an unrecognised extension, fall back
|
|
116
|
+
// to the content-based heuristic.
|
|
117
|
+
if (extension === "" || !extension.startsWith(".")) {
|
|
118
|
+
return detectBinaryByContent(absolutePath);
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Discovers source files within `sourcePath` that match the given glob
|
|
124
|
+
* pattern. Each file is classified as binary or text and returned as a
|
|
125
|
+
* `FileInfo` object.
|
|
126
|
+
*
|
|
127
|
+
* Key behaviours:
|
|
128
|
+
* - `sourcePath` and `destinationPath` are resolved to absolute paths.
|
|
129
|
+
* - Files within `destinationPath` are excluded when the destination
|
|
130
|
+
* overlaps with the source (FR-015).
|
|
131
|
+
* - Binary files are included in the returned array with
|
|
132
|
+
* `isBinary: true` so callers can count/warn about them.
|
|
133
|
+
*
|
|
134
|
+
* @param sourcePath - Root directory to scan.
|
|
135
|
+
* @param globPattern - Glob pattern relative to `sourcePath`.
|
|
136
|
+
* @param destinationPath - Destination directory for generated docs
|
|
137
|
+
* (excluded from results when overlapping with source).
|
|
138
|
+
* @returns Array of `FileInfo` objects for every matched file.
|
|
139
|
+
*/
|
|
140
|
+
export async function discoverFiles(sourcePath, globPattern, destinationPath) {
|
|
141
|
+
const absoluteSource = resolve(sourcePath);
|
|
142
|
+
const absoluteDestination = resolve(destinationPath);
|
|
143
|
+
// FR-015: When the destination directory overlaps with or is inside
|
|
144
|
+
// the source directory, add it as an ignore pattern so fast-glob
|
|
145
|
+
// skips those files entirely (better performance than post-hoc
|
|
146
|
+
// filtering for large trees). We also keep a post-hoc safety check
|
|
147
|
+
// below as a defence-in-depth measure.
|
|
148
|
+
const destIgnorePatterns = [];
|
|
149
|
+
if (isPathWithin(absoluteDestination, absoluteSource)) {
|
|
150
|
+
const relDest = relative(absoluteSource, absoluteDestination);
|
|
151
|
+
// fast-glob ignore patterns are relative to cwd.
|
|
152
|
+
destIgnorePatterns.push(relDest === "" ? "**/*" : `${relDest}/**`);
|
|
153
|
+
}
|
|
154
|
+
const gitignorePatterns = await loadGitignorePatterns(absoluteSource);
|
|
155
|
+
const allIgnorePatterns = [
|
|
156
|
+
...DEFAULT_IGNORE_PATTERNS,
|
|
157
|
+
...destIgnorePatterns,
|
|
158
|
+
...gitignorePatterns,
|
|
159
|
+
];
|
|
160
|
+
// Use fast-glob to discover files. The `cwd` option makes the
|
|
161
|
+
// pattern relative to the source directory. `dot: true` includes
|
|
162
|
+
// dotfiles; `onlyFiles: true` excludes directories.
|
|
163
|
+
const matches = await fg(globPattern, {
|
|
164
|
+
cwd: absoluteSource,
|
|
165
|
+
onlyFiles: true,
|
|
166
|
+
dot: true,
|
|
167
|
+
absolute: true,
|
|
168
|
+
ignore: allIgnorePatterns,
|
|
169
|
+
});
|
|
170
|
+
const results = [];
|
|
171
|
+
for (const absolutePath of matches) {
|
|
172
|
+
// Defence-in-depth: exclude files inside the destination directory
|
|
173
|
+
// even if fast-glob's ignore didn't catch them (e.g., symlinks,
|
|
174
|
+
// race conditions, or destination outside source that somehow
|
|
175
|
+
// appeared in results).
|
|
176
|
+
if (isPathWithin(absolutePath, absoluteDestination)) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const relativePath = relative(absoluteSource, absolutePath);
|
|
180
|
+
const extension = extname(absolutePath);
|
|
181
|
+
let fileSize;
|
|
182
|
+
try {
|
|
183
|
+
const fileStat = await stat(absolutePath);
|
|
184
|
+
fileSize = fileStat.size;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// Skip files we cannot stat (e.g., broken symlinks).
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const binary = await isBinaryFile(absolutePath, extension);
|
|
191
|
+
results.push({
|
|
192
|
+
path: relativePath,
|
|
193
|
+
absolutePath,
|
|
194
|
+
extension,
|
|
195
|
+
size: fileSize,
|
|
196
|
+
isBinary: binary,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
// Sort by relative path for deterministic ordering.
|
|
200
|
+
results.sort((a, b) => a.path.localeCompare(b.path));
|
|
201
|
+
return results;
|
|
202
|
+
}
|
|
203
|
+
// ── Project name detection ──────────────────────────────────────────────
|
|
204
|
+
/**
|
|
205
|
+
* Maximum number of ancestor directories to walk when searching for
|
|
206
|
+
* manifest files. Prevents runaway traversal.
|
|
207
|
+
*/
|
|
208
|
+
const MAX_WALK_DEPTH = 5;
|
|
209
|
+
/**
|
|
210
|
+
* Common directory basenames that are poor project names.
|
|
211
|
+
* If `basename(sourcePath)` matches one of these, we look harder.
|
|
212
|
+
*/
|
|
213
|
+
const GENERIC_DIR_NAMES = new Set([
|
|
214
|
+
"src",
|
|
215
|
+
"lib",
|
|
216
|
+
"app",
|
|
217
|
+
"source",
|
|
218
|
+
"sources",
|
|
219
|
+
"code",
|
|
220
|
+
"packages",
|
|
221
|
+
"modules",
|
|
222
|
+
"internal",
|
|
223
|
+
"cmd",
|
|
224
|
+
"pkg",
|
|
225
|
+
]);
|
|
226
|
+
/**
|
|
227
|
+
* Reads .gitignore at `dir` and converts its patterns to fast-glob
|
|
228
|
+
* ignore patterns. Returns an empty array if the file is absent.
|
|
229
|
+
* @internal
|
|
230
|
+
*/
|
|
231
|
+
async function loadGitignorePatterns(dir) {
|
|
232
|
+
const content = await tryReadFile(join(dir, ".gitignore"));
|
|
233
|
+
if (!content)
|
|
234
|
+
return [];
|
|
235
|
+
return content
|
|
236
|
+
.split("\n")
|
|
237
|
+
.map((line) => line.trim())
|
|
238
|
+
.filter((line) => line.length > 0 && !line.startsWith("#") && !line.startsWith("!"))
|
|
239
|
+
.flatMap((pattern) => {
|
|
240
|
+
// Strip leading slash — gitignore root-anchors with /, fast-glob uses cwd
|
|
241
|
+
const p = pattern.startsWith("/") ? pattern.slice(1) : pattern;
|
|
242
|
+
const bare = p.replace(/\/$/, "");
|
|
243
|
+
// Patterns with no interior slash match anywhere in the tree
|
|
244
|
+
const hasInteriorSlash = bare.includes("/");
|
|
245
|
+
if (!hasInteriorSlash) {
|
|
246
|
+
return [`**/${bare}`, `**/${bare}/**`];
|
|
247
|
+
}
|
|
248
|
+
return [bare, `${bare}/**`];
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Try to read a file at `filePath` and return its text content,
|
|
253
|
+
* or `undefined` if the file does not exist or cannot be read.
|
|
254
|
+
* @internal
|
|
255
|
+
*/
|
|
256
|
+
async function tryReadFile(filePath) {
|
|
257
|
+
try {
|
|
258
|
+
return await readFile(filePath, "utf-8");
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Extract the project name from a `package.json` file.
|
|
266
|
+
* Returns `undefined` if the file is missing, malformed, or the
|
|
267
|
+
* `name` field is absent/empty.
|
|
268
|
+
* @internal
|
|
269
|
+
*/
|
|
270
|
+
async function nameFromPackageJson(dir) {
|
|
271
|
+
const content = await tryReadFile(join(dir, "package.json"));
|
|
272
|
+
if (!content)
|
|
273
|
+
return undefined;
|
|
274
|
+
try {
|
|
275
|
+
const pkg = JSON.parse(content);
|
|
276
|
+
const raw = pkg.name;
|
|
277
|
+
if (typeof raw !== "string" || raw.trim().length === 0)
|
|
278
|
+
return undefined;
|
|
279
|
+
// Scoped packages: "@scope/foo" → "foo"
|
|
280
|
+
const name = raw.trim();
|
|
281
|
+
const slashIdx = name.indexOf("/");
|
|
282
|
+
return slashIdx >= 0 ? name.slice(slashIdx + 1) : name;
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Extract the project name from a `Cargo.toml` file.
|
|
290
|
+
* Uses a simple regex — avoids pulling in a TOML parser.
|
|
291
|
+
* @internal
|
|
292
|
+
*/
|
|
293
|
+
async function nameFromCargoToml(dir) {
|
|
294
|
+
const content = await tryReadFile(join(dir, "Cargo.toml"));
|
|
295
|
+
if (!content)
|
|
296
|
+
return undefined;
|
|
297
|
+
// Match `name = "..."` under `[package]` section
|
|
298
|
+
const packageMatch = content.match(/\[package\][^[]*?name\s*=\s*"([^"]+)"/s);
|
|
299
|
+
return packageMatch?.[1] || undefined;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Extract the project name from a `pyproject.toml` file.
|
|
303
|
+
* Checks `[project]` and `[tool.poetry]` sections.
|
|
304
|
+
* @internal
|
|
305
|
+
*/
|
|
306
|
+
async function nameFromPyprojectToml(dir) {
|
|
307
|
+
const content = await tryReadFile(join(dir, "pyproject.toml"));
|
|
308
|
+
if (!content)
|
|
309
|
+
return undefined;
|
|
310
|
+
// Try [project] section first
|
|
311
|
+
const projectMatch = content.match(/\[project\][^[]*?name\s*=\s*"([^"]+)"/s);
|
|
312
|
+
if (projectMatch?.[1])
|
|
313
|
+
return projectMatch[1];
|
|
314
|
+
// Try [tool.poetry] section
|
|
315
|
+
const poetryMatch = content.match(/\[tool\.poetry\][^[]*?name\s*=\s*"([^"]+)"/s);
|
|
316
|
+
return poetryMatch?.[1] || undefined;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Extract the project name from a `go.mod` file.
|
|
320
|
+
* The module path's last segment is used as the name.
|
|
321
|
+
* @internal
|
|
322
|
+
*/
|
|
323
|
+
async function nameFromGoMod(dir) {
|
|
324
|
+
const content = await tryReadFile(join(dir, "go.mod"));
|
|
325
|
+
if (!content)
|
|
326
|
+
return undefined;
|
|
327
|
+
const moduleMatch = content.match(/^module\s+(\S+)/m);
|
|
328
|
+
if (!moduleMatch?.[1])
|
|
329
|
+
return undefined;
|
|
330
|
+
const modulePath = moduleMatch[1];
|
|
331
|
+
const lastSegment = modulePath.split("/").pop();
|
|
332
|
+
return lastSegment || undefined;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Check whether a `.git` directory exists at the given path.
|
|
336
|
+
* @internal
|
|
337
|
+
*/
|
|
338
|
+
async function hasGitDir(dir) {
|
|
339
|
+
try {
|
|
340
|
+
await access(join(dir, ".git"));
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Detect the project name by walking up from the source directory
|
|
349
|
+
* and inspecting manifest files.
|
|
350
|
+
*
|
|
351
|
+
* Detection priority (first match wins):
|
|
352
|
+
* 1. `package.json` → `name` field (scoped names stripped)
|
|
353
|
+
* 2. `Cargo.toml` → `[package] name`
|
|
354
|
+
* 3. `pyproject.toml` → `[project] name` or `[tool.poetry] name`
|
|
355
|
+
* 4. `go.mod` → last segment of module path
|
|
356
|
+
*
|
|
357
|
+
* The walk starts at the parent of `sourcePath` (since `sourcePath`
|
|
358
|
+
* itself is typically `src/` or similar) and continues up to the
|
|
359
|
+
* git root or a maximum of {@link MAX_WALK_DEPTH} levels.
|
|
360
|
+
*
|
|
361
|
+
* If no manifest is found, falls back to the basename of the
|
|
362
|
+
* directory containing the first manifest-like file, the parent
|
|
363
|
+
* directory, or ultimately `basename(sourcePath)`.
|
|
364
|
+
*
|
|
365
|
+
* @param sourcePath - The absolute source directory path.
|
|
366
|
+
* @returns The detected project name.
|
|
367
|
+
*/
|
|
368
|
+
export async function detectProjectName(sourcePath) {
|
|
369
|
+
const absoluteSource = resolve(sourcePath);
|
|
370
|
+
let dir = dirname(absoluteSource); // Start at parent of source
|
|
371
|
+
const detectors = [
|
|
372
|
+
nameFromPackageJson,
|
|
373
|
+
nameFromCargoToml,
|
|
374
|
+
nameFromPyprojectToml,
|
|
375
|
+
nameFromGoMod,
|
|
376
|
+
];
|
|
377
|
+
for (let depth = 0; depth < MAX_WALK_DEPTH; depth++) {
|
|
378
|
+
// Try each manifest detector at this directory level
|
|
379
|
+
for (const detect of detectors) {
|
|
380
|
+
const name = await detect(dir);
|
|
381
|
+
if (name)
|
|
382
|
+
return name;
|
|
383
|
+
}
|
|
384
|
+
// If we hit a .git directory, this is the repo root — stop walking
|
|
385
|
+
if (await hasGitDir(dir)) {
|
|
386
|
+
// Use the repo root directory name as project name
|
|
387
|
+
const repoName = basename(dir);
|
|
388
|
+
if (repoName && !GENERIC_DIR_NAMES.has(repoName.toLowerCase())) {
|
|
389
|
+
return repoName;
|
|
390
|
+
}
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
// Move up one level
|
|
394
|
+
const parent = dirname(dir);
|
|
395
|
+
if (parent === dir)
|
|
396
|
+
break; // Reached filesystem root
|
|
397
|
+
dir = parent;
|
|
398
|
+
}
|
|
399
|
+
// Fallback chain: parent basename → source basename
|
|
400
|
+
const parentName = basename(dirname(absoluteSource));
|
|
401
|
+
if (parentName && !GENERIC_DIR_NAMES.has(parentName.toLowerCase())) {
|
|
402
|
+
return parentName;
|
|
403
|
+
}
|
|
404
|
+
return basename(absoluteSource);
|
|
405
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File grouping module for Hem.
|
|
3
|
+
*
|
|
4
|
+
* Analyses discovered source files and groups them by feature vertical
|
|
5
|
+
* (e.g., "user", "order") or architectural layer (e.g., controllers,
|
|
6
|
+
* services). Each file appears in at most one group; feature grouping
|
|
7
|
+
* takes priority over layer grouping.
|
|
8
|
+
*
|
|
9
|
+
* Reference: FR-003, data-model.md lines 93-108.
|
|
10
|
+
*/
|
|
11
|
+
import type { FileInfo, FileGroup } from "./types.js";
|
|
12
|
+
/**
|
|
13
|
+
* Finds the common parent directory for a set of file paths.
|
|
14
|
+
*
|
|
15
|
+
* @param files - Array of FileInfo objects.
|
|
16
|
+
* @returns The common parent directory (relative path), or `"."` for root.
|
|
17
|
+
*/
|
|
18
|
+
export declare function commonDirectory(files: FileInfo[]): string;
|
|
19
|
+
/**
|
|
20
|
+
* Groups discovered files by feature vertical or architectural layer.
|
|
21
|
+
*
|
|
22
|
+
* Grouping strategy:
|
|
23
|
+
* 1. Filter out binary files.
|
|
24
|
+
* 2. Attempt to assign each file to a **feature vertical** group
|
|
25
|
+
* based on its directory structure (e.g., files under `user/` →
|
|
26
|
+
* "User" feature group).
|
|
27
|
+
* 3. Files not assigned to a feature group are checked for
|
|
28
|
+
* **architectural layer** membership based on file name suffixes
|
|
29
|
+
* (e.g., `.controller.ts` → "Controllers" layer) or containing
|
|
30
|
+
* directory (e.g., `services/` → "Services").
|
|
31
|
+
* 4. Remaining files go into a catch-all "Other" group.
|
|
32
|
+
* 5. Each file appears in at most one group.
|
|
33
|
+
*
|
|
34
|
+
* @param files - Discovered files (may include binary files).
|
|
35
|
+
* @returns Array of `FileGroup` objects.
|
|
36
|
+
*/
|
|
37
|
+
export declare function groupFiles(files: FileInfo[]): FileGroup[];
|