@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,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module core/workspace
|
|
3
|
+
* Creates and manages NexusFlow workspaces — directories that group
|
|
4
|
+
* worktrees for a multi-repo feature together with a `nexusflow.json`
|
|
5
|
+
* manifest.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'node:fs/promises';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
|
|
11
|
+
import type { Feature, RepoInfo } from '../types.js';
|
|
12
|
+
import { createWorktree } from './worktree.js';
|
|
13
|
+
|
|
14
|
+
/** Name of the per-workspace manifest file. */
|
|
15
|
+
const MANIFEST_FILE = 'nexusflow.json';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Derives the workspace directory path for a given branch name.
|
|
19
|
+
*
|
|
20
|
+
* @param workspacesDir - The root workspaces directory (e.g. ~/dev/workspaces).
|
|
21
|
+
* @param branchName - The feature branch name used as the workspace folder.
|
|
22
|
+
* @returns Absolute path to the workspace directory.
|
|
23
|
+
*/
|
|
24
|
+
export function getWorkspacePath(
|
|
25
|
+
workspacesDir: string,
|
|
26
|
+
branchName: string,
|
|
27
|
+
): string {
|
|
28
|
+
return path.join(workspacesDir, branchName);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates a full workspace for a feature:
|
|
33
|
+
* 1. Creates the workspace directory.
|
|
34
|
+
* 2. Creates a git worktree for every repo in the feature.
|
|
35
|
+
* 3. Saves the feature manifest (`nexusflow.json`).
|
|
36
|
+
*
|
|
37
|
+
* @param feature - The feature definition.
|
|
38
|
+
* @param repos - Resolved repo metadata for every repo in the feature.
|
|
39
|
+
* @returns The absolute path to the newly created workspace.
|
|
40
|
+
*/
|
|
41
|
+
export async function createWorkspace(
|
|
42
|
+
feature: Feature,
|
|
43
|
+
repos: RepoInfo[],
|
|
44
|
+
): Promise<string> {
|
|
45
|
+
const workspacePath = feature.workspacePath;
|
|
46
|
+
|
|
47
|
+
// Ensure the workspace directory exists.
|
|
48
|
+
await fs.mkdir(workspacePath, { recursive: true });
|
|
49
|
+
|
|
50
|
+
// Create a worktree for each repo inside the workspace.
|
|
51
|
+
for (const repo of repos) {
|
|
52
|
+
const worktreeTarget = path.join(workspacePath, repo.name);
|
|
53
|
+
await createWorktree(
|
|
54
|
+
repo.path,
|
|
55
|
+
worktreeTarget,
|
|
56
|
+
feature.branchName,
|
|
57
|
+
repo.defaultBranch,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Persist the feature manifest.
|
|
62
|
+
await saveFeatureConfig(workspacePath, feature);
|
|
63
|
+
|
|
64
|
+
return workspacePath;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Lists all existing workspaces by reading `nexusflow.json` manifests from
|
|
69
|
+
* each subdirectory of {@link workspacesDir}.
|
|
70
|
+
*
|
|
71
|
+
* Directories that do not contain a valid manifest are silently skipped.
|
|
72
|
+
*
|
|
73
|
+
* @param workspacesDir - The root workspaces directory.
|
|
74
|
+
* @returns An array of {@link Feature} objects for each discovered workspace.
|
|
75
|
+
*/
|
|
76
|
+
export async function listWorkspaces(
|
|
77
|
+
workspacesDir: string,
|
|
78
|
+
): Promise<Feature[]> {
|
|
79
|
+
const features: Feature[] = [];
|
|
80
|
+
|
|
81
|
+
async function scan(dir: string, depth: number) {
|
|
82
|
+
if (depth > 3) return;
|
|
83
|
+
|
|
84
|
+
let entries: import('node:fs').Dirent[];
|
|
85
|
+
try {
|
|
86
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
87
|
+
} catch {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check if current directory contains a nexusflow.json manifest
|
|
92
|
+
const hasManifest = entries.some(
|
|
93
|
+
(e) => e.isFile() && e.name === MANIFEST_FILE,
|
|
94
|
+
);
|
|
95
|
+
if (hasManifest) {
|
|
96
|
+
const loaded = await loadFeatureConfig(dir);
|
|
97
|
+
if (loaded) {
|
|
98
|
+
features.push(loaded);
|
|
99
|
+
return; // Workspaces do not nest.
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Otherwise, recursively scan subdirectories
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
if (entry.isDirectory()) {
|
|
106
|
+
await scan(path.join(dir, entry.name), depth + 1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await scan(workspacesDir, 1);
|
|
112
|
+
return features;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Saves a {@link Feature} as `nexusflow.json` inside the given workspace.
|
|
117
|
+
*
|
|
118
|
+
* @param workspacePath - Absolute path to the workspace directory.
|
|
119
|
+
* @param feature - The feature definition to persist.
|
|
120
|
+
*/
|
|
121
|
+
export async function saveFeatureConfig(
|
|
122
|
+
workspacePath: string,
|
|
123
|
+
feature: Feature,
|
|
124
|
+
): Promise<void> {
|
|
125
|
+
const manifestPath = path.join(workspacePath, MANIFEST_FILE);
|
|
126
|
+
const data = JSON.stringify(feature, null, 2) + '\n';
|
|
127
|
+
await fs.writeFile(manifestPath, data, 'utf-8');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Loads a {@link Feature} from the `nexusflow.json` manifest inside a
|
|
132
|
+
* workspace directory.
|
|
133
|
+
*
|
|
134
|
+
* @param workspacePath - Absolute path to the workspace directory.
|
|
135
|
+
* @returns The loaded feature, or `null` if the manifest doesn't exist or is
|
|
136
|
+
* invalid.
|
|
137
|
+
*/
|
|
138
|
+
export async function loadFeatureConfig(
|
|
139
|
+
workspacePath: string,
|
|
140
|
+
): Promise<Feature | null> {
|
|
141
|
+
const manifestPath = path.join(workspacePath, MANIFEST_FILE);
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const raw = await fs.readFile(manifestPath, 'utf-8');
|
|
145
|
+
return JSON.parse(raw) as Feature;
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module core/worktree
|
|
3
|
+
* Git worktree operations — create and remove worktrees for feature branches.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execa } from 'execa';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates a new git worktree for a feature branch.
|
|
10
|
+
*
|
|
11
|
+
* Runs:
|
|
12
|
+
* 1. `git fetch origin` — ensure we have the latest remote refs.
|
|
13
|
+
* 2. `git worktree add <targetPath> -b <branchName> origin/<baseBranch>`
|
|
14
|
+
*
|
|
15
|
+
* @param repoPath - Absolute path to the main repo checkout.
|
|
16
|
+
* @param targetPath - Absolute path where the worktree should be created.
|
|
17
|
+
* @param branchName - Name of the new local branch to create.
|
|
18
|
+
* @param baseBranch - Remote branch to base the new branch on (e.g. 'main').
|
|
19
|
+
*/
|
|
20
|
+
export async function createWorktree(
|
|
21
|
+
repoPath: string,
|
|
22
|
+
targetPath: string,
|
|
23
|
+
branchName: string,
|
|
24
|
+
baseBranch: string,
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
// Fetch latest remote state.
|
|
27
|
+
await execa('git', ['fetch', 'origin'], { cwd: repoPath });
|
|
28
|
+
|
|
29
|
+
// Create the worktree with a new branch based on the remote base branch.
|
|
30
|
+
await execa(
|
|
31
|
+
'git',
|
|
32
|
+
['worktree', 'add', targetPath, '-b', branchName, `origin/${baseBranch}`],
|
|
33
|
+
{ cwd: repoPath },
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Removes an existing git worktree.
|
|
39
|
+
*
|
|
40
|
+
* Runs `git worktree remove <worktreePath>` from the main repo.
|
|
41
|
+
*
|
|
42
|
+
* @param repoPath - Absolute path to the main repo checkout.
|
|
43
|
+
* @param worktreePath - Absolute path to the worktree to remove.
|
|
44
|
+
*/
|
|
45
|
+
export async function removeWorktree(
|
|
46
|
+
repoPath: string,
|
|
47
|
+
worktreePath: string,
|
|
48
|
+
): Promise<void> {
|
|
49
|
+
await execa('git', ['worktree', 'remove', worktreePath], { cwd: repoPath });
|
|
50
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module generators/base
|
|
3
|
+
* Builds the shared markdown context content that all AI assistant generators
|
|
4
|
+
* use as their foundation. Now includes rich project analysis data when available.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { WorkspaceContext, ProjectAnalysis } from '../types.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Formats a ProjectAnalysis into a readable markdown section.
|
|
11
|
+
*/
|
|
12
|
+
function formatProjectSection(analysis: ProjectAnalysis): string {
|
|
13
|
+
const lines: string[] = [];
|
|
14
|
+
|
|
15
|
+
lines.push(`### ${analysis.name}`);
|
|
16
|
+
|
|
17
|
+
// Tech stack
|
|
18
|
+
const { techStack } = analysis;
|
|
19
|
+
if (techStack.languages.length > 0 && techStack.languages[0] !== 'other') {
|
|
20
|
+
const langStr = techStack.languages.join(', ');
|
|
21
|
+
const fwStr = techStack.frameworks.length > 0 ? techStack.frameworks.join(', ') : 'none detected';
|
|
22
|
+
const buildStr = techStack.buildTools.length > 0 ? techStack.buildTools.join(', ') : 'none detected';
|
|
23
|
+
|
|
24
|
+
lines.push(`- **Type**: ${techStack.projectType}`);
|
|
25
|
+
lines.push(`- **Languages**: ${langStr}`);
|
|
26
|
+
lines.push(`- **Frameworks**: ${fwStr}`);
|
|
27
|
+
lines.push(`- **Build tools**: ${buildStr}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// README summary
|
|
31
|
+
if (analysis.readmeSummary) {
|
|
32
|
+
// Take first 200 chars of README summary as purpose
|
|
33
|
+
const purpose = analysis.readmeSummary
|
|
34
|
+
.replace(/^#\s+.+\n?/, '') // Remove h1 title
|
|
35
|
+
.trim()
|
|
36
|
+
.slice(0, 200)
|
|
37
|
+
.trim();
|
|
38
|
+
if (purpose) {
|
|
39
|
+
lines.push(`- **Purpose**: ${purpose}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// API endpoints
|
|
44
|
+
if (analysis.endpoints.length > 0) {
|
|
45
|
+
lines.push(`- **API endpoints** (${analysis.endpoints.length} detected):`);
|
|
46
|
+
// Show up to 10 endpoints
|
|
47
|
+
const shown = analysis.endpoints.slice(0, 10);
|
|
48
|
+
for (const ep of shown) {
|
|
49
|
+
lines.push(` - \`${ep.method} ${ep.path}\``);
|
|
50
|
+
}
|
|
51
|
+
if (analysis.endpoints.length > 10) {
|
|
52
|
+
lines.push(` - _...and ${analysis.endpoints.length - 10} more_`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Ports
|
|
57
|
+
if (analysis.ports.length > 0) {
|
|
58
|
+
const portStr = analysis.ports.map((p) => `${p.port} (${p.protocol}, from ${p.source})`).join(', ');
|
|
59
|
+
lines.push(`- **Ports**: ${portStr}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Path
|
|
63
|
+
lines.push(`- **Path**: \`${analysis.path}\``);
|
|
64
|
+
|
|
65
|
+
return lines.join('\n');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Builds the shared markdown context content that all AI assistant generators
|
|
70
|
+
* use as their foundation. Includes analysis data if available.
|
|
71
|
+
*
|
|
72
|
+
* @param ctx - The workspace context containing feature metadata, repo list, and optional analysis.
|
|
73
|
+
* @returns A markdown string with feature details, repo listing, and task instructions.
|
|
74
|
+
*/
|
|
75
|
+
export function buildContextContent(ctx: WorkspaceContext): string {
|
|
76
|
+
const { feature, repos, analysis } = ctx;
|
|
77
|
+
|
|
78
|
+
// Build project sections — rich if analysis is available, simple if not
|
|
79
|
+
let projectSections: string;
|
|
80
|
+
|
|
81
|
+
if (analysis && analysis.size > 0) {
|
|
82
|
+
const sections = repos.map((r) => {
|
|
83
|
+
const a = analysis.get(r.path);
|
|
84
|
+
if (a) return formatProjectSection(a);
|
|
85
|
+
return `### ${r.name}\n- **Path**: \`${r.path}\``;
|
|
86
|
+
});
|
|
87
|
+
projectSections = sections.join('\n\n');
|
|
88
|
+
} else {
|
|
89
|
+
projectSections = repos
|
|
90
|
+
.map((r) => `- **${r.name}** — \`${r.path}\` (default branch: \`${r.defaultBranch}\`)`)
|
|
91
|
+
.join('\n');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Build existing AI configs section
|
|
95
|
+
let existingConfigsSection = '';
|
|
96
|
+
if (analysis && analysis.size > 0) {
|
|
97
|
+
const allConfigs: string[] = [];
|
|
98
|
+
for (const [, a] of analysis) {
|
|
99
|
+
for (const config of a.existingAIConfigs) {
|
|
100
|
+
allConfigs.push(`- **${a.name}** has \`${config.relativePath}\` (${config.assistant})`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (allConfigs.length > 0) {
|
|
104
|
+
existingConfigsSection = `
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Existing AI Configurations
|
|
108
|
+
|
|
109
|
+
The following repos already have AI assistant configuration files.
|
|
110
|
+
Incorporate their instructions when working in those repos:
|
|
111
|
+
|
|
112
|
+
${allConfigs.join('\n')}
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Resumption commands section
|
|
118
|
+
let resumptionSection = '';
|
|
119
|
+
if (feature.resumption) {
|
|
120
|
+
const { testCommand, mockCommand, startCommand } = feature.resumption;
|
|
121
|
+
const parts: string[] = [];
|
|
122
|
+
if (mockCommand) parts.push(`- **Setup/Mock Command**: \`${mockCommand}\``);
|
|
123
|
+
if (startCommand) parts.push(`- **Start/Run Command**: \`${startCommand}\``);
|
|
124
|
+
if (testCommand) parts.push(`- **Verification/Test Command**: \`${testCommand}\``);
|
|
125
|
+
|
|
126
|
+
if (parts.length > 0) {
|
|
127
|
+
resumptionSection = `
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Workspace Resumption & Verification Commands
|
|
131
|
+
|
|
132
|
+
Use these pre-configured commands to spin up mocks, run background services, and verify your changes:
|
|
133
|
+
|
|
134
|
+
${parts.join('\n')}
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return `# Multi-Repo Workspace Context
|
|
140
|
+
|
|
141
|
+
## Feature: ${feature.id}
|
|
142
|
+
|
|
143
|
+
**Description:** ${feature.description}
|
|
144
|
+
|
|
145
|
+
**Branch:** \`${feature.branchName}\`
|
|
146
|
+
**Created:** ${feature.createdAt}
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Projects
|
|
151
|
+
|
|
152
|
+
This workspace contains the following projects, each checked out as a
|
|
153
|
+
git worktree on the feature branch:
|
|
154
|
+
|
|
155
|
+
${projectSections}
|
|
156
|
+
${existingConfigsSection}
|
|
157
|
+
${resumptionSection}
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Task & Step-by-Step Initialization
|
|
161
|
+
|
|
162
|
+
Your very first task upon entering this workspace is to analyze the codebase and document it in a universal reference file:
|
|
163
|
+
|
|
164
|
+
1. **Create \`nexusflow-overview.md\`** at the workspace root.
|
|
165
|
+
2. **Project Assumptions**: For each project, write down a clear assumption of what it does, its primary tech stack, and its core responsibilities.
|
|
166
|
+
3. **Inter-Repo Relationships**: Document how the repos relate:
|
|
167
|
+
- Shared libraries/packages (producers and consumers).
|
|
168
|
+
- API boundaries (which repos expose APIs, which ones consume them).
|
|
169
|
+
- Data flows and dependencies.
|
|
170
|
+
4. **Clarifying Questions**: If any feature requirements, architectural patterns, or API contracts are unclear, list them explicitly under a section called **"Clarifying Questions for the User"**.
|
|
171
|
+
5. **Universal Reference**: Keep this file updated. This acts as a universal reference so that any LLM assistant (Claude, Antigravity, Codex, Cursor, Copilot) joining this workspace instantly understands the project landscape.
|
|
172
|
+
|
|
173
|
+
Once you have created \`nexusflow-overview.md\` and compiled your questions, ask the user to verify your assumptions and answer your questions before proceeding to write code.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Guidelines
|
|
178
|
+
|
|
179
|
+
- Read each project's existing \`README.md\` and any doc files before proposing changes.
|
|
180
|
+
- When modifying a shared library, check every downstream consumer for breakage.
|
|
181
|
+
- Prefer small, focused commits that touch one repo at a time when possible.
|
|
182
|
+
- If a change must span repos, describe the ordering and any migration steps.
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
`;
|
|
187
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module generators/claude
|
|
3
|
+
* Generates a CLAUDE.md file for Claude Code / Antigravity.
|
|
4
|
+
* Now uses the shared base content builder which includes analysis data.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import fse from 'fs-extra';
|
|
9
|
+
import type { WorkspaceContext } from '../types.js';
|
|
10
|
+
import { buildContextContent } from './base.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generates a `CLAUDE.md` file at the workspace root.
|
|
14
|
+
*
|
|
15
|
+
* Claude Code reads this file automatically when opened in a directory,
|
|
16
|
+
* so it is the primary way to give Claude long-lived project context.
|
|
17
|
+
*
|
|
18
|
+
* @param ctx - The workspace context (feature + repos + analysis).
|
|
19
|
+
* @param workspacePath - Absolute path to the workspace root directory.
|
|
20
|
+
*/
|
|
21
|
+
export async function generateClaudeConfig(
|
|
22
|
+
ctx: WorkspaceContext,
|
|
23
|
+
workspacePath: string,
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
const content = buildContextContent(ctx);
|
|
26
|
+
const filePath = path.join(workspacePath, 'CLAUDE.md');
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
await fse.writeFile(filePath, content, 'utf-8');
|
|
30
|
+
} catch (error) {
|
|
31
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
32
|
+
throw new Error(`Failed to write CLAUDE.md: ${message}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module generators/codex
|
|
3
|
+
* Generates an AGENTS.md file for OpenAI Codex.
|
|
4
|
+
* Uses the shared base content and adds Codex-specific sections.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import fse from 'fs-extra';
|
|
9
|
+
import type { WorkspaceContext } from '../types.js';
|
|
10
|
+
import { buildContextContent } from './base.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generates an `AGENTS.md` file at the workspace root.
|
|
14
|
+
*
|
|
15
|
+
* Codex reads AGENTS.md for persistent context about the workspace.
|
|
16
|
+
*
|
|
17
|
+
* @param ctx - The workspace context (feature + repos + analysis).
|
|
18
|
+
* @param workspacePath - Absolute path to the workspace root directory.
|
|
19
|
+
*/
|
|
20
|
+
export async function generateCodexConfig(
|
|
21
|
+
ctx: WorkspaceContext,
|
|
22
|
+
workspacePath: string,
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
const baseContent = buildContextContent(ctx);
|
|
25
|
+
|
|
26
|
+
const codexExtra = `
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Codex-Specific Notes
|
|
30
|
+
|
|
31
|
+
- Each project subdirectory may contain its own \`AGENTS.md\` or
|
|
32
|
+
\`AGENTS.override.md\` with module-specific context.
|
|
33
|
+
- When working in a subdirectory, check for local overrides before
|
|
34
|
+
applying workspace-level guidance.
|
|
35
|
+
- Use \`codex --approval-mode suggest\` for cross-repo changes to
|
|
36
|
+
review each change before applying.
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
const content = baseContent + codexExtra;
|
|
40
|
+
const filePath = path.join(workspacePath, 'AGENTS.md');
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await fse.writeFile(filePath, content, 'utf-8');
|
|
44
|
+
} catch (error) {
|
|
45
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
46
|
+
throw new Error(`Failed to write AGENTS.md: ${message}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module generators/copilot
|
|
3
|
+
* Generates a .github/copilot-instructions.md file for GitHub Copilot.
|
|
4
|
+
* Uses the shared base content and adds Copilot-specific sections.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import fse from 'fs-extra';
|
|
9
|
+
import type { WorkspaceContext } from '../types.js';
|
|
10
|
+
import { buildContextContent } from './base.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generates a `.github/copilot-instructions.md` file at the workspace root.
|
|
14
|
+
*
|
|
15
|
+
* Copilot reads this file for repository-level guidance in Chat and Agent modes.
|
|
16
|
+
*
|
|
17
|
+
* @param ctx - The workspace context (feature + repos + analysis).
|
|
18
|
+
* @param workspacePath - Absolute path to the workspace root directory.
|
|
19
|
+
*/
|
|
20
|
+
export async function generateCopilotConfig(
|
|
21
|
+
ctx: WorkspaceContext,
|
|
22
|
+
workspacePath: string,
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
const baseContent = buildContextContent(ctx);
|
|
25
|
+
|
|
26
|
+
const copilotExtra = `
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Copilot-Specific Notes
|
|
30
|
+
|
|
31
|
+
- Path-scoped instructions can be added in \`.github/instructions/\`
|
|
32
|
+
using \`*.instructions.md\` files with YAML frontmatter to target
|
|
33
|
+
specific file patterns.
|
|
34
|
+
- Example path-scoped file:
|
|
35
|
+
\`\`\`markdown
|
|
36
|
+
---
|
|
37
|
+
applyTo:
|
|
38
|
+
- src/frontend/**
|
|
39
|
+
---
|
|
40
|
+
# Frontend Guidelines
|
|
41
|
+
- Use TypeScript strict mode.
|
|
42
|
+
\`\`\`
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
const content = baseContent + copilotExtra;
|
|
46
|
+
const githubDir = path.join(workspacePath, '.github');
|
|
47
|
+
const filePath = path.join(githubDir, 'copilot-instructions.md');
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
await fse.ensureDir(githubDir);
|
|
51
|
+
await fse.writeFile(filePath, content, 'utf-8');
|
|
52
|
+
} catch (error) {
|
|
53
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
54
|
+
throw new Error(`Failed to write copilot-instructions.md: ${message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module generators/cursor
|
|
3
|
+
* Generates a .cursor/rules/nexusflow.mdc file for Cursor.
|
|
4
|
+
* Uses the shared base content with Cursor-specific YAML frontmatter.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import fse from 'fs-extra';
|
|
9
|
+
import type { WorkspaceContext } from '../types.js';
|
|
10
|
+
import { buildContextContent } from './base.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generates a `.cursor/rules/nexusflow.mdc` file at the workspace root.
|
|
14
|
+
*
|
|
15
|
+
* Cursor reads `.mdc` rule files from `.cursor/rules/` for project-wide
|
|
16
|
+
* instructions. The YAML frontmatter controls when the rule is applied.
|
|
17
|
+
*
|
|
18
|
+
* @param ctx - The workspace context (feature + repos + analysis).
|
|
19
|
+
* @param workspacePath - Absolute path to the workspace root directory.
|
|
20
|
+
*/
|
|
21
|
+
export async function generateCursorConfig(
|
|
22
|
+
ctx: WorkspaceContext,
|
|
23
|
+
workspacePath: string,
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
const baseContent = buildContextContent(ctx);
|
|
26
|
+
|
|
27
|
+
const content = `---
|
|
28
|
+
description: "NexusFlow workspace context for multi-repo feature development"
|
|
29
|
+
alwaysApply: true
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
${baseContent}`;
|
|
33
|
+
|
|
34
|
+
const rulesDir = path.join(workspacePath, '.cursor', 'rules');
|
|
35
|
+
const filePath = path.join(rulesDir, 'nexusflow.mdc');
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
await fse.ensureDir(rulesDir);
|
|
39
|
+
await fse.writeFile(filePath, content, 'utf-8');
|
|
40
|
+
} catch (error) {
|
|
41
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
42
|
+
throw new Error(`Failed to write nexusflow.mdc: ${message}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fse from 'fs-extra';
|
|
4
|
+
import type { AIAssistant, WorkspaceContext } from '../types.js';
|
|
5
|
+
import { generateClaudeConfig } from './claude.js';
|
|
6
|
+
import { generateCodexConfig } from './codex.js';
|
|
7
|
+
import { generateCopilotConfig } from './copilot.js';
|
|
8
|
+
import { generateCursorConfig } from './cursor.js';
|
|
9
|
+
import { buildContextContent } from './base.js';
|
|
10
|
+
|
|
11
|
+
/** Maps each assistant to its generator function and the file it produces. */
|
|
12
|
+
const GENERATORS: Record<
|
|
13
|
+
AIAssistant,
|
|
14
|
+
{
|
|
15
|
+
generate: (ctx: WorkspaceContext, workspacePath: string) => Promise<void>;
|
|
16
|
+
outputFile: string;
|
|
17
|
+
}
|
|
18
|
+
> = {
|
|
19
|
+
claude: { generate: generateClaudeConfig, outputFile: 'CLAUDE.md' },
|
|
20
|
+
antigravity: { generate: generateClaudeConfig, outputFile: 'CLAUDE.md' },
|
|
21
|
+
codex: { generate: generateCodexConfig, outputFile: 'AGENTS.md' },
|
|
22
|
+
copilot: { generate: generateCopilotConfig, outputFile: '.github/copilot-instructions.md' },
|
|
23
|
+
cursor: { generate: generateCursorConfig, outputFile: '.cursor/rules/nexusflow.mdc' },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generates AI context files for each of the selected assistants.
|
|
28
|
+
*
|
|
29
|
+
* Iterates over the provided assistant list, invokes the corresponding
|
|
30
|
+
* generator, and logs success / failure for each one.
|
|
31
|
+
*
|
|
32
|
+
* @param ctx - The workspace context (feature + repos).
|
|
33
|
+
* @param assistants - Which AI assistants to generate context files for.
|
|
34
|
+
* @param workspacePath - Absolute path to the workspace root directory.
|
|
35
|
+
*/
|
|
36
|
+
export async function generateContextFiles(
|
|
37
|
+
ctx: WorkspaceContext,
|
|
38
|
+
assistants: AIAssistant[],
|
|
39
|
+
workspacePath: string,
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
// Always generate a universal WORKSPACE.md at the workspace root
|
|
42
|
+
try {
|
|
43
|
+
const content = buildContextContent(ctx);
|
|
44
|
+
const filePath = path.join(workspacePath, 'WORKSPACE.md');
|
|
45
|
+
await fse.writeFile(filePath, content, 'utf-8');
|
|
46
|
+
console.log(
|
|
47
|
+
chalk.green(' ✔'),
|
|
48
|
+
`Generated universal ${chalk.bold('WORKSPACE.md')}`,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Generate session.md template if it does not exist
|
|
52
|
+
const sessionPath = path.join(workspacePath, 'session.md');
|
|
53
|
+
if (!(await fse.pathExists(sessionPath))) {
|
|
54
|
+
const sessionContent = `# AI Session Handover Memo
|
|
55
|
+
|
|
56
|
+
- **Feature Branch**: \`${ctx.feature.branchName}\`
|
|
57
|
+
- **Session Started**: ${new Date().toLocaleDateString()}
|
|
58
|
+
- **Last Modified Files**: None (Start of session)
|
|
59
|
+
|
|
60
|
+
## Summary of Accomplished Work
|
|
61
|
+
- Setup workspace with ${ctx.repos.length} mapped repositories.
|
|
62
|
+
|
|
63
|
+
## Active State & Blockers
|
|
64
|
+
- No active blockers.
|
|
65
|
+
|
|
66
|
+
## Next Steps
|
|
67
|
+
1. Open this workspace in your selected editor.
|
|
68
|
+
2. Ask the assistant to read \`WORKSPACE.md\` and \`session.md\` to get started.
|
|
69
|
+
`;
|
|
70
|
+
await fse.writeFile(sessionPath, sessionContent, 'utf-8');
|
|
71
|
+
console.log(chalk.green(' ✔'), `Generated initial session.md`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Generate plan.md template if it does not exist
|
|
75
|
+
const planPath = path.join(workspacePath, 'plan.md');
|
|
76
|
+
if (!(await fse.pathExists(planPath))) {
|
|
77
|
+
const planContent = `# Feature Development Plan
|
|
78
|
+
|
|
79
|
+
## Checklist
|
|
80
|
+
- [ ] Read \`WORKSPACE.md\` and analyze the codebase structure.
|
|
81
|
+
- [ ] Initialize context by creating \`nexusflow-overview.md\` outlining assumptions and raising clarifying questions.
|
|
82
|
+
- [ ] Review user's responses to clarifying questions.
|
|
83
|
+
- [ ] Implement feature logic.
|
|
84
|
+
- [ ] Verify build and run tests.
|
|
85
|
+
- [ ] Compile final changes in the session handover memo.
|
|
86
|
+
`;
|
|
87
|
+
await fse.writeFile(planPath, planContent, 'utf-8');
|
|
88
|
+
console.log(chalk.green(' ✔'), `Generated initial plan.md`);
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
92
|
+
console.error(
|
|
93
|
+
chalk.red(' ✖'),
|
|
94
|
+
`Failed to generate universal context files: ${message}`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const assistant of assistants) {
|
|
99
|
+
const entry = GENERATORS[assistant];
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
await entry.generate(ctx, workspacePath);
|
|
103
|
+
console.log(
|
|
104
|
+
chalk.green(' ✔'),
|
|
105
|
+
`Generated ${chalk.bold(entry.outputFile)} for ${assistant}`,
|
|
106
|
+
);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
109
|
+
console.error(
|
|
110
|
+
chalk.red(' ✖'),
|
|
111
|
+
`Failed to generate ${entry.outputFile} for ${assistant}: ${message}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Re-export individual generators for direct use
|
|
118
|
+
export { generateClaudeConfig } from './claude.js';
|
|
119
|
+
export { generateCodexConfig } from './codex.js';
|
|
120
|
+
export { generateCopilotConfig } from './copilot.js';
|
|
121
|
+
export { generateCursorConfig } from './cursor.js';
|
|
122
|
+
export { buildContextContent } from './base.js';
|