@mrpatronz/nexusflow 0.1.9 → 0.1.11
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/.github/dependabot.yml +27 -0
- package/.github/workflows/release.yml +3 -0
- package/dist/commands/add-repo.d.ts +12 -0
- package/dist/commands/add-repo.d.ts.map +1 -0
- package/dist/commands/add-repo.js +126 -0
- package/dist/commands/add-repo.js.map +1 -0
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +6 -2
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/remove.d.ts +11 -0
- package/dist/commands/remove.d.ts.map +1 -0
- package/dist/commands/remove.js +82 -0
- package/dist/commands/remove.js.map +1 -0
- package/dist/core/workspace.d.ts +20 -0
- package/dist/core/workspace.d.ts.map +1 -1
- package/dist/core/workspace.js +131 -1
- package/dist/core/workspace.js.map +1 -1
- package/dist/core/worktree.d.ts +3 -2
- package/dist/core/worktree.d.ts.map +1 -1
- package/dist/core/worktree.js +9 -3
- package/dist/core/worktree.js.map +1 -1
- package/dist/generators/base.d.ts.map +1 -1
- package/dist/generators/base.js +9 -0
- package/dist/generators/base.js.map +1 -1
- package/dist/gui/assets/index-C8W6FoPa.js +21 -0
- package/dist/gui/assets/index-CJn2LW8K.css +2 -0
- package/dist/gui/index.html +2 -2
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +76 -4
- package/dist/server.js.map +1 -1
- package/dist/utils/update-check.d.ts +11 -0
- package/dist/utils/update-check.d.ts.map +1 -1
- package/dist/utils/update-check.js +105 -0
- package/dist/utils/update-check.js.map +1 -1
- package/extension/package-lock.json +8 -8
- package/extension/package.json +1 -1
- package/gui/src/App.tsx +268 -6
- package/gui/src/features/workspace/WorkspaceList.tsx +58 -8
- package/package.json +1 -1
- package/src/commands/add-repo.ts +141 -0
- package/src/commands/create.ts +6 -2
- package/src/commands/remove.ts +90 -0
- package/src/core/workspace.ts +146 -2
- package/src/core/worktree.ts +10 -2
- package/src/generators/base.ts +9 -0
- package/src/index.ts +39 -0
- package/src/server.ts +79 -4
- package/src/utils/update-check.ts +116 -0
- package/dist/gui/assets/index-7IR7ZrSG.css +0 -2
- package/dist/gui/assets/index-BfNp4LNj.js +0 -21
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module commands/add-repo
|
|
3
|
+
* Adds a repository to an existing NexusFlow workspace.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import { select } from '@inquirer/prompts';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
|
|
11
|
+
import { loadConfig } from '../core/config.js';
|
|
12
|
+
import { scanForRepos } from '../core/scanner.js';
|
|
13
|
+
import { listWorkspaces, loadFeatureConfig, addRepoToWorkspace } from '../core/workspace.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Executes the add-repo command.
|
|
17
|
+
*
|
|
18
|
+
* @param repoPathArg - Optional repository path to add.
|
|
19
|
+
* @param workspaceArg - Optional workspace path or name.
|
|
20
|
+
*/
|
|
21
|
+
export async function addRepoCommand(
|
|
22
|
+
repoPathArg?: string,
|
|
23
|
+
workspaceArg?: string,
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
console.log(chalk.bold.cyan('\n➕ NexusFlow — Adding Repository to Workspace\n'));
|
|
26
|
+
|
|
27
|
+
const config = await loadConfig();
|
|
28
|
+
|
|
29
|
+
// 1. Resolve workspace
|
|
30
|
+
let workspacePath: string | null = null;
|
|
31
|
+
let workspaceName = '';
|
|
32
|
+
|
|
33
|
+
if (workspaceArg) {
|
|
34
|
+
const resolvedPath = path.isAbsolute(workspaceArg)
|
|
35
|
+
? workspaceArg
|
|
36
|
+
: path.resolve(config.workspacesDir, workspaceArg);
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const manifest = await loadFeatureConfig(resolvedPath);
|
|
40
|
+
if (manifest) {
|
|
41
|
+
workspacePath = resolvedPath;
|
|
42
|
+
workspaceName = manifest.branchName;
|
|
43
|
+
}
|
|
44
|
+
} catch {}
|
|
45
|
+
|
|
46
|
+
if (!workspacePath) {
|
|
47
|
+
const directPath = path.join(config.workspacesDir, workspaceArg);
|
|
48
|
+
const manifest = await loadFeatureConfig(directPath);
|
|
49
|
+
if (manifest) {
|
|
50
|
+
workspacePath = directPath;
|
|
51
|
+
workspaceName = manifest.branchName;
|
|
52
|
+
} else {
|
|
53
|
+
console.error(chalk.red(`✖ Invalid workspace: No nexusflow.json found at ${workspaceArg}`));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
const cwdFeature = await loadFeatureConfig(process.cwd());
|
|
59
|
+
if (cwdFeature) {
|
|
60
|
+
workspacePath = process.cwd();
|
|
61
|
+
workspaceName = cwdFeature.branchName;
|
|
62
|
+
} else {
|
|
63
|
+
const workspaces = await listWorkspaces(config.workspacesDir);
|
|
64
|
+
if (workspaces.length === 0) {
|
|
65
|
+
console.log(chalk.yellow('No workspaces found.\n'));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const selected = await select({
|
|
70
|
+
message: 'Select a workspace to add a repository to:',
|
|
71
|
+
choices: workspaces.map((ws) => ({
|
|
72
|
+
name: `${ws.branchName} ${chalk.dim(`(${ws.repos.length} repos)`)}`,
|
|
73
|
+
value: ws,
|
|
74
|
+
})),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
workspacePath = selected.workspacePath;
|
|
78
|
+
workspaceName = selected.branchName;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!workspacePath) return;
|
|
83
|
+
|
|
84
|
+
const feature = await loadFeatureConfig(workspacePath);
|
|
85
|
+
if (!feature) {
|
|
86
|
+
console.error(chalk.red('✖ Failed to load workspace configuration.'));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 2. Resolve repository to add
|
|
91
|
+
let repoPathToAdd = '';
|
|
92
|
+
|
|
93
|
+
if (repoPathArg) {
|
|
94
|
+
repoPathToAdd = path.resolve(repoPathArg);
|
|
95
|
+
} else {
|
|
96
|
+
const scanSpinner = ora('Scanning for projects...').start();
|
|
97
|
+
let repos = [];
|
|
98
|
+
try {
|
|
99
|
+
repos = await scanForRepos(config.devDir, config.scanDepth);
|
|
100
|
+
scanSpinner.succeed(`Found ${chalk.bold(repos.length)} projects in ${config.devDir}`);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
scanSpinner.fail('Failed to scan for projects');
|
|
103
|
+
console.error(error);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const availableRepos = repos.filter(
|
|
108
|
+
(r) => !feature.repos.includes(r.path)
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
if (availableRepos.length === 0) {
|
|
112
|
+
console.log(chalk.yellow('All scanned repositories are already in the workspace.\n'));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
repoPathToAdd = await select({
|
|
117
|
+
message: 'Select a repository to add:',
|
|
118
|
+
choices: availableRepos.map((r) => ({
|
|
119
|
+
name: `${r.name} ${chalk.dim(`(${r.path})`)}`,
|
|
120
|
+
value: r.path,
|
|
121
|
+
})),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const repoName = path.basename(repoPathToAdd);
|
|
126
|
+
const spinner = ora(`Adding ${repoName} to workspace ${workspaceName}...`).start();
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await addRepoToWorkspace(workspacePath, repoPathToAdd);
|
|
130
|
+
spinner.succeed(`Successfully added ${chalk.bold(repoName)} to workspace ${chalk.bold(workspaceName)}`);
|
|
131
|
+
console.log(chalk.dim(' - Checked out git worktree'));
|
|
132
|
+
console.log(chalk.dim(' - Updated nexusflow.json and .gitignore'));
|
|
133
|
+
console.log(chalk.dim(' - Re-analyzed workspace codebases'));
|
|
134
|
+
console.log(chalk.dim(' - Regenerated LLM instruction context files'));
|
|
135
|
+
console.log(chalk.dim(' - Repacked codebase context using Repomix'));
|
|
136
|
+
console.log();
|
|
137
|
+
} catch (error) {
|
|
138
|
+
spinner.fail(`Failed to add repository: ${error instanceof Error ? error.message : String(error)}`);
|
|
139
|
+
console.log();
|
|
140
|
+
}
|
|
141
|
+
}
|
package/src/commands/create.ts
CHANGED
|
@@ -107,10 +107,14 @@ export async function createCommand(): Promise<void> {
|
|
|
107
107
|
|
|
108
108
|
// ── 7. Analyze projects ─────────────────────────────────────────────
|
|
109
109
|
console.log(chalk.cyan('\nAnalyzing projects...'));
|
|
110
|
-
const
|
|
110
|
+
const workspaceRepos = selectedRepos.map((repo) => ({
|
|
111
|
+
...repo,
|
|
112
|
+
path: path.join(workspacePath, repo.name),
|
|
113
|
+
}));
|
|
114
|
+
const analysis = await analyzeAllRepos(workspaceRepos);
|
|
111
115
|
|
|
112
116
|
// ── 8. Generate AI context files ────────────────────────────────────
|
|
113
|
-
const ctx: WorkspaceContext = { feature, repos:
|
|
117
|
+
const ctx: WorkspaceContext = { feature, repos: workspaceRepos, analysis };
|
|
114
118
|
console.log(chalk.cyan('\nGenerating AI context files...'));
|
|
115
119
|
await generateContextFiles(ctx, selectedAI, workspacePath);
|
|
116
120
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module commands/remove
|
|
3
|
+
* Deletes a NexusFlow workspace and cleanly removes/prunes its git worktrees.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import { select, confirm } from '@inquirer/prompts';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import * as fs from 'node:fs/promises';
|
|
11
|
+
|
|
12
|
+
import { loadConfig } from '../core/config.js';
|
|
13
|
+
import { listWorkspaces, deleteWorkspace } from '../core/workspace.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Executes the remove command.
|
|
17
|
+
*
|
|
18
|
+
* @param workspaceArg - Optional workspace path or branch name from CLI.
|
|
19
|
+
*/
|
|
20
|
+
export async function removeCommand(workspaceArg?: string): Promise<void> {
|
|
21
|
+
console.log(chalk.bold.red('\n🗑️ NexusFlow — Deleting Workspace\n'));
|
|
22
|
+
|
|
23
|
+
const config = await loadConfig();
|
|
24
|
+
let workspacePath: string | null = null;
|
|
25
|
+
let workspaceName = '';
|
|
26
|
+
|
|
27
|
+
if (workspaceArg) {
|
|
28
|
+
const resolvedPath = path.isAbsolute(workspaceArg)
|
|
29
|
+
? workspaceArg
|
|
30
|
+
: path.resolve(config.workspacesDir, workspaceArg);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
await fs.access(resolvedPath);
|
|
34
|
+
workspacePath = resolvedPath;
|
|
35
|
+
workspaceName = path.basename(resolvedPath);
|
|
36
|
+
} catch {
|
|
37
|
+
const directPath = path.join(config.workspacesDir, workspaceArg);
|
|
38
|
+
try {
|
|
39
|
+
await fs.access(directPath);
|
|
40
|
+
workspacePath = directPath;
|
|
41
|
+
workspaceName = workspaceArg;
|
|
42
|
+
} catch {
|
|
43
|
+
console.error(chalk.red(`✖ Workspace not found: ${workspaceArg}`));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
const workspaces = await listWorkspaces(config.workspacesDir);
|
|
49
|
+
|
|
50
|
+
if (workspaces.length === 0) {
|
|
51
|
+
console.log(chalk.yellow('No workspaces found.\n'));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const selected = await select({
|
|
56
|
+
message: 'Select a workspace to delete:',
|
|
57
|
+
choices: workspaces.map((ws) => ({
|
|
58
|
+
name: `${ws.branchName} ${chalk.dim(`(${ws.repos.length} repos)`)}`,
|
|
59
|
+
value: ws,
|
|
60
|
+
})),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
workspacePath = selected.workspacePath;
|
|
64
|
+
workspaceName = selected.branchName;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!workspacePath) return;
|
|
68
|
+
|
|
69
|
+
const confirmDelete = await confirm({
|
|
70
|
+
message: `Are you absolutely sure you want to delete the workspace "${workspaceName}"?\n This will FORCE remove all associated git worktrees and delete the folder from disk.`,
|
|
71
|
+
default: false,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (!confirmDelete) {
|
|
75
|
+
console.log(chalk.yellow('\nCancelled.\n'));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const spinner = ora('Deleting workspace and pruning git worktrees...').start();
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
await deleteWorkspace(workspacePath);
|
|
83
|
+
spinner.succeed(`Successfully deleted workspace ${chalk.bold(workspaceName)}`);
|
|
84
|
+
console.log();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
spinner.fail(`Failed to delete workspace ${workspaceName}`);
|
|
87
|
+
console.error(chalk.red(` ${error instanceof Error ? error.message : String(error)}`));
|
|
88
|
+
console.log();
|
|
89
|
+
}
|
|
90
|
+
}
|
package/src/core/workspace.ts
CHANGED
|
@@ -9,8 +9,12 @@ import * as fs from 'node:fs/promises';
|
|
|
9
9
|
import * as path from 'node:path';
|
|
10
10
|
import { execa } from 'execa';
|
|
11
11
|
|
|
12
|
-
import type { Feature, RepoInfo } from '../types.js';
|
|
13
|
-
import { createWorktree } from './worktree.js';
|
|
12
|
+
import type { Feature, RepoInfo, WorkspaceContext } from '../types.js';
|
|
13
|
+
import { createWorktree, removeWorktree } from './worktree.js';
|
|
14
|
+
import { detectDefaultBranch } from '../utils/git.js';
|
|
15
|
+
import { analyzeAllRepos } from '../analyzers/index.js';
|
|
16
|
+
import { generateContextFiles } from '../generators/index.js';
|
|
17
|
+
import { packWorkspace } from './packer.js';
|
|
14
18
|
|
|
15
19
|
/** Name of the per-workspace manifest file. */
|
|
16
20
|
const MANIFEST_FILE = 'nexusflow.json';
|
|
@@ -161,3 +165,143 @@ export async function loadFeatureConfig(
|
|
|
161
165
|
return null;
|
|
162
166
|
}
|
|
163
167
|
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Resolves a repo path to a full RepoInfo object.
|
|
171
|
+
*
|
|
172
|
+
* @param repoPath - Absolute path to the original repository.
|
|
173
|
+
*/
|
|
174
|
+
export async function resolveRepoInfo(repoPath: string): Promise<RepoInfo> {
|
|
175
|
+
const defaultBranch = await detectDefaultBranch(repoPath);
|
|
176
|
+
return {
|
|
177
|
+
name: path.basename(repoPath),
|
|
178
|
+
path: repoPath,
|
|
179
|
+
defaultBranch,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Deletes a workspace cleanly, removing all associated git worktrees first,
|
|
185
|
+
* then deleting the directory from disk.
|
|
186
|
+
*
|
|
187
|
+
* @param workspacePath - Absolute path to the workspace directory.
|
|
188
|
+
*/
|
|
189
|
+
export async function deleteWorkspace(
|
|
190
|
+
workspacePath: string,
|
|
191
|
+
): Promise<void> {
|
|
192
|
+
const feature = await loadFeatureConfig(workspacePath);
|
|
193
|
+
if (feature) {
|
|
194
|
+
for (const repoPath of feature.repos) {
|
|
195
|
+
const repoName = path.basename(repoPath);
|
|
196
|
+
const worktreePath = path.join(workspacePath, repoName);
|
|
197
|
+
try {
|
|
198
|
+
await removeWorktree(repoPath, worktreePath, true);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.warn(`Warning: failed to remove worktree for ${repoName} in ${repoPath}:`, error);
|
|
201
|
+
try {
|
|
202
|
+
await execa('git', ['worktree', 'prune'], { cwd: repoPath });
|
|
203
|
+
} catch (pruneError) {
|
|
204
|
+
console.warn(`Warning: failed to prune worktrees in ${repoPath}:`, pruneError);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
// Manifest is missing. Try to detect worktrees by scanning subdirectories
|
|
210
|
+
try {
|
|
211
|
+
const entries = await fs.readdir(workspacePath, { withFileTypes: true });
|
|
212
|
+
for (const entry of entries) {
|
|
213
|
+
if (entry.isDirectory()) {
|
|
214
|
+
const subPath = path.join(workspacePath, entry.name);
|
|
215
|
+
const gitFilePath = path.join(subPath, '.git');
|
|
216
|
+
try {
|
|
217
|
+
const stat = await fs.stat(gitFilePath);
|
|
218
|
+
if (stat.isFile()) {
|
|
219
|
+
const content = await fs.readFile(gitFilePath, 'utf-8');
|
|
220
|
+
const match = content.match(/gitdir:\s*(.+)\.git\/worktrees/);
|
|
221
|
+
if (match && match[1]) {
|
|
222
|
+
const mainRepoPath = path.resolve(match[1].trim());
|
|
223
|
+
await removeWorktree(mainRepoPath, subPath, true);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} catch {}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} catch {}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Delete the directory itself
|
|
233
|
+
try {
|
|
234
|
+
await fs.rm(workspacePath, { recursive: true, force: true });
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.error(`Failed to delete workspace directory ${workspacePath}:`, error);
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Adds a repository to an existing workspace.
|
|
243
|
+
*
|
|
244
|
+
* @param workspacePath - Absolute path to the workspace directory.
|
|
245
|
+
* @param repoPath - Absolute path to the repository to add.
|
|
246
|
+
*/
|
|
247
|
+
export async function addRepoToWorkspace(
|
|
248
|
+
workspacePath: string,
|
|
249
|
+
repoPath: string,
|
|
250
|
+
): Promise<void> {
|
|
251
|
+
const feature = await loadFeatureConfig(workspacePath);
|
|
252
|
+
if (!feature) {
|
|
253
|
+
throw new Error(`Workspace manifest not found at ${workspacePath}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (feature.repos.includes(repoPath)) {
|
|
257
|
+
throw new Error(`Repository ${repoPath} is already in the workspace`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const newRepoInfo = await resolveRepoInfo(repoPath);
|
|
261
|
+
const worktreeTarget = path.join(workspacePath, newRepoInfo.name);
|
|
262
|
+
|
|
263
|
+
// 1. Create the worktree
|
|
264
|
+
await createWorktree(
|
|
265
|
+
newRepoInfo.path,
|
|
266
|
+
worktreeTarget,
|
|
267
|
+
feature.branchName,
|
|
268
|
+
newRepoInfo.defaultBranch,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// 2. Update manifest
|
|
272
|
+
feature.repos.push(repoPath);
|
|
273
|
+
await saveFeatureConfig(workspacePath, feature);
|
|
274
|
+
|
|
275
|
+
// 3. Update .gitignore at workspace root
|
|
276
|
+
try {
|
|
277
|
+
const gitignorePath = path.join(workspacePath, '.gitignore');
|
|
278
|
+
let gitignoreContent = '';
|
|
279
|
+
try {
|
|
280
|
+
gitignoreContent = await fs.readFile(gitignorePath, 'utf-8');
|
|
281
|
+
} catch {}
|
|
282
|
+
|
|
283
|
+
const entry = `/${newRepoInfo.name}/`;
|
|
284
|
+
if (!gitignoreContent.includes(entry)) {
|
|
285
|
+
gitignoreContent = gitignoreContent.trim() + '\n' + entry + '\n';
|
|
286
|
+
await fs.writeFile(gitignorePath, gitignoreContent, 'utf-8');
|
|
287
|
+
}
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.warn('Warning: Failed to update .gitignore:', error);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 4. Re-run analysis, update configs, and repack workspace
|
|
293
|
+
const allRepos = await Promise.all(feature.repos.map(resolveRepoInfo));
|
|
294
|
+
const workspaceRepos = allRepos.map((repo) => ({
|
|
295
|
+
...repo,
|
|
296
|
+
path: path.join(workspacePath, repo.name),
|
|
297
|
+
}));
|
|
298
|
+
const analysis = await analyzeAllRepos(workspaceRepos);
|
|
299
|
+
const ctx: WorkspaceContext = {
|
|
300
|
+
feature,
|
|
301
|
+
repos: workspaceRepos,
|
|
302
|
+
analysis,
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
await generateContextFiles(ctx, feature.assistants, workspacePath);
|
|
306
|
+
await packWorkspace(workspacePath);
|
|
307
|
+
}
|
package/src/core/worktree.ts
CHANGED
|
@@ -90,14 +90,22 @@ export async function createWorktree(
|
|
|
90
90
|
/**
|
|
91
91
|
* Removes an existing git worktree.
|
|
92
92
|
*
|
|
93
|
-
* Runs `git worktree remove <worktreePath>` from the main repo.
|
|
93
|
+
* Runs `git worktree remove [--force] <worktreePath>` from the main repo.
|
|
94
94
|
*
|
|
95
95
|
* @param repoPath - Absolute path to the main repo checkout.
|
|
96
96
|
* @param worktreePath - Absolute path to the worktree to remove.
|
|
97
|
+
* @param force - Whether to force removal (cleanly removes modified files).
|
|
97
98
|
*/
|
|
98
99
|
export async function removeWorktree(
|
|
99
100
|
repoPath: string,
|
|
100
101
|
worktreePath: string,
|
|
102
|
+
force = false,
|
|
101
103
|
): Promise<void> {
|
|
102
|
-
|
|
104
|
+
const args = ['worktree', 'remove'];
|
|
105
|
+
if (force) {
|
|
106
|
+
args.push('--force');
|
|
107
|
+
}
|
|
108
|
+
args.push(worktreePath);
|
|
109
|
+
await execa('git', args, { cwd: repoPath });
|
|
103
110
|
}
|
|
111
|
+
|
package/src/generators/base.ts
CHANGED
|
@@ -157,6 +157,15 @@ ${existingConfigsSection}
|
|
|
157
157
|
${resumptionSection}
|
|
158
158
|
---
|
|
159
159
|
|
|
160
|
+
## Codebase Context (Repomix)
|
|
161
|
+
|
|
162
|
+
A packed, token-efficient version of the entire multi-repo codebase is automatically generated and updated at the workspace root:
|
|
163
|
+
- **File**: \`nexusflow-context.xml\`
|
|
164
|
+
- **Purpose**: Contains the aggregated source code and structure of all projects in the workspace.
|
|
165
|
+
- **Usage**: You can read this file directly to understand relationships or search for code patterns across all projects without needing to manually traverse directories.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
160
169
|
## Task & Step-by-Step Initialization
|
|
161
170
|
|
|
162
171
|
Your very first task upon entering this workspace is to analyze the codebase and document it in a universal reference file:
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,8 @@ import { syncCommand } from './commands/sync.js';
|
|
|
24
24
|
import { commitCommand } from './commands/commit.js';
|
|
25
25
|
import { diffCommand } from './commands/diff.js';
|
|
26
26
|
import { packCommand } from './commands/pack.js';
|
|
27
|
+
import { removeCommand } from './commands/remove.js';
|
|
28
|
+
import { addRepoCommand } from './commands/add-repo.js';
|
|
27
29
|
import { mcpRunCommand, mcpSetupCommand } from './commands/mcp.js';
|
|
28
30
|
import { getCurrentVersion, checkForUpdates, printUpdateBanner } from './utils/update-check.js';
|
|
29
31
|
|
|
@@ -253,6 +255,43 @@ program
|
|
|
253
255
|
}
|
|
254
256
|
});
|
|
255
257
|
|
|
258
|
+
program
|
|
259
|
+
.command('remove')
|
|
260
|
+
.alias('rm')
|
|
261
|
+
.description('Delete a workspace and cleanly prune/remove its git worktrees')
|
|
262
|
+
.argument('[workspace]', 'Workspace name or path')
|
|
263
|
+
.action(async (workspace?: string) => {
|
|
264
|
+
try {
|
|
265
|
+
await removeCommand(workspace);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
if (error instanceof Error && error.message.includes('User force closed')) {
|
|
268
|
+
console.log('\nCancelled.');
|
|
269
|
+
process.exit(0);
|
|
270
|
+
}
|
|
271
|
+
console.error(error);
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
program
|
|
277
|
+
.command('add-repo')
|
|
278
|
+
.alias('add')
|
|
279
|
+
.description('Add a repository to an existing workspace and update configurations')
|
|
280
|
+
.argument('[repo-path]', 'Path to the repository to add')
|
|
281
|
+
.argument('[workspace]', 'Workspace name or path')
|
|
282
|
+
.action(async (repoPath?: string, workspace?: string) => {
|
|
283
|
+
try {
|
|
284
|
+
await addRepoCommand(repoPath, workspace);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
if (error instanceof Error && error.message.includes('User force closed')) {
|
|
287
|
+
console.log('\nCancelled.');
|
|
288
|
+
process.exit(0);
|
|
289
|
+
}
|
|
290
|
+
console.error(error);
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
256
295
|
const mcp = program.command('mcp').description('Manage the NexusFlow MCP Server for AI assistants');
|
|
257
296
|
|
|
258
297
|
mcp
|
package/src/server.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { execa } from 'execa';
|
|
|
15
15
|
|
|
16
16
|
import { loadConfig, saveConfig, getConfigDir } from './core/config.js';
|
|
17
17
|
import { scanForRepos } from './core/scanner.js';
|
|
18
|
-
import { createWorkspace, listWorkspaces, loadFeatureConfig } from './core/workspace.js';
|
|
18
|
+
import { createWorkspace, listWorkspaces, loadFeatureConfig, deleteWorkspace, addRepoToWorkspace } from './core/workspace.js';
|
|
19
19
|
import { analyzeAllRepos } from './analyzers/index.js';
|
|
20
20
|
import { generateContextFiles } from './generators/index.js';
|
|
21
21
|
import { packWorkspace } from './core/packer.js';
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
stopServices,
|
|
31
31
|
loadRunningState,
|
|
32
32
|
} from './orchestration/index.js';
|
|
33
|
-
import { checkForUpdates, getCurrentVersion } from './utils/update-check.js';
|
|
33
|
+
import { checkForUpdates, getCurrentVersion, getToolsStatus } from './utils/update-check.js';
|
|
34
34
|
import type { Feature, RepoInfo, WorkspaceContext } from './types.js';
|
|
35
35
|
|
|
36
36
|
// Resolve static files directory
|
|
@@ -220,14 +220,18 @@ async function runCreationJob(jobId: string, body: any, config: any) {
|
|
|
220
220
|
|
|
221
221
|
// Step 2: Analyze repos
|
|
222
222
|
updateJobStep(jobId, 'analysis', 'running', 'Analyzing projects and dependencies...');
|
|
223
|
-
const
|
|
223
|
+
const workspaceRepos = body.repos.map((repo: any) => ({
|
|
224
|
+
...repo,
|
|
225
|
+
path: path.join(workspacePath, repo.name),
|
|
226
|
+
}));
|
|
227
|
+
const analysis = await analyzeAllRepos(workspaceRepos);
|
|
224
228
|
updateJobStep(jobId, 'analysis', 'completed', 'Project analysis complete.');
|
|
225
229
|
|
|
226
230
|
// Step 3: Generate AI context files
|
|
227
231
|
updateJobStep(jobId, 'context', 'running', 'Generating AI context files...');
|
|
228
232
|
const ctx: WorkspaceContext = {
|
|
229
233
|
feature,
|
|
230
|
-
repos:
|
|
234
|
+
repos: workspaceRepos,
|
|
231
235
|
analysis,
|
|
232
236
|
};
|
|
233
237
|
await generateContextFiles(ctx, body.assistants, workspacePath);
|
|
@@ -353,6 +357,37 @@ app.get('/api/workspace/create-stream/:jobId', async (c) => {
|
|
|
353
357
|
});
|
|
354
358
|
});
|
|
355
359
|
|
|
360
|
+
// 7.6. Delete workspace
|
|
361
|
+
app.delete('/api/workspace/:id', async (c) => {
|
|
362
|
+
try {
|
|
363
|
+
const id = decodeURIComponent(c.req.param('id'));
|
|
364
|
+
const config = await loadConfig();
|
|
365
|
+
const workspacePath = path.join(config.workspacesDir, id);
|
|
366
|
+
|
|
367
|
+
await deleteWorkspace(workspacePath);
|
|
368
|
+
return c.json({ success: true });
|
|
369
|
+
} catch (error) {
|
|
370
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
371
|
+
return c.json({ error: msg }, 500);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// 7.7. Add repo to workspace
|
|
376
|
+
app.post('/api/workspace/:id/repo', async (c) => {
|
|
377
|
+
try {
|
|
378
|
+
const id = decodeURIComponent(c.req.param('id'));
|
|
379
|
+
const { repoPath } = await c.req.json() as { repoPath: string };
|
|
380
|
+
const config = await loadConfig();
|
|
381
|
+
const workspacePath = path.join(config.workspacesDir, id);
|
|
382
|
+
|
|
383
|
+
await addRepoToWorkspace(workspacePath, repoPath);
|
|
384
|
+
return c.json({ success: true });
|
|
385
|
+
} catch (error) {
|
|
386
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
387
|
+
return c.json({ error: msg }, 500);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
356
391
|
// 8. Open workspace in editor
|
|
357
392
|
app.post('/api/open-editor', async (c) => {
|
|
358
393
|
try {
|
|
@@ -785,6 +820,46 @@ app.get('/api/update-status', async (c) => {
|
|
|
785
820
|
}
|
|
786
821
|
});
|
|
787
822
|
|
|
823
|
+
// 17.5. Check tools updates status
|
|
824
|
+
app.get('/api/updates/tools', async (c) => {
|
|
825
|
+
try {
|
|
826
|
+
const force = c.req.query('force') === 'true';
|
|
827
|
+
const status = await getToolsStatus(force);
|
|
828
|
+
return c.json(status);
|
|
829
|
+
} catch (error) {
|
|
830
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
831
|
+
return c.json({ error: msg }, 500);
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
// 17.6. Install updates for a specific tool
|
|
836
|
+
app.post('/api/updates/install', async (c) => {
|
|
837
|
+
try {
|
|
838
|
+
const { toolId } = await c.req.json() as { toolId: string };
|
|
839
|
+
const tools = [
|
|
840
|
+
{ id: 'nexusflow', cmd: 'npm', args: ['install', '-g', '@mrpatronz/nexusflow'] },
|
|
841
|
+
{ id: 'repomix', cmd: 'npm', args: ['install', '-g', 'repomix'] },
|
|
842
|
+
{ id: 'antigravity', cmd: 'agy', args: ['update'] },
|
|
843
|
+
{ id: 'claude', cmd: 'npm', args: ['install', '-g', '@anthropic-ai/claude-code'] },
|
|
844
|
+
];
|
|
845
|
+
|
|
846
|
+
const target = tools.find(t => t.id === toolId);
|
|
847
|
+
if (!target) {
|
|
848
|
+
return c.json({ error: 'Tool not found' }, 404);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const result = await execa(target.cmd, target.args, { reject: false });
|
|
852
|
+
if (result.exitCode === 0) {
|
|
853
|
+
return c.json({ success: true, output: result.stdout });
|
|
854
|
+
} else {
|
|
855
|
+
return c.json({ error: `Update failed: ${result.stderr || result.stdout}` }, 500);
|
|
856
|
+
}
|
|
857
|
+
} catch (error) {
|
|
858
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
859
|
+
return c.json({ error: msg }, 500);
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
|
|
788
863
|
// 18. Pack workspace codebase and download
|
|
789
864
|
app.get('/api/workspace/:id/pack', async (c) => {
|
|
790
865
|
try {
|