@shrkcrft/workspace 0.1.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +15 -0
  3. package/dist/folder-scanner.d.ts +13 -0
  4. package/dist/folder-scanner.d.ts.map +1 -0
  5. package/dist/folder-scanner.js +73 -0
  6. package/dist/framework-detector.d.ts +9 -0
  7. package/dist/framework-detector.d.ts.map +1 -0
  8. package/dist/framework-detector.js +45 -0
  9. package/dist/index.d.ts +11 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +10 -0
  12. package/dist/package-json-reader.d.ts +21 -0
  13. package/dist/package-json-reader.d.ts.map +1 -0
  14. package/dist/package-json-reader.js +18 -0
  15. package/dist/package-manager-detector.d.ts +15 -0
  16. package/dist/package-manager-detector.d.ts.map +1 -0
  17. package/dist/package-manager-detector.js +47 -0
  18. package/dist/profile-detector.d.ts +47 -0
  19. package/dist/profile-detector.d.ts.map +1 -0
  20. package/dist/profile-detector.js +162 -0
  21. package/dist/project-root-detector.d.ts +6 -0
  22. package/dist/project-root-detector.d.ts.map +1 -0
  23. package/dist/project-root-detector.js +22 -0
  24. package/dist/project-shape.d.ts +57 -0
  25. package/dist/project-shape.d.ts.map +1 -0
  26. package/dist/project-shape.js +212 -0
  27. package/dist/tsconfig-reader.d.ts +12 -0
  28. package/dist/tsconfig-reader.d.ts.map +1 -0
  29. package/dist/tsconfig-reader.js +37 -0
  30. package/dist/workspace-inspector.d.ts +7 -0
  31. package/dist/workspace-inspector.d.ts.map +1 -0
  32. package/dist/workspace-inspector.js +57 -0
  33. package/dist/workspace-summary.d.ts +29 -0
  34. package/dist/workspace-summary.d.ts.map +1 -0
  35. package/dist/workspace-summary.js +1 -0
  36. package/package.json +51 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SharkCraft contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # @shrkcrft/workspace
