@mrpatronz/nexusflow 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/dependabot.yml +25 -0
- package/.github/workflows/release.yml +90 -0
- package/.vscode/launch.json +17 -0
- package/.vscode/tasks.json +17 -0
- package/README.md +1 -1
- package/dist/commands/commit.d.ts +18 -0
- package/dist/commands/commit.d.ts.map +1 -0
- package/dist/commands/commit.js +105 -0
- package/dist/commands/commit.js.map +1 -0
- package/dist/commands/diff.d.ts +11 -0
- package/dist/commands/diff.d.ts.map +1 -0
- package/dist/commands/diff.js +101 -0
- package/dist/commands/diff.js.map +1 -0
- package/dist/commands/sync.d.ts +11 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +96 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/generators/base.d.ts.map +1 -1
- package/dist/generators/base.js +3 -0
- package/dist/generators/base.js.map +1 -1
- package/dist/generators/index.d.ts +5 -2
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +88 -38
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/plan-generator.d.ts +40 -0
- package/dist/generators/plan-generator.d.ts.map +1 -0
- package/dist/generators/plan-generator.js +312 -0
- package/dist/generators/plan-generator.js.map +1 -0
- package/dist/gui/assets/index-B9Ph2bHH.css +2 -0
- package/dist/gui/assets/index-s4nRkpqY.js +21 -0
- package/dist/gui/index.html +2 -2
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +141 -1
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/multi-git.d.ts +108 -0
- package/dist/utils/multi-git.d.ts.map +1 -0
- package/dist/utils/multi-git.js +206 -0
- package/dist/utils/multi-git.js.map +1 -0
- package/extension/package-lock.json +2811 -0
- package/extension/package.json +61 -0
- package/extension/src/extension.ts +168 -0
- package/extension/tsconfig.json +19 -0
- package/gui/src/App.tsx +377 -24
- package/gui/src/features/changes/ChangesViewer.tsx +212 -0
- package/gui/src/features/knowledge/KnowledgeBase.tsx +84 -0
- package/gui/src/features/onboarding/OnboardingWizard.tsx +230 -0
- package/gui/src/features/plan/ImplementationPlan.tsx +32 -0
- package/gui/src/features/services/ServiceConsole.tsx +159 -0
- package/gui/src/features/sessions/SessionHistory.tsx +195 -0
- package/gui/src/features/workspace/WorkspaceBuilder.tsx +414 -0
- package/gui/src/features/workspace/WorkspaceList.tsx +382 -0
- package/gui/src/types.ts +61 -0
- package/package.json +6 -4
- package/src/commands/commit.ts +131 -0
- package/src/commands/diff.ts +125 -0
- package/src/commands/sync.ts +110 -0
- package/src/generators/base.ts +3 -0
- package/src/generators/index.ts +95 -40
- package/src/generators/plan-generator.ts +390 -0
- package/src/index.ts +59 -0
- package/src/server.ts +152 -1
- package/src/types.ts +17 -0
- package/src/utils/multi-git.ts +306 -0
- package/dist/gui/assets/index-Cq9V485S.js +0 -21
- package/dist/gui/assets/index-Dh1b5gSZ.css +0 -2
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module plan-generator
|
|
3
|
+
* Analyzes inter-repo dependencies within a workspace and generates a
|
|
4
|
+
* `nexusflow-plan.md` implementation plan with build-order phases.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import fse from 'fs-extra';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import type {
|
|
11
|
+
WorkspaceContext,
|
|
12
|
+
ProjectAnalysis,
|
|
13
|
+
RepoInfo,
|
|
14
|
+
DependencyNode,
|
|
15
|
+
DependencyGraph,
|
|
16
|
+
} from '../types.js';
|
|
17
|
+
|
|
18
|
+
// ─── Constants ────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/** Repo-name substrings that signal a shared/foundation package. */
|
|
21
|
+
const SHARED_PACKAGE_KEYWORDS = ['shared', 'common', 'contracts', 'types'];
|
|
22
|
+
|
|
23
|
+
/** Project types that act as backend producers. */
|
|
24
|
+
const BACKEND_TYPES = ['api', 'backend', 'service'];
|
|
25
|
+
|
|
26
|
+
/** Project types that act as frontend consumers. */
|
|
27
|
+
const FRONTEND_TYPES = ['frontend', 'webapp', 'app'];
|
|
28
|
+
|
|
29
|
+
// ─── Dependency Graph Builder ─────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build a dependency graph by analysing package dependencies, API
|
|
33
|
+
* relationships and shared-package conventions across the workspace repos.
|
|
34
|
+
*
|
|
35
|
+
* @param analysis Per-repo analysis results, keyed by repo path.
|
|
36
|
+
* @param repos Metadata for every repo in the workspace.
|
|
37
|
+
* @returns A map of repo name → {@link DependencyNode}.
|
|
38
|
+
*/
|
|
39
|
+
export function buildDependencyGraph(
|
|
40
|
+
analysis: Map<string, ProjectAnalysis>,
|
|
41
|
+
repos: RepoInfo[],
|
|
42
|
+
): DependencyGraph {
|
|
43
|
+
const graph: DependencyGraph = new Map();
|
|
44
|
+
|
|
45
|
+
// ── Initialise a node for each repo ──────────────────────────────────
|
|
46
|
+
for (const repo of repos) {
|
|
47
|
+
graph.set(repo.name, {
|
|
48
|
+
repoName: repo.name,
|
|
49
|
+
repoPath: repo.path,
|
|
50
|
+
dependsOn: [],
|
|
51
|
+
dependedOnBy: [],
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Build a quick lookup: repo name → ProjectAnalysis
|
|
56
|
+
const analysisByName = new Map<string, ProjectAnalysis>();
|
|
57
|
+
for (const repo of repos) {
|
|
58
|
+
const a = analysis.get(repo.path);
|
|
59
|
+
if (a) analysisByName.set(repo.name, a);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const repoNames = new Set(repos.map((r) => r.name));
|
|
63
|
+
|
|
64
|
+
// ── 1. Shared-package dependencies ───────────────────────────────────
|
|
65
|
+
for (const repo of repos) {
|
|
66
|
+
const a = analysisByName.get(repo.name);
|
|
67
|
+
if (!a) continue;
|
|
68
|
+
|
|
69
|
+
for (const dep of a.dependencies) {
|
|
70
|
+
// Direct name match — e.g. "@acme/shared-contracts" contains "shared-contracts"
|
|
71
|
+
for (const otherName of repoNames) {
|
|
72
|
+
if (otherName === repo.name) continue;
|
|
73
|
+
if (dep.name === otherName || dep.name.includes(otherName)) {
|
|
74
|
+
addEdge(graph, repo.name, otherName);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── 2. API relationships (frontend → backend heuristic) ─────────────
|
|
81
|
+
for (const repoA of repos) {
|
|
82
|
+
const analysisA = analysisByName.get(repoA.name);
|
|
83
|
+
if (!analysisA) continue;
|
|
84
|
+
|
|
85
|
+
const typeA = analysisA.techStack.projectType;
|
|
86
|
+
|
|
87
|
+
if (!BACKEND_TYPES.includes(typeA)) continue;
|
|
88
|
+
|
|
89
|
+
for (const repoB of repos) {
|
|
90
|
+
if (repoB.name === repoA.name) continue;
|
|
91
|
+
|
|
92
|
+
const analysisB = analysisByName.get(repoB.name);
|
|
93
|
+
if (!analysisB) continue;
|
|
94
|
+
|
|
95
|
+
const typeB = analysisB.techStack.projectType;
|
|
96
|
+
if (FRONTEND_TYPES.includes(typeB)) {
|
|
97
|
+
// B (frontend) depends on A (backend)
|
|
98
|
+
addEdge(graph, repoB.name, repoA.name);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── 3. Shared-type packages are always foundation ───────────────────
|
|
104
|
+
for (const repo of repos) {
|
|
105
|
+
const isShared = SHARED_PACKAGE_KEYWORDS.some((kw) =>
|
|
106
|
+
repo.name.toLowerCase().includes(kw),
|
|
107
|
+
);
|
|
108
|
+
if (!isShared) continue;
|
|
109
|
+
|
|
110
|
+
const node = graph.get(repo.name);
|
|
111
|
+
if (!node) continue;
|
|
112
|
+
|
|
113
|
+
// Ensure no outgoing deps (it's a leaf producer)
|
|
114
|
+
node.dependsOn = [];
|
|
115
|
+
|
|
116
|
+
// Every other repo that doesn't already depend on it — add edge
|
|
117
|
+
for (const other of repos) {
|
|
118
|
+
if (other.name === repo.name) continue;
|
|
119
|
+
addEdge(graph, other.name, repo.name);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return graph;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── Topological Sort ─────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Topologically sort the dependency graph into build phases.
|
|
130
|
+
* Each phase is a group of repos that can be built in parallel
|
|
131
|
+
* because all of their dependencies appear in earlier phases.
|
|
132
|
+
*
|
|
133
|
+
* If a cycle is detected, the remaining nodes are placed in a final phase
|
|
134
|
+
* with a warning logged to the console.
|
|
135
|
+
*
|
|
136
|
+
* @param graph The workspace dependency graph.
|
|
137
|
+
* @returns An array of phases, where each phase is an array of repo names.
|
|
138
|
+
*/
|
|
139
|
+
export function topologicalSort(graph: DependencyGraph): string[][] {
|
|
140
|
+
// Calculate in-degrees
|
|
141
|
+
const inDegree = new Map<string, number>();
|
|
142
|
+
for (const [name, node] of graph) {
|
|
143
|
+
inDegree.set(name, node.dependsOn.length);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const phases: string[][] = [];
|
|
147
|
+
const placed = new Set<string>();
|
|
148
|
+
|
|
149
|
+
while (placed.size < graph.size) {
|
|
150
|
+
// Collect nodes whose in-degree is 0 and haven't been placed yet
|
|
151
|
+
const phase: string[] = [];
|
|
152
|
+
for (const [name, degree] of inDegree) {
|
|
153
|
+
if (degree === 0 && !placed.has(name)) {
|
|
154
|
+
phase.push(name);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Cycle detection — no zero-in-degree nodes remain
|
|
159
|
+
if (phase.length === 0) {
|
|
160
|
+
const remaining = [...graph.keys()].filter((n) => !placed.has(n));
|
|
161
|
+
console.log(
|
|
162
|
+
chalk.yellow(' ⚠'),
|
|
163
|
+
`Dependency cycle detected among: ${remaining.join(', ')}`,
|
|
164
|
+
);
|
|
165
|
+
phases.push(remaining);
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
phase.sort(); // Deterministic ordering within a phase
|
|
170
|
+
phases.push(phase);
|
|
171
|
+
|
|
172
|
+
// "Remove" placed nodes and decrement dependents' in-degrees
|
|
173
|
+
for (const name of phase) {
|
|
174
|
+
placed.add(name);
|
|
175
|
+
const node = graph.get(name)!;
|
|
176
|
+
for (const dependent of node.dependedOnBy) {
|
|
177
|
+
inDegree.set(dependent, (inDegree.get(dependent) ?? 1) - 1);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return phases;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ─── Plan Generator ───────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Generate a `nexusflow-plan.md` implementation plan for the workspace.
|
|
189
|
+
*
|
|
190
|
+
* The plan includes:
|
|
191
|
+
* - A Mermaid dependency diagram
|
|
192
|
+
* - Phased implementation order derived from topological sort
|
|
193
|
+
* - A dependency cross-reference table
|
|
194
|
+
*
|
|
195
|
+
* @param ctx The current workspace context (feature + repos + analysis).
|
|
196
|
+
* @param workspacePath Absolute path to the workspace root directory.
|
|
197
|
+
*/
|
|
198
|
+
export async function generateImplementationPlan(
|
|
199
|
+
ctx: WorkspaceContext,
|
|
200
|
+
workspacePath: string,
|
|
201
|
+
): Promise<void> {
|
|
202
|
+
try {
|
|
203
|
+
const { feature, repos, analysis } = ctx;
|
|
204
|
+
|
|
205
|
+
// ── Fallback: no analysis available ─────────────────────────────────
|
|
206
|
+
if (!analysis || analysis.size === 0) {
|
|
207
|
+
const lines = [
|
|
208
|
+
`# Implementation Plan — ${feature.id}`,
|
|
209
|
+
'',
|
|
210
|
+
'> Auto-generated by NexusFlow.',
|
|
211
|
+
'> No project analysis data was available, so repos are listed alphabetically.',
|
|
212
|
+
'',
|
|
213
|
+
'## Repos',
|
|
214
|
+
'',
|
|
215
|
+
...repos
|
|
216
|
+
.map((r) => r.name)
|
|
217
|
+
.sort()
|
|
218
|
+
.map((n) => `- ${n}`),
|
|
219
|
+
'',
|
|
220
|
+
];
|
|
221
|
+
await fse.outputFile(
|
|
222
|
+
path.join(workspacePath, 'nexusflow-plan.md'),
|
|
223
|
+
lines.join('\n'),
|
|
224
|
+
);
|
|
225
|
+
console.log(chalk.green(' ✔'), 'Generated nexusflow-plan.md');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── Build graph & sort ──────────────────────────────────────────────
|
|
230
|
+
const graph = buildDependencyGraph(analysis, repos);
|
|
231
|
+
const phases = topologicalSort(graph);
|
|
232
|
+
|
|
233
|
+
// ── Render markdown ─────────────────────────────────────────────────
|
|
234
|
+
const md: string[] = [];
|
|
235
|
+
|
|
236
|
+
md.push(`# Implementation Plan — ${feature.id}`);
|
|
237
|
+
md.push('');
|
|
238
|
+
md.push(
|
|
239
|
+
'> Auto-generated by NexusFlow based on dependency analysis between repos.',
|
|
240
|
+
);
|
|
241
|
+
md.push(
|
|
242
|
+
'> Follow the phase order to avoid blocking yourself on cross-repo dependencies.',
|
|
243
|
+
);
|
|
244
|
+
md.push('');
|
|
245
|
+
|
|
246
|
+
// ── Mermaid diagram ─────────────────────────────────────────────────
|
|
247
|
+
md.push('## Dependency Diagram');
|
|
248
|
+
md.push('');
|
|
249
|
+
md.push('```mermaid');
|
|
250
|
+
md.push('graph TD');
|
|
251
|
+
|
|
252
|
+
const alias = buildAliasMap(graph);
|
|
253
|
+
|
|
254
|
+
for (const [name, node] of graph) {
|
|
255
|
+
if (node.dependsOn.length === 0 && node.dependedOnBy.length === 0) {
|
|
256
|
+
// Isolated node — still show it
|
|
257
|
+
md.push(` ${alias.get(name)}["${name}"]`);
|
|
258
|
+
}
|
|
259
|
+
for (const dep of node.dependsOn) {
|
|
260
|
+
// Arrow: dependency → dependent (dep is built first)
|
|
261
|
+
md.push(
|
|
262
|
+
` ${alias.get(dep)}["${dep}"] --> ${alias.get(name)}["${name}"]`,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
md.push('```');
|
|
268
|
+
md.push('');
|
|
269
|
+
|
|
270
|
+
// ── Phase descriptions ──────────────────────────────────────────────
|
|
271
|
+
md.push('## Suggested Implementation Order');
|
|
272
|
+
md.push('');
|
|
273
|
+
|
|
274
|
+
const phaseLabels = [
|
|
275
|
+
'Foundation',
|
|
276
|
+
'Core Services',
|
|
277
|
+
'Integration Layer',
|
|
278
|
+
'Consumers',
|
|
279
|
+
'Final',
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
for (let i = 0; i < phases.length; i++) {
|
|
283
|
+
const phase = phases[i];
|
|
284
|
+
const label = phaseLabels[Math.min(i, phaseLabels.length - 1)];
|
|
285
|
+
const ordinal = ordinalWord(i + 1);
|
|
286
|
+
|
|
287
|
+
md.push(`### Phase ${i + 1}: ${label}`);
|
|
288
|
+
md.push('');
|
|
289
|
+
md.push(`**Repos:** ${phase.join(', ')}`);
|
|
290
|
+
md.push('');
|
|
291
|
+
|
|
292
|
+
if (i === 0) {
|
|
293
|
+
md.push(
|
|
294
|
+
`**Why first:** These repos have no dependencies on other workspace repos. Other repos depend on them.`,
|
|
295
|
+
);
|
|
296
|
+
} else if (i === phases.length - 1) {
|
|
297
|
+
md.push(
|
|
298
|
+
`**Why ${ordinal}:** Depends on APIs and types from earlier phases.`,
|
|
299
|
+
);
|
|
300
|
+
} else {
|
|
301
|
+
const prevPhases = phases
|
|
302
|
+
.slice(0, i)
|
|
303
|
+
.flat()
|
|
304
|
+
.join(', ');
|
|
305
|
+
md.push(
|
|
306
|
+
`**Why ${ordinal}:** Depends on Phase ${i === 1 ? '1' : `1–${i}`} repos (${prevPhases}). Build these before the consumers.`,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
md.push('');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ── Dependency table ────────────────────────────────────────────────
|
|
314
|
+
md.push('## Dependency Table');
|
|
315
|
+
md.push('');
|
|
316
|
+
md.push('| Repo | Depends On | Depended On By |');
|
|
317
|
+
md.push('|:---|:---|:---|');
|
|
318
|
+
|
|
319
|
+
// Sort repos by phase order for a natural reading experience
|
|
320
|
+
const orderedNames = phases.flat();
|
|
321
|
+
for (const name of orderedNames) {
|
|
322
|
+
const node = graph.get(name)!;
|
|
323
|
+
const deps = node.dependsOn.length > 0 ? node.dependsOn.join(', ') : '—';
|
|
324
|
+
const rdeps =
|
|
325
|
+
node.dependedOnBy.length > 0 ? node.dependedOnBy.join(', ') : '—';
|
|
326
|
+
md.push(`| ${name} | ${deps} | ${rdeps} |`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
md.push('');
|
|
330
|
+
|
|
331
|
+
// ── Write file ──────────────────────────────────────────────────────
|
|
332
|
+
const outPath = path.join(workspacePath, 'nexusflow-plan.md');
|
|
333
|
+
await fse.outputFile(outPath, md.join('\n'));
|
|
334
|
+
console.log(chalk.green(' ✔'), 'Generated nexusflow-plan.md');
|
|
335
|
+
} catch (error) {
|
|
336
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
337
|
+
console.error(
|
|
338
|
+
chalk.red(' ✖'),
|
|
339
|
+
`Failed to generate implementation plan: ${message}`,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Add a directed edge: `from` depends on `to`.
|
|
348
|
+
* Idempotent — duplicate edges are ignored.
|
|
349
|
+
*/
|
|
350
|
+
function addEdge(graph: DependencyGraph, from: string, to: string): void {
|
|
351
|
+
const fromNode = graph.get(from);
|
|
352
|
+
const toNode = graph.get(to);
|
|
353
|
+
if (!fromNode || !toNode) return;
|
|
354
|
+
|
|
355
|
+
if (!fromNode.dependsOn.includes(to)) {
|
|
356
|
+
fromNode.dependsOn.push(to);
|
|
357
|
+
}
|
|
358
|
+
if (!toNode.dependedOnBy.includes(from)) {
|
|
359
|
+
toNode.dependedOnBy.push(from);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Build a short single-letter alias map for Mermaid node IDs.
|
|
365
|
+
* Falls back to sanitised names when there are more than 26 repos.
|
|
366
|
+
*/
|
|
367
|
+
function buildAliasMap(graph: DependencyGraph): Map<string, string> {
|
|
368
|
+
const map = new Map<string, string>();
|
|
369
|
+
const names = [...graph.keys()].sort();
|
|
370
|
+
|
|
371
|
+
if (names.length <= 26) {
|
|
372
|
+
let code = 65; // 'A'
|
|
373
|
+
for (const name of names) {
|
|
374
|
+
map.set(name, String.fromCharCode(code++));
|
|
375
|
+
}
|
|
376
|
+
} else {
|
|
377
|
+
for (const name of names) {
|
|
378
|
+
map.set(name, name.replace(/[^a-zA-Z0-9]/g, '_'));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return map;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Return an ordinal word for small numbers, or "nth" for larger ones. */
|
|
386
|
+
function ordinalWord(n: number): string {
|
|
387
|
+
const words = ['first', 'second', 'third', 'fourth', 'fifth'];
|
|
388
|
+
if (n >= 1 && n <= words.length) return words[n - 1];
|
|
389
|
+
return `${n}th`;
|
|
390
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,10 @@ import { stopCommand } from './commands/stop.js';
|
|
|
20
20
|
import { logsCommand } from './commands/logs.js';
|
|
21
21
|
import { statusCommand } from './commands/status.js';
|
|
22
22
|
import { uiCommand } from './commands/ui.js';
|
|
23
|
+
import { syncCommand } from './commands/sync.js';
|
|
24
|
+
import { commitCommand } from './commands/commit.js';
|
|
25
|
+
import { diffCommand } from './commands/diff.js';
|
|
26
|
+
|
|
23
27
|
|
|
24
28
|
const program = new Command();
|
|
25
29
|
|
|
@@ -175,4 +179,59 @@ program
|
|
|
175
179
|
}
|
|
176
180
|
});
|
|
177
181
|
|
|
182
|
+
program
|
|
183
|
+
.command('sync')
|
|
184
|
+
.description('Sync all repositories in a workspace')
|
|
185
|
+
.argument('[workspace]', 'Path to workspace (auto-detects from CWD)')
|
|
186
|
+
.action(async (workspace?: string) => {
|
|
187
|
+
try {
|
|
188
|
+
await syncCommand(workspace);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (error instanceof Error && error.message.includes('User force closed')) {
|
|
191
|
+
console.log('\nCancelled.');
|
|
192
|
+
process.exit(0);
|
|
193
|
+
}
|
|
194
|
+
console.error(error);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
program
|
|
200
|
+
.command('commit')
|
|
201
|
+
.description('Commit changes across all repositories in a workspace')
|
|
202
|
+
.argument('[workspace]', 'Path to workspace (auto-detects from CWD)')
|
|
203
|
+
.requiredOption('-m, --message <msg>', 'Commit message')
|
|
204
|
+
.option('--no-push', 'Stage and commit changes without pushing to remote')
|
|
205
|
+
.option('--dry-run', 'Preview changes without committing')
|
|
206
|
+
.action(async (workspace: string | undefined, options: { message: string; noPush?: boolean; dryRun?: boolean }) => {
|
|
207
|
+
try {
|
|
208
|
+
await commitCommand(options.message, workspace, options);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (error instanceof Error && error.message.includes('User force closed')) {
|
|
211
|
+
console.log('\nCancelled.');
|
|
212
|
+
process.exit(0);
|
|
213
|
+
}
|
|
214
|
+
console.error(error);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
program
|
|
220
|
+
.command('diff')
|
|
221
|
+
.description('Display a unified summary of changes across all repositories')
|
|
222
|
+
.argument('[workspace]', 'Path to workspace (auto-detects from CWD)')
|
|
223
|
+
.action(async (workspace?: string) => {
|
|
224
|
+
try {
|
|
225
|
+
await diffCommand(workspace);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (error instanceof Error && error.message.includes('User force closed')) {
|
|
228
|
+
console.log('\nCancelled.');
|
|
229
|
+
process.exit(0);
|
|
230
|
+
}
|
|
231
|
+
console.error(error);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
178
236
|
program.parse();
|
|
237
|
+
|
package/src/server.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { generateContextFiles } from './generators/index.js';
|
|
|
20
20
|
import { detectAIAssistants } from './utils/detect-ai.js';
|
|
21
21
|
import { detectEditors } from './utils/detect-editors.js';
|
|
22
22
|
import { findSessions, getSessionTranscript } from './utils/session-finder.js';
|
|
23
|
+
import { getWorkspaceRepos, rebaseRepo, commitAndPush, getRepoStatus } from './utils/multi-git.js';
|
|
23
24
|
import {
|
|
24
25
|
detectAllServices,
|
|
25
26
|
detectOrchestrationTools,
|
|
@@ -301,6 +302,27 @@ app.get('/api/workspace/:id/changes', async (c) => {
|
|
|
301
302
|
const { stdout } = await execa('git', ['status', '--porcelain'], { cwd: worktreePath });
|
|
302
303
|
const lines = stdout.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
303
304
|
|
|
305
|
+
// Get numstat to determine additions and deletions per file
|
|
306
|
+
const numstatMap = new Map<string, { additions: number; deletions: number }>();
|
|
307
|
+
try {
|
|
308
|
+
const { stdout: numstatRaw } = await execa('git', ['diff', 'HEAD', '--numstat'], {
|
|
309
|
+
cwd: worktreePath,
|
|
310
|
+
});
|
|
311
|
+
const numstatLines = numstatRaw.split('\n').filter(Boolean);
|
|
312
|
+
for (const numLine of numstatLines) {
|
|
313
|
+
const parts = numLine.trim().split(/\s+/);
|
|
314
|
+
if (parts.length >= 3) {
|
|
315
|
+
const [add, del, file] = parts;
|
|
316
|
+
numstatMap.set(file, {
|
|
317
|
+
additions: add === '-' ? 0 : parseInt(add, 10) || 0,
|
|
318
|
+
deletions: del === '-' ? 0 : parseInt(del, 10) || 0,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
} catch (e) {
|
|
323
|
+
// Ignore diff errors
|
|
324
|
+
}
|
|
325
|
+
|
|
304
326
|
const files = lines.map((line) => {
|
|
305
327
|
const status = line.slice(0, 2).trim();
|
|
306
328
|
const file = line.slice(2).trim();
|
|
@@ -309,7 +331,15 @@ app.get('/api/workspace/:id/changes', async (c) => {
|
|
|
309
331
|
if (status === 'A' || status === '??') type = 'added';
|
|
310
332
|
else if (status === 'D') type = 'deleted';
|
|
311
333
|
|
|
312
|
-
|
|
334
|
+
const stats = numstatMap.get(file) || { additions: 0, deletions: 0 };
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
file,
|
|
338
|
+
type,
|
|
339
|
+
rawStatus: status,
|
|
340
|
+
additions: stats.additions,
|
|
341
|
+
deletions: stats.deletions,
|
|
342
|
+
};
|
|
313
343
|
});
|
|
314
344
|
|
|
315
345
|
results.push({
|
|
@@ -334,6 +364,127 @@ app.get('/api/workspace/:id/changes', async (c) => {
|
|
|
334
364
|
}
|
|
335
365
|
});
|
|
336
366
|
|
|
367
|
+
// 13a. Get workspace knowledge (nexusflow-knowledge.md)
|
|
368
|
+
app.get('/api/workspace/:id/knowledge', async (c) => {
|
|
369
|
+
try {
|
|
370
|
+
const id = c.req.param('id');
|
|
371
|
+
const config = await loadConfig();
|
|
372
|
+
const workspacePath = path.join(config.workspacesDir, id);
|
|
373
|
+
const knowledgeFile = path.join(workspacePath, 'nexusflow-knowledge.md');
|
|
374
|
+
|
|
375
|
+
let content = '';
|
|
376
|
+
try {
|
|
377
|
+
content = await fs.readFile(knowledgeFile, 'utf-8');
|
|
378
|
+
} catch {
|
|
379
|
+
content = '# Workspace Knowledge\n\nNo knowledge file yet.';
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return c.json({ content });
|
|
383
|
+
} catch (error) {
|
|
384
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
385
|
+
return c.json({ error: msg }, 500);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// 13b. Update workspace knowledge (nexusflow-knowledge.md)
|
|
390
|
+
app.put('/api/workspace/:id/knowledge', async (c) => {
|
|
391
|
+
try {
|
|
392
|
+
const id = c.req.param('id');
|
|
393
|
+
const { content } = await c.req.json() as { content: string };
|
|
394
|
+
const config = await loadConfig();
|
|
395
|
+
const workspacePath = path.join(config.workspacesDir, id);
|
|
396
|
+
const knowledgeFile = path.join(workspacePath, 'nexusflow-knowledge.md');
|
|
397
|
+
|
|
398
|
+
await fs.writeFile(knowledgeFile, content, 'utf-8');
|
|
399
|
+
return c.json({ success: true });
|
|
400
|
+
} catch (error) {
|
|
401
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
402
|
+
return c.json({ error: msg }, 500);
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// 13c. Get workspace plan (nexusflow-plan.md)
|
|
407
|
+
app.get('/api/workspace/:id/plan', async (c) => {
|
|
408
|
+
try {
|
|
409
|
+
const id = c.req.param('id');
|
|
410
|
+
const config = await loadConfig();
|
|
411
|
+
const workspacePath = path.join(config.workspacesDir, id);
|
|
412
|
+
const planFile = path.join(workspacePath, 'nexusflow-plan.md');
|
|
413
|
+
|
|
414
|
+
let content = '';
|
|
415
|
+
try {
|
|
416
|
+
content = await fs.readFile(planFile, 'utf-8');
|
|
417
|
+
} catch {
|
|
418
|
+
content = '# Workspace Plan\n\nNo implementation plan file yet.';
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return c.json({ content });
|
|
422
|
+
} catch (error) {
|
|
423
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
424
|
+
return c.json({ error: msg }, 500);
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// 13d. Sync all repositories in workspace
|
|
429
|
+
app.post('/api/workspace/:id/sync', async (c) => {
|
|
430
|
+
try {
|
|
431
|
+
const id = c.req.param('id');
|
|
432
|
+
const config = await loadConfig();
|
|
433
|
+
const workspacePath = path.join(config.workspacesDir, id);
|
|
434
|
+
|
|
435
|
+
const repos = await getWorkspaceRepos(workspacePath);
|
|
436
|
+
const results = [];
|
|
437
|
+
|
|
438
|
+
for (const repo of repos) {
|
|
439
|
+
const result = await rebaseRepo(repo.path, 'main');
|
|
440
|
+
results.push({
|
|
441
|
+
repoName: repo.name,
|
|
442
|
+
success: result.success,
|
|
443
|
+
message: result.message,
|
|
444
|
+
conflict: result.conflict,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return c.json({ results });
|
|
449
|
+
} catch (error) {
|
|
450
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
451
|
+
return c.json({ error: msg }, 500);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// 13e. Commit changes in all repositories in workspace
|
|
456
|
+
app.post('/api/workspace/:id/commit', async (c) => {
|
|
457
|
+
try {
|
|
458
|
+
const id = c.req.param('id');
|
|
459
|
+
const { message } = await c.req.json() as { message: string };
|
|
460
|
+
const config = await loadConfig();
|
|
461
|
+
const workspacePath = path.join(config.workspacesDir, id);
|
|
462
|
+
|
|
463
|
+
const repos = await getWorkspaceRepos(workspacePath);
|
|
464
|
+
const results = [];
|
|
465
|
+
|
|
466
|
+
for (const repo of repos) {
|
|
467
|
+
const status = await getRepoStatus(repo.path);
|
|
468
|
+
if (status.hasChanges) {
|
|
469
|
+
const result = await commitAndPush(repo.path, message, repo.branchName);
|
|
470
|
+
results.push({
|
|
471
|
+
repoName: repo.name,
|
|
472
|
+
success: result.success,
|
|
473
|
+
commitHash: result.commitHash,
|
|
474
|
+
filesChanged: result.filesChanged,
|
|
475
|
+
message: result.message,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return c.json({ results });
|
|
481
|
+
} catch (error) {
|
|
482
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
483
|
+
return c.json({ error: msg }, 500);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
|
|
337
488
|
// 14. Resume session in workspace (copies CLI resume command and opens editor)
|
|
338
489
|
app.post('/api/workspace/:id/resume', async (c) => {
|
|
339
490
|
try {
|
package/src/types.ts
CHANGED
|
@@ -292,3 +292,20 @@ export interface RunningState {
|
|
|
292
292
|
/** Timestamp when the state was last updated. */
|
|
293
293
|
updatedAt: string;
|
|
294
294
|
}
|
|
295
|
+
|
|
296
|
+
// ─── Phase 3: Dependency Graph Types ──────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
/** A node in the workspace dependency graph. */
|
|
299
|
+
export interface DependencyNode {
|
|
300
|
+
/** Repository name. */
|
|
301
|
+
repoName: string;
|
|
302
|
+
/** Absolute path to the repository. */
|
|
303
|
+
repoPath: string;
|
|
304
|
+
/** Names of repos this one depends on. */
|
|
305
|
+
dependsOn: string[];
|
|
306
|
+
/** Names of repos that depend on this one. */
|
|
307
|
+
dependedOnBy: string[];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** The full dependency graph for a workspace. */
|
|
311
|
+
export type DependencyGraph = Map<string, DependencyNode>;
|