@mrpatronz/nexusflow 0.2.13 → 0.2.17
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 +52 -52
- package/.github/workflows/dependabot-merge.yml +53 -0
- package/.github/workflows/release.yml +112 -100
- package/.vscode/launch.json +17 -17
- package/.vscode/tasks.json +17 -17
- package/GETTING_STARTED.md +114 -109
- package/README.md +336 -311
- package/desktop/build-installer.js +228 -0
- package/desktop/build.js +118 -0
- package/desktop/installer/nexusflow.iss +35 -0
- package/desktop/neutralino.config.json +39 -0
- package/desktop/package-lock.json +1060 -0
- package/desktop/package.json +14 -0
- package/dist/commands/desktop.d.ts +12 -0
- package/dist/commands/desktop.d.ts.map +1 -0
- package/dist/commands/desktop.js +119 -0
- package/dist/commands/desktop.js.map +1 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +1 -3
- package/dist/commands/sync.js.map +1 -1
- package/dist/commands/tui.d.ts +8 -0
- package/dist/commands/tui.d.ts.map +1 -0
- package/dist/commands/tui.js +386 -0
- package/dist/commands/tui.js.map +1 -0
- package/dist/commands/ui.d.ts +3 -1
- package/dist/commands/ui.d.ts.map +1 -1
- package/dist/commands/ui.js +76 -19
- package/dist/commands/ui.js.map +1 -1
- package/dist/generators/base.d.ts.map +1 -1
- package/dist/generators/base.js +78 -66
- package/dist/generators/base.js.map +1 -1
- package/dist/generators/codex.js +11 -11
- package/dist/generators/copilot.js +17 -17
- package/dist/generators/cursor.js +5 -5
- package/dist/generators/index.js +50 -50
- package/dist/generators/map-generator.js +14 -14
- package/dist/generators/skills-generator.js +28 -28
- package/dist/gui/app_logo.png +0 -0
- package/dist/gui/assets/index-Ct3qq-X4.js +25 -0
- package/dist/gui/assets/index-P_ZrgHXg.css +1 -0
- package/dist/gui/icons.svg +24 -24
- package/dist/gui/index.html +18 -18
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +182 -1
- package/dist/server.js.map +1 -1
- package/dist/server.test.js +78 -0
- package/dist/server.test.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/git.d.ts +16 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +52 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/multi-git.d.ts +2 -0
- package/dist/utils/multi-git.d.ts.map +1 -1
- package/dist/utils/multi-git.js +5 -2
- package/dist/utils/multi-git.js.map +1 -1
- package/dist/utils/update-check.d.ts +3 -1
- package/dist/utils/update-check.d.ts.map +1 -1
- package/dist/utils/update-check.js +23 -6
- package/dist/utils/update-check.js.map +1 -1
- package/dist/utils/workflows.d.ts +11 -0
- package/dist/utils/workflows.d.ts.map +1 -0
- package/dist/utils/workflows.js +156 -0
- package/dist/utils/workflows.js.map +1 -0
- package/dist/utils/workflows.test.d.ts +2 -0
- package/dist/utils/workflows.test.d.ts.map +1 -0
- package/dist/utils/workflows.test.js +86 -0
- package/dist/utils/workflows.test.js.map +1 -0
- package/extension/media/icon.svg +1 -0
- package/extension/package-lock.json +2673 -2811
- package/extension/package.json +108 -70
- package/extension/src/extension.ts +631 -204
- package/extension/tsconfig.json +21 -19
- package/gui/README.md +73 -73
- package/gui/e2e/wizard.spec.ts +346 -327
- package/gui/eslint.config.js +26 -26
- package/gui/index.html +17 -17
- package/gui/package-lock.json +3079 -3118
- package/gui/package.json +36 -36
- package/gui/playwright.config.ts +41 -41
- package/gui/public/app_logo.png +0 -0
- package/gui/public/icons.svg +24 -24
- package/gui/src/App.css +1 -1
- package/gui/src/App.tsx +3407 -2722
- package/gui/src/assets/vite.svg +1 -1
- package/gui/src/features/changes/ChangesViewer.tsx +388 -388
- package/gui/src/features/knowledge/KnowledgeBase.tsx +84 -84
- package/gui/src/features/onboarding/OnboardingWizard.tsx +230 -230
- package/gui/src/features/plan/ImplementationPlan.tsx +32 -32
- package/gui/src/features/services/ServiceConsole.tsx +250 -250
- package/gui/src/features/sessions/SessionHistory.tsx +195 -195
- package/gui/src/features/workspace/WorkspaceList.tsx +666 -666
- package/gui/src/index.css +152 -110
- package/gui/src/main.tsx +94 -10
- package/gui/src/types.ts +62 -62
- package/gui/tsconfig.app.json +25 -25
- package/gui/tsconfig.json +7 -7
- package/gui/tsconfig.node.json +24 -24
- package/gui/vite.config.ts +12 -12
- package/package.json +56 -56
- package/resources/workflows/plan-implement-review.md +8 -0
- package/resources/workflows/research-verify.md +6 -0
- package/resources/workflows/solo-developer.md +3 -0
- package/scripts/simulate-workspaces.ts +55 -55
- package/src/analyzers/detect-apis.ts +290 -290
- package/src/analyzers/detect-deps.ts +315 -315
- package/src/analyzers/detect-existing.ts +74 -74
- package/src/analyzers/detect-ports.ts +110 -110
- package/src/analyzers/index.ts +97 -97
- package/src/analyzers/messaging-analyzer.ts +254 -254
- package/src/analyzers/readme-summarizer.ts +102 -102
- package/src/analyzers/run-analyzer.ts +269 -269
- package/src/analyzers/tech-stack.ts +283 -283
- package/src/commands/add-repo.ts +157 -157
- package/src/commands/commands.test.ts +155 -155
- package/src/commands/commit.ts +131 -131
- package/src/commands/create.ts +207 -207
- package/src/commands/desktop.ts +128 -0
- package/src/commands/diff.ts +125 -125
- package/src/commands/doctor.ts +357 -357
- package/src/commands/handoff.ts +266 -266
- package/src/commands/init.ts +134 -134
- package/src/commands/list.ts +46 -46
- package/src/commands/logs.ts +63 -63
- package/src/commands/mcp.ts +92 -92
- package/src/commands/open.ts +129 -129
- package/src/commands/pack.ts +73 -73
- package/src/commands/refresh.ts +147 -147
- package/src/commands/remove.ts +98 -98
- package/src/commands/start.ts +117 -117
- package/src/commands/status.ts +54 -54
- package/src/commands/stop.ts +55 -55
- package/src/commands/sync.ts +151 -153
- package/src/commands/tui.ts +424 -0
- package/src/commands/ui.ts +111 -47
- package/src/core/config.test.ts +96 -96
- package/src/core/config.ts +115 -115
- package/src/core/graph.ts +344 -344
- package/src/core/packer.test.ts +99 -99
- package/src/core/packer.ts +107 -107
- package/src/core/scanner.test.ts +61 -61
- package/src/core/scanner.ts +91 -91
- package/src/core/workspace.ts +380 -380
- package/src/core/worktree.ts +120 -120
- package/src/generators/antigravity.ts +32 -32
- package/src/generators/base.ts +240 -227
- package/src/generators/claude.ts +34 -34
- package/src/generators/codex.ts +48 -48
- package/src/generators/copilot.ts +56 -56
- package/src/generators/cursor.ts +44 -44
- package/src/generators/index.ts +250 -250
- package/src/generators/map-generator.test.ts +159 -159
- package/src/generators/map-generator.ts +542 -542
- package/src/generators/plan-generator.ts +481 -481
- package/src/generators/skills-generator.ts +259 -259
- package/src/index.ts +426 -382
- package/src/mcp/server.ts +294 -294
- package/src/orchestration/detect.ts +313 -313
- package/src/orchestration/index.ts +7 -7
- package/src/orchestration/runner.ts +283 -283
- package/src/server.test.ts +555 -464
- package/src/server.ts +1308 -1104
- package/src/types.ts +408 -403
- package/src/utils/detect-ai.test.ts +44 -44
- package/src/utils/detect-ai.ts +84 -84
- package/src/utils/detect-editors.test.ts +48 -48
- package/src/utils/detect-editors.ts +57 -57
- package/src/utils/git.test.ts +74 -74
- package/src/utils/git.ts +117 -61
- package/src/utils/local-ai.test.ts +130 -130
- package/src/utils/local-ai.ts +111 -111
- package/src/utils/multi-git.ts +313 -306
- package/src/utils/prompts.ts +209 -209
- package/src/utils/session-finder.ts +483 -483
- package/src/utils/system-scanner.test.ts +89 -89
- package/src/utils/system-scanner.ts +96 -96
- package/src/utils/update-check.ts +309 -275
- package/src/utils/workflows.test.ts +114 -0
- package/src/utils/workflows.ts +178 -0
- package/tsconfig.json +19 -19
- package/vitest.config.ts +14 -14
- package/dist/gui/assets/index-CFWwqFux.js +0 -23
- package/dist/gui/assets/index-Dvc9QMvl.css +0 -2
package/src/core/workspace.ts
CHANGED
|
@@ -1,380 +1,380 @@
|
|
|
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
|
-
import { execa } from 'execa';
|
|
11
|
-
|
|
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';
|
|
18
|
-
import { loadConfig } from './config.js';
|
|
19
|
-
|
|
20
|
-
/** Name of the per-workspace manifest file. */
|
|
21
|
-
const MANIFEST_FILE = 'nexusflow.json';
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Derives the workspace directory path for a given branch name.
|
|
25
|
-
*
|
|
26
|
-
* @param workspacesDir - The root workspaces directory (e.g. ~/dev/workspaces).
|
|
27
|
-
* @param branchName - The feature branch name used as the workspace folder.
|
|
28
|
-
* @returns Absolute path to the workspace directory.
|
|
29
|
-
*/
|
|
30
|
-
export function getWorkspacePath(
|
|
31
|
-
workspacesDir: string,
|
|
32
|
-
branchName: string,
|
|
33
|
-
): string {
|
|
34
|
-
return path.join(workspacesDir, branchName);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Creates a full workspace for a feature:
|
|
39
|
-
* 1. Creates the workspace directory.
|
|
40
|
-
* 2. Creates a git worktree for every repo in the feature.
|
|
41
|
-
* 3. Saves the feature manifest (`nexusflow.json`).
|
|
42
|
-
*
|
|
43
|
-
* @param feature - The feature definition.
|
|
44
|
-
* @param repos - Resolved repo metadata for every repo in the feature.
|
|
45
|
-
* @returns The absolute path to the newly created workspace.
|
|
46
|
-
*/
|
|
47
|
-
export async function createWorkspace(
|
|
48
|
-
feature: Feature,
|
|
49
|
-
repos: RepoInfo[],
|
|
50
|
-
): Promise<string> {
|
|
51
|
-
const workspacePath = feature.workspacePath;
|
|
52
|
-
|
|
53
|
-
// Ensure the workspace directory exists.
|
|
54
|
-
await fs.mkdir(workspacePath, { recursive: true });
|
|
55
|
-
|
|
56
|
-
// Initialize git repository at workspace root to prevent AI assistants (like Claude)
|
|
57
|
-
// from climbing up to parent git repositories (main/master).
|
|
58
|
-
try {
|
|
59
|
-
await execa('git', ['init'], { cwd: workspacePath });
|
|
60
|
-
|
|
61
|
-
// Write a .gitignore to ignore the sub-repositories
|
|
62
|
-
const gitignoreContent = repos.map((repo) => `/${repo.name}/`).join('\n') + '\n';
|
|
63
|
-
await fs.writeFile(path.join(workspacePath, '.gitignore'), gitignoreContent, 'utf-8');
|
|
64
|
-
} catch (error) {
|
|
65
|
-
// Silently ignore or log warning if git init fails
|
|
66
|
-
console.warn('Warning: Failed to initialize git repository at workspace root:', error);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Create .vscode/settings.json to allow VS Code search to query inside ignored sub-repos
|
|
70
|
-
try {
|
|
71
|
-
const vscodeDir = path.join(workspacePath, '.vscode');
|
|
72
|
-
await fs.mkdir(vscodeDir, { recursive: true });
|
|
73
|
-
const settings = {
|
|
74
|
-
"search.useIgnoreFiles": false
|
|
75
|
-
};
|
|
76
|
-
await fs.writeFile(
|
|
77
|
-
path.join(vscodeDir, 'settings.json'),
|
|
78
|
-
JSON.stringify(settings, null, 2) + '\n',
|
|
79
|
-
'utf-8'
|
|
80
|
-
);
|
|
81
|
-
} catch (error) {
|
|
82
|
-
console.warn('Warning: Failed to create .vscode/settings.json:', error);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Create .cursor/mcp.json for workspace-local Cursor MCP configuration
|
|
86
|
-
try {
|
|
87
|
-
const cursorDir = path.join(workspacePath, '.cursor');
|
|
88
|
-
await fs.mkdir(cursorDir, { recursive: true });
|
|
89
|
-
const cursorMcp = {
|
|
90
|
-
"mcpServers": {
|
|
91
|
-
"nexusflow": {
|
|
92
|
-
"command": "npx",
|
|
93
|
-
"args": ["-y", "@mrpatronz/nexusflow", "mcp", "run", workspacePath]
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
await fs.writeFile(
|
|
98
|
-
path.join(cursorDir, 'mcp.json'),
|
|
99
|
-
JSON.stringify(cursorMcp, null, 2) + '\n',
|
|
100
|
-
'utf-8'
|
|
101
|
-
);
|
|
102
|
-
} catch (error) {
|
|
103
|
-
console.warn('Warning: Failed to create .cursor/mcp.json:', error);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Create a worktree for each repo inside the workspace.
|
|
107
|
-
for (const repo of repos) {
|
|
108
|
-
const worktreeTarget = path.join(workspacePath, repo.name);
|
|
109
|
-
await createWorktree(
|
|
110
|
-
repo.path,
|
|
111
|
-
worktreeTarget,
|
|
112
|
-
feature.branchName,
|
|
113
|
-
repo.defaultBranch,
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Persist the feature manifest.
|
|
118
|
-
await saveFeatureConfig(workspacePath, feature);
|
|
119
|
-
|
|
120
|
-
return workspacePath;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Lists all existing workspaces by reading `nexusflow.json` manifests from
|
|
125
|
-
* each subdirectory of {@link workspacesDir}.
|
|
126
|
-
*
|
|
127
|
-
* Directories that do not contain a valid manifest are silently skipped.
|
|
128
|
-
*
|
|
129
|
-
* @param workspacesDir - The root workspaces directory.
|
|
130
|
-
* @returns An array of {@link Feature} objects for each discovered workspace.
|
|
131
|
-
*/
|
|
132
|
-
export async function listWorkspaces(
|
|
133
|
-
workspacesDir: string,
|
|
134
|
-
): Promise<Feature[]> {
|
|
135
|
-
const features: Feature[] = [];
|
|
136
|
-
|
|
137
|
-
async function scan(dir: string, depth: number) {
|
|
138
|
-
if (depth > 3) return;
|
|
139
|
-
|
|
140
|
-
let entries: import('node:fs').Dirent[];
|
|
141
|
-
try {
|
|
142
|
-
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
143
|
-
} catch {
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Check if current directory contains a nexusflow.json manifest
|
|
148
|
-
const hasManifest = entries.some(
|
|
149
|
-
(e) => e.isFile() && e.name === MANIFEST_FILE,
|
|
150
|
-
);
|
|
151
|
-
if (hasManifest) {
|
|
152
|
-
const loaded = await loadFeatureConfig(dir);
|
|
153
|
-
if (loaded) {
|
|
154
|
-
features.push(loaded);
|
|
155
|
-
return; // Workspaces do not nest.
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Otherwise, recursively scan subdirectories
|
|
160
|
-
for (const entry of entries) {
|
|
161
|
-
if (entry.isDirectory()) {
|
|
162
|
-
await scan(path.join(dir, entry.name), depth + 1);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
await scan(workspacesDir, 1);
|
|
168
|
-
return features;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Saves a {@link Feature} as `nexusflow.json` inside the given workspace.
|
|
173
|
-
*
|
|
174
|
-
* @param workspacePath - Absolute path to the workspace directory.
|
|
175
|
-
* @param feature - The feature definition to persist.
|
|
176
|
-
*/
|
|
177
|
-
export async function saveFeatureConfig(
|
|
178
|
-
workspacePath: string,
|
|
179
|
-
feature: Feature,
|
|
180
|
-
): Promise<void> {
|
|
181
|
-
const manifestPath = path.join(workspacePath, MANIFEST_FILE);
|
|
182
|
-
const data = JSON.stringify(feature, null, 2) + '\n';
|
|
183
|
-
await fs.writeFile(manifestPath, data, 'utf-8');
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Loads a {@link Feature} from the `nexusflow.json` manifest inside a
|
|
188
|
-
* workspace directory.
|
|
189
|
-
*
|
|
190
|
-
* @param workspacePath - Absolute path to the workspace directory.
|
|
191
|
-
* @returns The loaded feature, or `null` if the manifest doesn't exist or is
|
|
192
|
-
* invalid.
|
|
193
|
-
*/
|
|
194
|
-
export async function loadFeatureConfig(
|
|
195
|
-
workspacePath: string,
|
|
196
|
-
): Promise<Feature | null> {
|
|
197
|
-
const rootDir = await findWorkspaceRoot(workspacePath);
|
|
198
|
-
if (!rootDir) return null;
|
|
199
|
-
|
|
200
|
-
const manifestPath = path.join(rootDir, MANIFEST_FILE);
|
|
201
|
-
try {
|
|
202
|
-
const raw = await fs.readFile(manifestPath, 'utf-8');
|
|
203
|
-
return JSON.parse(raw) as Feature;
|
|
204
|
-
} catch {
|
|
205
|
-
return null;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Traverses up parent directories to find a directory containing `nexusflow.json`.
|
|
211
|
-
*
|
|
212
|
-
* @param startPath - Path to start searching from.
|
|
213
|
-
* @returns Absolute path to the workspace root directory, or null if not found.
|
|
214
|
-
*/
|
|
215
|
-
export async function findWorkspaceRoot(startPath: string): Promise<string | null> {
|
|
216
|
-
let current = path.resolve(startPath);
|
|
217
|
-
while (true) {
|
|
218
|
-
const manifestPath = path.join(current, MANIFEST_FILE);
|
|
219
|
-
try {
|
|
220
|
-
await fs.access(manifestPath);
|
|
221
|
-
return current;
|
|
222
|
-
} catch {
|
|
223
|
-
const parent = path.dirname(current);
|
|
224
|
-
if (parent === current) {
|
|
225
|
-
break; // Reached root directory
|
|
226
|
-
}
|
|
227
|
-
current = parent;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Resolves a repo path to a full RepoInfo object.
|
|
235
|
-
*
|
|
236
|
-
* @param repoPath - Absolute path to the original repository.
|
|
237
|
-
*/
|
|
238
|
-
export async function resolveRepoInfo(repoPath: string): Promise<RepoInfo> {
|
|
239
|
-
const defaultBranch = await detectDefaultBranch(repoPath);
|
|
240
|
-
return {
|
|
241
|
-
name: path.basename(repoPath),
|
|
242
|
-
path: repoPath,
|
|
243
|
-
defaultBranch,
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Deletes a workspace cleanly, removing all associated git worktrees first,
|
|
249
|
-
* then deleting the directory from disk.
|
|
250
|
-
*
|
|
251
|
-
* @param workspacePath - Absolute path to the workspace directory.
|
|
252
|
-
*/
|
|
253
|
-
export async function deleteWorkspace(
|
|
254
|
-
workspacePath: string,
|
|
255
|
-
): Promise<void> {
|
|
256
|
-
const feature = await loadFeatureConfig(workspacePath);
|
|
257
|
-
if (feature) {
|
|
258
|
-
const origRepos = feature.originalRepos || [];
|
|
259
|
-
for (let i = 0; i < feature.repos.length; i++) {
|
|
260
|
-
const worktreePath = feature.repos[i]!;
|
|
261
|
-
const originalPath = origRepos[i] || worktreePath;
|
|
262
|
-
const repoName = path.basename(worktreePath);
|
|
263
|
-
try {
|
|
264
|
-
await removeWorktree(originalPath, worktreePath, true);
|
|
265
|
-
} catch (error) {
|
|
266
|
-
console.warn(`Warning: failed to remove worktree for ${repoName} in ${originalPath}:`, error);
|
|
267
|
-
try {
|
|
268
|
-
await execa('git', ['worktree', 'prune'], { cwd: originalPath });
|
|
269
|
-
} catch (pruneError) {
|
|
270
|
-
console.warn(`Warning: failed to prune worktrees in ${originalPath}:`, pruneError);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
} else {
|
|
275
|
-
// Manifest is missing. Try to detect worktrees by scanning subdirectories
|
|
276
|
-
try {
|
|
277
|
-
const entries = await fs.readdir(workspacePath, { withFileTypes: true });
|
|
278
|
-
for (const entry of entries) {
|
|
279
|
-
if (entry.isDirectory()) {
|
|
280
|
-
const subPath = path.join(workspacePath, entry.name);
|
|
281
|
-
const gitFilePath = path.join(subPath, '.git');
|
|
282
|
-
try {
|
|
283
|
-
const stat = await fs.stat(gitFilePath);
|
|
284
|
-
if (stat.isFile()) {
|
|
285
|
-
const content = await fs.readFile(gitFilePath, 'utf-8');
|
|
286
|
-
const match = content.match(/gitdir:\s*(.+)\.git\/worktrees/);
|
|
287
|
-
if (match && match[1]) {
|
|
288
|
-
const mainRepoPath = path.resolve(match[1].trim());
|
|
289
|
-
await removeWorktree(mainRepoPath, subPath, true);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
} catch {}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
} catch {}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Delete the directory itself
|
|
299
|
-
try {
|
|
300
|
-
await fs.rm(workspacePath, { recursive: true, force: true });
|
|
301
|
-
} catch (error) {
|
|
302
|
-
console.error(`Failed to delete workspace directory ${workspacePath}:`, error);
|
|
303
|
-
throw error;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Adds a repository to an existing workspace.
|
|
309
|
-
*
|
|
310
|
-
* @param workspacePath - Absolute path to the workspace directory.
|
|
311
|
-
* @param repoPath - Absolute path to the repository to add.
|
|
312
|
-
*/
|
|
313
|
-
export async function addRepoToWorkspace(
|
|
314
|
-
workspacePath: string,
|
|
315
|
-
repoPath: string,
|
|
316
|
-
): Promise<void> {
|
|
317
|
-
const feature = await loadFeatureConfig(workspacePath);
|
|
318
|
-
if (!feature) {
|
|
319
|
-
throw new Error(`Workspace manifest not found at ${workspacePath}`);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const newRepoInfo = await resolveRepoInfo(repoPath);
|
|
323
|
-
const worktreeTarget = path.join(workspacePath, newRepoInfo.name);
|
|
324
|
-
|
|
325
|
-
if (feature.repos.includes(worktreeTarget)) {
|
|
326
|
-
throw new Error(`Repository ${repoPath} is already in the workspace`);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// 1. Create the worktree
|
|
330
|
-
await createWorktree(
|
|
331
|
-
newRepoInfo.path,
|
|
332
|
-
worktreeTarget,
|
|
333
|
-
feature.branchName,
|
|
334
|
-
newRepoInfo.defaultBranch,
|
|
335
|
-
);
|
|
336
|
-
|
|
337
|
-
// 2. Update manifest
|
|
338
|
-
feature.repos.push(worktreeTarget);
|
|
339
|
-
if (!feature.originalRepos) {
|
|
340
|
-
feature.originalRepos = [];
|
|
341
|
-
}
|
|
342
|
-
feature.originalRepos.push(repoPath);
|
|
343
|
-
await saveFeatureConfig(workspacePath, feature);
|
|
344
|
-
|
|
345
|
-
// 3. Update .gitignore at workspace root
|
|
346
|
-
try {
|
|
347
|
-
const gitignorePath = path.join(workspacePath, '.gitignore');
|
|
348
|
-
let gitignoreContent = '';
|
|
349
|
-
try {
|
|
350
|
-
gitignoreContent = await fs.readFile(gitignorePath, 'utf-8');
|
|
351
|
-
} catch {}
|
|
352
|
-
|
|
353
|
-
const entry = `/${newRepoInfo.name}/`;
|
|
354
|
-
if (!gitignoreContent.includes(entry)) {
|
|
355
|
-
gitignoreContent = gitignoreContent.trim() + '\n' + entry + '\n';
|
|
356
|
-
await fs.writeFile(gitignorePath, gitignoreContent, 'utf-8');
|
|
357
|
-
}
|
|
358
|
-
} catch (error) {
|
|
359
|
-
console.warn('Warning: Failed to update .gitignore:', error);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// 4. Re-run analysis, update configs, and repack workspace
|
|
363
|
-
const allRepos = await Promise.all(feature.repos.map(resolveRepoInfo));
|
|
364
|
-
const workspaceRepos = allRepos.map((repo) => ({
|
|
365
|
-
...repo,
|
|
366
|
-
path: path.join(workspacePath, repo.name),
|
|
367
|
-
}));
|
|
368
|
-
const analysis = await analyzeAllRepos(workspaceRepos);
|
|
369
|
-
const ctx: WorkspaceContext = {
|
|
370
|
-
feature,
|
|
371
|
-
repos: workspaceRepos,
|
|
372
|
-
analysis,
|
|
373
|
-
};
|
|
374
|
-
|
|
375
|
-
await generateContextFiles(ctx, feature.assistants, workspacePath);
|
|
376
|
-
const config = await loadConfig();
|
|
377
|
-
if (config.packContextXml) {
|
|
378
|
-
await packWorkspace(workspacePath);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
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
|
+
import { execa } from 'execa';
|
|
11
|
+
|
|
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';
|
|
18
|
+
import { loadConfig } from './config.js';
|
|
19
|
+
|
|
20
|
+
/** Name of the per-workspace manifest file. */
|
|
21
|
+
const MANIFEST_FILE = 'nexusflow.json';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Derives the workspace directory path for a given branch name.
|
|
25
|
+
*
|
|
26
|
+
* @param workspacesDir - The root workspaces directory (e.g. ~/dev/workspaces).
|
|
27
|
+
* @param branchName - The feature branch name used as the workspace folder.
|
|
28
|
+
* @returns Absolute path to the workspace directory.
|
|
29
|
+
*/
|
|
30
|
+
export function getWorkspacePath(
|
|
31
|
+
workspacesDir: string,
|
|
32
|
+
branchName: string,
|
|
33
|
+
): string {
|
|
34
|
+
return path.join(workspacesDir, branchName);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Creates a full workspace for a feature:
|
|
39
|
+
* 1. Creates the workspace directory.
|
|
40
|
+
* 2. Creates a git worktree for every repo in the feature.
|
|
41
|
+
* 3. Saves the feature manifest (`nexusflow.json`).
|
|
42
|
+
*
|
|
43
|
+
* @param feature - The feature definition.
|
|
44
|
+
* @param repos - Resolved repo metadata for every repo in the feature.
|
|
45
|
+
* @returns The absolute path to the newly created workspace.
|
|
46
|
+
*/
|
|
47
|
+
export async function createWorkspace(
|
|
48
|
+
feature: Feature,
|
|
49
|
+
repos: RepoInfo[],
|
|
50
|
+
): Promise<string> {
|
|
51
|
+
const workspacePath = feature.workspacePath;
|
|
52
|
+
|
|
53
|
+
// Ensure the workspace directory exists.
|
|
54
|
+
await fs.mkdir(workspacePath, { recursive: true });
|
|
55
|
+
|
|
56
|
+
// Initialize git repository at workspace root to prevent AI assistants (like Claude)
|
|
57
|
+
// from climbing up to parent git repositories (main/master).
|
|
58
|
+
try {
|
|
59
|
+
await execa('git', ['init'], { cwd: workspacePath });
|
|
60
|
+
|
|
61
|
+
// Write a .gitignore to ignore the sub-repositories
|
|
62
|
+
const gitignoreContent = repos.map((repo) => `/${repo.name}/`).join('\n') + '\n';
|
|
63
|
+
await fs.writeFile(path.join(workspacePath, '.gitignore'), gitignoreContent, 'utf-8');
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// Silently ignore or log warning if git init fails
|
|
66
|
+
console.warn('Warning: Failed to initialize git repository at workspace root:', error);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Create .vscode/settings.json to allow VS Code search to query inside ignored sub-repos
|
|
70
|
+
try {
|
|
71
|
+
const vscodeDir = path.join(workspacePath, '.vscode');
|
|
72
|
+
await fs.mkdir(vscodeDir, { recursive: true });
|
|
73
|
+
const settings = {
|
|
74
|
+
"search.useIgnoreFiles": false
|
|
75
|
+
};
|
|
76
|
+
await fs.writeFile(
|
|
77
|
+
path.join(vscodeDir, 'settings.json'),
|
|
78
|
+
JSON.stringify(settings, null, 2) + '\n',
|
|
79
|
+
'utf-8'
|
|
80
|
+
);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.warn('Warning: Failed to create .vscode/settings.json:', error);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Create .cursor/mcp.json for workspace-local Cursor MCP configuration
|
|
86
|
+
try {
|
|
87
|
+
const cursorDir = path.join(workspacePath, '.cursor');
|
|
88
|
+
await fs.mkdir(cursorDir, { recursive: true });
|
|
89
|
+
const cursorMcp = {
|
|
90
|
+
"mcpServers": {
|
|
91
|
+
"nexusflow": {
|
|
92
|
+
"command": "npx",
|
|
93
|
+
"args": ["-y", "@mrpatronz/nexusflow", "mcp", "run", workspacePath]
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
await fs.writeFile(
|
|
98
|
+
path.join(cursorDir, 'mcp.json'),
|
|
99
|
+
JSON.stringify(cursorMcp, null, 2) + '\n',
|
|
100
|
+
'utf-8'
|
|
101
|
+
);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.warn('Warning: Failed to create .cursor/mcp.json:', error);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Create a worktree for each repo inside the workspace.
|
|
107
|
+
for (const repo of repos) {
|
|
108
|
+
const worktreeTarget = path.join(workspacePath, repo.name);
|
|
109
|
+
await createWorktree(
|
|
110
|
+
repo.path,
|
|
111
|
+
worktreeTarget,
|
|
112
|
+
feature.branchName,
|
|
113
|
+
repo.defaultBranch,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Persist the feature manifest.
|
|
118
|
+
await saveFeatureConfig(workspacePath, feature);
|
|
119
|
+
|
|
120
|
+
return workspacePath;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Lists all existing workspaces by reading `nexusflow.json` manifests from
|
|
125
|
+
* each subdirectory of {@link workspacesDir}.
|
|
126
|
+
*
|
|
127
|
+
* Directories that do not contain a valid manifest are silently skipped.
|
|
128
|
+
*
|
|
129
|
+
* @param workspacesDir - The root workspaces directory.
|
|
130
|
+
* @returns An array of {@link Feature} objects for each discovered workspace.
|
|
131
|
+
*/
|
|
132
|
+
export async function listWorkspaces(
|
|
133
|
+
workspacesDir: string,
|
|
134
|
+
): Promise<Feature[]> {
|
|
135
|
+
const features: Feature[] = [];
|
|
136
|
+
|
|
137
|
+
async function scan(dir: string, depth: number) {
|
|
138
|
+
if (depth > 3) return;
|
|
139
|
+
|
|
140
|
+
let entries: import('node:fs').Dirent[];
|
|
141
|
+
try {
|
|
142
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
143
|
+
} catch {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check if current directory contains a nexusflow.json manifest
|
|
148
|
+
const hasManifest = entries.some(
|
|
149
|
+
(e) => e.isFile() && e.name === MANIFEST_FILE,
|
|
150
|
+
);
|
|
151
|
+
if (hasManifest) {
|
|
152
|
+
const loaded = await loadFeatureConfig(dir);
|
|
153
|
+
if (loaded) {
|
|
154
|
+
features.push(loaded);
|
|
155
|
+
return; // Workspaces do not nest.
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Otherwise, recursively scan subdirectories
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
if (entry.isDirectory()) {
|
|
162
|
+
await scan(path.join(dir, entry.name), depth + 1);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
await scan(workspacesDir, 1);
|
|
168
|
+
return features;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Saves a {@link Feature} as `nexusflow.json` inside the given workspace.
|
|
173
|
+
*
|
|
174
|
+
* @param workspacePath - Absolute path to the workspace directory.
|
|
175
|
+
* @param feature - The feature definition to persist.
|
|
176
|
+
*/
|
|
177
|
+
export async function saveFeatureConfig(
|
|
178
|
+
workspacePath: string,
|
|
179
|
+
feature: Feature,
|
|
180
|
+
): Promise<void> {
|
|
181
|
+
const manifestPath = path.join(workspacePath, MANIFEST_FILE);
|
|
182
|
+
const data = JSON.stringify(feature, null, 2) + '\n';
|
|
183
|
+
await fs.writeFile(manifestPath, data, 'utf-8');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Loads a {@link Feature} from the `nexusflow.json` manifest inside a
|
|
188
|
+
* workspace directory.
|
|
189
|
+
*
|
|
190
|
+
* @param workspacePath - Absolute path to the workspace directory.
|
|
191
|
+
* @returns The loaded feature, or `null` if the manifest doesn't exist or is
|
|
192
|
+
* invalid.
|
|
193
|
+
*/
|
|
194
|
+
export async function loadFeatureConfig(
|
|
195
|
+
workspacePath: string,
|
|
196
|
+
): Promise<Feature | null> {
|
|
197
|
+
const rootDir = await findWorkspaceRoot(workspacePath);
|
|
198
|
+
if (!rootDir) return null;
|
|
199
|
+
|
|
200
|
+
const manifestPath = path.join(rootDir, MANIFEST_FILE);
|
|
201
|
+
try {
|
|
202
|
+
const raw = await fs.readFile(manifestPath, 'utf-8');
|
|
203
|
+
return JSON.parse(raw) as Feature;
|
|
204
|
+
} catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Traverses up parent directories to find a directory containing `nexusflow.json`.
|
|
211
|
+
*
|
|
212
|
+
* @param startPath - Path to start searching from.
|
|
213
|
+
* @returns Absolute path to the workspace root directory, or null if not found.
|
|
214
|
+
*/
|
|
215
|
+
export async function findWorkspaceRoot(startPath: string): Promise<string | null> {
|
|
216
|
+
let current = path.resolve(startPath);
|
|
217
|
+
while (true) {
|
|
218
|
+
const manifestPath = path.join(current, MANIFEST_FILE);
|
|
219
|
+
try {
|
|
220
|
+
await fs.access(manifestPath);
|
|
221
|
+
return current;
|
|
222
|
+
} catch {
|
|
223
|
+
const parent = path.dirname(current);
|
|
224
|
+
if (parent === current) {
|
|
225
|
+
break; // Reached root directory
|
|
226
|
+
}
|
|
227
|
+
current = parent;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Resolves a repo path to a full RepoInfo object.
|
|
235
|
+
*
|
|
236
|
+
* @param repoPath - Absolute path to the original repository.
|
|
237
|
+
*/
|
|
238
|
+
export async function resolveRepoInfo(repoPath: string): Promise<RepoInfo> {
|
|
239
|
+
const defaultBranch = await detectDefaultBranch(repoPath);
|
|
240
|
+
return {
|
|
241
|
+
name: path.basename(repoPath),
|
|
242
|
+
path: repoPath,
|
|
243
|
+
defaultBranch,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Deletes a workspace cleanly, removing all associated git worktrees first,
|
|
249
|
+
* then deleting the directory from disk.
|
|
250
|
+
*
|
|
251
|
+
* @param workspacePath - Absolute path to the workspace directory.
|
|
252
|
+
*/
|
|
253
|
+
export async function deleteWorkspace(
|
|
254
|
+
workspacePath: string,
|
|
255
|
+
): Promise<void> {
|
|
256
|
+
const feature = await loadFeatureConfig(workspacePath);
|
|
257
|
+
if (feature) {
|
|
258
|
+
const origRepos = feature.originalRepos || [];
|
|
259
|
+
for (let i = 0; i < feature.repos.length; i++) {
|
|
260
|
+
const worktreePath = feature.repos[i]!;
|
|
261
|
+
const originalPath = origRepos[i] || worktreePath;
|
|
262
|
+
const repoName = path.basename(worktreePath);
|
|
263
|
+
try {
|
|
264
|
+
await removeWorktree(originalPath, worktreePath, true);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
console.warn(`Warning: failed to remove worktree for ${repoName} in ${originalPath}:`, error);
|
|
267
|
+
try {
|
|
268
|
+
await execa('git', ['worktree', 'prune'], { cwd: originalPath });
|
|
269
|
+
} catch (pruneError) {
|
|
270
|
+
console.warn(`Warning: failed to prune worktrees in ${originalPath}:`, pruneError);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
// Manifest is missing. Try to detect worktrees by scanning subdirectories
|
|
276
|
+
try {
|
|
277
|
+
const entries = await fs.readdir(workspacePath, { withFileTypes: true });
|
|
278
|
+
for (const entry of entries) {
|
|
279
|
+
if (entry.isDirectory()) {
|
|
280
|
+
const subPath = path.join(workspacePath, entry.name);
|
|
281
|
+
const gitFilePath = path.join(subPath, '.git');
|
|
282
|
+
try {
|
|
283
|
+
const stat = await fs.stat(gitFilePath);
|
|
284
|
+
if (stat.isFile()) {
|
|
285
|
+
const content = await fs.readFile(gitFilePath, 'utf-8');
|
|
286
|
+
const match = content.match(/gitdir:\s*(.+)\.git\/worktrees/);
|
|
287
|
+
if (match && match[1]) {
|
|
288
|
+
const mainRepoPath = path.resolve(match[1].trim());
|
|
289
|
+
await removeWorktree(mainRepoPath, subPath, true);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
} catch {}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
} catch {}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Delete the directory itself
|
|
299
|
+
try {
|
|
300
|
+
await fs.rm(workspacePath, { recursive: true, force: true });
|
|
301
|
+
} catch (error) {
|
|
302
|
+
console.error(`Failed to delete workspace directory ${workspacePath}:`, error);
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Adds a repository to an existing workspace.
|
|
309
|
+
*
|
|
310
|
+
* @param workspacePath - Absolute path to the workspace directory.
|
|
311
|
+
* @param repoPath - Absolute path to the repository to add.
|
|
312
|
+
*/
|
|
313
|
+
export async function addRepoToWorkspace(
|
|
314
|
+
workspacePath: string,
|
|
315
|
+
repoPath: string,
|
|
316
|
+
): Promise<void> {
|
|
317
|
+
const feature = await loadFeatureConfig(workspacePath);
|
|
318
|
+
if (!feature) {
|
|
319
|
+
throw new Error(`Workspace manifest not found at ${workspacePath}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const newRepoInfo = await resolveRepoInfo(repoPath);
|
|
323
|
+
const worktreeTarget = path.join(workspacePath, newRepoInfo.name);
|
|
324
|
+
|
|
325
|
+
if (feature.repos.includes(worktreeTarget)) {
|
|
326
|
+
throw new Error(`Repository ${repoPath} is already in the workspace`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// 1. Create the worktree
|
|
330
|
+
await createWorktree(
|
|
331
|
+
newRepoInfo.path,
|
|
332
|
+
worktreeTarget,
|
|
333
|
+
feature.branchName,
|
|
334
|
+
newRepoInfo.defaultBranch,
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
// 2. Update manifest
|
|
338
|
+
feature.repos.push(worktreeTarget);
|
|
339
|
+
if (!feature.originalRepos) {
|
|
340
|
+
feature.originalRepos = [];
|
|
341
|
+
}
|
|
342
|
+
feature.originalRepos.push(repoPath);
|
|
343
|
+
await saveFeatureConfig(workspacePath, feature);
|
|
344
|
+
|
|
345
|
+
// 3. Update .gitignore at workspace root
|
|
346
|
+
try {
|
|
347
|
+
const gitignorePath = path.join(workspacePath, '.gitignore');
|
|
348
|
+
let gitignoreContent = '';
|
|
349
|
+
try {
|
|
350
|
+
gitignoreContent = await fs.readFile(gitignorePath, 'utf-8');
|
|
351
|
+
} catch {}
|
|
352
|
+
|
|
353
|
+
const entry = `/${newRepoInfo.name}/`;
|
|
354
|
+
if (!gitignoreContent.includes(entry)) {
|
|
355
|
+
gitignoreContent = gitignoreContent.trim() + '\n' + entry + '\n';
|
|
356
|
+
await fs.writeFile(gitignorePath, gitignoreContent, 'utf-8');
|
|
357
|
+
}
|
|
358
|
+
} catch (error) {
|
|
359
|
+
console.warn('Warning: Failed to update .gitignore:', error);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// 4. Re-run analysis, update configs, and repack workspace
|
|
363
|
+
const allRepos = await Promise.all(feature.repos.map(resolveRepoInfo));
|
|
364
|
+
const workspaceRepos = allRepos.map((repo) => ({
|
|
365
|
+
...repo,
|
|
366
|
+
path: path.join(workspacePath, repo.name),
|
|
367
|
+
}));
|
|
368
|
+
const analysis = await analyzeAllRepos(workspaceRepos);
|
|
369
|
+
const ctx: WorkspaceContext = {
|
|
370
|
+
feature,
|
|
371
|
+
repos: workspaceRepos,
|
|
372
|
+
analysis,
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
await generateContextFiles(ctx, feature.assistants, workspacePath);
|
|
376
|
+
const config = await loadConfig();
|
|
377
|
+
if (config.packContextXml) {
|
|
378
|
+
await packWorkspace(workspacePath);
|
|
379
|
+
}
|
|
380
|
+
}
|