@compilr-dev/sdk 0.9.8 → 0.9.9

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.
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Common Detection Utilities
3
+ *
4
+ * Shared by all type-specific strategies: git detection, README parsing,
5
+ * file counting, folder name parsing.
6
+ */
7
+ import type { DetectionResult } from './types.js';
8
+ /**
9
+ * Detect common project info (runs for ALL project types).
10
+ * Returns a base DetectionResult with name, git, README, file counts.
11
+ */
12
+ export declare function detectCommon(projectPath: string): DetectionResult;
13
+ /**
14
+ * List files matching a pattern in a directory (non-recursive or single level).
15
+ */
16
+ export declare function findFiles(dir: string, predicate: (name: string, ext: string) => boolean, maxDepth?: number): string[];
17
+ /**
18
+ * Check if a directory exists within the project.
19
+ */
20
+ export declare function hasDirectory(projectPath: string, dirName: string): boolean;
21
+ /**
22
+ * Read a file's content if it exists, or return undefined.
23
+ */
24
+ export declare function readFileIfExists(filePath: string): string | undefined;
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Common Detection Utilities
3
+ *
4
+ * Shared by all type-specific strategies: git detection, README parsing,
5
+ * file counting, folder name parsing.
6
+ */
7
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
8
+ import { join, basename, extname } from 'node:path';
9
+ /** Directories to skip when scanning */
10
+ const IGNORED_DIRS = new Set([
11
+ 'node_modules',
12
+ '.git',
13
+ '__pycache__',
14
+ '.venv',
15
+ 'venv',
16
+ '.tox',
17
+ '.mypy_cache',
18
+ '.pytest_cache',
19
+ '.next',
20
+ '.nuxt',
21
+ 'dist',
22
+ 'build',
23
+ 'target',
24
+ '.gradle',
25
+ '.idea',
26
+ '.vscode',
27
+ '.DS_Store',
28
+ 'vendor',
29
+ '.compilr',
30
+ '.compilr-dev',
31
+ ]);
32
+ const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg']);
33
+ const MARKDOWN_EXTENSIONS = new Set(['.md', '.markdown', '.mdx']);
34
+ /**
35
+ * Detect common project info (runs for ALL project types).
36
+ * Returns a base DetectionResult with name, git, README, file counts.
37
+ */
38
+ export function detectCommon(projectPath) {
39
+ const folderName = basename(projectPath);
40
+ const name = slugify(folderName);
41
+ const displayName = prettify(folderName);
42
+ // Git detection
43
+ const hasGit = existsSync(join(projectPath, '.git'));
44
+ let gitRemote;
45
+ if (hasGit) {
46
+ gitRemote = readGitRemote(projectPath);
47
+ }
48
+ // COMPILR.md
49
+ const hasCompilrMd = existsSync(join(projectPath, 'COMPILR.md'));
50
+ // Description from README
51
+ const description = readDescription(projectPath);
52
+ // File counting
53
+ const content = countFiles(projectPath);
54
+ return {
55
+ name,
56
+ displayName,
57
+ description,
58
+ gitRemote,
59
+ hasGit,
60
+ hasCompilrMd,
61
+ content,
62
+ };
63
+ }
64
+ /**
65
+ * Read the git remote URL from .git/config.
66
+ */
67
+ function readGitRemote(projectPath) {
68
+ const configPath = join(projectPath, '.git', 'config');
69
+ if (!existsSync(configPath))
70
+ return undefined;
71
+ try {
72
+ const config = readFileSync(configPath, 'utf-8');
73
+ const match = config.match(/\[remote "origin"\]\s*\n\s*url\s*=\s*(.+)/);
74
+ return match?.[1]?.trim();
75
+ }
76
+ catch {
77
+ return undefined;
78
+ }
79
+ }
80
+ /**
81
+ * Read description from README.md or COMPILR.md (first non-heading paragraph).
82
+ */
83
+ function readDescription(projectPath) {
84
+ const candidates = ['README.md', 'readme.md', 'COMPILR.md'];
85
+ for (const file of candidates) {
86
+ const filePath = join(projectPath, file);
87
+ if (!existsSync(filePath))
88
+ continue;
89
+ try {
90
+ const content = readFileSync(filePath, 'utf-8');
91
+ const lines = content.split('\n');
92
+ // Find first non-empty, non-heading line
93
+ for (const line of lines) {
94
+ const trimmed = line.trim();
95
+ if (!trimmed)
96
+ continue;
97
+ if (trimmed.startsWith('#'))
98
+ continue;
99
+ if (trimmed.startsWith('!['))
100
+ continue; // badge images
101
+ if (trimmed.startsWith('<!--'))
102
+ continue;
103
+ // Found a content line — take up to 200 chars
104
+ return trimmed.length > 200 ? trimmed.slice(0, 200) + '...' : trimmed;
105
+ }
106
+ }
107
+ catch {
108
+ continue;
109
+ }
110
+ }
111
+ return undefined;
112
+ }
113
+ /**
114
+ * Count files in a project directory (non-recursive into ignored dirs).
115
+ */
116
+ function countFiles(projectPath) {
117
+ let fileCount = 0;
118
+ let totalSizeBytes = 0;
119
+ let markdownFiles = 0;
120
+ let imageFiles = 0;
121
+ function walk(dir, depth) {
122
+ if (depth > 8)
123
+ return; // Don't go too deep
124
+ let entries;
125
+ try {
126
+ entries = readdirSync(dir);
127
+ }
128
+ catch {
129
+ return;
130
+ }
131
+ for (const entry of entries) {
132
+ if (IGNORED_DIRS.has(entry))
133
+ continue;
134
+ if (entry.startsWith('.') && entry !== '.git')
135
+ continue;
136
+ const fullPath = join(dir, entry);
137
+ let stat;
138
+ try {
139
+ stat = statSync(fullPath);
140
+ }
141
+ catch {
142
+ continue;
143
+ }
144
+ if (stat.isDirectory()) {
145
+ walk(fullPath, depth + 1);
146
+ }
147
+ else if (stat.isFile()) {
148
+ fileCount++;
149
+ totalSizeBytes += stat.size;
150
+ const ext = extname(entry).toLowerCase();
151
+ if (MARKDOWN_EXTENSIONS.has(ext))
152
+ markdownFiles++;
153
+ if (IMAGE_EXTENSIONS.has(ext))
154
+ imageFiles++;
155
+ }
156
+ }
157
+ }
158
+ walk(projectPath, 0);
159
+ return {
160
+ fileCount,
161
+ totalSizeKB: Math.round(totalSizeBytes / 1024),
162
+ markdownFiles,
163
+ imageFiles,
164
+ };
165
+ }
166
+ /**
167
+ * List files matching a pattern in a directory (non-recursive or single level).
168
+ */
169
+ export function findFiles(dir, predicate, maxDepth = 2) {
170
+ const results = [];
171
+ function walk(currentDir, depth) {
172
+ if (depth > maxDepth)
173
+ return;
174
+ let entries;
175
+ try {
176
+ entries = readdirSync(currentDir);
177
+ }
178
+ catch {
179
+ return;
180
+ }
181
+ for (const entry of entries) {
182
+ if (IGNORED_DIRS.has(entry))
183
+ continue;
184
+ const fullPath = join(currentDir, entry);
185
+ let stat;
186
+ try {
187
+ stat = statSync(fullPath);
188
+ }
189
+ catch {
190
+ continue;
191
+ }
192
+ if (stat.isDirectory()) {
193
+ walk(fullPath, depth + 1);
194
+ }
195
+ else if (stat.isFile()) {
196
+ const ext = extname(entry).toLowerCase();
197
+ if (predicate(entry, ext)) {
198
+ // Return relative path from root dir
199
+ results.push(fullPath.slice(dir.length + 1));
200
+ }
201
+ }
202
+ }
203
+ }
204
+ walk(dir, 0);
205
+ return results;
206
+ }
207
+ /**
208
+ * Check if a directory exists within the project.
209
+ */
210
+ export function hasDirectory(projectPath, dirName) {
211
+ const dirPath = join(projectPath, dirName);
212
+ try {
213
+ return existsSync(dirPath) && statSync(dirPath).isDirectory();
214
+ }
215
+ catch {
216
+ return false;
217
+ }
218
+ }
219
+ /**
220
+ * Read a file's content if it exists, or return undefined.
221
+ */
222
+ export function readFileIfExists(filePath) {
223
+ try {
224
+ if (existsSync(filePath))
225
+ return readFileSync(filePath, 'utf-8');
226
+ }
227
+ catch {
228
+ /* ignore */
229
+ }
230
+ return undefined;
231
+ }
232
+ /**
233
+ * Slugify a folder name for use as project name.
234
+ */
235
+ function slugify(name) {
236
+ return name
237
+ .toLowerCase()
238
+ .replace(/[^a-z0-9-]/g, '-')
239
+ .replace(/-+/g, '-')
240
+ .replace(/^-|-$/g, '');
241
+ }
242
+ /**
243
+ * Prettify a folder name for display.
244
+ */
245
+ function prettify(name) {
246
+ return name.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
247
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Project Detection — Universal project content detection.
3
+ */
4
+ export { detectProject, suggestProjectType } from './project-detector.js';
5
+ export { detectCommon } from './common.js';
6
+ export type { DetectProjectOptions, DetectionResult, ContentSummary, DetectionStrategy, } from './types.js';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Project Detection — Universal project content detection.
3
+ */
4
+ export { detectProject, suggestProjectType } from './project-detector.js';
5
+ export { detectCommon } from './common.js';
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Project Detector — Universal project detection dispatcher.
3
+ *
4
+ * Runs common detection (git, README, file counts) for all types,
5
+ * then delegates to a type-specific strategy for additional scanning.
6
+ */
7
+ import type { DetectProjectOptions, DetectionResult } from './types.js';
8
+ /**
9
+ * Detect project contents based on the specified type.
10
+ *
11
+ * 1. Runs common detection (git, README, file counts) for all types
12
+ * 2. Delegates to type-specific strategy for additional scanning
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const result = await detectProject({
17
+ * path: '/Users/me/projects/my-app',
18
+ * type: 'web',
19
+ * });
20
+ * console.log(result.content.language); // 'typescript'
21
+ * console.log(result.content.framework); // 'react + express'
22
+ * ```
23
+ */
24
+ export declare function detectProject(options: DetectProjectOptions): DetectionResult;
25
+ /**
26
+ * Quick check to suggest a project type based on folder contents.
27
+ * Returns the most likely type, or 'general' if uncertain.
28
+ */
29
+ export declare function suggestProjectType(projectPath: string): string;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Project Detector — Universal project detection dispatcher.
3
+ *
4
+ * Runs common detection (git, README, file counts) for all types,
5
+ * then delegates to a type-specific strategy for additional scanning.
6
+ */
7
+ import { existsSync, readdirSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { detectCommon } from './common.js';
10
+ import { detectSoftware } from './strategies/software.js';
11
+ import { detectResearch } from './strategies/research.js';
12
+ import { detectBook } from './strategies/book.js';
13
+ import { detectBusiness } from './strategies/business.js';
14
+ import { detectContent } from './strategies/content.js';
15
+ import { detectTechDocs } from './strategies/tech-docs.js';
16
+ import { detectCourse } from './strategies/course.js';
17
+ import { detectGeneral } from './strategies/general.js';
18
+ /**
19
+ * Strategy map — maps project type IDs to detection functions.
20
+ * Software subtypes (web, cli, library, api) all use the software strategy.
21
+ */
22
+ const STRATEGIES = {
23
+ // Software types
24
+ web: detectSoftware,
25
+ cli: detectSoftware,
26
+ library: detectSoftware,
27
+ api: detectSoftware,
28
+ // Non-software types
29
+ research: detectResearch,
30
+ book: detectBook,
31
+ 'business-plan': detectBusiness,
32
+ content: detectContent,
33
+ 'tech-docs': detectTechDocs,
34
+ course: detectCourse,
35
+ // Catch-all
36
+ general: detectGeneral,
37
+ };
38
+ /**
39
+ * Detect project contents based on the specified type.
40
+ *
41
+ * 1. Runs common detection (git, README, file counts) for all types
42
+ * 2. Delegates to type-specific strategy for additional scanning
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * const result = await detectProject({
47
+ * path: '/Users/me/projects/my-app',
48
+ * type: 'web',
49
+ * });
50
+ * console.log(result.content.language); // 'typescript'
51
+ * console.log(result.content.framework); // 'react + express'
52
+ * ```
53
+ */
54
+ export function detectProject(options) {
55
+ // Step 1: Common detection (runs for ALL types)
56
+ const base = detectCommon(options.path);
57
+ // Step 2: Type-specific strategy
58
+ const strategy = STRATEGIES[options.type] ?? detectGeneral;
59
+ const enrichedContent = strategy(options.path, base.content);
60
+ return {
61
+ ...base,
62
+ content: enrichedContent,
63
+ };
64
+ }
65
+ /**
66
+ * Quick check to suggest a project type based on folder contents.
67
+ * Returns the most likely type, or 'general' if uncertain.
68
+ */
69
+ export function suggestProjectType(projectPath) {
70
+ // Software indicators (strongest signals)
71
+ if (existsSync(join(projectPath, 'package.json')))
72
+ return 'web';
73
+ if (existsSync(join(projectPath, 'go.mod')))
74
+ return 'web';
75
+ if (existsSync(join(projectPath, 'Cargo.toml')))
76
+ return 'web';
77
+ if (existsSync(join(projectPath, 'pyproject.toml')) ||
78
+ existsSync(join(projectPath, 'requirements.txt'))) {
79
+ // Python could be software or research — check for research indicators
80
+ if (existsSync(join(projectPath, 'references.bib')) ||
81
+ existsSync(join(projectPath, 'main.tex'))) {
82
+ return 'research';
83
+ }
84
+ return 'web';
85
+ }
86
+ if (existsSync(join(projectPath, 'pom.xml')) || existsSync(join(projectPath, 'build.gradle')))
87
+ return 'web';
88
+ // Research indicators
89
+ try {
90
+ const entries = readdirSync(projectPath);
91
+ if (entries.some((f) => f.endsWith('.bib')))
92
+ return 'research';
93
+ if (entries.includes('main.tex'))
94
+ return 'research';
95
+ // Tech docs indicators
96
+ if (entries.includes('mkdocs.yml') || entries.includes('docusaurus.config.js'))
97
+ return 'tech-docs';
98
+ // Book indicators
99
+ if (entries.some((f) => /^(chapter|ch)[-_]?\d/i.test(f)))
100
+ return 'book';
101
+ }
102
+ catch {
103
+ /* unreadable directory */
104
+ }
105
+ return 'general';
106
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Book Detection Strategy
3
+ *
4
+ * Detects: chapter files, outline, manuscript/draft folders.
5
+ */
6
+ import type { ContentSummary } from '../types.js';
7
+ export declare function detectBook(path: string, base: ContentSummary): ContentSummary;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Book Detection Strategy
3
+ *
4
+ * Detects: chapter files, outline, manuscript/draft folders.
5
+ */
6
+ import { join } from 'node:path';
7
+ import { findFiles, hasDirectory } from '../common.js';
8
+ export function detectBook(path, base) {
9
+ const result = { ...base };
10
+ // Chapter files (chapter-1.md, ch01.md, chapters/01-intro.md, etc.)
11
+ const chapterPattern = /^(chapter|ch)[-_]?\d/i;
12
+ const chapterFiles = findFiles(path, (name) => chapterPattern.test(name), 2);
13
+ // Also check chapters/ directory
14
+ if (hasDirectory(path, 'chapters')) {
15
+ const inDir = findFiles(join(path, 'chapters'), (_name, ext) => ext === '.md' || ext === '.txt', 1);
16
+ chapterFiles.push(...inDir.map((f) => `chapters/${f}`));
17
+ }
18
+ if (chapterFiles.length > 0) {
19
+ result.chapterCount = chapterFiles.length;
20
+ }
21
+ // Outline file
22
+ const outlineCandidates = [
23
+ 'outline.md',
24
+ 'outline.txt',
25
+ 'OUTLINE.md',
26
+ 'toc.md',
27
+ 'table-of-contents.md',
28
+ ];
29
+ for (const candidate of outlineCandidates) {
30
+ const found = findFiles(path, (name) => name.toLowerCase() === candidate.toLowerCase(), 0);
31
+ if (found.length > 0) {
32
+ result.outlineFile = found[0];
33
+ break;
34
+ }
35
+ }
36
+ // Draft/manuscript files
37
+ const draftDirs = ['manuscript', 'drafts', 'draft', 'content'];
38
+ for (const dir of draftDirs) {
39
+ if (hasDirectory(path, dir)) {
40
+ const drafts = findFiles(join(path, dir), (_name, ext) => ext === '.md' || ext === '.txt', 1);
41
+ if (drafts.length > 0) {
42
+ result.draftFiles = drafts.map((f) => `${dir}/${f}`);
43
+ break;
44
+ }
45
+ }
46
+ }
47
+ return result;
48
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Business Plan Detection Strategy
3
+ *
4
+ * Detects: spreadsheets, presentations, analysis folders.
5
+ */
6
+ import type { ContentSummary } from '../types.js';
7
+ export declare function detectBusiness(path: string, base: ContentSummary): ContentSummary;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Business Plan Detection Strategy
3
+ *
4
+ * Detects: spreadsheets, presentations, analysis folders.
5
+ */
6
+ import { findFiles } from '../common.js';
7
+ export function detectBusiness(path, base) {
8
+ const result = { ...base };
9
+ // Spreadsheet files
10
+ const spreadsheetExts = new Set(['.xlsx', '.xls', '.csv', '.tsv', '.ods']);
11
+ const spreadsheets = findFiles(path, (_name, ext) => spreadsheetExts.has(ext), 2);
12
+ if (spreadsheets.length > 0) {
13
+ result.spreadsheetFiles = spreadsheets.length;
14
+ }
15
+ // Presentation files
16
+ const presentationExts = new Set(['.pptx', '.ppt', '.pdf', '.key']);
17
+ const presentations = findFiles(path, (_name, ext) => presentationExts.has(ext), 2);
18
+ if (presentations.length > 0) {
19
+ result.presentationFiles = presentations.length;
20
+ }
21
+ return result;
22
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Content & Marketing Detection Strategy
3
+ *
4
+ * Detects: article folders, editorial calendar, media assets.
5
+ */
6
+ import type { ContentSummary } from '../types.js';
7
+ export declare function detectContent(path: string, base: ContentSummary): ContentSummary;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Content & Marketing Detection Strategy
3
+ *
4
+ * Detects: article folders, editorial calendar, media assets.
5
+ */
6
+ import { findFiles, hasDirectory } from '../common.js';
7
+ export function detectContent(path, base) {
8
+ const result = { ...base };
9
+ // Draft files in common content directories
10
+ const contentDirs = ['articles', 'posts', 'content', 'blog', 'drafts'];
11
+ for (const dir of contentDirs) {
12
+ if (hasDirectory(path, dir)) {
13
+ const drafts = findFiles(`${path}/${dir}`, (_name, ext) => ext === '.md' || ext === '.txt', 1);
14
+ if (drafts.length > 0) {
15
+ result.draftFiles = drafts.map((f) => `${dir}/${f}`);
16
+ break;
17
+ }
18
+ }
19
+ }
20
+ return result;
21
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Course / Training Detection Strategy
3
+ *
4
+ * Detects: lesson/module directories, exercise files, syllabus.
5
+ */
6
+ import type { ContentSummary } from '../types.js';
7
+ export declare function detectCourse(path: string, base: ContentSummary): ContentSummary;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Course / Training Detection Strategy
3
+ *
4
+ * Detects: lesson/module directories, exercise files, syllabus.
5
+ */
6
+ import { join } from 'node:path';
7
+ import { findFiles, hasDirectory } from '../common.js';
8
+ export function detectCourse(path, base) {
9
+ const result = { ...base };
10
+ // Lesson/module directories
11
+ const lessonDirs = ['modules', 'lessons', 'units', 'weeks', 'sessions'];
12
+ for (const dir of lessonDirs) {
13
+ if (hasDirectory(path, dir)) {
14
+ // Count subdirectories as lessons
15
+ const items = findFiles(join(path, dir), () => true, 0);
16
+ result.lessonCount = items.length;
17
+ break;
18
+ }
19
+ }
20
+ // Exercise files
21
+ const exerciseDirs = ['exercises', 'homework', 'labs', 'assignments'];
22
+ for (const dir of exerciseDirs) {
23
+ if (hasDirectory(path, dir)) {
24
+ const files = findFiles(join(path, dir), () => true, 1);
25
+ result.exerciseFiles = files.length;
26
+ break;
27
+ }
28
+ }
29
+ return result;
30
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * General Detection Strategy
3
+ *
4
+ * Minimal scan — just the common detection (git, README, file counts).
5
+ * No type-specific additions.
6
+ */
7
+ import type { ContentSummary } from '../types.js';
8
+ export declare function detectGeneral(_path: string, base: ContentSummary): ContentSummary;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * General Detection Strategy
3
+ *
4
+ * Minimal scan — just the common detection (git, README, file counts).
5
+ * No type-specific additions.
6
+ */
7
+ export function detectGeneral(_path, base) {
8
+ // Common detection already covers everything for general projects
9
+ return { ...base };
10
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Research Detection Strategy
3
+ *
4
+ * Detects: bibliography files, LaTeX, data files, draft folders.
5
+ */
6
+ import type { ContentSummary } from '../types.js';
7
+ export declare function detectResearch(path: string, base: ContentSummary): ContentSummary;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Research Detection Strategy
3
+ *
4
+ * Detects: bibliography files, LaTeX, data files, draft folders.
5
+ */
6
+ import { readFileSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { findFiles, hasDirectory } from '../common.js';
9
+ export function detectResearch(path, base) {
10
+ const result = { ...base };
11
+ // Bibliography (.bib files)
12
+ const bibFiles = findFiles(path, (_name, ext) => ext === '.bib', 1);
13
+ if (bibFiles.length > 0) {
14
+ result.bibliographyFile = bibFiles[0];
15
+ try {
16
+ const content = readFileSync(join(path, bibFiles[0]), 'utf-8');
17
+ result.bibliographyEntries = (content.match(/@\w+\{/g) ?? []).length;
18
+ }
19
+ catch {
20
+ /* ignore */
21
+ }
22
+ }
23
+ // LaTeX files
24
+ const texFiles = findFiles(path, (_name, ext) => ext === '.tex', 2);
25
+ if (texFiles.length > 0) {
26
+ result.latexFiles = texFiles;
27
+ }
28
+ // Data files (.csv, .xlsx, .json in data/ or root)
29
+ const dataExtensions = new Set(['.csv', '.xlsx', '.xls', '.json', '.tsv']);
30
+ const dataFiles = findFiles(path, (_name, ext) => dataExtensions.has(ext), 2);
31
+ if (dataFiles.length > 0) {
32
+ result.dataFiles = dataFiles.length;
33
+ }
34
+ // Check for common research folders
35
+ if (hasDirectory(path, 'drafts') || hasDirectory(path, 'papers')) {
36
+ // Count draft markdown files
37
+ const draftDir = hasDirectory(path, 'drafts') ? 'drafts' : 'papers';
38
+ const drafts = findFiles(join(path, draftDir), (_name, ext) => ext === '.md' || ext === '.tex', 1);
39
+ if (drafts.length > 0) {
40
+ result.draftFiles = drafts.map((f) => `${draftDir}/${f}`);
41
+ }
42
+ }
43
+ return result;
44
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Software Detection Strategy
3
+ *
4
+ * Detects: language, framework, package manager, entry points, test framework.
5
+ * Uses file-based heuristics (package.json, go.mod, pyproject.toml, etc.).
6
+ */
7
+ import type { ContentSummary } from '../types.js';
8
+ export declare function detectSoftware(path: string, base: ContentSummary): ContentSummary;
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Software Detection Strategy
3
+ *
4
+ * Detects: language, framework, package manager, entry points, test framework.
5
+ * Uses file-based heuristics (package.json, go.mod, pyproject.toml, etc.).
6
+ */
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { findFiles, readFileIfExists } from '../common.js';
10
+ export function detectSoftware(path, base) {
11
+ const result = { ...base };
12
+ // ── Node.js / JavaScript / TypeScript ─────────────────────────────
13
+ const pkgJsonPath = join(path, 'package.json');
14
+ if (existsSync(pkgJsonPath)) {
15
+ try {
16
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
17
+ const rawDeps = (pkg.dependencies ?? {});
18
+ const rawDevDeps = (pkg.devDependencies ?? {});
19
+ const deps = { ...rawDeps, ...rawDevDeps };
20
+ // Language
21
+ result.language =
22
+ deps.typescript || existsSync(join(path, 'tsconfig.json')) ? 'typescript' : 'javascript';
23
+ // Framework
24
+ if (deps.next)
25
+ result.framework = 'next';
26
+ else if (deps.react)
27
+ result.framework = deps.express ? 'react + express' : 'react';
28
+ else if (deps.vue)
29
+ result.framework = deps.express ? 'vue + express' : 'vue';
30
+ else if (deps.svelte || deps['@sveltejs/kit'])
31
+ result.framework = 'svelte';
32
+ else if (deps.express)
33
+ result.framework = 'express';
34
+ else if (deps.fastify)
35
+ result.framework = 'fastify';
36
+ else if (deps.hono)
37
+ result.framework = 'hono';
38
+ // Package manager
39
+ if (existsSync(join(path, 'pnpm-lock.yaml')))
40
+ result.packageManager = 'pnpm';
41
+ else if (existsSync(join(path, 'yarn.lock')))
42
+ result.packageManager = 'yarn';
43
+ else if (existsSync(join(path, 'bun.lockb')))
44
+ result.packageManager = 'bun';
45
+ else
46
+ result.packageManager = 'npm';
47
+ // Test framework
48
+ if (deps.vitest)
49
+ result.testFramework = 'vitest';
50
+ else if (deps.jest)
51
+ result.testFramework = 'jest';
52
+ else if (deps.mocha)
53
+ result.testFramework = 'mocha';
54
+ else if (deps['@playwright/test'])
55
+ result.testFramework = 'playwright';
56
+ // Entry points
57
+ const entries = [];
58
+ for (const candidate of [
59
+ 'src/index.ts',
60
+ 'src/index.js',
61
+ 'src/main.ts',
62
+ 'src/main.js',
63
+ 'src/app.ts',
64
+ 'src/app.js',
65
+ 'index.ts',
66
+ 'index.js',
67
+ ]) {
68
+ if (existsSync(join(path, candidate)))
69
+ entries.push(candidate);
70
+ }
71
+ if (entries.length > 0)
72
+ result.entryPoints = entries;
73
+ }
74
+ catch {
75
+ /* invalid package.json */
76
+ }
77
+ return result;
78
+ }
79
+ // ── Python ────────────────────────────────────────────────────────
80
+ const hasPyproject = existsSync(join(path, 'pyproject.toml'));
81
+ const hasRequirements = existsSync(join(path, 'requirements.txt'));
82
+ const hasSetupPy = existsSync(join(path, 'setup.py'));
83
+ if (hasPyproject || hasRequirements || hasSetupPy) {
84
+ result.language = 'python';
85
+ if (hasPyproject) {
86
+ const content = readFileIfExists(join(path, 'pyproject.toml'));
87
+ if (content) {
88
+ if (content.includes('poetry'))
89
+ result.packageManager = 'poetry';
90
+ else if (content.includes('pdm'))
91
+ result.packageManager = 'pdm';
92
+ else
93
+ result.packageManager = 'pip';
94
+ if (content.includes('fastapi'))
95
+ result.framework = 'fastapi';
96
+ else if (content.includes('django'))
97
+ result.framework = 'django';
98
+ else if (content.includes('flask'))
99
+ result.framework = 'flask';
100
+ if (content.includes('pytest'))
101
+ result.testFramework = 'pytest';
102
+ }
103
+ }
104
+ else {
105
+ result.packageManager = 'pip';
106
+ const reqs = readFileIfExists(join(path, 'requirements.txt'));
107
+ if (reqs) {
108
+ if (reqs.includes('fastapi'))
109
+ result.framework = 'fastapi';
110
+ else if (reqs.includes('django'))
111
+ result.framework = 'django';
112
+ else if (reqs.includes('flask'))
113
+ result.framework = 'flask';
114
+ if (reqs.includes('pytest'))
115
+ result.testFramework = 'pytest';
116
+ }
117
+ }
118
+ const entries = [];
119
+ for (const candidate of ['main.py', 'app.py', 'server.py', 'src/main.py', 'src/app.py']) {
120
+ if (existsSync(join(path, candidate)))
121
+ entries.push(candidate);
122
+ }
123
+ if (entries.length > 0)
124
+ result.entryPoints = entries;
125
+ return result;
126
+ }
127
+ // ── Go ────────────────────────────────────────────────────────────
128
+ if (existsSync(join(path, 'go.mod'))) {
129
+ result.language = 'go';
130
+ result.packageManager = 'go mod';
131
+ const goMod = readFileIfExists(join(path, 'go.mod'));
132
+ if (goMod) {
133
+ if (goMod.includes('github.com/gin-gonic/gin'))
134
+ result.framework = 'gin';
135
+ else if (goMod.includes('github.com/labstack/echo'))
136
+ result.framework = 'echo';
137
+ else if (goMod.includes('github.com/gofiber/fiber'))
138
+ result.framework = 'fiber';
139
+ }
140
+ const entries = [];
141
+ for (const candidate of ['main.go', 'cmd/main.go', 'cmd/server/main.go']) {
142
+ if (existsSync(join(path, candidate)))
143
+ entries.push(candidate);
144
+ }
145
+ if (entries.length > 0)
146
+ result.entryPoints = entries;
147
+ return result;
148
+ }
149
+ // ── Rust ──────────────────────────────────────────────────────────
150
+ if (existsSync(join(path, 'Cargo.toml'))) {
151
+ result.language = 'rust';
152
+ result.packageManager = 'cargo';
153
+ const cargo = readFileIfExists(join(path, 'Cargo.toml'));
154
+ if (cargo) {
155
+ if (cargo.includes('actix-web'))
156
+ result.framework = 'actix';
157
+ else if (cargo.includes('axum'))
158
+ result.framework = 'axum';
159
+ else if (cargo.includes('rocket'))
160
+ result.framework = 'rocket';
161
+ }
162
+ if (existsSync(join(path, 'src', 'main.rs')))
163
+ result.entryPoints = ['src/main.rs'];
164
+ else if (existsSync(join(path, 'src', 'lib.rs')))
165
+ result.entryPoints = ['src/lib.rs'];
166
+ return result;
167
+ }
168
+ // ── Java / Kotlin ─────────────────────────────────────────────────
169
+ if (existsSync(join(path, 'pom.xml'))) {
170
+ result.language = 'java';
171
+ result.packageManager = 'maven';
172
+ return result;
173
+ }
174
+ if (existsSync(join(path, 'build.gradle')) || existsSync(join(path, 'build.gradle.kts'))) {
175
+ result.language = existsSync(join(path, 'build.gradle.kts')) ? 'kotlin' : 'java';
176
+ result.packageManager = 'gradle';
177
+ return result;
178
+ }
179
+ // ── Fallback: check for common source files ───────────────────────
180
+ const sourceFiles = findFiles(path, (_name, ext) => ['.ts', '.js', '.py', '.go', '.rs', '.java', '.kt', '.rb', '.php', '.cs', '.swift'].includes(ext), 1);
181
+ if (sourceFiles.length > 0) {
182
+ const ext = sourceFiles[0].split('.').pop();
183
+ const langMap = {
184
+ ts: 'typescript',
185
+ js: 'javascript',
186
+ py: 'python',
187
+ go: 'go',
188
+ rs: 'rust',
189
+ java: 'java',
190
+ kt: 'kotlin',
191
+ rb: 'ruby',
192
+ php: 'php',
193
+ cs: 'csharp',
194
+ swift: 'swift',
195
+ };
196
+ result.language = ext ? langMap[ext] : undefined;
197
+ }
198
+ return result;
199
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Tech Docs Detection Strategy
3
+ *
4
+ * Detects: documentation frameworks (mkdocs, docusaurus, sphinx), docs folder.
5
+ */
6
+ import type { ContentSummary } from '../types.js';
7
+ export declare function detectTechDocs(path: string, base: ContentSummary): ContentSummary;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Tech Docs Detection Strategy
3
+ *
4
+ * Detects: documentation frameworks (mkdocs, docusaurus, sphinx), docs folder.
5
+ */
6
+ import { existsSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { hasDirectory } from '../common.js';
9
+ export function detectTechDocs(path, base) {
10
+ const result = { ...base };
11
+ // MkDocs
12
+ if (existsSync(join(path, 'mkdocs.yml')) || existsSync(join(path, 'mkdocs.yaml'))) {
13
+ result.docsFramework = 'mkdocs';
14
+ result.docsConfigFile = existsSync(join(path, 'mkdocs.yml')) ? 'mkdocs.yml' : 'mkdocs.yaml';
15
+ return result;
16
+ }
17
+ // Docusaurus
18
+ if (existsSync(join(path, 'docusaurus.config.js')) ||
19
+ existsSync(join(path, 'docusaurus.config.ts'))) {
20
+ result.docsFramework = 'docusaurus';
21
+ result.docsConfigFile = existsSync(join(path, 'docusaurus.config.js'))
22
+ ? 'docusaurus.config.js'
23
+ : 'docusaurus.config.ts';
24
+ return result;
25
+ }
26
+ // Sphinx
27
+ if (existsSync(join(path, 'conf.py')) || existsSync(join(path, 'source', 'conf.py'))) {
28
+ result.docsFramework = 'sphinx';
29
+ result.docsConfigFile = existsSync(join(path, 'conf.py')) ? 'conf.py' : 'source/conf.py';
30
+ return result;
31
+ }
32
+ // VitePress
33
+ if (existsSync(join(path, '.vitepress', 'config.ts')) ||
34
+ existsSync(join(path, '.vitepress', 'config.js'))) {
35
+ result.docsFramework = 'vitepress';
36
+ result.docsConfigFile = '.vitepress/config.ts';
37
+ return result;
38
+ }
39
+ // GitBook
40
+ if (existsSync(join(path, 'SUMMARY.md'))) {
41
+ result.docsFramework = 'gitbook';
42
+ result.docsConfigFile = 'SUMMARY.md';
43
+ return result;
44
+ }
45
+ // Generic docs/ folder
46
+ if (hasDirectory(path, 'docs')) {
47
+ result.docsFramework = 'custom';
48
+ return result;
49
+ }
50
+ return result;
51
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Project Detection Types
3
+ *
4
+ * Universal detection result that works across all project types.
5
+ * Type-specific fields are optional — only populated when relevant.
6
+ */
7
+ export interface DetectProjectOptions {
8
+ /** Absolute path to the project folder */
9
+ path: string;
10
+ /** Project type — determines detection strategy */
11
+ type: string;
12
+ }
13
+ export interface DetectionResult {
14
+ /** Inferred project name (slug: lowercase, hyphens) */
15
+ name: string;
16
+ /** Display name (prettified from folder name or config) */
17
+ displayName: string;
18
+ /** Description (from README first paragraph, package.json, etc.) */
19
+ description?: string;
20
+ /** Git remote URL (if .git exists with a remote) */
21
+ gitRemote?: string;
22
+ /** Whether .git directory exists */
23
+ hasGit: boolean;
24
+ /** Whether COMPILR.md exists in the project root */
25
+ hasCompilrMd: boolean;
26
+ /** Type-specific detection results */
27
+ content: ContentSummary;
28
+ }
29
+ export interface ContentSummary {
30
+ /** Total files found (excluding ignored dirs) */
31
+ fileCount: number;
32
+ /** Total size in KB */
33
+ totalSizeKB: number;
34
+ /** Markdown files count */
35
+ markdownFiles: number;
36
+ /** Image files count */
37
+ imageFiles: number;
38
+ /** Detected language (typescript, python, go, rust, java, etc.) */
39
+ language?: string;
40
+ /** Detected framework (react, next, express, fastapi, gin, etc.) */
41
+ framework?: string;
42
+ /** Package manager (npm, yarn, pnpm, pip, poetry, go mod, cargo) */
43
+ packageManager?: string;
44
+ /** Main entry points found */
45
+ entryPoints?: string[];
46
+ /** Test framework detected */
47
+ testFramework?: string;
48
+ /** Path to .bib file */
49
+ bibliographyFile?: string;
50
+ /** Number of bib entries */
51
+ bibliographyEntries?: number;
52
+ /** LaTeX files found */
53
+ latexFiles?: string[];
54
+ /** Data files (.csv, .xlsx, .json in data/ or root) */
55
+ dataFiles?: number;
56
+ /** Number of chapter files detected */
57
+ chapterCount?: number;
58
+ /** Path to outline file */
59
+ outlineFile?: string;
60
+ /** Manuscript/draft files */
61
+ draftFiles?: string[];
62
+ /** Spreadsheet files (.xlsx, .csv) */
63
+ spreadsheetFiles?: number;
64
+ /** Presentation files (.pptx, .pdf) */
65
+ presentationFiles?: number;
66
+ /** Documentation framework (mkdocs, docusaurus, sphinx, etc.) */
67
+ docsFramework?: string;
68
+ /** Config file path */
69
+ docsConfigFile?: string;
70
+ /** Lesson/module directories */
71
+ lessonCount?: number;
72
+ /** Exercise files */
73
+ exerciseFiles?: number;
74
+ }
75
+ /**
76
+ * Detection strategy function signature.
77
+ * Each strategy receives the project path and a base summary,
78
+ * then adds type-specific fields.
79
+ */
80
+ export type DetectionStrategy = (path: string, base: ContentSummary) => ContentSummary;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Project Detection Types
3
+ *
4
+ * Universal detection result that works across all project types.
5
+ * Type-specific fields are optional — only populated when relevant.
6
+ */
7
+ export {};
package/dist/index.d.ts CHANGED
@@ -58,6 +58,8 @@ export { createSQLiteRepositories, SQLiteProjectRepository, SQLiteWorkItemReposi
58
58
  export type { SQLiteRepositories, CreateSQLiteRepositoriesOptions, ProjectDeleteHooks, ProjectRecord, WorkItemRecord, ProjectDocumentRecord, WorkItemCommentRecord, } from './platform/index.js';
59
59
  export { createAskUserTool, createAskUserSimpleTool } from './tools/index.js';
60
60
  export type { AskUserQuestion, AskUserInput, AskUserResult, AskUserHandler, AskUserSimpleInput, AskUserSimpleResult, AskUserSimpleHandler, } from './tools/index.js';
61
+ export { detectProject, suggestProjectType, detectCommon } from './detection/index.js';
62
+ export type { DetectProjectOptions, DetectionResult, ContentSummary } from './detection/index.js';
61
63
  export { createGuideTool, SHARED_GUIDE_ENTRIES, searchGuideEntries, topicToGuideEntry, } from './guide/index.js';
62
64
  export type { GuideEntry, ContentTopic, ContentSection, GuideToolConfig } from './guide/index.js';
63
65
  export { createPlatformTools, createProjectTools, createWorkItemTools, createDocumentTools, createPlanTools, createBacklogTools, createAnchorTools, createArtifactTools, createEpisodeTools, createImageTools, ProjectAnchorStore, } from './platform/index.js';
package/dist/index.js CHANGED
@@ -129,6 +129,10 @@ export { createSQLiteRepositories, SQLiteProjectRepository, SQLiteWorkItemReposi
129
129
  // =============================================================================
130
130
  export { createAskUserTool, createAskUserSimpleTool } from './tools/index.js';
131
131
  // =============================================================================
132
+ // Project Detection (universal project content detection)
133
+ // =============================================================================
134
+ export { detectProject, suggestProjectType, detectCommon } from './detection/index.js';
135
+ // =============================================================================
132
136
  // Guide Tool (environment-aware documentation)
133
137
  // =============================================================================
134
138
  export { createGuideTool, SHARED_GUIDE_ENTRIES, searchGuideEntries, topicToGuideEntry, } from './guide/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/sdk",
3
- "version": "0.9.8",
3
+ "version": "0.9.9",
4
4
  "description": "Universal agent runtime for building AI-powered applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",