2
+
3
+ SharkCraft workspace inspector: project root, package.json, package manager, frameworks, tsconfig.
4
+
5
+ Part of [SharkCraft](https://github.com/shrkcrft/sharkcraft) — a deterministic, local-first toolkit that gives AI coding agents durable project context. See the main repo for documentation, examples, and the `shrk` CLI.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ bun add @shrkcrft/workspace
11
+ ```
12
+
13
+ ## License
14
+
15
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,13 @@
1
+ export interface IFolderInfo {
2
+ path: string;
3
+ exists: boolean;
4
+ files: number;
5
+ dirs: number;
6
+ }
7
+ export declare function shallowScanFolder(dir: string): IFolderInfo;
8
+ export declare function listTopLevelDirs(projectRoot: string, limit?: number): string[];
9
+ export declare function findFiles(startDir: string, pattern: RegExp, options?: {
10
+ maxDepth?: number;
11
+ ignore?: Set<string>;
12
+ }): string[];
13
+ //# sourceMappingURL=folder-scanner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"folder-scanner.d.ts","sourceRoot":"","sources":["../src/folder-scanner.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAeD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAc1D;AAED,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,MAAM,EAAE,CAU1E;AAED,wBAAgB,SAAS,CACvB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAO,GACxD,MAAM,EAAE,CAoBV"}
@@ -0,0 +1,73 @@
1
+ import { existsSync, readdirSync, statSync } from 'node:fs';
2
+ import * as nodePath from 'node:path';
3
+ const DEFAULT_IGNORE = new Set([
4
+ 'node_modules',
5
+ '.git',
6
+ '.idea',
7
+ '.vscode',
8
+ 'dist',
9
+ 'build',
10
+ '.cache',
11
+ '.nx',
12
+ '.turbo',
13
+ 'coverage',
14
+ ]);
15
+ export function shallowScanFolder(dir) {
16
+ if (!existsSync(dir))
17
+ return { path: dir, exists: false, files: 0, dirs: 0 };
18
+ try {
19
+ let files = 0;
20
+ let dirs = 0;
21
+ const entries = readdirSync(dir, { withFileTypes: true });
22
+ for (const e of entries) {
23
+ if (e.isDirectory())
24
+ dirs += 1;
25
+ else if (e.isFile())
26
+ files += 1;
27
+ }
28
+ return { path: dir, exists: true, files, dirs };
29
+ }
30
+ catch {
31
+ return { path: dir, exists: true, files: 0, dirs: 0 };
32
+ }
33
+ }
34
+ export function listTopLevelDirs(projectRoot, limit = 40) {
35
+ if (!existsSync(projectRoot))
36
+ return [];
37
+ try {
38
+ return readdirSync(projectRoot, { withFileTypes: true })
39
+ .filter((e) => e.isDirectory() && !DEFAULT_IGNORE.has(e.name))
40
+ .map((e) => e.name)
41
+ .slice(0, limit);
42
+ }
43
+ catch {
44
+ return [];
45
+ }
46
+ }
47
+ export function findFiles(startDir, pattern, options = {}) {
48
+ const { maxDepth = 4, ignore = DEFAULT_IGNORE } = options;
49
+ const out = [];
50
+ function walk(dir, depth) {
51
+ if (depth > maxDepth)
52
+ return;
53
+ try {
54
+ const entries = readdirSync(dir, { withFileTypes: true });
55
+ for (const e of entries) {
56
+ const name = String(e.name);
57
+ if (ignore.has(name))
58
+ continue;
59
+ const full = nodePath.join(dir, name);
60
+ if (e.isDirectory())
61
+ walk(full, depth + 1);
62
+ else if (e.isFile() && pattern.test(name))
63
+ out.push(full);
64
+ }
65
+ }
66
+ catch {
67
+ return;
68
+ }
69
+ }
70
+ if (existsSync(startDir) && statSync(startDir).isDirectory())
71
+ walk(startDir, 0);
72
+ return out;
73
+ }
@@ -0,0 +1,9 @@
1
+ import type { IPackageJson } from './package-json-reader.js';
2
+ export interface IFrameworkInfo {
3
+ id: string;
4
+ name: string;
5
+ version?: string;
6
+ evidence: string[];
7
+ }
8
+ export declare function detectFrameworks(projectRoot: string, pkg: IPackageJson | null): IFrameworkInfo[];
9
+ //# sourceMappingURL=framework-detector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"framework-detector.d.ts","sourceRoot":"","sources":["../src/framework-detector.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAE7D,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AA0BD,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,IAAI,GAAG,cAAc,EAAE,CA6BhG"}
@@ -0,0 +1,45 @@
1
+ import { existsSync } from 'node:fs';
2
+ import * as nodePath from 'node:path';
3
+ const FRAMEWORKS = [
4
+ { id: 'angular', name: 'Angular', packages: ['@angular/core', '@angular/cli'], fileMarkers: ['angular.json'] },
5
+ { id: 'react', name: 'React', packages: ['react'] },
6
+ { id: 'vue', name: 'Vue', packages: ['vue'] },
7
+ { id: 'svelte', name: 'Svelte', packages: ['svelte'] },
8
+ { id: 'nextjs', name: 'Next.js', packages: ['next'] },
9
+ { id: 'nuxt', name: 'Nuxt', packages: ['nuxt'] },
10
+ { id: 'nestjs', name: 'NestJS', packages: ['@nestjs/core'] },
11
+ { id: 'express', name: 'Express', packages: ['express'] },
12
+ { id: 'fastify', name: 'Fastify', packages: ['fastify'] },
13
+ { id: 'nx', name: 'Nx', packages: ['nx', '@nx/workspace'], fileMarkers: ['nx.json'] },
14
+ { id: 'aws-lambda', name: 'AWS Lambda', packages: ['aws-lambda', '@types/aws-lambda'] },
15
+ { id: 'electron', name: 'Electron', packages: ['electron'] },
16
+ { id: 'typescript', name: 'TypeScript', packages: ['typescript'], fileMarkers: ['tsconfig.json', 'tsconfig.base.json'] },
17
+ { id: 'bun', name: 'Bun', packages: ['bun-types', '@types/bun'], fileMarkers: ['bun.lockb', 'bun.lock'] },
18
+ ];
19
+ export function detectFrameworks(projectRoot, pkg) {
20
+ const out = [];
21
+ const allDeps = {
22
+ ...(pkg?.dependencies ?? {}),
23
+ ...(pkg?.devDependencies ?? {}),
24
+ ...(pkg?.peerDependencies ?? {}),
25
+ };
26
+ for (const def of FRAMEWORKS) {
27
+ const evidence = [];
28
+ let version;
29
+ for (const pkgName of def.packages) {
30
+ if (pkgName in allDeps) {
31
+ evidence.push(`depends on ${pkgName}`);
32
+ version = version ?? allDeps[pkgName];
33
+ }
34
+ }
35
+ for (const marker of def.fileMarkers ?? []) {
36
+ if (existsSync(nodePath.join(projectRoot, marker))) {
37
+ evidence.push(`${marker} exists`);
38
+ }
39
+ }
40
+ if (evidence.length > 0) {
41
+ out.push({ id: def.id, name: def.name, version, evidence });
42
+ }
43
+ }
44
+ return out;
45
+ }
@@ -0,0 +1,11 @@
1
+ export * from './project-root-detector.js';
2
+ export * from './package-json-reader.js';
3
+ export * from './package-manager-detector.js';
4
+ export * from './framework-detector.js';
5
+ export * from './tsconfig-reader.js';
6
+ export * from './folder-scanner.js';
7
+ export * from './profile-detector.js';
8
+ export * from './workspace-summary.js';
9
+ export * from './workspace-inspector.js';
10
+ export * from './project-shape.js';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,0BAA0B,CAAC;AACzC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,oBAAoB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export * from "./project-root-detector.js";
2
+ export * from "./package-json-reader.js";
3
+ export * from "./package-manager-detector.js";
4
+ export * from "./framework-detector.js";
5
+ export * from "./tsconfig-reader.js";
6
+ export * from "./folder-scanner.js";
7
+ export * from "./profile-detector.js";
8
+ export * from "./workspace-summary.js";
9
+ export * from "./workspace-inspector.js";
10
+ export * from "./project-shape.js";
@@ -0,0 +1,21 @@
1
+ import { type AppError, type Result } from '@shrkcrft/core';
2
+ export interface IPackageJson {
3
+ name?: string;
4
+ version?: string;
5
+ description?: string;
6
+ private?: boolean;
7
+ type?: 'module' | 'commonjs';
8
+ scripts?: Record<string, string>;
9
+ dependencies?: Record<string, string>;
10
+ devDependencies?: Record<string, string>;
11
+ peerDependencies?: Record<string, string>;
12
+ workspaces?: string[] | {
13
+ packages?: string[];
14
+ };
15
+ packageManager?: string;
16
+ engines?: Record<string, string>;
17
+ bin?: string | Record<string, string>;
18
+ [key: string]: unknown;
19
+ }
20
+ export declare function readPackageJson(projectRoot: string): Result<IPackageJson | null, AppError>;
21
+ //# sourceMappingURL=package-json-reader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package-json-reader.d.ts","sourceRoot":"","sources":["../src/package-json-reader.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsC,KAAK,QAAQ,EAAE,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAEhG,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAChD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAAC,YAAY,GAAG,IAAI,EAAE,QAAQ,CAAC,CAc1F"}
@@ -0,0 +1,18 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import * as nodePath from 'node:path';
3
+ import { AppErrorImpl, ERROR_CODES, err, ok } from '@shrkcrft/core';
4
+ export function readPackageJson(projectRoot) {
5
+ const pkgPath = nodePath.join(projectRoot, 'package.json');
6
+ if (!existsSync(pkgPath))
7
+ return ok(null);
8
+ try {
9
+ const raw = readFileSync(pkgPath, 'utf8');
10
+ return ok(JSON.parse(raw));
11
+ }
12
+ catch (e) {
13
+ return err(new AppErrorImpl(ERROR_CODES.FILE_READ_ERROR, `Failed to parse package.json: ${pkgPath}`, {
14
+ details: { pkgPath },
15
+ cause: e,
16
+ }));
17
+ }
18
+ }
@@ -0,0 +1,15 @@
1
+ import type { IPackageJson } from './package-json-reader.js';
2
+ export declare enum PackageManager {
3
+ Bun = "bun",
4
+ Pnpm = "pnpm",
5
+ Yarn = "yarn",
6
+ Npm = "npm",
7
+ Unknown = "unknown"
8
+ }
9
+ export interface IPackageManagerInfo {
10
+ manager: PackageManager;
11
+ version?: string;
12
+ evidence: string[];
13
+ }
14
+ export declare function detectPackageManager(projectRoot: string, pkg: IPackageJson | null): IPackageManagerInfo;
15
+ //# sourceMappingURL=package-manager-detector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package-manager-detector.d.ts","sourceRoot":"","sources":["../src/package-manager-detector.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAE7D,oBAAY,cAAc;IACxB,GAAG,QAAQ;IACX,IAAI,SAAS;IACb,IAAI,SAAS;IACb,GAAG,QAAQ;IACX,OAAO,YAAY;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,cAAc,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,IAAI,GAAG,mBAAmB,CAmCvG"}
@@ -0,0 +1,47 @@
1
+ import { existsSync } from 'node:fs';
2
+ import * as nodePath from 'node:path';
3
+ export var PackageManager;
4
+ (function (PackageManager) {
5
+ PackageManager["Bun"] = "bun";
6
+ PackageManager["Pnpm"] = "pnpm";
7
+ PackageManager["Yarn"] = "yarn";
8
+ PackageManager["Npm"] = "npm";
9
+ PackageManager["Unknown"] = "unknown";
10
+ })(PackageManager || (PackageManager = {}));
11
+ export function detectPackageManager(projectRoot, pkg) {
12
+ const evidence = [];
13
+ if (pkg?.packageManager) {
14
+ const [name, version] = pkg.packageManager.split('@');
15
+ evidence.push(`packageManager field: ${pkg.packageManager}`);
16
+ const manager = (name ?? '').toLowerCase();
17
+ if (manager === 'bun')
18
+ return { manager: PackageManager.Bun, version, evidence };
19
+ if (manager === 'pnpm')
20
+ return { manager: PackageManager.Pnpm, version, evidence };
21
+ if (manager === 'yarn')
22
+ return { manager: PackageManager.Yarn, version, evidence };
23
+ if (manager === 'npm')
24
+ return { manager: PackageManager.Npm, version, evidence };
25
+ }
26
+ if (existsSync(nodePath.join(projectRoot, 'bun.lockb'))) {
27
+ evidence.push('bun.lockb present');
28
+ return { manager: PackageManager.Bun, evidence };
29
+ }
30
+ if (existsSync(nodePath.join(projectRoot, 'bun.lock'))) {
31
+ evidence.push('bun.lock present');
32
+ return { manager: PackageManager.Bun, evidence };
33
+ }
34
+ if (existsSync(nodePath.join(projectRoot, 'pnpm-lock.yaml'))) {
35
+ evidence.push('pnpm-lock.yaml present');
36
+ return { manager: PackageManager.Pnpm, evidence };
37
+ }
38
+ if (existsSync(nodePath.join(projectRoot, 'yarn.lock'))) {
39
+ evidence.push('yarn.lock present');
40
+ return { manager: PackageManager.Yarn, evidence };
41
+ }
42
+ if (existsSync(nodePath.join(projectRoot, 'package-lock.json'))) {
43
+ evidence.push('package-lock.json present');
44
+ return { manager: PackageManager.Npm, evidence };
45
+ }
46
+ return { manager: PackageManager.Unknown, evidence };
47
+ }
@@ -0,0 +1,47 @@
1
+ import type { IPackageJson } from './package-json-reader.js';
2
+ import type { IFrameworkInfo } from './framework-detector.js';
3
+ export declare enum WorkspaceProfile {
4
+ HasBun = "has-bun",
5
+ HasTypeScript = "has-typescript",
6
+ HasNx = "has-nx",
7
+ HasTurborepo = "has-turborepo",
8
+ HasReact = "has-react",
9
+ HasNext = "has-next",
10
+ HasAngular = "has-angular",
11
+ HasVue = "has-vue",
12
+ HasNestJS = "has-nestjs",
13
+ HasMcpSdk = "has-mcp-sdk",
14
+ HasTests = "has-tests",
15
+ HasEslint = "has-eslint",
16
+ HasBiome = "has-biome",
17
+ HasVitest = "has-vitest",
18
+ HasJest = "has-jest",
19
+ HasBunTest = "has-bun-test",
20
+ HasGithubActions = "has-github-actions",
21
+ HasPackageWorkspaces = "has-package-workspaces",
22
+ IsLibrary = "is-library",
23
+ IsService = "is-service",
24
+ IsMonorepo = "is-monorepo",
25
+ IsFrontend = "is-frontend",
26
+ IsBackend = "is-backend"
27
+ }
28
+ export interface IProfileEvidence {
29
+ profile: WorkspaceProfile;
30
+ reason: string;
31
+ }
32
+ export interface IProfileDetectionResult {
33
+ profiles: WorkspaceProfile[];
34
+ evidence: IProfileEvidence[];
35
+ }
36
+ export interface IDetectProfilesInput {
37
+ packageJson: IPackageJson | null;
38
+ frameworks: readonly IFrameworkInfo[];
39
+ topLevelDirs: readonly string[];
40
+ hasTsConfig: boolean;
41
+ }
42
+ /**
43
+ * Compute structured profile tags from a workspace inspection. Pure function:
44
+ * no I/O. Each detected profile has an `evidence` entry explaining why.
45
+ */
46
+ export declare function detectProfiles(input: IDetectProfilesInput): IProfileDetectionResult;
47
+ //# sourceMappingURL=profile-detector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile-detector.d.ts","sourceRoot":"","sources":["../src/profile-detector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAE9D,oBAAY,gBAAgB;IAC1B,MAAM,YAAY;IAClB,aAAa,mBAAmB;IAChC,KAAK,WAAW;IAChB,YAAY,kBAAkB;IAC9B,QAAQ,cAAc;IACtB,OAAO,aAAa;IACpB,UAAU,gBAAgB;IAC1B,MAAM,YAAY;IAClB,SAAS,eAAe;IACxB,SAAS,gBAAgB;IACzB,QAAQ,cAAc;IACtB,SAAS,eAAe;IACxB,QAAQ,cAAc;IACtB,SAAS,eAAe;IACxB,OAAO,aAAa;IACpB,UAAU,iBAAiB;IAC3B,gBAAgB,uBAAuB;IACvC,oBAAoB,2BAA2B;IAC/C,SAAS,eAAe;IACxB,SAAS,eAAe;IACxB,UAAU,gBAAgB;IAC1B,UAAU,gBAAgB;IAC1B,SAAS,eAAe;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,gBAAgB,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,QAAQ,EAAE,gBAAgB,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,YAAY,GAAG,IAAI,CAAC;IACjC,UAAU,EAAE,SAAS,cAAc,EAAE,CAAC;IACtC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,WAAW,EAAE,OAAO,CAAC;CACtB;AA6BD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,oBAAoB,GAAG,uBAAuB,CAuInF"}
@@ -0,0 +1,162 @@
1
+ export var WorkspaceProfile;
2
+ (function (WorkspaceProfile) {
3
+ WorkspaceProfile["HasBun"] = "has-bun";
4
+ WorkspaceProfile["HasTypeScript"] = "has-typescript";
5
+ WorkspaceProfile["HasNx"] = "has-nx";
6
+ WorkspaceProfile["HasTurborepo"] = "has-turborepo";
7
+ WorkspaceProfile["HasReact"] = "has-react";
8
+ WorkspaceProfile["HasNext"] = "has-next";
9
+ WorkspaceProfile["HasAngular"] = "has-angular";
10
+ WorkspaceProfile["HasVue"] = "has-vue";
11
+ WorkspaceProfile["HasNestJS"] = "has-nestjs";
12
+ WorkspaceProfile["HasMcpSdk"] = "has-mcp-sdk";
13
+ WorkspaceProfile["HasTests"] = "has-tests";
14
+ WorkspaceProfile["HasEslint"] = "has-eslint";
15
+ WorkspaceProfile["HasBiome"] = "has-biome";
16
+ WorkspaceProfile["HasVitest"] = "has-vitest";
17
+ WorkspaceProfile["HasJest"] = "has-jest";
18
+ WorkspaceProfile["HasBunTest"] = "has-bun-test";
19
+ WorkspaceProfile["HasGithubActions"] = "has-github-actions";
20
+ WorkspaceProfile["HasPackageWorkspaces"] = "has-package-workspaces";
21
+ WorkspaceProfile["IsLibrary"] = "is-library";
22
+ WorkspaceProfile["IsService"] = "is-service";
23
+ WorkspaceProfile["IsMonorepo"] = "is-monorepo";
24
+ WorkspaceProfile["IsFrontend"] = "is-frontend";
25
+ WorkspaceProfile["IsBackend"] = "is-backend";
26
+ })(WorkspaceProfile || (WorkspaceProfile = {}));
27
+ function hasDep(pkg, name) {
28
+ if (!pkg)
29
+ return false;
30
+ return (Boolean(pkg.dependencies?.[name]) ||
31
+ Boolean(pkg.devDependencies?.[name]) ||
32
+ Boolean(pkg.peerDependencies?.[name]));
33
+ }
34
+ function hasAnyDep(pkg, names) {
35
+ return names.some((n) => hasDep(pkg, n));
36
+ }
37
+ function hasFramework(frameworks, id) {
38
+ return frameworks.some((f) => f.id === id);
39
+ }
40
+ function hasScriptIncluding(pkg, ...needles) {
41
+ if (!pkg)
42
+ return false;
43
+ for (const v of Object.values(pkg.scripts ?? {})) {
44
+ for (const n of needles) {
45
+ if (typeof v === 'string' && v.includes(n))
46
+ return true;
47
+ }
48
+ }
49
+ return false;
50
+ }
51
+ /**
52
+ * Compute structured profile tags from a workspace inspection. Pure function:
53
+ * no I/O. Each detected profile has an `evidence` entry explaining why.
54
+ */
55
+ export function detectProfiles(input) {
56
+ const { packageJson: pkg, frameworks, topLevelDirs, hasTsConfig } = input;
57
+ const evidence = [];
58
+ const add = (profile, reason) => {
59
+ if (!evidence.some((e) => e.profile === profile)) {
60
+ evidence.push({ profile, reason });
61
+ }
62
+ };
63
+ // ── Language / runtime ────────────────────────────────────────────────
64
+ if (hasFramework(frameworks, 'bun') || hasDep(pkg, 'bun') || hasDep(pkg, '@types/bun')) {
65
+ add(WorkspaceProfile.HasBun, 'bun runtime detected via deps or framework signal');
66
+ }
67
+ if (hasTsConfig || hasDep(pkg, 'typescript') || hasFramework(frameworks, 'typescript')) {
68
+ add(WorkspaceProfile.HasTypeScript, 'tsconfig.json or typescript dependency present');
69
+ }
70
+ // ── Build / workspace tooling ─────────────────────────────────────────
71
+ if (hasFramework(frameworks, 'nx') || hasDep(pkg, 'nx') || hasDep(pkg, '@nx/workspace')) {
72
+ add(WorkspaceProfile.HasNx, 'nx workspace detected');
73
+ }
74
+ if (hasDep(pkg, 'turbo') ||
75
+ topLevelDirs.includes('turbo.json') ||
76
+ topLevelDirs.includes('.turbo')) {
77
+ add(WorkspaceProfile.HasTurborepo, 'turbo dependency or turbo.json present');
78
+ }
79
+ if (Array.isArray(pkg?.workspaces)) {
80
+ add(WorkspaceProfile.HasPackageWorkspaces, 'package.json workspaces array');
81
+ }
82
+ if (hasFramework(frameworks, 'nx') ||
83
+ Array.isArray(pkg?.workspaces) ||
84
+ topLevelDirs.includes('packages') ||
85
+ topLevelDirs.includes('libs') ||
86
+ topLevelDirs.includes('apps')) {
87
+ add(WorkspaceProfile.IsMonorepo, 'workspaces / Nx / packages/libs dirs present');
88
+ }
89
+ // ── UI frameworks ─────────────────────────────────────────────────────
90
+ if (hasFramework(frameworks, 'react') ||
91
+ hasAnyDep(pkg, ['react', 'react-dom', 'next', '@remix-run/react'])) {
92
+ add(WorkspaceProfile.HasReact, 'react family dependency or framework signal');
93
+ }
94
+ if (hasFramework(frameworks, 'next') || hasDep(pkg, 'next')) {
95
+ add(WorkspaceProfile.HasNext, 'next dependency or framework signal');
96
+ }
97
+ if (hasFramework(frameworks, 'angular') ||
98
+ hasAnyDep(pkg, ['@angular/core', '@angular/cli'])) {
99
+ add(WorkspaceProfile.HasAngular, '@angular/* dependency detected');
100
+ }
101
+ if (hasFramework(frameworks, 'vue') || hasAnyDep(pkg, ['vue', 'nuxt'])) {
102
+ add(WorkspaceProfile.HasVue, 'vue / nuxt dependency');
103
+ }
104
+ // ── Backend ───────────────────────────────────────────────────────────
105
+ if (hasFramework(frameworks, 'nestjs') ||
106
+ hasAnyDep(pkg, ['@nestjs/core', '@nestjs/common'])) {
107
+ add(WorkspaceProfile.HasNestJS, '@nestjs/* dependency');
108
+ }
109
+ if (hasDep(pkg, '@modelcontextprotocol/sdk')) {
110
+ add(WorkspaceProfile.HasMcpSdk, '@modelcontextprotocol/sdk dependency');
111
+ }
112
+ // ── Testing ───────────────────────────────────────────────────────────
113
+ if (hasDep(pkg, 'vitest'))
114
+ add(WorkspaceProfile.HasVitest, 'vitest dependency');
115
+ if (hasAnyDep(pkg, ['jest', '@jest/globals']))
116
+ add(WorkspaceProfile.HasJest, 'jest dependency');
117
+ if (hasScriptIncluding(pkg, 'bun test', 'bun:test') ||
118
+ (hasDep(pkg, '@types/bun') && hasScriptIncluding(pkg, 'test'))) {
119
+ add(WorkspaceProfile.HasBunTest, 'bun test script detected');
120
+ }
121
+ if (evidence.some((e) => e.profile === WorkspaceProfile.HasVitest) ||
122
+ evidence.some((e) => e.profile === WorkspaceProfile.HasJest) ||
123
+ evidence.some((e) => e.profile === WorkspaceProfile.HasBunTest) ||
124
+ hasScriptIncluding(pkg, 'test') ||
125
+ topLevelDirs.some((d) => d === 'tests' || d === '__tests__')) {
126
+ add(WorkspaceProfile.HasTests, 'test runner or test directory present');
127
+ }
128
+ // ── Lint ──────────────────────────────────────────────────────────────
129
+ if (hasAnyDep(pkg, ['eslint', '@eslint/js'])) {
130
+ add(WorkspaceProfile.HasEslint, 'eslint dependency');
131
+ }
132
+ if (hasAnyDep(pkg, ['@biomejs/biome'])) {
133
+ add(WorkspaceProfile.HasBiome, '@biomejs/biome dependency');
134
+ }
135
+ // ── CI ────────────────────────────────────────────────────────────────
136
+ if (topLevelDirs.includes('.github')) {
137
+ add(WorkspaceProfile.HasGithubActions, '.github directory present (likely workflows)');
138
+ }
139
+ // ── Library vs service vs frontend/backend ────────────────────────────
140
+ const looksLibrary = !!pkg?.main || !!pkg?.exports || !!pkg?.types;
141
+ if (looksLibrary && !topLevelDirs.includes('apps')) {
142
+ add(WorkspaceProfile.IsLibrary, 'package.json declares main/exports/types');
143
+ }
144
+ if (hasScriptIncluding(pkg, 'start', 'serve', 'dev:server') &&
145
+ !evidence.some((e) => e.profile === WorkspaceProfile.IsLibrary)) {
146
+ add(WorkspaceProfile.IsService, 'start/serve script detected');
147
+ }
148
+ const frontendSignal = evidence.some((e) => [WorkspaceProfile.HasReact, WorkspaceProfile.HasAngular, WorkspaceProfile.HasVue].includes(e.profile)) ||
149
+ hasAnyDep(pkg, ['vite', '@angular/build', 'next', 'remix']);
150
+ if (frontendSignal) {
151
+ add(WorkspaceProfile.IsFrontend, 'UI framework or frontend bundler dependency');
152
+ }
153
+ const backendSignal = evidence.some((e) => e.profile === WorkspaceProfile.HasNestJS) ||
154
+ hasAnyDep(pkg, ['express', 'fastify', 'koa', 'hono', '@nestjs/core']);
155
+ if (backendSignal) {
156
+ add(WorkspaceProfile.IsBackend, 'HTTP server framework dependency');
157
+ }
158
+ return {
159
+ profiles: evidence.map((e) => e.profile),
160
+ evidence,
161
+ };
162
+ }
@@ -0,0 +1,6 @@
1
+ export interface IProjectRoot {
2
+ root: string;
3
+ markers: string[];
4
+ }
5
+ export declare function detectProjectRoot(startDir?: string): IProjectRoot;
6
+ //# sourceMappingURL=project-root-detector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-root-detector.d.ts","sourceRoot":"","sources":["../src/project-root-detector.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAWD,wBAAgB,iBAAiB,CAAC,QAAQ,GAAE,MAAsB,GAAG,YAAY,CAShF"}
@@ -0,0 +1,22 @@
1
+ import { existsSync } from 'node:fs';
2
+ import * as nodePath from 'node:path';
3
+ const ROOT_MARKERS = [
4
+ 'package.json',
5
+ 'bun.lockb',
6
+ 'pnpm-workspace.yaml',
7
+ 'nx.json',
8
+ 'tsconfig.base.json',
9
+ '.git',
10
+ ];
11
+ export function detectProjectRoot(startDir = process.cwd()) {
12
+ let current = nodePath.resolve(startDir);
13
+ while (true) {
14
+ const found = ROOT_MARKERS.filter((m) => existsSync(nodePath.join(current, m)));
15
+ if (found.length > 0)
16
+ return { root: current, markers: found };
17
+ const parent = nodePath.dirname(current);
18
+ if (parent === current)
19
+ return { root: nodePath.resolve(startDir), markers: [] };
20
+ current = parent;
21
+ }
22
+ }
@@ -0,0 +1,57 @@
1
+ import type { IPackageJson } from './package-json-reader.js';
2
+ /**
3
+ * Coarse project-shape classification. Drives the default
4
+ * surface composition: a single-app repo hides monorepo-only
5
+ * commands by default; a library repo hides app-only commands.
6
+ */
7
+ export declare enum ProjectShape {
8
+ SingleApp = "single-app",
9
+ AppWithLibs = "app-with-libs",
10
+ Monorepo = "monorepo",
11
+ Library = "library",
12
+ Unknown = "unknown"
13
+ }
14
+ export interface IProjectShapeDetection {
15
+ shape: ProjectShape;
16
+ evidence: readonly string[];
17
+ /** Hint signals the resolver collected during detection. */
18
+ signals: {
19
+ hasAngularJson: boolean;
20
+ hasNxJson: boolean;
21
+ nxProjectCount: number | null;
22
+ workspaceCount: number | null;
23
+ hasAppsDir: boolean;
24
+ hasLibsDir: boolean;
25
+ hasDevServeScript: boolean;
26
+ hasOnlyBuildTestScripts: boolean;
27
+ };
28
+ }
29
+ export interface DetectProjectShapeOptions {
30
+ projectRoot: string;
31
+ /** Pre-loaded package.json, if available. */
32
+ packageJson?: IPackageJson | null;
33
+ }
34
+ /**
35
+ * Deterministic project-shape detector. No AI, no heuristics
36
+ * beyond file/dependency presence. Result is cacheable.
37
+ *
38
+ * Rules (first match wins; subsequent signals contribute evidence
39
+ * but do not change the verdict):
40
+ *
41
+ * 1. `nx.json` present AND ≥6 Nx projects discovered → Monorepo.
42
+ * 2. `package.json workspaces` field with ≥3 entries → Monorepo.
43
+ * 3. `angular.json` workspace with exactly one project → SingleApp.
44
+ * 4. `apps/` AND `libs/` dirs present (Nx-style) → AppWithLibs.
45
+ * 5. `package.json` with build/test scripts but NO dev/serve/start
46
+ * script AND no app dir → Library.
47
+ * 6. Otherwise → Unknown.
48
+ */
49
+ export declare function detectProjectShape(options: DetectProjectShapeOptions): IProjectShapeDetection;
50
+ export interface IProjectShapeCacheEntry {
51
+ schema: 'sharkcraft.shape.v1';
52
+ detection: IProjectShapeDetection;
53
+ cachedAt: string;
54
+ }
55
+ export declare function cacheProjectShape(projectRoot: string, detection: IProjectShapeDetection): string;
56
+ export declare function readCachedProjectShape(projectRoot: string): IProjectShapeCacheEntry | null;
57
+ //# sourceMappingURL=project-shape.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-shape.d.ts","sourceRoot":"","sources":["../src/project-shape.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAE7D;;;;GAIG;AACH,oBAAY,YAAY;IACtB,SAAS,eAAe;IACxB,WAAW,kBAAkB;IAC7B,QAAQ,aAAa;IACrB,OAAO,YAAY;IACnB,OAAO,YAAY;CACpB;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,YAAY,CAAC;IACpB,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,4DAA4D;IAC5D,OAAO,EAAE;QACP,cAAc,EAAE,OAAO,CAAC;QACxB,SAAS,EAAE,OAAO,CAAC;QACnB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;QAC9B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;QAC9B,UAAU,EAAE,OAAO,CAAC;QACpB,UAAU,EAAE,OAAO,CAAC;QACpB,iBAAiB,EAAE,OAAO,CAAC;QAC3B,uBAAuB,EAAE,OAAO,CAAC;KAClC,CAAC;CACH;AAED,MAAM,WAAW,yBAAyB;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,6CAA6C;IAC7C,WAAW,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;CACnC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,yBAAyB,GACjC,sBAAsB,CAuGxB;AAkED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,qBAAqB,CAAC;IAC9B,SAAS,EAAE,sBAAsB,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAKD,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,sBAAsB,GAChC,MAAM,CAWR;AAED,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,MAAM,GAClB,uBAAuB,GAAG,IAAI,CAQhC"}
@@ -0,0 +1,212 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import * as nodePath from 'node:path';
3
+ /**
4
+ * Coarse project-shape classification. Drives the default
5
+ * surface composition: a single-app repo hides monorepo-only
6
+ * commands by default; a library repo hides app-only commands.
7
+ */
8
+ export var ProjectShape;
9
+ (function (ProjectShape) {
10
+ ProjectShape["SingleApp"] = "single-app";
11
+ ProjectShape["AppWithLibs"] = "app-with-libs";
12
+ ProjectShape["Monorepo"] = "monorepo";
13
+ ProjectShape["Library"] = "library";
14
+ ProjectShape["Unknown"] = "unknown";
15
+ })(ProjectShape || (ProjectShape = {}));
16
+ /**
17
+ * Deterministic project-shape detector. No AI, no heuristics
18
+ * beyond file/dependency presence. Result is cacheable.
19
+ *
20
+ * Rules (first match wins; subsequent signals contribute evidence
21
+ * but do not change the verdict):
22
+ *
23
+ * 1. `nx.json` present AND ≥6 Nx projects discovered → Monorepo.
24
+ * 2. `package.json workspaces` field with ≥3 entries → Monorepo.
25
+ * 3. `angular.json` workspace with exactly one project → SingleApp.
26
+ * 4. `apps/` AND `libs/` dirs present (Nx-style) → AppWithLibs.
27
+ * 5. `package.json` with build/test scripts but NO dev/serve/start
28
+ * script AND no app dir → Library.
29
+ * 6. Otherwise → Unknown.
30
+ */
31
+ export function detectProjectShape(options) {
32
+ const { projectRoot, packageJson } = options;
33
+ const evidence = [];
34
+ const signals = {
35
+ hasAngularJson: existsSync(nodePath.join(projectRoot, 'angular.json')),
36
+ hasNxJson: existsSync(nodePath.join(projectRoot, 'nx.json')),
37
+ nxProjectCount: null,
38
+ workspaceCount: null,
39
+ hasAppsDir: existsSync(nodePath.join(projectRoot, 'apps')),
40
+ hasLibsDir: existsSync(nodePath.join(projectRoot, 'libs')),
41
+ hasDevServeScript: false,
42
+ hasOnlyBuildTestScripts: false,
43
+ };
44
+ // Nx project count (best-effort: read nx.json + count projects/).
45
+ if (signals.hasNxJson) {
46
+ signals.nxProjectCount = countNxProjects(projectRoot);
47
+ if (signals.nxProjectCount !== null) {
48
+ evidence.push(`nx.json present (${signals.nxProjectCount} projects)`);
49
+ }
50
+ else {
51
+ evidence.push('nx.json present');
52
+ }
53
+ }
54
+ // Workspaces count.
55
+ if (packageJson?.workspaces !== undefined) {
56
+ const ws = packageJson.workspaces;
57
+ const list = Array.isArray(ws) ? ws : (ws.packages ?? []);
58
+ signals.workspaceCount = list.length;
59
+ evidence.push(`package.json workspaces (${list.length} entries)`);
60
+ }
61
+ // Angular detection.
62
+ if (signals.hasAngularJson) {
63
+ evidence.push('angular.json present');
64
+ }
65
+ // Script-based signals.
66
+ const scripts = packageJson?.scripts ?? {};
67
+ const scriptNames = Object.keys(scripts);
68
+ signals.hasDevServeScript = scriptNames.some((s) => ['dev', 'serve', 'start'].includes(s));
69
+ signals.hasOnlyBuildTestScripts =
70
+ scriptNames.length > 0 &&
71
+ scriptNames.every((s) => /^(build|test|lint|format|prepublish|prepare)/.test(s));
72
+ if (signals.hasAppsDir)
73
+ evidence.push('apps/ directory');
74
+ if (signals.hasLibsDir)
75
+ evidence.push('libs/ directory');
76
+ if (signals.hasDevServeScript)
77
+ evidence.push('dev/serve/start script');
78
+ if (signals.hasOnlyBuildTestScripts)
79
+ evidence.push('only build/test scripts');
80
+ // Count workspaces-glob packages on disk (cheap dir count for
81
+ // `packages/`, `libs/`, `apps/` — a glob like `packages/*` matches
82
+ // any direct subdir).
83
+ const packagesDirCount = countDirectChildren(nodePath.join(projectRoot, 'packages'));
84
+ if (packagesDirCount > 0) {
85
+ evidence.push(`packages/ (${packagesDirCount} entries)`);
86
+ }
87
+ // Apply rules — strongest signals first. Conservative on SingleApp:
88
+ // require an unambiguous app signal (angular.json single project, or
89
+ // a dev script in a project with NO sibling packages and NO nx.json
90
+ // and NO workspaces field).
91
+ if (signals.hasNxJson && (signals.nxProjectCount ?? 0) >= 6) {
92
+ return { shape: ProjectShape.Monorepo, evidence, signals };
93
+ }
94
+ if ((signals.workspaceCount ?? 0) >= 3) {
95
+ return { shape: ProjectShape.Monorepo, evidence, signals };
96
+ }
97
+ if (signals.hasNxJson && packagesDirCount >= 6) {
98
+ return { shape: ProjectShape.Monorepo, evidence, signals };
99
+ }
100
+ if ((signals.workspaceCount ?? 0) >= 1 &&
101
+ packagesDirCount >= 3) {
102
+ return { shape: ProjectShape.Monorepo, evidence, signals };
103
+ }
104
+ if (signals.hasAngularJson) {
105
+ const projectCount = countAngularProjects(projectRoot);
106
+ if (projectCount !== null && projectCount > 1) {
107
+ evidence.push(`angular.json (${projectCount} projects)`);
108
+ return { shape: ProjectShape.AppWithLibs, evidence, signals };
109
+ }
110
+ return { shape: ProjectShape.SingleApp, evidence, signals };
111
+ }
112
+ if (signals.hasAppsDir && signals.hasLibsDir) {
113
+ return { shape: ProjectShape.AppWithLibs, evidence, signals };
114
+ }
115
+ if (signals.hasOnlyBuildTestScripts && !signals.hasAppsDir && packagesDirCount === 0) {
116
+ return { shape: ProjectShape.Library, evidence, signals };
117
+ }
118
+ if (signals.hasDevServeScript &&
119
+ !signals.hasLibsDir &&
120
+ !signals.hasNxJson &&
121
+ signals.workspaceCount === null &&
122
+ packagesDirCount === 0) {
123
+ return { shape: ProjectShape.SingleApp, evidence, signals };
124
+ }
125
+ return { shape: ProjectShape.Unknown, evidence, signals };
126
+ }
127
+ function countDirectChildren(dir) {
128
+ try {
129
+ return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).length;
130
+ }
131
+ catch {
132
+ return 0;
133
+ }
134
+ }
135
+ function countNxProjects(projectRoot) {
136
+ // Best-effort: count entries in nx.json `projects` (legacy) or count
137
+ // `project.json` files. Avoid recursing the whole tree; cap depth.
138
+ const nxFile = nodePath.join(projectRoot, 'nx.json');
139
+ try {
140
+ const nx = JSON.parse(readFileSync(nxFile, 'utf8'));
141
+ if (nx.projects && typeof nx.projects === 'object') {
142
+ return Object.keys(nx.projects).length;
143
+ }
144
+ }
145
+ catch {
146
+ // ignore
147
+ }
148
+ // Fallback: look in apps/ + libs/ for project.json sentinels.
149
+ let count = 0;
150
+ for (const root of ['apps', 'libs', 'packages']) {
151
+ const dir = nodePath.join(projectRoot, root);
152
+ if (!existsSync(dir))
153
+ continue;
154
+ try {
155
+ const entries = readJsonChildren(dir);
156
+ count += entries;
157
+ }
158
+ catch {
159
+ // ignore
160
+ }
161
+ }
162
+ return count > 0 ? count : null;
163
+ }
164
+ function countAngularProjects(projectRoot) {
165
+ try {
166
+ const angularJson = JSON.parse(readFileSync(nodePath.join(projectRoot, 'angular.json'), 'utf8'));
167
+ if (angularJson.projects && typeof angularJson.projects === 'object') {
168
+ return Object.keys(angularJson.projects).length;
169
+ }
170
+ }
171
+ catch {
172
+ // ignore
173
+ }
174
+ return null;
175
+ }
176
+ function readJsonChildren(dir) {
177
+ let count = 0;
178
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
179
+ if (e.isDirectory()) {
180
+ const projectJson = nodePath.join(dir, e.name, 'project.json');
181
+ if (existsSync(projectJson))
182
+ count += 1;
183
+ }
184
+ }
185
+ return count;
186
+ }
187
+ const CACHE_DIR = '.sharkcraft';
188
+ const CACHE_FILE = 'shape.json';
189
+ export function cacheProjectShape(projectRoot, detection) {
190
+ const dir = nodePath.join(projectRoot, CACHE_DIR);
191
+ if (!existsSync(dir))
192
+ mkdirSync(dir, { recursive: true });
193
+ const file = nodePath.join(dir, CACHE_FILE);
194
+ const entry = {
195
+ schema: 'sharkcraft.shape.v1',
196
+ detection,
197
+ cachedAt: new Date().toISOString(),
198
+ };
199
+ writeFileSync(file, JSON.stringify(entry, null, 2) + '\n', 'utf8');
200
+ return file;
201
+ }
202
+ export function readCachedProjectShape(projectRoot) {
203
+ const file = nodePath.join(projectRoot, CACHE_DIR, CACHE_FILE);
204
+ if (!existsSync(file))
205
+ return null;
206
+ try {
207
+ return JSON.parse(readFileSync(file, 'utf8'));
208
+ }
209
+ catch {
210
+ return null;
211
+ }
212
+ }
@@ -0,0 +1,12 @@
1
+ import { type AppError, type Result } from '@shrkcrft/core';
2
+ export interface ITsConfig {
3
+ target?: string;
4
+ module?: string;
5
+ strict?: boolean;
6
+ paths?: Record<string, string[]>;
7
+ baseUrl?: string;
8
+ extends?: string;
9
+ raw: Record<string, unknown>;
10
+ }
11
+ export declare function readTsConfig(projectRoot: string): Result<ITsConfig | null, AppError>;
12
+ //# sourceMappingURL=tsconfig-reader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tsconfig-reader.d.ts","sourceRoot":"","sources":["../src/tsconfig-reader.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsC,KAAK,QAAQ,EAAE,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAEhG,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAID,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAAC,SAAS,GAAG,IAAI,EAAE,QAAQ,CAAC,CAiCpF"}
@@ -0,0 +1,37 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import * as nodePath from 'node:path';
3
+ import { AppErrorImpl, ERROR_CODES, err, ok } from '@shrkcrft/core';
4
+ const TSCONFIG_NAMES = ['tsconfig.json', 'tsconfig.base.json'];
5
+ export function readTsConfig(projectRoot) {
6
+ for (const name of TSCONFIG_NAMES) {
7
+ const file = nodePath.join(projectRoot, name);
8
+ if (existsSync(file)) {
9
+ try {
10
+ const text = readFileSync(file, 'utf8');
11
+ // strip // comments and trailing commas to handle JSON-with-comments tsconfigs
12
+ const cleaned = text
13
+ .replace(/\/\*[\s\S]*?\*\//g, '')
14
+ .replace(/(^|[^:\\])\/\/.*$/gm, '$1')
15
+ .replace(/,(\s*[}\]])/g, '$1');
16
+ const parsed = JSON.parse(cleaned);
17
+ const compilerOptions = parsed.compilerOptions ?? {};
18
+ return ok({
19
+ target: compilerOptions.target,
20
+ module: compilerOptions.module,
21
+ strict: compilerOptions.strict,
22
+ paths: compilerOptions.paths,
23
+ baseUrl: compilerOptions.baseUrl,
24
+ extends: parsed.extends,
25
+ raw: parsed,
26
+ });
27
+ }
28
+ catch (e) {
29
+ return err(new AppErrorImpl(ERROR_CODES.FILE_READ_ERROR, `Failed to parse ${name}: ${file}`, {
30
+ details: { file },
31
+ cause: e,
32
+ }));
33
+ }
34
+ }
35
+ }
36
+ return ok(null);
37
+ }
@@ -0,0 +1,7 @@
1
+ import type { IWorkspaceSummary } from './workspace-summary.js';
2
+ export interface InspectWorkspaceOptions {
3
+ startDir?: string;
4
+ sharkcraftDirName?: string;
5
+ }
6
+ export declare function inspectWorkspace(options?: InspectWorkspaceOptions): Promise<IWorkspaceSummary>;
7
+ //# sourceMappingURL=workspace-inspector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-inspector.d.ts","sourceRoot":"","sources":["../src/workspace-inspector.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhE,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,wBAAsB,gBAAgB,CACpC,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,iBAAiB,CAAC,CA0C5B"}
@@ -0,0 +1,57 @@
1
+ import { existsSync, statSync } from 'node:fs';
2
+ import * as nodePath from 'node:path';
3
+ import { detectProjectRoot } from "./project-root-detector.js";
4
+ import { readPackageJson } from "./package-json-reader.js";
5
+ import { detectPackageManager } from "./package-manager-detector.js";
6
+ import { detectFrameworks } from "./framework-detector.js";
7
+ import { readTsConfig } from "./tsconfig-reader.js";
8
+ import { listTopLevelDirs } from "./folder-scanner.js";
9
+ import { detectProfiles } from "./profile-detector.js";
10
+ export async function inspectWorkspace(options = {}) {
11
+ const startDir = options.startDir ?? process.cwd();
12
+ const sharkcraftDirName = options.sharkcraftDirName ?? 'sharkcraft';
13
+ const { root } = detectProjectRoot(startDir);
14
+ const pkgResult = readPackageJson(root);
15
+ const pkg = pkgResult.ok ? pkgResult.value : null;
16
+ const pkgManager = detectPackageManager(root, pkg);
17
+ const frameworks = detectFrameworks(root, pkg);
18
+ const tsConfigResult = readTsConfig(root);
19
+ const tsConfig = tsConfigResult.ok ? tsConfigResult.value : null;
20
+ const sharkcraftPath = nodePath.join(root, sharkcraftDirName);
21
+ const hasSharkcraftFolder = existsSync(sharkcraftPath) && safeIsDir(sharkcraftPath);
22
+ const topLevelDirs = listTopLevelDirs(root);
23
+ const profileResult = detectProfiles({
24
+ packageJson: pkg,
25
+ frameworks,
26
+ topLevelDirs,
27
+ hasTsConfig: tsConfig !== null,
28
+ });
29
+ return {
30
+ projectRoot: root,
31
+ hasPackageJson: pkg !== null,
32
+ packageName: pkg?.name,
33
+ packageVersion: pkg?.version,
34
+ description: pkg?.description,
35
+ packageManager: pkgManager,
36
+ frameworks,
37
+ hasTypeScript: frameworks.some((f) => f.id === 'typescript') || tsConfig !== null,
38
+ tsConfig,
39
+ scripts: pkg?.scripts ?? {},
40
+ dependencies: pkg?.dependencies ?? {},
41
+ devDependencies: pkg?.devDependencies ?? {},
42
+ topLevelDirs,
43
+ hasSharkcraftFolder,
44
+ sharkcraftPath: hasSharkcraftFolder ? sharkcraftPath : null,
45
+ profiles: profileResult.profiles,
46
+ profileEvidence: profileResult.evidence,
47
+ raw: { packageJson: pkg },
48
+ };
49
+ }
50
+ function safeIsDir(p) {
51
+ try {
52
+ return statSync(p).isDirectory();
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ }
@@ -0,0 +1,29 @@
1
+ import type { IPackageJson } from './package-json-reader.js';
2
+ import type { IPackageManagerInfo } from './package-manager-detector.js';
3
+ import type { IFrameworkInfo } from './framework-detector.js';
4
+ import type { ITsConfig } from './tsconfig-reader.js';
5
+ import type { IProfileDetectionResult, WorkspaceProfile } from './profile-detector.js';
6
+ export interface IWorkspaceSummary {
7
+ projectRoot: string;
8
+ hasPackageJson: boolean;
9
+ packageName?: string;
10
+ packageVersion?: string;
11
+ description?: string;
12
+ packageManager: IPackageManagerInfo;
13
+ frameworks: IFrameworkInfo[];
14
+ hasTypeScript: boolean;
15
+ tsConfig: ITsConfig | null;
16
+ scripts: Record<string, string>;
17
+ dependencies: Record<string, string>;
18
+ devDependencies: Record<string, string>;
19
+ topLevelDirs: string[];
20
+ hasSharkcraftFolder: boolean;
21
+ sharkcraftPath: string | null;
22
+ /** Inferred profile tags (see WorkspaceProfile). */
23
+ profiles: readonly WorkspaceProfile[];
24
+ profileEvidence: IProfileDetectionResult['evidence'];
25
+ raw: {
26
+ packageJson: IPackageJson | null;
27
+ };
28
+ }
29
+ //# sourceMappingURL=workspace-summary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-summary.d.ts","sourceRoot":"","sources":["../src/workspace-summary.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,KAAK,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEvF,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,mBAAmB,CAAC;IACpC,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,SAAS,GAAG,IAAI,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,oDAAoD;IACpD,QAAQ,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACtC,eAAe,EAAE,uBAAuB,CAAC,UAAU,CAAC,CAAC;IACrD,GAAG,EAAE;QAAE,WAAW,EAAE,YAAY,GAAG,IAAI,CAAA;KAAE,CAAC;CAC3C"}
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@shrkcrft/workspace",
3
+ "version": "0.1.0-alpha.2",
4
+ "description": "SharkCraft workspace inspector: project root, package.json, package manager, frameworks, tsconfig.",
5
+ "license": "MIT",
6
+ "author": "SharkCraft contributors",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "bun": "./src/index.ts",
14
+ "import": "./dist/index.js",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/shrkcrft/sharkcraft.git",
26
+ "directory": "packages/workspace"
27
+ },
28
+ "homepage": "https://github.com/shrkcrft/sharkcraft",
29
+ "bugs": {
30
+ "url": "https://github.com/shrkcrft/sharkcraft/issues"
31
+ },
32
+ "keywords": [
33
+ "sharkcraft",
34
+ "workspace",
35
+ "project-root",
36
+ "frameworks"
37
+ ],
38
+ "engines": {
39
+ "bun": ">=1.1.0",
40
+ "node": ">=18"
41
+ },
42
+ "scripts": {
43
+ "typecheck": "tsc --noEmit -p tsconfig.json"
44
+ },
45
+ "dependencies": {
46
+ "@shrkcrft/core": "^0.1.0-alpha.2"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ }
51
+ }