@mrpatronz/nexusflow 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GETTING_STARTED.md +109 -0
- package/README.md +310 -0
- package/dist/analyzers/detect-apis.d.ts +19 -0
- package/dist/analyzers/detect-apis.d.ts.map +1 -0
- package/dist/analyzers/detect-apis.js +121 -0
- package/dist/analyzers/detect-apis.js.map +1 -0
- package/dist/analyzers/detect-deps.d.ts +30 -0
- package/dist/analyzers/detect-deps.d.ts.map +1 -0
- package/dist/analyzers/detect-deps.js +121 -0
- package/dist/analyzers/detect-deps.js.map +1 -0
- package/dist/analyzers/detect-existing.d.ts +15 -0
- package/dist/analyzers/detect-existing.d.ts.map +1 -0
- package/dist/analyzers/detect-existing.js +65 -0
- package/dist/analyzers/detect-existing.js.map +1 -0
- package/dist/analyzers/detect-ports.d.ts +20 -0
- package/dist/analyzers/detect-ports.d.ts.map +1 -0
- package/dist/analyzers/detect-ports.js +103 -0
- package/dist/analyzers/detect-ports.js.map +1 -0
- package/dist/analyzers/index.d.ts +29 -0
- package/dist/analyzers/index.d.ts.map +1 -0
- package/dist/analyzers/index.js +74 -0
- package/dist/analyzers/index.js.map +1 -0
- package/dist/analyzers/readme-summarizer.d.ts +13 -0
- package/dist/analyzers/readme-summarizer.d.ts.map +1 -0
- package/dist/analyzers/readme-summarizer.js +59 -0
- package/dist/analyzers/readme-summarizer.js.map +1 -0
- package/dist/analyzers/tech-stack.d.ts +15 -0
- package/dist/analyzers/tech-stack.d.ts.map +1 -0
- package/dist/analyzers/tech-stack.js +284 -0
- package/dist/analyzers/tech-stack.js.map +1 -0
- package/dist/commands/create.d.ts +17 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +111 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/init.d.ts +10 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +50 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/list.d.ts +9 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +32 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/logs.d.ts +12 -0
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/commands/logs.js +54 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/open.d.ts +9 -0
- package/dist/commands/open.d.ts.map +1 -0
- package/dist/commands/open.js +49 -0
- package/dist/commands/open.js.map +1 -0
- package/dist/commands/start.d.ts +13 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +97 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/status.d.ts +11 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +47 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/stop.d.ts +11 -0
- package/dist/commands/stop.d.ts.map +1 -0
- package/dist/commands/stop.js +47 -0
- package/dist/commands/stop.js.map +1 -0
- package/dist/commands/ui.d.ts +13 -0
- package/dist/commands/ui.d.ts.map +1 -0
- package/dist/commands/ui.js +44 -0
- package/dist/commands/ui.js.map +1 -0
- package/dist/core/config.d.ts +32 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +68 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/scanner.d.ts +18 -0
- package/dist/core/scanner.d.ts.map +1 -0
- package/dist/core/scanner.js +73 -0
- package/dist/core/scanner.js.map +1 -0
- package/dist/core/workspace.d.ts +53 -0
- package/dist/core/workspace.d.ts.map +1 -0
- package/dist/core/workspace.js +114 -0
- package/dist/core/workspace.js.map +1 -0
- package/dist/core/worktree.d.ts +27 -0
- package/dist/core/worktree.d.ts.map +1 -0
- package/dist/core/worktree.js +35 -0
- package/dist/core/worktree.js.map +1 -0
- package/dist/generators/base.d.ts +15 -0
- package/dist/generators/base.d.ts.map +1 -0
- package/dist/generators/base.js +175 -0
- package/dist/generators/base.js.map +1 -0
- package/dist/generators/claude.d.ts +17 -0
- package/dist/generators/claude.d.ts.map +1 -0
- package/dist/generators/claude.js +29 -0
- package/dist/generators/claude.js.map +1 -0
- package/dist/generators/codex.d.ts +16 -0
- package/dist/generators/codex.d.ts.map +1 -0
- package/dist/generators/codex.js +41 -0
- package/dist/generators/codex.js.map +1 -0
- package/dist/generators/copilot.d.ts +16 -0
- package/dist/generators/copilot.d.ts.map +1 -0
- package/dist/generators/copilot.js +49 -0
- package/dist/generators/copilot.js.map +1 -0
- package/dist/generators/cursor.d.ts +17 -0
- package/dist/generators/cursor.d.ts.map +1 -0
- package/dist/generators/cursor.js +37 -0
- package/dist/generators/cursor.js.map +1 -0
- package/dist/generators/index.d.ts +18 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +95 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/gui/assets/index-Cq9V485S.js +21 -0
- package/dist/gui/assets/index-Dh1b5gSZ.css +2 -0
- package/dist/gui/favicon.svg +1 -0
- package/dist/gui/icons.svg +24 -0
- package/dist/gui/index.html +18 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +169 -0
- package/dist/index.js.map +1 -0
- package/dist/orchestration/detect.d.ts +29 -0
- package/dist/orchestration/detect.d.ts.map +1 -0
- package/dist/orchestration/detect.js +292 -0
- package/dist/orchestration/detect.js.map +1 -0
- package/dist/orchestration/index.d.ts +7 -0
- package/dist/orchestration/index.d.ts.map +1 -0
- package/dist/orchestration/index.js +7 -0
- package/dist/orchestration/index.js.map +1 -0
- package/dist/orchestration/runner.d.ts +40 -0
- package/dist/orchestration/runner.d.ts.map +1 -0
- package/dist/orchestration/runner.js +242 -0
- package/dist/orchestration/runner.js.map +1 -0
- package/dist/server.d.ts +16 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +419 -0
- package/dist/server.js.map +1 -0
- package/dist/types.d.ts +210 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/detect-ai.d.ts +21 -0
- package/dist/utils/detect-ai.d.ts.map +1 -0
- package/dist/utils/detect-ai.js +76 -0
- package/dist/utils/detect-ai.js.map +1 -0
- package/dist/utils/detect-editors.d.ts +19 -0
- package/dist/utils/detect-editors.d.ts.map +1 -0
- package/dist/utils/detect-editors.js +49 -0
- package/dist/utils/detect-editors.js.map +1 -0
- package/dist/utils/git.d.ts +27 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +57 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/prompts.d.ts +27 -0
- package/dist/utils/prompts.d.ts.map +1 -0
- package/dist/utils/prompts.js +119 -0
- package/dist/utils/prompts.js.map +1 -0
- package/dist/utils/session-finder.d.ts +29 -0
- package/dist/utils/session-finder.d.ts.map +1 -0
- package/dist/utils/session-finder.js +462 -0
- package/dist/utils/session-finder.js.map +1 -0
- package/gui/README.md +73 -0
- package/gui/eslint.config.js +22 -0
- package/gui/index.html +17 -0
- package/gui/package-lock.json +3054 -0
- package/gui/package.json +33 -0
- package/gui/public/favicon.svg +1 -0
- package/gui/public/icons.svg +24 -0
- package/gui/src/App.css +1 -0
- package/gui/src/App.tsx +1949 -0
- package/gui/src/assets/hero.png +0 -0
- package/gui/src/assets/react.svg +1 -0
- package/gui/src/assets/vite.svg +1 -0
- package/gui/src/index.css +1 -0
- package/gui/src/main.tsx +10 -0
- package/gui/tsconfig.app.json +25 -0
- package/gui/tsconfig.json +7 -0
- package/gui/tsconfig.node.json +24 -0
- package/gui/vite.config.ts +12 -0
- package/package.json +48 -0
- package/src/analyzers/detect-apis.ts +138 -0
- package/src/analyzers/detect-deps.ts +138 -0
- package/src/analyzers/detect-existing.ts +74 -0
- package/src/analyzers/detect-ports.ts +110 -0
- package/src/analyzers/index.ts +85 -0
- package/src/analyzers/readme-summarizer.ts +68 -0
- package/src/analyzers/tech-stack.ts +255 -0
- package/src/commands/create.ts +142 -0
- package/src/commands/init.ts +58 -0
- package/src/commands/list.ts +46 -0
- package/src/commands/logs.ts +63 -0
- package/src/commands/open.ts +54 -0
- package/src/commands/start.ts +117 -0
- package/src/commands/status.ts +54 -0
- package/src/commands/stop.ts +55 -0
- package/src/commands/ui.ts +47 -0
- package/src/core/config.ts +79 -0
- package/src/core/scanner.ts +91 -0
- package/src/core/workspace.ts +149 -0
- package/src/core/worktree.ts +50 -0
- package/src/generators/base.ts +187 -0
- package/src/generators/claude.ts +34 -0
- package/src/generators/codex.ts +48 -0
- package/src/generators/copilot.ts +56 -0
- package/src/generators/cursor.ts +44 -0
- package/src/generators/index.ts +122 -0
- package/src/index.ts +178 -0
- package/src/orchestration/detect.ts +313 -0
- package/src/orchestration/index.ts +7 -0
- package/src/orchestration/runner.ts +279 -0
- package/src/server.ts +466 -0
- package/src/types.ts +294 -0
- package/src/utils/detect-ai.ts +81 -0
- package/src/utils/detect-editors.ts +54 -0
- package/src/utils/git.ts +61 -0
- package/src/utils/prompts.ts +135 -0
- package/src/utils/session-finder.ts +483 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module analyzers/index
|
|
3
|
+
* Orchestrates all project analyzers and produces a full ProjectAnalysis
|
|
4
|
+
* for each repository.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import ora from 'ora';
|
|
9
|
+
|
|
10
|
+
import type { ProjectAnalysis, RepoInfo } from '../types.js';
|
|
11
|
+
import { detectTechStack } from './tech-stack.js';
|
|
12
|
+
import { detectApis } from './detect-apis.js';
|
|
13
|
+
import { detectDependencies } from './detect-deps.js';
|
|
14
|
+
import { detectPorts } from './detect-ports.js';
|
|
15
|
+
import { detectExistingAIConfigs } from './detect-existing.js';
|
|
16
|
+
import { extractReadmeSummary } from './readme-summarizer.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Runs all analyzers against a single repository and returns
|
|
20
|
+
* a complete {@link ProjectAnalysis}.
|
|
21
|
+
*
|
|
22
|
+
* @param repo - The repo metadata.
|
|
23
|
+
* @returns Full analysis result.
|
|
24
|
+
*/
|
|
25
|
+
export async function analyzeRepo(repo: RepoInfo): Promise<ProjectAnalysis> {
|
|
26
|
+
const [techStack, endpoints, dependencies, ports, existingAIConfigs, readmeSummary] =
|
|
27
|
+
await Promise.all([
|
|
28
|
+
detectTechStack(repo.path),
|
|
29
|
+
detectApis(repo.path),
|
|
30
|
+
detectDependencies(repo.path),
|
|
31
|
+
detectPorts(repo.path),
|
|
32
|
+
detectExistingAIConfigs(repo.path),
|
|
33
|
+
extractReadmeSummary(repo.path),
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
name: repo.name,
|
|
38
|
+
path: repo.path,
|
|
39
|
+
techStack,
|
|
40
|
+
endpoints,
|
|
41
|
+
dependencies,
|
|
42
|
+
ports,
|
|
43
|
+
existingAIConfigs,
|
|
44
|
+
readmeSummary,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Analyzes all repos in a list and returns a Map of path → analysis.
|
|
50
|
+
* Shows a progress spinner while running.
|
|
51
|
+
*
|
|
52
|
+
* @param repos - Array of repos to analyze.
|
|
53
|
+
* @returns Map of repo path to {@link ProjectAnalysis}.
|
|
54
|
+
*/
|
|
55
|
+
export async function analyzeAllRepos(
|
|
56
|
+
repos: RepoInfo[],
|
|
57
|
+
): Promise<Map<string, ProjectAnalysis>> {
|
|
58
|
+
const spinner = ora('Analyzing projects...').start();
|
|
59
|
+
const results = new Map<string, ProjectAnalysis>();
|
|
60
|
+
|
|
61
|
+
for (let i = 0; i < repos.length; i++) {
|
|
62
|
+
const repo = repos[i]!;
|
|
63
|
+
spinner.text = `Analyzing ${chalk.bold(repo.name)} (${i + 1}/${repos.length})...`;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const analysis = await analyzeRepo(repo);
|
|
67
|
+
results.set(repo.path, analysis);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
70
|
+
spinner.warn(`Failed to analyze ${repo.name}: ${msg}`);
|
|
71
|
+
spinner.start();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
spinner.succeed(`Analyzed ${chalk.bold(results.size)} projects`);
|
|
76
|
+
return results;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Re-export individual analyzers
|
|
80
|
+
export { detectTechStack } from './tech-stack.js';
|
|
81
|
+
export { detectApis } from './detect-apis.js';
|
|
82
|
+
export { detectDependencies, findInterRepoDependencies } from './detect-deps.js';
|
|
83
|
+
export { detectPorts } from './detect-ports.js';
|
|
84
|
+
export { detectExistingAIConfigs } from './detect-existing.js';
|
|
85
|
+
export { extractReadmeSummary } from './readme-summarizer.js';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module analyzers/readme-summarizer
|
|
3
|
+
* Extracts a summary from a repository's README.md file.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'node:fs/promises';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
|
|
9
|
+
/** Maximum chars to extract from README for summary. */
|
|
10
|
+
const MAX_SUMMARY_LENGTH = 800;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Reads a repository's README.md and extracts the first meaningful
|
|
14
|
+
* section as a summary. Looks for README.md (case-insensitive).
|
|
15
|
+
*
|
|
16
|
+
* @param repoPath - Absolute path to the repository root.
|
|
17
|
+
* @returns The extracted summary text, or null if no README is found.
|
|
18
|
+
*/
|
|
19
|
+
export async function extractReadmeSummary(
|
|
20
|
+
repoPath: string,
|
|
21
|
+
): Promise<string | null> {
|
|
22
|
+
// Try common README filenames
|
|
23
|
+
const candidates = ['README.md', 'readme.md', 'Readme.md', 'README.MD', 'README'];
|
|
24
|
+
|
|
25
|
+
let content: string | null = null;
|
|
26
|
+
|
|
27
|
+
for (const filename of candidates) {
|
|
28
|
+
try {
|
|
29
|
+
content = await fs.readFile(path.join(repoPath, filename), 'utf-8');
|
|
30
|
+
break;
|
|
31
|
+
} catch {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!content) return null;
|
|
37
|
+
|
|
38
|
+
// Strip badges, images, and HTML at the top
|
|
39
|
+
const lines = content.split('\n');
|
|
40
|
+
const meaningfulLines: string[] = [];
|
|
41
|
+
let foundContent = false;
|
|
42
|
+
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
const trimmed = line.trim();
|
|
45
|
+
|
|
46
|
+
// Skip empty lines before content starts
|
|
47
|
+
if (!foundContent && !trimmed) continue;
|
|
48
|
+
|
|
49
|
+
// Skip badge lines ([]) and image lines (
|
|
50
|
+
if (trimmed.startsWith('[![') || (trimmed.startsWith('![') && trimmed.includes('http'))) continue;
|
|
51
|
+
|
|
52
|
+
// Skip HTML tags
|
|
53
|
+
if (trimmed.startsWith('<') && trimmed.endsWith('>')) continue;
|
|
54
|
+
|
|
55
|
+
// Skip horizontal rules
|
|
56
|
+
if (/^(-{3,}|={3,}|\*{3,})$/.test(trimmed)) continue;
|
|
57
|
+
|
|
58
|
+
foundContent = true;
|
|
59
|
+
meaningfulLines.push(line);
|
|
60
|
+
|
|
61
|
+
// Stop after we have enough content
|
|
62
|
+
const currentLength = meaningfulLines.join('\n').length;
|
|
63
|
+
if (currentLength >= MAX_SUMMARY_LENGTH) break;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const summary = meaningfulLines.join('\n').slice(0, MAX_SUMMARY_LENGTH).trim();
|
|
67
|
+
return summary || null;
|
|
68
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module analyzers/tech-stack
|
|
3
|
+
* Detects the language, framework, build tools, and project type
|
|
4
|
+
* of a repository by inspecting manifest files.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from 'node:fs/promises';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
|
|
10
|
+
import type { TechStack, Language, Framework, ProjectType } from '../types.js';
|
|
11
|
+
|
|
12
|
+
/** Check if a file exists in a directory. */
|
|
13
|
+
async function fileExists(dir: string, filename: string): Promise<boolean> {
|
|
14
|
+
try {
|
|
15
|
+
await fs.access(path.join(dir, filename));
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Safely read and parse a JSON file. Returns null on failure. */
|
|
23
|
+
async function readJson(filePath: string): Promise<Record<string, unknown> | null> {
|
|
24
|
+
try {
|
|
25
|
+
const raw = await fs.readFile(filePath, 'utf-8');
|
|
26
|
+
return JSON.parse(raw) as Record<string, unknown>;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Check if any file matching a glob-like pattern exists. */
|
|
33
|
+
async function hasFileWithExtension(dir: string, ext: string): Promise<boolean> {
|
|
34
|
+
try {
|
|
35
|
+
const entries = await fs.readdir(dir);
|
|
36
|
+
return entries.some((e) => e.endsWith(ext));
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Detects the tech stack of a repository by inspecting its root directory
|
|
44
|
+
* for manifest files, config files, and project structures.
|
|
45
|
+
*
|
|
46
|
+
* @param repoPath - Absolute path to the repository root.
|
|
47
|
+
* @returns A {@link TechStack} object describing the detected stack.
|
|
48
|
+
*/
|
|
49
|
+
export async function detectTechStack(repoPath: string): Promise<TechStack> {
|
|
50
|
+
const languages: Language[] = [];
|
|
51
|
+
const frameworks: Framework[] = [];
|
|
52
|
+
const buildTools: string[] = [];
|
|
53
|
+
let projectType: ProjectType = 'other';
|
|
54
|
+
|
|
55
|
+
// ── Node.js / JavaScript / TypeScript ──────────────────────────────
|
|
56
|
+
const hasPackageJson = await fileExists(repoPath, 'package.json');
|
|
57
|
+
if (hasPackageJson) {
|
|
58
|
+
const pkg = await readJson(path.join(repoPath, 'package.json'));
|
|
59
|
+
|
|
60
|
+
// Detect TypeScript vs JavaScript
|
|
61
|
+
const hasTsConfig = await fileExists(repoPath, 'tsconfig.json');
|
|
62
|
+
if (hasTsConfig) {
|
|
63
|
+
languages.push('typescript');
|
|
64
|
+
} else {
|
|
65
|
+
languages.push('javascript');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (pkg) {
|
|
69
|
+
const allDeps = {
|
|
70
|
+
...(pkg.dependencies as Record<string, string> | undefined),
|
|
71
|
+
...(pkg.devDependencies as Record<string, string> | undefined),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Frameworks
|
|
75
|
+
if (allDeps['next']) { frameworks.push('nextjs'); projectType = 'fullstack'; }
|
|
76
|
+
if (allDeps['react'] && !allDeps['next']) { frameworks.push('react'); projectType = 'frontend'; }
|
|
77
|
+
if (allDeps['@angular/core']) { frameworks.push('angular'); projectType = 'frontend'; }
|
|
78
|
+
if (allDeps['vue']) { frameworks.push('vue'); projectType = 'frontend'; }
|
|
79
|
+
if (allDeps['svelte']) { frameworks.push('svelte'); projectType = 'frontend'; }
|
|
80
|
+
if (allDeps['express']) { frameworks.push('express'); projectType = 'backend'; }
|
|
81
|
+
if (allDeps['@nestjs/core']) { frameworks.push('nestjs'); projectType = 'backend'; }
|
|
82
|
+
if (allDeps['fastify']) { frameworks.push('fastify'); projectType = 'backend'; }
|
|
83
|
+
if (allDeps['hono']) { frameworks.push('hono'); projectType = 'backend'; }
|
|
84
|
+
|
|
85
|
+
// Build tools
|
|
86
|
+
if (allDeps['vite'] || allDeps['@vitejs/plugin-react']) buildTools.push('vite');
|
|
87
|
+
if (allDeps['webpack']) buildTools.push('webpack');
|
|
88
|
+
if (allDeps['esbuild']) buildTools.push('esbuild');
|
|
89
|
+
if (allDeps['rollup']) buildTools.push('rollup');
|
|
90
|
+
if (allDeps['turbo']) buildTools.push('turborepo');
|
|
91
|
+
if (allDeps['nx']) buildTools.push('nx');
|
|
92
|
+
|
|
93
|
+
// Project type heuristics
|
|
94
|
+
if (pkg.bin) projectType = 'cli';
|
|
95
|
+
if (pkg.main && !pkg.bin && frameworks.length === 0) projectType = 'library';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── .NET / C# ─────────────────────────────────────────────────────
|
|
100
|
+
const hasCsproj = await hasFileWithExtension(repoPath, '.csproj');
|
|
101
|
+
const hasSln = await hasFileWithExtension(repoPath, '.sln');
|
|
102
|
+
if (hasCsproj || hasSln) {
|
|
103
|
+
languages.push('csharp');
|
|
104
|
+
|
|
105
|
+
// Try to detect ASP.NET or Blazor from csproj content
|
|
106
|
+
try {
|
|
107
|
+
const entries = await fs.readdir(repoPath);
|
|
108
|
+
const csprojFile = entries.find((e) => e.endsWith('.csproj'));
|
|
109
|
+
if (csprojFile) {
|
|
110
|
+
const content = await fs.readFile(path.join(repoPath, csprojFile), 'utf-8');
|
|
111
|
+
if (content.includes('Microsoft.NET.Sdk.Web')) {
|
|
112
|
+
frameworks.push('aspnet');
|
|
113
|
+
projectType = 'backend';
|
|
114
|
+
}
|
|
115
|
+
if (content.includes('Microsoft.NET.Sdk.BlazorWebAssembly') || content.includes('Blazor')) {
|
|
116
|
+
frameworks.push('blazor');
|
|
117
|
+
projectType = 'frontend';
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
// Ignore read errors
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
buildTools.push('dotnet');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Python ────────────────────────────────────────────────────────
|
|
128
|
+
const hasPyproject = await fileExists(repoPath, 'pyproject.toml');
|
|
129
|
+
const hasRequirements = await fileExists(repoPath, 'requirements.txt');
|
|
130
|
+
const hasSetupPy = await fileExists(repoPath, 'setup.py');
|
|
131
|
+
if (hasPyproject || hasRequirements || hasSetupPy) {
|
|
132
|
+
languages.push('python');
|
|
133
|
+
|
|
134
|
+
// Try to detect frameworks from requirements
|
|
135
|
+
try {
|
|
136
|
+
let content = '';
|
|
137
|
+
if (hasRequirements) {
|
|
138
|
+
content = await fs.readFile(path.join(repoPath, 'requirements.txt'), 'utf-8');
|
|
139
|
+
} else if (hasPyproject) {
|
|
140
|
+
content = await fs.readFile(path.join(repoPath, 'pyproject.toml'), 'utf-8');
|
|
141
|
+
}
|
|
142
|
+
const lower = content.toLowerCase();
|
|
143
|
+
if (lower.includes('django')) { frameworks.push('django'); projectType = 'backend'; }
|
|
144
|
+
if (lower.includes('flask')) { frameworks.push('flask'); projectType = 'backend'; }
|
|
145
|
+
if (lower.includes('fastapi')) { frameworks.push('fastapi'); projectType = 'backend'; }
|
|
146
|
+
} catch {
|
|
147
|
+
// Ignore
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (hasPyproject) buildTools.push('pyproject');
|
|
151
|
+
if (hasSetupPy) buildTools.push('setuptools');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── Go ────────────────────────────────────────────────────────────
|
|
155
|
+
const hasGoMod = await fileExists(repoPath, 'go.mod');
|
|
156
|
+
if (hasGoMod) {
|
|
157
|
+
languages.push('go');
|
|
158
|
+
buildTools.push('go');
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const content = await fs.readFile(path.join(repoPath, 'go.mod'), 'utf-8');
|
|
162
|
+
if (content.includes('github.com/gin-gonic/gin')) {
|
|
163
|
+
frameworks.push('gin');
|
|
164
|
+
projectType = 'backend';
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
// Ignore
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Java ──────────────────────────────────────────────────────────
|
|
172
|
+
const hasPom = await fileExists(repoPath, 'pom.xml');
|
|
173
|
+
const hasGradle = await fileExists(repoPath, 'build.gradle');
|
|
174
|
+
const hasGradleKts = await fileExists(repoPath, 'build.gradle.kts');
|
|
175
|
+
if (hasPom || hasGradle || hasGradleKts) {
|
|
176
|
+
languages.push('java');
|
|
177
|
+
if (hasPom) buildTools.push('maven');
|
|
178
|
+
if (hasGradle || hasGradleKts) buildTools.push('gradle');
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
let content = '';
|
|
182
|
+
if (hasPom) {
|
|
183
|
+
content = await fs.readFile(path.join(repoPath, 'pom.xml'), 'utf-8');
|
|
184
|
+
} else if (hasGradle) {
|
|
185
|
+
content = await fs.readFile(path.join(repoPath, 'build.gradle'), 'utf-8');
|
|
186
|
+
}
|
|
187
|
+
if (content.includes('spring')) {
|
|
188
|
+
frameworks.push('spring');
|
|
189
|
+
projectType = 'backend';
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
// Ignore
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── Rust ──────────────────────────────────────────────────────────
|
|
197
|
+
const hasCargo = await fileExists(repoPath, 'Cargo.toml');
|
|
198
|
+
if (hasCargo) {
|
|
199
|
+
languages.push('rust');
|
|
200
|
+
buildTools.push('cargo');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── Ruby ──────────────────────────────────────────────────────────
|
|
204
|
+
const hasGemfile = await fileExists(repoPath, 'Gemfile');
|
|
205
|
+
if (hasGemfile) {
|
|
206
|
+
languages.push('ruby');
|
|
207
|
+
buildTools.push('bundler');
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const content = await fs.readFile(path.join(repoPath, 'Gemfile'), 'utf-8');
|
|
211
|
+
if (content.includes('rails')) {
|
|
212
|
+
frameworks.push('rails');
|
|
213
|
+
projectType = 'backend';
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
// Ignore
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── PHP ───────────────────────────────────────────────────────────
|
|
221
|
+
const hasComposer = await fileExists(repoPath, 'composer.json');
|
|
222
|
+
if (hasComposer) {
|
|
223
|
+
languages.push('php');
|
|
224
|
+
buildTools.push('composer');
|
|
225
|
+
|
|
226
|
+
const composer = await readJson(path.join(repoPath, 'composer.json'));
|
|
227
|
+
if (composer) {
|
|
228
|
+
const req = composer.require as Record<string, string> | undefined;
|
|
229
|
+
if (req?.['laravel/framework']) {
|
|
230
|
+
frameworks.push('laravel');
|
|
231
|
+
projectType = 'backend';
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── Docker ────────────────────────────────────────────────────────
|
|
237
|
+
if (await fileExists(repoPath, 'Dockerfile')) {
|
|
238
|
+
buildTools.push('docker');
|
|
239
|
+
}
|
|
240
|
+
if (await fileExists(repoPath, 'docker-compose.yml') || await fileExists(repoPath, 'compose.yaml')) {
|
|
241
|
+
buildTools.push('docker-compose');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Fallback
|
|
245
|
+
if (languages.length === 0) {
|
|
246
|
+
languages.push('other');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
languages,
|
|
251
|
+
frameworks,
|
|
252
|
+
buildTools,
|
|
253
|
+
projectType,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module commands/create
|
|
3
|
+
* The main "create workspace" command — the core NexusFlow workflow.
|
|
4
|
+
* Mirrors and enhances the user's existing Create-Workspace.ps1 script.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import ora from 'ora';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { execa } from 'execa';
|
|
11
|
+
|
|
12
|
+
import { loadConfig } from '../core/config.js';
|
|
13
|
+
import { scanForRepos } from '../core/scanner.js';
|
|
14
|
+
import { createWorkspace } from '../core/workspace.js';
|
|
15
|
+
import { generateContextFiles } from '../generators/index.js';
|
|
16
|
+
import { analyzeAllRepos } from '../analyzers/index.js';
|
|
17
|
+
import { detectAIAssistants } from '../utils/detect-ai.js';
|
|
18
|
+
import { detectEditors } from '../utils/detect-editors.js';
|
|
19
|
+
import {
|
|
20
|
+
promptBranchName,
|
|
21
|
+
promptDescription,
|
|
22
|
+
promptSelectRepos,
|
|
23
|
+
promptSelectAI,
|
|
24
|
+
promptSelectEditor,
|
|
25
|
+
} from '../utils/prompts.js';
|
|
26
|
+
import type { Feature, WorkspaceContext } from '../types.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Executes the full "create workspace" flow:
|
|
30
|
+
* 1. Prompt for branch name
|
|
31
|
+
* 2. Prompt for feature description
|
|
32
|
+
* 3. Scan for repos and let user pick
|
|
33
|
+
* 4. Detect AI assistants and let user pick
|
|
34
|
+
* 5. Create workspace with git worktrees
|
|
35
|
+
* 6. Generate AI context files
|
|
36
|
+
* 7. Optionally open in editor
|
|
37
|
+
*/
|
|
38
|
+
export async function createCommand(): Promise<void> {
|
|
39
|
+
console.log(
|
|
40
|
+
chalk.bold.cyan('\n🚀 NexusFlow — Create Feature Workspace\n'),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// ── 1. Branch name ──────────────────────────────────────────────────
|
|
44
|
+
const branchName = await promptBranchName();
|
|
45
|
+
|
|
46
|
+
// ── 2. Feature description ──────────────────────────────────────────
|
|
47
|
+
const description = await promptDescription();
|
|
48
|
+
|
|
49
|
+
// ── 3. Scan for repos ───────────────────────────────────────────────
|
|
50
|
+
const config = await loadConfig();
|
|
51
|
+
const spinner = ora('Scanning for projects...').start();
|
|
52
|
+
|
|
53
|
+
let repos;
|
|
54
|
+
try {
|
|
55
|
+
repos = await scanForRepos(config.devDir, config.scanDepth);
|
|
56
|
+
spinner.succeed(`Found ${chalk.bold(repos.length)} projects in ${config.devDir}`);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
spinner.fail('Failed to scan for projects');
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (repos.length === 0) {
|
|
63
|
+
console.log(chalk.yellow('No git projects found. Check your devDir setting.'));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── 4. Select repos ────────────────────────────────────────────────
|
|
68
|
+
const selectedRepos = await promptSelectRepos(repos);
|
|
69
|
+
if (selectedRepos.length === 0) {
|
|
70
|
+
console.log(chalk.yellow('No projects selected. Exiting.'));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log(
|
|
75
|
+
chalk.dim(` Selected: ${selectedRepos.map((r) => r.name).join(', ')}`),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// ── 5. Detect and select AI assistants ──────────────────────────────
|
|
79
|
+
const detectedAI = await detectAIAssistants();
|
|
80
|
+
const selectedAI = await promptSelectAI(detectedAI);
|
|
81
|
+
|
|
82
|
+
// ── 6. Create workspace ─────────────────────────────────────────────
|
|
83
|
+
const workspacePath = path.join(config.workspacesDir, branchName);
|
|
84
|
+
const feature: Feature = {
|
|
85
|
+
id: branchName,
|
|
86
|
+
branchName,
|
|
87
|
+
description,
|
|
88
|
+
repos: selectedRepos.map((r) => r.path),
|
|
89
|
+
assistants: selectedAI,
|
|
90
|
+
workspacePath,
|
|
91
|
+
createdAt: new Date().toISOString(),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const wsSpinner = ora('Creating workspace with git worktrees...').start();
|
|
95
|
+
try {
|
|
96
|
+
await createWorkspace(feature, selectedRepos);
|
|
97
|
+
wsSpinner.succeed(`Workspace created at ${chalk.bold(workspacePath)}`);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
wsSpinner.fail('Failed to create workspace');
|
|
100
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
101
|
+
console.error(chalk.red(` ${message}`));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── 7. Analyze projects ─────────────────────────────────────────────
|
|
106
|
+
console.log(chalk.cyan('\nAnalyzing projects...'));
|
|
107
|
+
const analysis = await analyzeAllRepos(selectedRepos);
|
|
108
|
+
|
|
109
|
+
// ── 8. Generate AI context files ────────────────────────────────────
|
|
110
|
+
const ctx: WorkspaceContext = { feature, repos: selectedRepos, analysis };
|
|
111
|
+
console.log(chalk.cyan('\nGenerating AI context files...'));
|
|
112
|
+
await generateContextFiles(ctx, selectedAI, workspacePath);
|
|
113
|
+
|
|
114
|
+
// ── 8. Open in editor ───────────────────────────────────────────────
|
|
115
|
+
const detectedEditors = await detectEditors();
|
|
116
|
+
const editor = await promptSelectEditor(detectedEditors);
|
|
117
|
+
|
|
118
|
+
if (editor) {
|
|
119
|
+
const editorSpinner = ora(`Opening in ${editor.name}...`).start();
|
|
120
|
+
try {
|
|
121
|
+
await execa(editor.command, [workspacePath], { stdio: 'ignore' });
|
|
122
|
+
editorSpinner.succeed(`Opened in ${editor.name}`);
|
|
123
|
+
} catch {
|
|
124
|
+
editorSpinner.warn(
|
|
125
|
+
`Could not open ${editor.name}. Open manually:\n ${chalk.dim(`cd "${workspacePath}"`)}`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Done ────────────────────────────────────────────────────────────
|
|
131
|
+
console.log(
|
|
132
|
+
chalk.bold.green('\n✅ Workspace ready!\n'),
|
|
133
|
+
);
|
|
134
|
+
console.log(` ${chalk.dim('Path:')} ${workspacePath}`);
|
|
135
|
+
console.log(` ${chalk.dim('Branch:')} ${branchName}`);
|
|
136
|
+
console.log(` ${chalk.dim('Repos:')} ${selectedRepos.map((r) => r.name).join(', ')}`);
|
|
137
|
+
console.log(` ${chalk.dim('AI:')} ${selectedAI.join(', ')}`);
|
|
138
|
+
console.log(
|
|
139
|
+
`\n ${chalk.dim('To navigate:')} cd "${workspacePath}"`,
|
|
140
|
+
);
|
|
141
|
+
console.log();
|
|
142
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module commands/init
|
|
3
|
+
* Initializes the NexusFlow configuration.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { input } from '@inquirer/prompts';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
|
|
11
|
+
import { loadConfig, saveConfig, ensureConfigDir } from '../core/config.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Initializes NexusFlow configuration interactively.
|
|
15
|
+
* Creates ~/.nexusflow/config.json with user preferences.
|
|
16
|
+
*/
|
|
17
|
+
export async function initCommand(): Promise<void> {
|
|
18
|
+
console.log(chalk.bold.cyan('\n⚙️ NexusFlow — Initialize\n'));
|
|
19
|
+
|
|
20
|
+
await ensureConfigDir();
|
|
21
|
+
const existing = await loadConfig();
|
|
22
|
+
|
|
23
|
+
const devDir = await input({
|
|
24
|
+
message: 'Development directory (where your repos live):',
|
|
25
|
+
default: existing.devDir || path.join(os.homedir(), 'dev'),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const workspacesDir = await input({
|
|
29
|
+
message: 'Workspaces directory (where feature workspaces are created):',
|
|
30
|
+
default: existing.workspacesDir || path.join(devDir, 'workspaces'),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const scanDepth = await input({
|
|
34
|
+
message: 'How deep to scan for repos (directory levels):',
|
|
35
|
+
default: String(existing.scanDepth || 2),
|
|
36
|
+
validate: (value: string) => {
|
|
37
|
+
const num = parseInt(value, 10);
|
|
38
|
+
if (isNaN(num) || num < 1 || num > 10) return 'Enter a number between 1 and 10';
|
|
39
|
+
return true;
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const config = {
|
|
44
|
+
...existing,
|
|
45
|
+
devDir: devDir.trim(),
|
|
46
|
+
workspacesDir: workspacesDir.trim(),
|
|
47
|
+
scanDepth: parseInt(scanDepth, 10),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
await saveConfig(config);
|
|
51
|
+
|
|
52
|
+
console.log(chalk.green('\n✅ Configuration saved!\n'));
|
|
53
|
+
console.log(chalk.dim(' Config file: ~/.nexusflow/config.json'));
|
|
54
|
+
console.log(chalk.dim(` Dev dir: ${config.devDir}`));
|
|
55
|
+
console.log(chalk.dim(` Workspaces: ${config.workspacesDir}`));
|
|
56
|
+
console.log(chalk.dim(` Scan depth: ${config.scanDepth}`));
|
|
57
|
+
console.log(chalk.dim('\n Run "nexusflow create" to create your first workspace.\n'));
|
|
58
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module commands/list
|
|
3
|
+
* Lists all existing NexusFlow workspaces.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
import { loadConfig } from '../core/config.js';
|
|
9
|
+
import { listWorkspaces } from '../core/workspace.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Lists all existing workspaces, showing feature name, repos, and status.
|
|
13
|
+
*/
|
|
14
|
+
export async function listCommand(): Promise<void> {
|
|
15
|
+
const config = await loadConfig();
|
|
16
|
+
const workspaces = await listWorkspaces(config.workspacesDir);
|
|
17
|
+
|
|
18
|
+
if (workspaces.length === 0) {
|
|
19
|
+
console.log(chalk.yellow('\nNo workspaces found.'));
|
|
20
|
+
console.log(chalk.dim(` Workspaces directory: ${config.workspacesDir}`));
|
|
21
|
+
console.log(chalk.dim(' Run "nexusflow create" to create your first workspace.\n'));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log(chalk.bold.cyan(`\n📂 NexusFlow Workspaces (${workspaces.length})\n`));
|
|
26
|
+
|
|
27
|
+
for (const ws of workspaces) {
|
|
28
|
+
const repoCount = ws.repos.length;
|
|
29
|
+
const aiList = ws.assistants.join(', ');
|
|
30
|
+
const date = new Date(ws.createdAt).toLocaleDateString();
|
|
31
|
+
|
|
32
|
+
console.log(
|
|
33
|
+
` ${chalk.bold(ws.branchName)} ${chalk.dim(`(${date})`)}`,
|
|
34
|
+
);
|
|
35
|
+
console.log(
|
|
36
|
+
` ${chalk.dim('Repos:')} ${repoCount} project${repoCount !== 1 ? 's' : ''}`,
|
|
37
|
+
);
|
|
38
|
+
console.log(
|
|
39
|
+
` ${chalk.dim('AI:')} ${aiList || 'none'}`,
|
|
40
|
+
);
|
|
41
|
+
console.log(
|
|
42
|
+
` ${chalk.dim('Path:')} ${ws.workspacePath}`,
|
|
43
|
+
);
|
|
44
|
+
console.log();
|
|
45
|
+
}
|
|
46
|
+
}
|