@cesarandreslopez/occ 0.8.5 → 0.8.7
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 +61 -11
- package/dist/src/cli.js +2 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/src/code/build.d.ts +23 -0
- package/dist/src/code/build.js +88 -3
- package/dist/src/code/build.js.map +1 -1
- package/dist/src/code/chunk.d.ts +6 -4
- package/dist/src/code/chunk.js +44 -28
- package/dist/src/code/chunk.js.map +1 -1
- package/dist/src/code/index-io.d.ts +2 -1
- package/dist/src/code/index-io.js +2 -0
- package/dist/src/code/index-io.js.map +1 -1
- package/dist/src/code/isolated-runner.d.ts +1 -0
- package/dist/src/code/isolated-runner.js +38 -0
- package/dist/src/code/isolated-runner.js.map +1 -0
- package/dist/src/code/isolated.d.ts +9 -0
- package/dist/src/code/isolated.js +98 -0
- package/dist/src/code/isolated.js.map +1 -0
- package/dist/src/code/languages.d.ts +2 -0
- package/dist/src/code/languages.js +9 -1
- package/dist/src/code/languages.js.map +1 -1
- package/dist/src/code/parsers.js +78 -0
- package/dist/src/code/parsers.js.map +1 -1
- package/dist/src/code/preview.d.ts +34 -0
- package/dist/src/code/preview.js +90 -0
- package/dist/src/code/preview.js.map +1 -0
- package/dist/src/code/query.js +20 -7
- package/dist/src/code/query.js.map +1 -1
- package/dist/src/code/session.d.ts +2 -4
- package/dist/src/code/session.js.map +1 -1
- package/dist/src/code/slim.d.ts +247 -0
- package/dist/src/code/slim.js +40 -0
- package/dist/src/code/slim.js.map +1 -0
- package/dist/src/code/store.d.ts +6 -0
- package/dist/src/code/store.js +13 -0
- package/dist/src/code/store.js.map +1 -1
- package/dist/src/code/types.d.ts +27 -0
- package/dist/src/code/types.js +10 -0
- package/dist/src/code/types.js.map +1 -1
- package/dist/src/index.d.ts +25 -5
- package/dist/src/index.js +14 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/progress-event.d.ts +2 -1
- package/dist/src/workspace/command.d.ts +1 -0
- package/dist/src/workspace/command.js +72 -0
- package/dist/src/workspace/command.js.map +1 -1
- package/dist/src/workspace/describe-output.d.ts +7 -0
- package/dist/src/workspace/describe-output.js +85 -0
- package/dist/src/workspace/describe-output.js.map +1 -0
- package/dist/src/workspace/describe.d.ts +9 -0
- package/dist/src/workspace/describe.js +660 -0
- package/dist/src/workspace/describe.js.map +1 -0
- package/dist/src/workspace/types.d.ts +170 -0
- package/dist/src/workspace/types.js +89 -0
- package/dist/src/workspace/types.js.map +1 -1
- package/package.json +30 -1
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
import fg from 'fast-glob';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
5
|
+
import { CODE_EXTENSIONS, LANGUAGE_SPECS } from '../code/languages.js';
|
|
6
|
+
import { OFFICE_EXTENSIONS, buildExcludeDirGlobs, createIgnoreMatcher, getExtension, normalizeRelativePath, } from '../utils.js';
|
|
7
|
+
const DEFAULT_EXCLUDE_DIR = ['node_modules', '.git', 'dist', 'vendor', 'build', 'coverage', 'target'];
|
|
8
|
+
const CATEGORIES = [
|
|
9
|
+
'code',
|
|
10
|
+
'office',
|
|
11
|
+
'prose',
|
|
12
|
+
'data',
|
|
13
|
+
'config',
|
|
14
|
+
'image',
|
|
15
|
+
'binary',
|
|
16
|
+
'other',
|
|
17
|
+
];
|
|
18
|
+
const PROSE_EXTENSIONS = new Set(['md', 'markdown', 'rst', 'txt', 'adoc']);
|
|
19
|
+
const DATA_EXTENSIONS = new Set(['json', 'yaml', 'yml', 'toml', 'csv', 'tsv', 'parquet', 'ipynb', 'xml']);
|
|
20
|
+
const IMAGE_EXTENSIONS = new Set(['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico']);
|
|
21
|
+
const BINARY_EXTENSIONS = new Set(['exe', 'dll', 'so', 'dylib', 'zip', 'tar', 'gz', 'tgz', '7z']);
|
|
22
|
+
const CODE_EXTENSION_SET = new Set(CODE_EXTENSIONS);
|
|
23
|
+
const OFFICE_EXTENSION_SET = new Set(OFFICE_EXTENSIONS);
|
|
24
|
+
const CONFIG_FILENAMES = [
|
|
25
|
+
'.env',
|
|
26
|
+
'.gitignore',
|
|
27
|
+
'.npmrc',
|
|
28
|
+
'.nvmrc',
|
|
29
|
+
'.eslintrc',
|
|
30
|
+
'.prettierrc',
|
|
31
|
+
'Makefile',
|
|
32
|
+
'Dockerfile',
|
|
33
|
+
'package.json',
|
|
34
|
+
'package-lock.json',
|
|
35
|
+
'pnpm-lock.yaml',
|
|
36
|
+
'pnpm-workspace.yaml',
|
|
37
|
+
'yarn.lock',
|
|
38
|
+
'bun.lock',
|
|
39
|
+
'bun.lockb',
|
|
40
|
+
'deno.lock',
|
|
41
|
+
'tsconfig.json',
|
|
42
|
+
'pyproject.toml',
|
|
43
|
+
'requirements.txt',
|
|
44
|
+
'setup.py',
|
|
45
|
+
'Pipfile',
|
|
46
|
+
'go.mod',
|
|
47
|
+
'Cargo.toml',
|
|
48
|
+
'pom.xml',
|
|
49
|
+
'build.gradle',
|
|
50
|
+
'build.gradle.kts',
|
|
51
|
+
'Gemfile',
|
|
52
|
+
'composer.json',
|
|
53
|
+
'mix.exs',
|
|
54
|
+
'mkdocs.yml',
|
|
55
|
+
'_config.yml',
|
|
56
|
+
'hugo.toml',
|
|
57
|
+
'CMakeLists.txt',
|
|
58
|
+
];
|
|
59
|
+
const CONFIG_PREFIXES = ['.env.', '.eslintrc.', '.prettierrc.', 'tsconfig.'];
|
|
60
|
+
const MANIFEST_RULES = [
|
|
61
|
+
{ file: 'package.json', kind: 'node', label: 'Node.js package', ecosystem: 'node', codeEcosystem: true },
|
|
62
|
+
{ file: 'pnpm-workspace.yaml', kind: 'node-monorepo', label: 'pnpm workspace', ecosystem: 'node', codeEcosystem: true },
|
|
63
|
+
{ file: 'tsconfig.json', kind: 'typescript', label: 'TypeScript config', ecosystem: 'typescript', codeEcosystem: true },
|
|
64
|
+
{ file: 'pyproject.toml', kind: 'python', label: 'Python project', ecosystem: 'python', codeEcosystem: true },
|
|
65
|
+
{ file: 'requirements.txt', kind: 'python', label: 'Python requirements', ecosystem: 'python', codeEcosystem: true },
|
|
66
|
+
{ file: 'setup.py', kind: 'python', label: 'Python setup', ecosystem: 'python', codeEcosystem: true },
|
|
67
|
+
{ file: 'Pipfile', kind: 'python', label: 'Pipenv project', ecosystem: 'python', codeEcosystem: true },
|
|
68
|
+
{ file: 'go.mod', kind: 'go', label: 'Go module', ecosystem: 'go', codeEcosystem: true },
|
|
69
|
+
{ file: 'Cargo.toml', kind: 'rust', label: 'Rust crate', ecosystem: 'rust', codeEcosystem: true },
|
|
70
|
+
{ file: 'pom.xml', kind: 'java', label: 'Maven project', ecosystem: 'java', codeEcosystem: true },
|
|
71
|
+
{ file: 'build.gradle', kind: 'java', label: 'Gradle project', ecosystem: 'java', codeEcosystem: true },
|
|
72
|
+
{ file: 'build.gradle.kts', kind: 'java', label: 'Gradle Kotlin project', ecosystem: 'java', codeEcosystem: true },
|
|
73
|
+
{ file: 'Gemfile', kind: 'ruby', label: 'Ruby bundle', ecosystem: 'ruby', codeEcosystem: true },
|
|
74
|
+
{ file: 'composer.json', kind: 'php', label: 'Composer package', ecosystem: 'php', codeEcosystem: true },
|
|
75
|
+
{ file: 'mix.exs', kind: 'elixir', label: 'Elixir Mix project', ecosystem: 'elixir', codeEcosystem: true },
|
|
76
|
+
{ file: 'Dockerfile', kind: 'docker', label: 'Docker project' },
|
|
77
|
+
{ file: 'mkdocs.yml', kind: 'mkdocs', label: 'MkDocs site', siteGenerator: 'MkDocs' },
|
|
78
|
+
{ file: '_config.yml', kind: 'jekyll', label: 'Jekyll site', siteGenerator: 'Jekyll' },
|
|
79
|
+
{ file: 'hugo.toml', kind: 'hugo', label: 'Hugo site', siteGenerator: 'Hugo' },
|
|
80
|
+
];
|
|
81
|
+
const NODE_FRAMEWORKS = {
|
|
82
|
+
next: 'next',
|
|
83
|
+
react: 'react',
|
|
84
|
+
vue: 'vue',
|
|
85
|
+
svelte: 'svelte',
|
|
86
|
+
'@angular/core': 'angular',
|
|
87
|
+
'@nestjs/core': 'nest',
|
|
88
|
+
express: 'express',
|
|
89
|
+
fastify: 'fastify',
|
|
90
|
+
vite: 'vite',
|
|
91
|
+
astro: 'astro',
|
|
92
|
+
electron: 'electron',
|
|
93
|
+
};
|
|
94
|
+
const NODE_TEST_FRAMEWORKS = ['vitest', 'jest', 'mocha', 'ava', 'tap', 'playwright', 'cypress'];
|
|
95
|
+
const PYTHON_FRAMEWORKS = ['django', 'flask', 'fastapi', 'streamlit', 'jupyter'];
|
|
96
|
+
const RUBY_FRAMEWORKS = ['rails', 'sinatra'];
|
|
97
|
+
const PHP_FRAMEWORKS = {
|
|
98
|
+
'laravel/framework': 'laravel',
|
|
99
|
+
'symfony/framework-bundle': 'symfony',
|
|
100
|
+
'symfony/symfony': 'symfony',
|
|
101
|
+
};
|
|
102
|
+
const LOCKFILE_PACKAGE_MANAGERS = {
|
|
103
|
+
'package-lock.json': 'npm',
|
|
104
|
+
'npm-shrinkwrap.json': 'npm',
|
|
105
|
+
'pnpm-lock.yaml': 'pnpm',
|
|
106
|
+
'yarn.lock': 'yarn',
|
|
107
|
+
'bun.lock': 'bun',
|
|
108
|
+
'bun.lockb': 'bun',
|
|
109
|
+
'deno.lock': 'deno',
|
|
110
|
+
};
|
|
111
|
+
const LANGUAGE_BY_EXTENSION = new Map();
|
|
112
|
+
for (const spec of LANGUAGE_SPECS) {
|
|
113
|
+
for (const extension of spec.extensions) {
|
|
114
|
+
LANGUAGE_BY_EXTENSION.set(extension, spec.name);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function emptyCategoryBreakdown() {
|
|
118
|
+
return Object.fromEntries(CATEGORIES.map((category) => [category, { files: 0, bytes: 0, pct: 0 }]));
|
|
119
|
+
}
|
|
120
|
+
function categoryForPath(relativePath) {
|
|
121
|
+
const baseName = path.basename(relativePath);
|
|
122
|
+
const ext = getExtension(relativePath);
|
|
123
|
+
if (CONFIG_FILENAMES.includes(baseName) || CONFIG_PREFIXES.some((prefix) => baseName.startsWith(prefix)) || baseName.endsWith('.sln')) {
|
|
124
|
+
return 'config';
|
|
125
|
+
}
|
|
126
|
+
if (CODE_EXTENSION_SET.has(ext))
|
|
127
|
+
return 'code';
|
|
128
|
+
if (OFFICE_EXTENSION_SET.has(ext))
|
|
129
|
+
return 'office';
|
|
130
|
+
if (PROSE_EXTENSIONS.has(ext))
|
|
131
|
+
return 'prose';
|
|
132
|
+
if (DATA_EXTENSIONS.has(ext))
|
|
133
|
+
return 'data';
|
|
134
|
+
if (IMAGE_EXTENSIONS.has(ext))
|
|
135
|
+
return 'image';
|
|
136
|
+
if (BINARY_EXTENSIONS.has(ext))
|
|
137
|
+
return 'binary';
|
|
138
|
+
return 'other';
|
|
139
|
+
}
|
|
140
|
+
function markerRuleForPath(relativePath) {
|
|
141
|
+
const baseName = path.basename(relativePath);
|
|
142
|
+
if (baseName.endsWith('.sln')) {
|
|
143
|
+
return { file: baseName, kind: 'dotnet', label: '.NET solution', ecosystem: 'dotnet', codeEcosystem: true };
|
|
144
|
+
}
|
|
145
|
+
return MANIFEST_RULES.find((rule) => rule.file === baseName);
|
|
146
|
+
}
|
|
147
|
+
function projectRootForMarker(relativePath) {
|
|
148
|
+
const dir = normalizeRelativePath(path.dirname(relativePath));
|
|
149
|
+
return dir === '.' ? '.' : dir;
|
|
150
|
+
}
|
|
151
|
+
function titleCase(value) {
|
|
152
|
+
if (value === 'javascript')
|
|
153
|
+
return 'JavaScript';
|
|
154
|
+
if (value === 'typescript')
|
|
155
|
+
return 'TypeScript';
|
|
156
|
+
if (value === 'dotnet')
|
|
157
|
+
return '.NET';
|
|
158
|
+
return value.slice(0, 1).toUpperCase() + value.slice(1);
|
|
159
|
+
}
|
|
160
|
+
function dedupe(values) {
|
|
161
|
+
return [...new Set(values.filter(Boolean))];
|
|
162
|
+
}
|
|
163
|
+
function sortByCountDesc(entries) {
|
|
164
|
+
return entries.sort((left, right) => right.files - left.files || left.ext.localeCompare(right.ext));
|
|
165
|
+
}
|
|
166
|
+
function dominantLanguage(files) {
|
|
167
|
+
const counts = new Map();
|
|
168
|
+
for (const file of files) {
|
|
169
|
+
if (file.category !== 'code')
|
|
170
|
+
continue;
|
|
171
|
+
const language = LANGUAGE_BY_EXTENSION.get(file.ext) ?? file.ext;
|
|
172
|
+
counts.set(language, (counts.get(language) ?? 0) + 1);
|
|
173
|
+
}
|
|
174
|
+
const [top] = [...counts.entries()].sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]));
|
|
175
|
+
return top?.[0];
|
|
176
|
+
}
|
|
177
|
+
function dominantOfficeExtension(files) {
|
|
178
|
+
const counts = new Map();
|
|
179
|
+
for (const file of files) {
|
|
180
|
+
if (file.category !== 'office')
|
|
181
|
+
continue;
|
|
182
|
+
counts.set(file.ext, (counts.get(file.ext) ?? 0) + 1);
|
|
183
|
+
}
|
|
184
|
+
const [top] = [...counts.entries()].sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]));
|
|
185
|
+
return top?.[0];
|
|
186
|
+
}
|
|
187
|
+
function packageManagerFromPackageJson(value) {
|
|
188
|
+
if (!value || typeof value !== 'object')
|
|
189
|
+
return undefined;
|
|
190
|
+
const record = value;
|
|
191
|
+
const packageManager = record.packageManager;
|
|
192
|
+
if (typeof packageManager === 'string' && packageManager.length > 0) {
|
|
193
|
+
const name = packageManager.split('@')[0];
|
|
194
|
+
if (name)
|
|
195
|
+
return name;
|
|
196
|
+
}
|
|
197
|
+
const devEngines = record.devEngines;
|
|
198
|
+
if (devEngines && typeof devEngines === 'object') {
|
|
199
|
+
const packageManagerConfig = devEngines.packageManager;
|
|
200
|
+
if (packageManagerConfig && typeof packageManagerConfig === 'object') {
|
|
201
|
+
const name = packageManagerConfig.name;
|
|
202
|
+
if (typeof name === 'string' && name.length > 0)
|
|
203
|
+
return name;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
function dependencyKeys(value) {
|
|
209
|
+
if (!value || typeof value !== 'object')
|
|
210
|
+
return [];
|
|
211
|
+
return Object.keys(value);
|
|
212
|
+
}
|
|
213
|
+
function recordKeys(value) {
|
|
214
|
+
if (!value || typeof value !== 'object')
|
|
215
|
+
return [];
|
|
216
|
+
return Object.keys(value);
|
|
217
|
+
}
|
|
218
|
+
function collectPackageEntryPoints(parsed) {
|
|
219
|
+
const points = [];
|
|
220
|
+
for (const field of ['main', 'module', 'types', 'typings', 'browser']) {
|
|
221
|
+
const value = parsed[field];
|
|
222
|
+
if (typeof value === 'string' && value.length > 0)
|
|
223
|
+
points.push(value);
|
|
224
|
+
}
|
|
225
|
+
const bin = parsed.bin;
|
|
226
|
+
if (typeof bin === 'string' && bin.length > 0)
|
|
227
|
+
points.push(bin);
|
|
228
|
+
if (bin && typeof bin === 'object') {
|
|
229
|
+
for (const value of Object.values(bin)) {
|
|
230
|
+
if (typeof value === 'string' && value.length > 0)
|
|
231
|
+
points.push(value);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const exportsField = parsed.exports;
|
|
235
|
+
if (typeof exportsField === 'string' && exportsField.length > 0)
|
|
236
|
+
points.push(exportsField);
|
|
237
|
+
if (exportsField && typeof exportsField === 'object') {
|
|
238
|
+
points.push(...Object.keys(exportsField).filter((key) => key.startsWith('.')));
|
|
239
|
+
}
|
|
240
|
+
return dedupe(points).slice(0, 20);
|
|
241
|
+
}
|
|
242
|
+
function detectNodeBuildSystem(names, scripts) {
|
|
243
|
+
const haystack = `${names.join(' ')} ${scripts.join(' ')}`.toLowerCase();
|
|
244
|
+
if (haystack.includes('vite'))
|
|
245
|
+
return 'vite';
|
|
246
|
+
if (haystack.includes('webpack'))
|
|
247
|
+
return 'webpack';
|
|
248
|
+
if (haystack.includes('turbo'))
|
|
249
|
+
return 'turbo';
|
|
250
|
+
if (haystack.includes('nx'))
|
|
251
|
+
return 'nx';
|
|
252
|
+
if (haystack.includes('tsup'))
|
|
253
|
+
return 'tsup';
|
|
254
|
+
if (haystack.includes('rollup'))
|
|
255
|
+
return 'rollup';
|
|
256
|
+
if (haystack.includes('esbuild'))
|
|
257
|
+
return 'esbuild';
|
|
258
|
+
if (haystack.includes('tsc'))
|
|
259
|
+
return 'tsc';
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
function detectNodeTestFramework(names, scripts) {
|
|
263
|
+
const haystack = `${names.join(' ')} ${scripts.join(' ')}`.toLowerCase();
|
|
264
|
+
return NODE_TEST_FRAMEWORKS.find((name) => haystack.includes(name));
|
|
265
|
+
}
|
|
266
|
+
function detectNodePlatforms(rootDir, names) {
|
|
267
|
+
const platforms = [];
|
|
268
|
+
if (names.includes('electron'))
|
|
269
|
+
platforms.push('electron');
|
|
270
|
+
if (names.includes('@tauri-apps/api') || names.includes('@tauri-apps/cli') || existsSync(path.join(rootDir, 'src-tauri', 'Cargo.toml'))) {
|
|
271
|
+
platforms.push('tauri');
|
|
272
|
+
}
|
|
273
|
+
if (names.includes('react-native') || names.includes('expo'))
|
|
274
|
+
platforms.push('mobile');
|
|
275
|
+
if (names.includes('@capacitor/core'))
|
|
276
|
+
platforms.push('capacitor');
|
|
277
|
+
return dedupe(platforms);
|
|
278
|
+
}
|
|
279
|
+
async function readSmallText(filePath, maxBytes = 1024 * 1024) {
|
|
280
|
+
try {
|
|
281
|
+
const fileStat = await stat(filePath);
|
|
282
|
+
if (!fileStat.isFile() || fileStat.size > maxBytes)
|
|
283
|
+
return undefined;
|
|
284
|
+
return await readFile(filePath, 'utf8');
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async function detectNodeFrameworks(rootDir) {
|
|
291
|
+
const packageJson = await readSmallText(path.join(rootDir, 'package.json'));
|
|
292
|
+
if (!packageJson)
|
|
293
|
+
return detectLockfilePackageManagers(rootDir);
|
|
294
|
+
try {
|
|
295
|
+
const parsed = JSON.parse(packageJson);
|
|
296
|
+
const names = [
|
|
297
|
+
...dependencyKeys(parsed.dependencies),
|
|
298
|
+
...dependencyKeys(parsed.devDependencies),
|
|
299
|
+
...dependencyKeys(parsed.peerDependencies),
|
|
300
|
+
...dependencyKeys(parsed.optionalDependencies),
|
|
301
|
+
];
|
|
302
|
+
const scripts = recordKeys(parsed.scripts);
|
|
303
|
+
const frameworks = dedupe(names.flatMap((name) => NODE_FRAMEWORKS[name] ? [NODE_FRAMEWORKS[name]] : []));
|
|
304
|
+
const packageManager = packageManagerFromPackageJson(parsed) ?? detectPackageManagerByLockfile(rootDir);
|
|
305
|
+
const entryPoints = collectPackageEntryPoints(parsed);
|
|
306
|
+
const buildSystem = detectNodeBuildSystem(names, scripts);
|
|
307
|
+
const testFramework = detectNodeTestFramework(names, scripts);
|
|
308
|
+
const platforms = detectNodePlatforms(rootDir, names);
|
|
309
|
+
return {
|
|
310
|
+
frameworks,
|
|
311
|
+
packageManager,
|
|
312
|
+
...(entryPoints.length > 0 ? { entryPoints } : {}),
|
|
313
|
+
...(scripts.length > 0 ? { scripts } : {}),
|
|
314
|
+
...(buildSystem ? { buildSystem } : {}),
|
|
315
|
+
...(testFramework ? { testFramework } : {}),
|
|
316
|
+
...(platforms.length > 0 ? { platforms } : {}),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return { frameworks: [], packageManager: detectPackageManagerByLockfile(rootDir) };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async function detectPythonFrameworks(rootDir) {
|
|
324
|
+
const texts = await Promise.all([
|
|
325
|
+
readSmallText(path.join(rootDir, 'pyproject.toml')),
|
|
326
|
+
readSmallText(path.join(rootDir, 'requirements.txt')),
|
|
327
|
+
]);
|
|
328
|
+
const combined = texts.filter((text) => Boolean(text)).join('\n').toLowerCase();
|
|
329
|
+
if (!combined)
|
|
330
|
+
return [];
|
|
331
|
+
return PYTHON_FRAMEWORKS.filter((name) => new RegExp(`(^|[^a-z0-9_-])${name}([^a-z0-9_-]|$)`).test(combined));
|
|
332
|
+
}
|
|
333
|
+
async function detectRubyFrameworks(rootDir) {
|
|
334
|
+
const gemfile = (await readSmallText(path.join(rootDir, 'Gemfile')))?.toLowerCase();
|
|
335
|
+
if (!gemfile)
|
|
336
|
+
return [];
|
|
337
|
+
return RUBY_FRAMEWORKS.filter((name) => new RegExp(`gem\\s+['"]${name}['"]`).test(gemfile));
|
|
338
|
+
}
|
|
339
|
+
async function detectPhpFrameworks(rootDir) {
|
|
340
|
+
const composer = await readSmallText(path.join(rootDir, 'composer.json'));
|
|
341
|
+
if (!composer)
|
|
342
|
+
return [];
|
|
343
|
+
try {
|
|
344
|
+
const parsed = JSON.parse(composer);
|
|
345
|
+
const names = [
|
|
346
|
+
...dependencyKeys(parsed.require),
|
|
347
|
+
...dependencyKeys(parsed['require-dev']),
|
|
348
|
+
];
|
|
349
|
+
return dedupe(names.flatMap((name) => PHP_FRAMEWORKS[name] ? [PHP_FRAMEWORKS[name]] : []));
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
return [];
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function detectPackageManagerByLockfile(rootDir) {
|
|
356
|
+
for (const [fileName, manager] of Object.entries(LOCKFILE_PACKAGE_MANAGERS)) {
|
|
357
|
+
if (existsSync(path.join(rootDir, fileName)))
|
|
358
|
+
return manager;
|
|
359
|
+
}
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
function detectLockfilePackageManagers(rootDir) {
|
|
363
|
+
return { frameworks: [], packageManager: detectPackageManagerByLockfile(rootDir) };
|
|
364
|
+
}
|
|
365
|
+
async function detectFrameworks(rootDir, markerKinds) {
|
|
366
|
+
const [node, python, ruby, php] = await Promise.all([
|
|
367
|
+
markerKinds.has('node') || markerKinds.has('node-monorepo') ? detectNodeFrameworks(rootDir) : detectLockfilePackageManagers(rootDir),
|
|
368
|
+
markerKinds.has('python') ? detectPythonFrameworks(rootDir) : Promise.resolve([]),
|
|
369
|
+
markerKinds.has('ruby') ? detectRubyFrameworks(rootDir) : Promise.resolve([]),
|
|
370
|
+
markerKinds.has('php') ? detectPhpFrameworks(rootDir) : Promise.resolve([]),
|
|
371
|
+
]);
|
|
372
|
+
return {
|
|
373
|
+
frameworks: dedupe([...node.frameworks, ...python, ...ruby, ...php]),
|
|
374
|
+
packageManager: node.packageManager,
|
|
375
|
+
...(node.entryPoints ? { entryPoints: node.entryPoints } : {}),
|
|
376
|
+
...(node.scripts ? { scripts: node.scripts } : {}),
|
|
377
|
+
...(node.buildSystem ? { buildSystem: node.buildSystem } : {}),
|
|
378
|
+
...(node.testFramework ? { testFramework: node.testFramework } : {}),
|
|
379
|
+
...(node.platforms ? { platforms: node.platforms } : {}),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
function classify(input) {
|
|
383
|
+
const category = emptyCategoryBreakdown();
|
|
384
|
+
let totalBytes = 0;
|
|
385
|
+
for (const file of input.files) {
|
|
386
|
+
category[file.category].files++;
|
|
387
|
+
category[file.category].bytes += file.size;
|
|
388
|
+
totalBytes += file.size;
|
|
389
|
+
}
|
|
390
|
+
const markerKinds = new Set(input.manifests.map((marker) => marker.kind));
|
|
391
|
+
const hasCodeManifest = MANIFEST_RULES.some((rule) => rule.codeEcosystem && markerKinds.has(rule.kind));
|
|
392
|
+
const siteGenerator = MANIFEST_RULES.find((rule) => rule.siteGenerator && markerKinds.has(rule.kind))?.siteGenerator;
|
|
393
|
+
const language = dominantLanguage(input.files);
|
|
394
|
+
const officeExt = dominantOfficeExtension(input.files);
|
|
395
|
+
const labels = dedupe([
|
|
396
|
+
...input.manifests.flatMap((marker) => marker.kind === 'typescript' ? ['typescript'] : marker.kind.includes('node') ? ['node'] : [marker.kind]),
|
|
397
|
+
...(language ? [language] : []),
|
|
398
|
+
...input.frameworks,
|
|
399
|
+
...(input.packageManager ? [input.packageManager] : []),
|
|
400
|
+
].map((label) => label.toLowerCase()));
|
|
401
|
+
let primaryType;
|
|
402
|
+
let confidence = 'medium';
|
|
403
|
+
let summary;
|
|
404
|
+
if (input.files.length === 0) {
|
|
405
|
+
primaryType = 'empty-directory';
|
|
406
|
+
confidence = 'high';
|
|
407
|
+
summary = 'Empty directory';
|
|
408
|
+
}
|
|
409
|
+
else if (category.code.files > 0 && category.office.files > 0) {
|
|
410
|
+
primaryType = 'mixed-project';
|
|
411
|
+
confidence = 'high';
|
|
412
|
+
summary = 'Mixed project: code + documents';
|
|
413
|
+
}
|
|
414
|
+
else if (siteGenerator && category.prose.files > 0) {
|
|
415
|
+
primaryType = 'documentation-project';
|
|
416
|
+
confidence = 'high';
|
|
417
|
+
summary = `Documentation project (${siteGenerator})`;
|
|
418
|
+
}
|
|
419
|
+
else if (hasCodeManifest && category.code.files > 0) {
|
|
420
|
+
primaryType = 'coding-project';
|
|
421
|
+
confidence = 'high';
|
|
422
|
+
const languageLabel = language ? titleCase(language) : 'Code';
|
|
423
|
+
const frameworkSuffix = input.frameworks.length > 0
|
|
424
|
+
? ` - ${input.frameworks.slice(0, 2).map(titleCase).join(', ')}`
|
|
425
|
+
: '';
|
|
426
|
+
summary = `${languageLabel} code project${frameworkSuffix}`;
|
|
427
|
+
}
|
|
428
|
+
else if (category.code.files > 0) {
|
|
429
|
+
primaryType = 'coding-project';
|
|
430
|
+
confidence = 'medium';
|
|
431
|
+
const languageLabel = language ? titleCase(language) : 'Code';
|
|
432
|
+
summary = `${languageLabel} code project`;
|
|
433
|
+
}
|
|
434
|
+
else if (category.office.files > 0) {
|
|
435
|
+
primaryType = 'office-project';
|
|
436
|
+
confidence = category.office.files === input.files.length ? 'high' : 'medium';
|
|
437
|
+
summary = officeExt ? `Office project (${officeExt.toUpperCase()}-dominant)` : 'Office project';
|
|
438
|
+
}
|
|
439
|
+
else if (category.data.files > 0 && category.code.files === 0 && category.office.files === 0) {
|
|
440
|
+
primaryType = 'data-project';
|
|
441
|
+
confidence = category.data.files === input.files.length ? 'high' : 'medium';
|
|
442
|
+
const hasNotebook = input.files.some((file) => file.ext === 'ipynb');
|
|
443
|
+
summary = hasNotebook ? 'Data / notebook project' : 'Data project';
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
primaryType = 'general-folder';
|
|
447
|
+
confidence = totalBytes > 0 ? 'low' : 'medium';
|
|
448
|
+
summary = 'General folder';
|
|
449
|
+
}
|
|
450
|
+
return { primaryType, confidence, summary, labels };
|
|
451
|
+
}
|
|
452
|
+
function recommendedCommands(primaryType) {
|
|
453
|
+
if (primaryType === 'coding-project')
|
|
454
|
+
return ['occ code index --path . --format json'];
|
|
455
|
+
if (primaryType === 'office-project')
|
|
456
|
+
return ['occ workspace analyze --no-code --format json'];
|
|
457
|
+
if (primaryType === 'documentation-project')
|
|
458
|
+
return ['occ workspace documents --format json'];
|
|
459
|
+
if (primaryType === 'mixed-project')
|
|
460
|
+
return ['occ workspace analyze --format json'];
|
|
461
|
+
if (primaryType === 'data-project')
|
|
462
|
+
return ['occ --quick --format json .'];
|
|
463
|
+
return ['occ --quick .'];
|
|
464
|
+
}
|
|
465
|
+
function recommendedCalls(primaryType) {
|
|
466
|
+
if (primaryType === 'coding-project') {
|
|
467
|
+
return [
|
|
468
|
+
{ namespace: 'code', method: 'previewSize', args: { repoRoot: '.' } },
|
|
469
|
+
{ namespace: 'code', method: 'buildIndexIsolated', args: { repoRoot: '.', contentMode: 'excerpt' } },
|
|
470
|
+
];
|
|
471
|
+
}
|
|
472
|
+
if (primaryType === 'office-project') {
|
|
473
|
+
return [
|
|
474
|
+
{ namespace: 'workspace', method: 'analyze', args: { rootDir: '.', includeCode: false } },
|
|
475
|
+
{ namespace: 'workspace', method: 'inspectDocuments', args: { rootDir: '.' } },
|
|
476
|
+
];
|
|
477
|
+
}
|
|
478
|
+
if (primaryType === 'documentation-project') {
|
|
479
|
+
return [{ namespace: 'workspace', method: 'inspectDocuments', args: { rootDir: '.', includeMarkdown: true } }];
|
|
480
|
+
}
|
|
481
|
+
if (primaryType === 'mixed-project') {
|
|
482
|
+
return [
|
|
483
|
+
{ namespace: 'workspace', method: 'analyze', args: { rootDir: '.' } },
|
|
484
|
+
{ namespace: 'code', method: 'previewSize', args: { repoRoot: '.' } },
|
|
485
|
+
{ namespace: 'workspace', method: 'inspectDocuments', args: { rootDir: '.' } },
|
|
486
|
+
];
|
|
487
|
+
}
|
|
488
|
+
if (primaryType === 'data-project') {
|
|
489
|
+
return [{ namespace: 'workspace', method: 'describe', args: { rootDir: '.' } }];
|
|
490
|
+
}
|
|
491
|
+
return [{ namespace: 'workspace', method: 'describe', args: { rootDir: '.' } }];
|
|
492
|
+
}
|
|
493
|
+
function buildPresenceSignals(files) {
|
|
494
|
+
return {
|
|
495
|
+
hasCode: files.some((file) => file.category === 'code'),
|
|
496
|
+
hasDocuments: files.some((file) => file.category === 'office' || file.category === 'prose'),
|
|
497
|
+
hasOfficeDocuments: files.some((file) => file.category === 'office'),
|
|
498
|
+
hasTables: files.some((file) => ['xlsx', 'xlsm', 'xls', 'ods', 'csv', 'tsv'].includes(file.ext)),
|
|
499
|
+
hasNotebooks: files.some((file) => file.ext === 'ipynb'),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
async function discoverFiles(rootDir, options) {
|
|
503
|
+
const excludeDir = options.excludeDir ?? DEFAULT_EXCLUDE_DIR;
|
|
504
|
+
const ignoreMatcher = await createIgnoreMatcher(rootDir, {
|
|
505
|
+
excludeDir,
|
|
506
|
+
noGitignore: options.noGitignore,
|
|
507
|
+
ignorePatterns: options.ignorePatterns,
|
|
508
|
+
overlayIgnorePatterns: options.overlayIgnorePatterns,
|
|
509
|
+
});
|
|
510
|
+
const discovered = await fg('**/*', {
|
|
511
|
+
cwd: rootDir,
|
|
512
|
+
absolute: true,
|
|
513
|
+
dot: true,
|
|
514
|
+
onlyFiles: true,
|
|
515
|
+
followSymbolicLinks: false,
|
|
516
|
+
ignore: buildExcludeDirGlobs(excludeDir),
|
|
517
|
+
unique: true,
|
|
518
|
+
});
|
|
519
|
+
const files = [];
|
|
520
|
+
const sorted = discovered.sort();
|
|
521
|
+
const batchSize = 100;
|
|
522
|
+
for (let i = 0; i < sorted.length; i += batchSize) {
|
|
523
|
+
const batch = sorted.slice(i, i + batchSize);
|
|
524
|
+
const stats = await Promise.allSettled(batch.map((filePath) => stat(filePath)));
|
|
525
|
+
for (let j = 0; j < batch.length; j++) {
|
|
526
|
+
const filePath = batch[j];
|
|
527
|
+
const fileStat = stats[j];
|
|
528
|
+
if (fileStat.status === 'rejected' || !fileStat.value.isFile())
|
|
529
|
+
continue;
|
|
530
|
+
const relativePath = normalizeRelativePath(path.relative(rootDir, filePath));
|
|
531
|
+
if (ignoreMatcher.ignores(relativePath))
|
|
532
|
+
continue;
|
|
533
|
+
const ext = getExtension(filePath);
|
|
534
|
+
files.push({
|
|
535
|
+
path: filePath,
|
|
536
|
+
relativePath,
|
|
537
|
+
size: fileStat.value.size,
|
|
538
|
+
ext,
|
|
539
|
+
category: categoryForPath(relativePath),
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return files;
|
|
544
|
+
}
|
|
545
|
+
function collectMarkers(files) {
|
|
546
|
+
const manifests = [];
|
|
547
|
+
const markers = [];
|
|
548
|
+
for (const file of files) {
|
|
549
|
+
const rule = markerRuleForPath(file.relativePath);
|
|
550
|
+
if (rule) {
|
|
551
|
+
manifests.push({ path: file.relativePath, kind: rule.kind, label: rule.label });
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
const baseName = path.basename(file.relativePath);
|
|
555
|
+
if (baseName.toLowerCase() === 'readme.md') {
|
|
556
|
+
markers.push({ path: file.relativePath, kind: 'readme', label: 'README' });
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return { manifests, markers };
|
|
560
|
+
}
|
|
561
|
+
function buildComposition(files) {
|
|
562
|
+
const byCategory = emptyCategoryBreakdown();
|
|
563
|
+
const byExtension = {};
|
|
564
|
+
let bytes = 0;
|
|
565
|
+
for (const file of files) {
|
|
566
|
+
bytes += file.size;
|
|
567
|
+
byCategory[file.category].files++;
|
|
568
|
+
byCategory[file.category].bytes += file.size;
|
|
569
|
+
const ext = file.ext || '[none]';
|
|
570
|
+
byExtension[ext] = (byExtension[ext] ?? 0) + 1;
|
|
571
|
+
}
|
|
572
|
+
for (const breakdown of Object.values(byCategory)) {
|
|
573
|
+
breakdown.pct = bytes === 0 ? 0 : Number(((breakdown.bytes / bytes) * 100).toFixed(1));
|
|
574
|
+
}
|
|
575
|
+
return {
|
|
576
|
+
totals: {
|
|
577
|
+
files: files.length,
|
|
578
|
+
bytes,
|
|
579
|
+
},
|
|
580
|
+
byCategory,
|
|
581
|
+
byExtension,
|
|
582
|
+
dominantExtensions: sortByCountDesc(Object.entries(byExtension).map(([ext, count]) => ({ ext, files: count }))).slice(0, 5),
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
async function buildProjects(rootDir, manifests, maxProjects) {
|
|
586
|
+
const byRoot = new Map();
|
|
587
|
+
for (const marker of manifests) {
|
|
588
|
+
const projectRoot = projectRootForMarker(marker.path);
|
|
589
|
+
const current = byRoot.get(projectRoot) ?? [];
|
|
590
|
+
current.push(marker);
|
|
591
|
+
byRoot.set(projectRoot, current);
|
|
592
|
+
}
|
|
593
|
+
const allProjects = [];
|
|
594
|
+
for (const [projectPath, projectMarkers] of [...byRoot.entries()].sort((left, right) => left[0].localeCompare(right[0]))) {
|
|
595
|
+
const markerKinds = new Set(projectMarkers.map((marker) => marker.kind));
|
|
596
|
+
const absoluteProjectPath = projectPath === '.' ? rootDir : path.join(rootDir, projectPath);
|
|
597
|
+
const frameworkDetection = await detectFrameworks(absoluteProjectPath, markerKinds);
|
|
598
|
+
allProjects.push({
|
|
599
|
+
path: projectPath,
|
|
600
|
+
kinds: dedupe(projectMarkers.map((marker) => marker.kind)),
|
|
601
|
+
markers: projectMarkers.map((marker) => marker.path),
|
|
602
|
+
frameworks: frameworkDetection.frameworks,
|
|
603
|
+
...(frameworkDetection.packageManager ? { packageManager: frameworkDetection.packageManager } : {}),
|
|
604
|
+
...(frameworkDetection.entryPoints ? { entryPoints: frameworkDetection.entryPoints } : {}),
|
|
605
|
+
...(frameworkDetection.scripts ? { scripts: frameworkDetection.scripts } : {}),
|
|
606
|
+
...(frameworkDetection.buildSystem ? { buildSystem: frameworkDetection.buildSystem } : {}),
|
|
607
|
+
...(frameworkDetection.testFramework ? { testFramework: frameworkDetection.testFramework } : {}),
|
|
608
|
+
...(frameworkDetection.platforms ? { platforms: frameworkDetection.platforms } : {}),
|
|
609
|
+
confidence: 'high',
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
const projects = maxProjects <= 0 ? [] : allProjects.slice(0, maxProjects);
|
|
613
|
+
return {
|
|
614
|
+
projects,
|
|
615
|
+
summary: {
|
|
616
|
+
total: allProjects.length,
|
|
617
|
+
shown: projects.length,
|
|
618
|
+
truncated: projects.length < allProjects.length,
|
|
619
|
+
},
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
export async function describeWorkspace(rootDir, options = {}) {
|
|
623
|
+
const resolvedRoot = path.resolve(rootDir);
|
|
624
|
+
const rootStat = await stat(resolvedRoot);
|
|
625
|
+
if (!rootStat.isDirectory()) {
|
|
626
|
+
throw new Error(`Not a directory: ${resolvedRoot}`);
|
|
627
|
+
}
|
|
628
|
+
const files = await discoverFiles(resolvedRoot, options);
|
|
629
|
+
const { manifests, markers } = collectMarkers(files);
|
|
630
|
+
const markerKinds = new Set(manifests.filter((marker) => projectRootForMarker(marker.path) === '.').map((marker) => marker.kind));
|
|
631
|
+
const frameworkDetection = await detectFrameworks(resolvedRoot, markerKinds);
|
|
632
|
+
const maxProjects = Math.max(0, options.maxProjects ?? 25);
|
|
633
|
+
const projectResult = await buildProjects(resolvedRoot, manifests, maxProjects);
|
|
634
|
+
const composition = buildComposition(files);
|
|
635
|
+
const presenceSignals = buildPresenceSignals(files);
|
|
636
|
+
const classification = classify({
|
|
637
|
+
files,
|
|
638
|
+
manifests,
|
|
639
|
+
frameworks: frameworkDetection.frameworks,
|
|
640
|
+
packageManager: frameworkDetection.packageManager,
|
|
641
|
+
});
|
|
642
|
+
return {
|
|
643
|
+
schemaVersion: 1,
|
|
644
|
+
rootDir: resolvedRoot,
|
|
645
|
+
classification,
|
|
646
|
+
composition,
|
|
647
|
+
signals: {
|
|
648
|
+
manifests,
|
|
649
|
+
markers,
|
|
650
|
+
frameworks: frameworkDetection.frameworks,
|
|
651
|
+
...(frameworkDetection.packageManager ? { packageManager: frameworkDetection.packageManager } : {}),
|
|
652
|
+
...presenceSignals,
|
|
653
|
+
},
|
|
654
|
+
projects: projectResult.projects,
|
|
655
|
+
projectSummary: projectResult.summary,
|
|
656
|
+
recommendedCommands: recommendedCommands(classification.primaryType),
|
|
657
|
+
recommendedCalls: recommendedCalls(classification.primaryType),
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
//# sourceMappingURL=describe.js.map
|