@mrpatronz/nexusflow 0.2.18 → 0.2.19
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/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +37 -50
- package/dist/commands/sync.js.map +1 -1
- package/dist/core/sync.d.ts +44 -0
- package/dist/core/sync.d.ts.map +1 -0
- package/dist/core/sync.js +82 -0
- package/dist/core/sync.js.map +1 -0
- package/dist/core/workspace-state.d.ts +51 -0
- package/dist/core/workspace-state.d.ts.map +1 -0
- package/dist/core/workspace-state.js +108 -0
- package/dist/core/workspace-state.js.map +1 -0
- package/dist/core/workspace-state.test.d.ts +2 -0
- package/dist/core/workspace-state.test.d.ts.map +1 -0
- package/dist/core/workspace-state.test.js +84 -0
- package/dist/core/workspace-state.test.js.map +1 -0
- package/dist/gui/assets/index-Bdbld6yY.css +1 -0
- package/dist/gui/assets/index-D8iJPvnk.js +26 -0
- package/dist/gui/index.html +2 -2
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +42 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +94 -13
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +31 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/multi-git.d.ts +33 -8
- package/dist/utils/multi-git.d.ts.map +1 -1
- package/dist/utils/multi-git.js +91 -19
- package/dist/utils/multi-git.js.map +1 -1
- package/dist/utils/multi-git.test.d.ts +2 -0
- package/dist/utils/multi-git.test.d.ts.map +1 -0
- package/dist/utils/multi-git.test.js +125 -0
- package/dist/utils/multi-git.test.js.map +1 -0
- package/gui/e2e/dashboard.spec.ts +61 -0
- package/gui/e2e/wizard.spec.ts +2 -2
- package/gui/package-lock.json +62 -4
- package/gui/package.json +2 -1
- package/gui/src/App.tsx +168 -227
- package/gui/src/app/AppSidebar.tsx +69 -0
- package/gui/src/components/AddRepoPicker.tsx +81 -0
- package/gui/src/components/ui/Button.tsx +40 -0
- package/gui/src/components/ui/Card.tsx +6 -0
- package/gui/src/components/ui/EmptyState.tsx +22 -0
- package/gui/src/components/ui/Input.tsx +21 -0
- package/gui/src/components/ui/Menu.tsx +71 -0
- package/gui/src/components/ui/Modal.tsx +39 -0
- package/gui/src/components/ui/PageHeader.tsx +21 -0
- package/gui/src/components/ui/RepoStatusStrip.tsx +33 -0
- package/gui/src/components/ui/Skeleton.tsx +5 -0
- package/gui/src/components/ui/StatusPill.tsx +36 -0
- package/gui/src/components/ui/Tabs.tsx +40 -0
- package/gui/src/components/ui/cn.ts +4 -0
- package/gui/src/components/ui/index.ts +16 -0
- package/gui/src/features/services/ServiceConsole.tsx +4 -39
- package/gui/src/index.css +140 -125
- package/gui/src/lib/status.ts +24 -0
- package/gui/src/pages/DashboardPage.tsx +129 -0
- package/gui/src/pages/WorkspacesPage.tsx +352 -0
- package/gui/src/types.ts +19 -0
- package/package.json +1 -1
- package/src/commands/sync.ts +36 -58
- package/src/core/sync.ts +121 -0
- package/src/core/workspace-state.test.ts +108 -0
- package/src/core/workspace-state.ts +130 -0
- package/src/mcp/server.ts +44 -0
- package/src/server.ts +103 -15
- package/src/types.ts +36 -0
- package/src/utils/multi-git.test.ts +158 -0
- package/src/utils/multi-git.ts +117 -25
- package/dist/gui/assets/index-CB-jWded.css +0 -1
- package/dist/gui/assets/index-DDXpZZu3.js +0 -25
- package/gui/src/features/workspace/WorkspaceList.tsx +0 -666
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* Provides helpers for querying repo status, rebasing, committing, pushing,
|
|
6
6
|
* and generating diff summaries across all repos in a NexusFlow workspace.
|
|
7
7
|
*/
|
|
8
|
+
import type { SyncStatus } from '../types.js';
|
|
9
|
+
export type { SyncStatus };
|
|
8
10
|
/** Metadata about a single repo within a workspace. */
|
|
9
11
|
export interface WorkspaceRepo {
|
|
10
12
|
/** Directory name of the repo (e.g. 'api-gateway'). */
|
|
@@ -25,14 +27,32 @@ export interface RepoStatus {
|
|
|
25
27
|
/** Human-readable summary, e.g. '3 files changed'. */
|
|
26
28
|
summary: string;
|
|
27
29
|
}
|
|
28
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Result of a rebase operation.
|
|
32
|
+
*
|
|
33
|
+
* The classified {@link SyncStatus} values mean:
|
|
34
|
+
* - `up-to-date` — branch already contained the latest base commits.
|
|
35
|
+
* - `rebased` — new base commits were applied cleanly.
|
|
36
|
+
* - `conflict` — a genuine merge conflict; the rebase was aborted.
|
|
37
|
+
* - `stash-conflict` — the rebase succeeded but re-applying auto-stashed local
|
|
38
|
+
* changes conflicted; the stash is preserved for manual merge.
|
|
39
|
+
* - `error` — an infrastructure failure (network/auth on fetch, etc.) that
|
|
40
|
+
* is *not* a merge conflict.
|
|
41
|
+
*/
|
|
29
42
|
export interface RebaseResult {
|
|
30
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Whether the rebase itself completed. True for `up-to-date`, `rebased`, and
|
|
45
|
+
* `stash-conflict` (the rebase landed; only the stash pop needs attention).
|
|
46
|
+
*/
|
|
31
47
|
success: boolean;
|
|
48
|
+
/** Classified outcome. */
|
|
49
|
+
status: SyncStatus;
|
|
32
50
|
/** Human-readable outcome message. */
|
|
33
51
|
message: string;
|
|
34
|
-
/** Stderr output
|
|
52
|
+
/** Stderr output — populated only for `status === 'conflict'`. */
|
|
35
53
|
conflict?: string;
|
|
54
|
+
/** Whether dirty working-tree changes were auto-stashed during the operation. */
|
|
55
|
+
stashed?: boolean;
|
|
36
56
|
}
|
|
37
57
|
/** Result of a commit-and-push operation. */
|
|
38
58
|
export interface CommitResult {
|
|
@@ -74,15 +94,20 @@ export declare function getWorkspaceRepos(workspacePath: string): Promise<Worksp
|
|
|
74
94
|
*/
|
|
75
95
|
export declare function getRepoStatus(repoPath: string): Promise<RepoStatus>;
|
|
76
96
|
/**
|
|
77
|
-
* Rebases the current branch on top of the latest upstream base branch
|
|
97
|
+
* Rebases the current branch on top of the latest upstream base branch, while
|
|
98
|
+
* safely preserving any uncommitted local changes.
|
|
99
|
+
*
|
|
100
|
+
* Unlike a bare `git rebase`, this is safe to call from non-interactive agents
|
|
101
|
+
* against a *dirty* working tree: dirty changes are stashed first and restored
|
|
102
|
+
* afterwards. The outcome is classified (see {@link SyncStatus}) so that a dirty
|
|
103
|
+
* tree or a network failure is never mis-reported as a merge conflict.
|
|
78
104
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* returned.
|
|
105
|
+
* Sequence: `git fetch` → (stash if dirty) → `git rebase origin/{base}` →
|
|
106
|
+
* (pop the stash if one was made).
|
|
82
107
|
*
|
|
83
108
|
* @param repoPath - Absolute path to the repo root.
|
|
84
109
|
* @param baseBranch - The upstream branch to rebase onto (e.g. 'main').
|
|
85
|
-
* @returns
|
|
110
|
+
* @returns Classified result; `conflict` text is set only for real merge conflicts.
|
|
86
111
|
*/
|
|
87
112
|
export declare function rebaseRepo(repoPath: string, baseBranch: string): Promise<RebaseResult>;
|
|
88
113
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"multi-git.d.ts","sourceRoot":"","sources":["../../src/utils/multi-git.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"multi-git.d.ts","sourceRoot":"","sources":["../../src/utils/multi-git.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,KAAK,EAAW,UAAU,EAAE,MAAM,aAAa,CAAC;AAGvD,YAAY,EAAE,UAAU,EAAE,CAAC;AAI3B,uDAAuD;AACvD,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAC;IACb,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,8DAA8D;AAC9D,MAAM,WAAW,UAAU;IACzB,iEAAiE;IACjE,UAAU,EAAE,OAAO,CAAC;IACpB,0DAA0D;IAC1D,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB,0BAA0B;IAC1B,MAAM,EAAE,UAAU,CAAC;IACnB,sCAAsC;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,6CAA6C;AAC7C,MAAM,WAAW,YAAY;IAC3B,wDAAwD;IACxD,OAAO,EAAE,OAAO,CAAC;IACjB,yCAAyC;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,8CAA8C;IAC9C,YAAY,EAAE,MAAM,CAAC;IACrB,sCAAsC;IACtC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,gCAAgC;AAChC,MAAM,WAAW,WAAW;IAC1B,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,qCAAqC;IACrC,SAAS,EAAE,MAAM,CAAC;CACnB;AA8BD;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,aAAa,EAAE,CAAC,CAkB1B;AAED;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAwBzE;AAaD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,YAAY,CAAC,CA+FvB;AAED;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAC7B,OAAO,CAAC,YAAY,CAAC,CAmCvB;AAED;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAiC3E"}
|
package/dist/utils/multi-git.js
CHANGED
|
@@ -89,47 +89,119 @@ export async function getRepoStatus(repoPath) {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
/**
|
|
92
|
-
*
|
|
92
|
+
* Extracts the stderr (falling back to the message) from a thrown execa error.
|
|
93
|
+
*/
|
|
94
|
+
function errText(error) {
|
|
95
|
+
if (error instanceof Error && 'stderr' in error) {
|
|
96
|
+
const stderr = String(error.stderr).trim();
|
|
97
|
+
if (stderr)
|
|
98
|
+
return stderr;
|
|
99
|
+
}
|
|
100
|
+
return error instanceof Error ? error.message : String(error);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Rebases the current branch on top of the latest upstream base branch, while
|
|
104
|
+
* safely preserving any uncommitted local changes.
|
|
105
|
+
*
|
|
106
|
+
* Unlike a bare `git rebase`, this is safe to call from non-interactive agents
|
|
107
|
+
* against a *dirty* working tree: dirty changes are stashed first and restored
|
|
108
|
+
* afterwards. The outcome is classified (see {@link SyncStatus}) so that a dirty
|
|
109
|
+
* tree or a network failure is never mis-reported as a merge conflict.
|
|
93
110
|
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* returned.
|
|
111
|
+
* Sequence: `git fetch` → (stash if dirty) → `git rebase origin/{base}` →
|
|
112
|
+
* (pop the stash if one was made).
|
|
97
113
|
*
|
|
98
114
|
* @param repoPath - Absolute path to the repo root.
|
|
99
115
|
* @param baseBranch - The upstream branch to rebase onto (e.g. 'main').
|
|
100
|
-
* @returns
|
|
116
|
+
* @returns Classified result; `conflict` text is set only for real merge conflicts.
|
|
101
117
|
*/
|
|
102
118
|
export async function rebaseRepo(repoPath, baseBranch) {
|
|
119
|
+
// 1. Fetch latest from origin. A failure here is infrastructure (network/auth),
|
|
120
|
+
// not a conflict.
|
|
103
121
|
try {
|
|
104
|
-
// Fetch latest from origin.
|
|
105
122
|
await execa('git', ['fetch', 'origin'], { cwd: repoPath });
|
|
106
|
-
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
return {
|
|
126
|
+
success: false,
|
|
127
|
+
status: 'error',
|
|
128
|
+
message: `Fetch failed: ${errText(error).split('\n')[0]}`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
// 2. Stash dirty changes (including untracked) so the rebase has a clean tree.
|
|
132
|
+
let stashed = false;
|
|
133
|
+
const status = await getRepoStatus(repoPath);
|
|
134
|
+
if (status.hasChanges) {
|
|
135
|
+
try {
|
|
136
|
+
await execa('git', ['stash', 'push', '-u', '-m', 'nexusflow-autostash'], { cwd: repoPath });
|
|
137
|
+
stashed = true;
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
return {
|
|
141
|
+
success: false,
|
|
142
|
+
status: 'error',
|
|
143
|
+
message: `Failed to stash local changes: ${errText(error).split('\n')[0]}`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// 3. Attempt the rebase.
|
|
148
|
+
let rebaseStatus;
|
|
149
|
+
let rebaseMessage;
|
|
150
|
+
try {
|
|
107
151
|
const { stdout } = await execa('git', ['rebase', `origin/${baseBranch}`], { cwd: repoPath });
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
152
|
+
if (stdout.includes('up to date') || stdout.includes('up-to-date')) {
|
|
153
|
+
rebaseStatus = 'up-to-date';
|
|
154
|
+
rebaseMessage = 'Up to date';
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
rebaseStatus = 'rebased';
|
|
158
|
+
rebaseMessage = 'Rebased onto latest base';
|
|
159
|
+
}
|
|
115
160
|
}
|
|
116
161
|
catch (error) {
|
|
117
|
-
//
|
|
162
|
+
// Genuine merge conflict (or other rebase failure). Abort so the repo is
|
|
163
|
+
// never left mid-rebase, then restore the user's stashed work.
|
|
118
164
|
try {
|
|
119
165
|
await execa('git', ['rebase', '--abort'], { cwd: repoPath });
|
|
120
166
|
}
|
|
121
167
|
catch {
|
|
122
168
|
// Best-effort abort; ignore if it fails.
|
|
123
169
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
170
|
+
if (stashed) {
|
|
171
|
+
try {
|
|
172
|
+
await execa('git', ['stash', 'pop'], { cwd: repoPath });
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Stash pop after an abort should normally succeed (tree is unchanged);
|
|
176
|
+
// if it doesn't, the stash is preserved for manual recovery.
|
|
177
|
+
}
|
|
178
|
+
}
|
|
127
179
|
return {
|
|
128
180
|
success: false,
|
|
181
|
+
status: 'conflict',
|
|
129
182
|
message: 'Conflict during rebase',
|
|
130
|
-
conflict:
|
|
183
|
+
conflict: errText(error),
|
|
184
|
+
stashed,
|
|
131
185
|
};
|
|
132
186
|
}
|
|
187
|
+
// 4. Restore stashed local changes, if any.
|
|
188
|
+
if (stashed) {
|
|
189
|
+
try {
|
|
190
|
+
await execa('git', ['stash', 'pop'], { cwd: repoPath });
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
// The rebase landed, but re-applying local changes conflicts. `git stash
|
|
194
|
+
// pop` leaves the stash in place on conflict, so the work is not lost.
|
|
195
|
+
return {
|
|
196
|
+
success: true,
|
|
197
|
+
status: 'stash-conflict',
|
|
198
|
+
message: 'Rebased; local changes need manual merge — stash preserved',
|
|
199
|
+
conflict: errText(error),
|
|
200
|
+
stashed,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return { success: true, status: rebaseStatus, message: rebaseMessage, stashed };
|
|
133
205
|
}
|
|
134
206
|
/**
|
|
135
207
|
* Stages all changes, commits with the given message, and pushes to origin.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"multi-git.js","sourceRoot":"","sources":["../../src/utils/multi-git.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAG9B,OAAO,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"multi-git.js","sourceRoot":"","sources":["../../src/utils/multi-git.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAG9B,OAAO,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AA8E/C,iFAAiF;AAEjF;;;;;;GAMG;AACH,SAAS,eAAe,CAAC,UAAkB;IACzC,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,IAAI,SAAS,GAAG,CAAC,CAAC;IAElB,6CAA6C;IAC7C,sDAAsD;IACtD,MAAM,WAAW,GAAG,UAAU,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;IAE9D,MAAM,WAAW,GAAG,WAAW,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC;IAC3D,IAAI,WAAW;QAAE,SAAS,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAE1D,MAAM,WAAW,GAAG,WAAW,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC;IAC1D,IAAI,WAAW;QAAE,SAAS,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAE1D,OAAO,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;AAChC,CAAC;AAED,iFAAiF;AAEjF;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,aAAqB;IAErB,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,gBAAgB,CAAC,CAAC;IAChE,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IACrD,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAY,CAAC;IAE3C,OAAO,OAAO,CAAC,GAAG,CAChB,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE;QACnC,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACrC,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;QACvD,MAAM,aAAa,GAAG,MAAM,mBAAmB,CAAC,YAAY,CAAC,CAAC;QAC9D,OAAO;YACL,IAAI;YACJ,IAAI,EAAE,YAAY;YAClB,UAAU,EAAE,OAAO,CAAC,UAAU;YAC9B,aAAa;SACd,CAAC;IACJ,CAAC,CAAC,CACH,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,QAAgB;IAClD,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,aAAa,CAAC,EAAE;YAC/D,GAAG,EAAE,QAAQ;SACd,CAAC,CAAC;QAEH,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACxD,MAAM,YAAY,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QAE/D,OAAO;YACL,UAAU,EAAE,YAAY,CAAC,MAAM,GAAG,CAAC;YACnC,YAAY;YACZ,OAAO,EACL,YAAY,CAAC,MAAM,KAAK,CAAC;gBACvB,CAAC,CAAC,OAAO;gBACT,CAAC,CAAC,GAAG,YAAY,CAAC,MAAM,QAAQ,YAAY,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,UAAU;SACnF,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO;YACL,UAAU,EAAE,KAAK;YACjB,YAAY,EAAE,EAAE;YAChB,OAAO,EAAE,UAAU,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE;SAC5E,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;GAEG;AACH,SAAS,OAAO,CAAC,KAAc;IAC7B,IAAI,KAAK,YAAY,KAAK,IAAI,QAAQ,IAAI,KAAK,EAAE,CAAC;QAChD,MAAM,MAAM,GAAG,MAAM,CAAE,KAA6B,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QACpE,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC;IAC5B,CAAC;IACD,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAChE,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,QAAgB,EAChB,UAAkB;IAElB,gFAAgF;IAChF,qBAAqB;IACrB,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC7D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO;YACL,OAAO,EAAE,KAAK;YACd,MAAM,EAAE,OAAO;YACf,OAAO,EAAE,iBAAiB,OAAO,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE;SAC1D,CAAC;IACJ,CAAC;IAED,+EAA+E;IAC/E,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,QAAQ,CAAC,CAAC;IAC7C,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;QACtB,IAAI,CAAC;YACH,MAAM,KAAK,CACT,KAAK,EACL,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,qBAAqB,CAAC,EACpD,EAAE,GAAG,EAAE,QAAQ,EAAE,CAClB,CAAC;YACF,OAAO,GAAG,IAAI,CAAC;QACjB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO;gBACL,OAAO,EAAE,KAAK;gBACd,MAAM,EAAE,OAAO;gBACf,OAAO,EAAE,kCAAkC,OAAO,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE;aAC3E,CAAC;QACJ,CAAC;IACH,CAAC;IAED,yBAAyB;IACzB,IAAI,YAAwB,CAAC;IAC7B,IAAI,aAAqB,CAAC;IAC1B,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,KAAK,CAC5B,KAAK,EACL,CAAC,QAAQ,EAAE,UAAU,UAAU,EAAE,CAAC,EAClC,EAAE,GAAG,EAAE,QAAQ,EAAE,CAClB,CAAC;QAEF,IAAI,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;YACnE,YAAY,GAAG,YAAY,CAAC;YAC5B,aAAa,GAAG,YAAY,CAAC;QAC/B,CAAC;aAAM,CAAC;YACN,YAAY,GAAG,SAAS,CAAC;YACzB,aAAa,GAAG,0BAA0B,CAAC;QAC7C,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,yEAAyE;QACzE,+DAA+D;QAC/D,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,SAAS,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC/D,CAAC;QAAC,MAAM,CAAC;YACP,yCAAyC;QAC3C,CAAC;QAED,IAAI,OAAO,EAAE,CAAC;YACZ,IAAI,CAAC;gBACH,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;YAC1D,CAAC;YAAC,MAAM,CAAC;gBACP,wEAAwE;gBACxE,6DAA6D;YAC/D,CAAC;QACH,CAAC;QAED,OAAO;YACL,OAAO,EAAE,KAAK;YACd,MAAM,EAAE,UAAU;YAClB,OAAO,EAAE,wBAAwB;YACjC,QAAQ,EAAE,OAAO,CAAC,KAAK,CAAC;YACxB,OAAO;SACR,CAAC;IACJ,CAAC;IAED,4CAA4C;IAC5C,IAAI,OAAO,EAAE,CAAC;QACZ,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC1D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,yEAAyE;YACzE,uEAAuE;YACvE,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,MAAM,EAAE,gBAAgB;gBACxB,OAAO,EAAE,4DAA4D;gBACrE,QAAQ,EAAE,OAAO,CAAC,KAAK,CAAC;gBACxB,OAAO;aACR,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC;AAClF,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,QAAgB,EAChB,OAAe,EACf,UAAkB,EAClB,OAA8B;IAE9B,IAAI,CAAC;QACH,oBAAoB;QACpB,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;QAEpD,UAAU;QACV,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,KAAK,CAC1C,KAAK,EACL,CAAC,QAAQ,EAAE,IAAI,EAAE,OAAO,CAAC,EACzB,EAAE,GAAG,EAAE,QAAQ,EAAE,CAClB,CAAC;QAEF,2EAA2E;QAC3E,MAAM,SAAS,GAAG,YAAY,CAAC,KAAK,CAAC,4BAA4B,CAAC,CAAC;QACnE,MAAM,UAAU,GAAG,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAEjD,4CAA4C;QAC5C,MAAM,SAAS,GAAG,YAAY,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;QACrD,MAAM,YAAY,GAAG,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAEhE,yBAAyB;QACzB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;YACrB,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,UAAU,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;QACxE,CAAC;QAED,MAAM,MAAM,GAAG,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,sBAAsB,CAAC;QACtE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IACtE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO;YACL,OAAO,EAAE,KAAK;YACd,UAAU,EAAE,EAAE;YACd,YAAY,EAAE,CAAC;YACf,OAAO,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;SAChE,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,QAAgB;IACnD,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE;YAClE,GAAG,EAAE,QAAQ;SACd,CAAC,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,KAAK,CACpC,KAAK,EACL,CAAC,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAC,EAC9B,EAAE,GAAG,EAAE,QAAQ,EAAE,CAClB,CAAC;QAEF,MAAM,CAAC,WAAW,EAAE,WAAW,CAAC,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;QAC7D,MAAM,CAAC,SAAS,EAAE,SAAS,CAAC,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;QAEvD,MAAM,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC;QAC1C,MAAM,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC;QAE1C,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,IAAI,QAAQ,CAAC,IAAI,EAAE;YAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;QACjD,IAAI,MAAM,CAAC,IAAI,EAAE;YAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QAE7C,OAAO;YACL,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,YAAY;YACzC,SAAS;YACT,SAAS;SACV,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO;YACL,OAAO,EAAE,UAAU,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE;YAC3E,SAAS,EAAE,CAAC;YACZ,SAAS,EAAE,CAAC;SACb,CAAC;IACJ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multi-git.test.d.ts","sourceRoot":"","sources":["../../src/utils/multi-git.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { execa } from 'execa';
|
|
3
|
+
import { rebaseRepo } from './multi-git.js';
|
|
4
|
+
vi.mock('execa');
|
|
5
|
+
/** Builds an execa-style error carrying stderr (as execa does on non-zero exit). */
|
|
6
|
+
function gitError(stderr) {
|
|
7
|
+
return Object.assign(new Error('git failed'), { stderr });
|
|
8
|
+
}
|
|
9
|
+
/** Resolves a single mocked `git` call by subcommand. */
|
|
10
|
+
async function routeGit(a, opts) {
|
|
11
|
+
const sub = a[0];
|
|
12
|
+
if (sub === 'fetch') {
|
|
13
|
+
if (opts.fetchError)
|
|
14
|
+
throw opts.fetchError;
|
|
15
|
+
return { stdout: '' };
|
|
16
|
+
}
|
|
17
|
+
if (sub === 'status') {
|
|
18
|
+
return { stdout: opts.statusStdout ?? '' };
|
|
19
|
+
}
|
|
20
|
+
if (sub === 'stash' && a[1] === 'push') {
|
|
21
|
+
if (opts.stashPushError)
|
|
22
|
+
throw opts.stashPushError;
|
|
23
|
+
return { stdout: '' };
|
|
24
|
+
}
|
|
25
|
+
if (sub === 'stash' && a[1] === 'pop') {
|
|
26
|
+
if (opts.stashPopError)
|
|
27
|
+
throw opts.stashPopError;
|
|
28
|
+
return { stdout: '' };
|
|
29
|
+
}
|
|
30
|
+
if (sub === 'rebase' && a[1] === '--abort') {
|
|
31
|
+
return { stdout: '' };
|
|
32
|
+
}
|
|
33
|
+
if (sub === 'rebase') {
|
|
34
|
+
if (opts.rebaseError)
|
|
35
|
+
throw opts.rebaseError;
|
|
36
|
+
return { stdout: opts.rebaseStdout ?? 'Successfully rebased.' };
|
|
37
|
+
}
|
|
38
|
+
return { stdout: '' };
|
|
39
|
+
}
|
|
40
|
+
/** Routes mocked `git` calls by subcommand so each test declares only what matters. */
|
|
41
|
+
function mockGit(opts) {
|
|
42
|
+
vi.mocked(execa).mockImplementation(((_file, args) => routeGit(args, opts)));
|
|
43
|
+
}
|
|
44
|
+
/** Returns the recorded execa calls as arrays of git args. */
|
|
45
|
+
function gitCalls() {
|
|
46
|
+
return vi.mocked(execa).mock.calls.map((c) => c[1]);
|
|
47
|
+
}
|
|
48
|
+
describe('rebaseRepo', () => {
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
vi.clearAllMocks();
|
|
51
|
+
});
|
|
52
|
+
it('reports "rebased" for a clean tree with new base commits', async () => {
|
|
53
|
+
mockGit({ statusStdout: '', rebaseStdout: 'Successfully rebased and updated.' });
|
|
54
|
+
const result = await rebaseRepo('/repo', 'main');
|
|
55
|
+
expect(result.success).toBe(true);
|
|
56
|
+
expect(result.status).toBe('rebased');
|
|
57
|
+
expect(result.stashed).toBeFalsy();
|
|
58
|
+
// No stash should be created for a clean tree.
|
|
59
|
+
expect(gitCalls().some((c) => c[0] === 'stash')).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
it('reports "up-to-date" when already current', async () => {
|
|
62
|
+
mockGit({ statusStdout: '', rebaseStdout: 'Current branch feature is up to date.' });
|
|
63
|
+
const result = await rebaseRepo('/repo', 'main');
|
|
64
|
+
expect(result.success).toBe(true);
|
|
65
|
+
expect(result.status).toBe('up-to-date');
|
|
66
|
+
});
|
|
67
|
+
it('auto-stashes a dirty tree and restores it (the regression fix)', async () => {
|
|
68
|
+
mockGit({ statusStdout: ' M src/app.ts\n', rebaseStdout: 'Successfully rebased.' });
|
|
69
|
+
const result = await rebaseRepo('/repo', 'main');
|
|
70
|
+
expect(result.success).toBe(true);
|
|
71
|
+
expect(result.status).toBe('rebased');
|
|
72
|
+
expect(result.stashed).toBe(true);
|
|
73
|
+
const calls = gitCalls();
|
|
74
|
+
// Stash push (with untracked) before rebase, stash pop after.
|
|
75
|
+
expect(calls).toContainEqual(['stash', 'push', '-u', '-m', 'nexusflow-autostash']);
|
|
76
|
+
expect(calls).toContainEqual(['stash', 'pop']);
|
|
77
|
+
});
|
|
78
|
+
it('classifies a fetch failure as "error", not a conflict', async () => {
|
|
79
|
+
mockGit({ fetchError: gitError('fatal: unable to access origin: Could not resolve host') });
|
|
80
|
+
const result = await rebaseRepo('/repo', 'main');
|
|
81
|
+
expect(result.success).toBe(false);
|
|
82
|
+
expect(result.status).toBe('error');
|
|
83
|
+
expect(result.message).toMatch(/^Fetch failed:/);
|
|
84
|
+
expect(result.conflict).toBeUndefined();
|
|
85
|
+
// Should never attempt a rebase if fetch failed.
|
|
86
|
+
expect(gitCalls().some((c) => c[0] === 'rebase')).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
it('classifies a real merge conflict as "conflict" and aborts', async () => {
|
|
89
|
+
mockGit({
|
|
90
|
+
statusStdout: '',
|
|
91
|
+
rebaseError: gitError('CONFLICT (content): Merge conflict in src/app.ts'),
|
|
92
|
+
});
|
|
93
|
+
const result = await rebaseRepo('/repo', 'main');
|
|
94
|
+
expect(result.success).toBe(false);
|
|
95
|
+
expect(result.status).toBe('conflict');
|
|
96
|
+
expect(result.conflict).toContain('CONFLICT');
|
|
97
|
+
expect(gitCalls()).toContainEqual(['rebase', '--abort']);
|
|
98
|
+
});
|
|
99
|
+
it('restores the stash after aborting a conflicting rebase on a dirty tree', async () => {
|
|
100
|
+
mockGit({
|
|
101
|
+
statusStdout: ' M src/app.ts\n',
|
|
102
|
+
rebaseError: gitError('CONFLICT (content): Merge conflict'),
|
|
103
|
+
});
|
|
104
|
+
const result = await rebaseRepo('/repo', 'main');
|
|
105
|
+
expect(result.status).toBe('conflict');
|
|
106
|
+
expect(result.stashed).toBe(true);
|
|
107
|
+
const calls = gitCalls();
|
|
108
|
+
expect(calls).toContainEqual(['rebase', '--abort']);
|
|
109
|
+
expect(calls).toContainEqual(['stash', 'pop']); // local work restored
|
|
110
|
+
});
|
|
111
|
+
it('reports "stash-conflict" when the rebase lands but the stash pop conflicts', async () => {
|
|
112
|
+
mockGit({
|
|
113
|
+
statusStdout: ' M src/app.ts\n',
|
|
114
|
+
rebaseStdout: 'Successfully rebased.',
|
|
115
|
+
stashPopError: gitError('CONFLICT (content): Merge conflict in src/app.ts'),
|
|
116
|
+
});
|
|
117
|
+
const result = await rebaseRepo('/repo', 'main');
|
|
118
|
+
// The rebase itself succeeded, so success is true, but it needs attention.
|
|
119
|
+
expect(result.success).toBe(true);
|
|
120
|
+
expect(result.status).toBe('stash-conflict');
|
|
121
|
+
expect(result.stashed).toBe(true);
|
|
122
|
+
expect(result.message).toMatch(/stash preserved/i);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
//# sourceMappingURL=multi-git.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multi-git.test.js","sourceRoot":"","sources":["../../src/utils/multi-git.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { test, expect, type Route } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
/** Minimal API mocks so the redesigned shell can render without a backend. */
|
|
4
|
+
async function mockApi(page: import('@playwright/test').Page) {
|
|
5
|
+
const feature = {
|
|
6
|
+
id: 'feature-x',
|
|
7
|
+
branchName: 'feature-x',
|
|
8
|
+
description: 'Test feature workspace',
|
|
9
|
+
repos: ['C:/dev/api-gateway'],
|
|
10
|
+
assistants: [],
|
|
11
|
+
workspacePath: 'C:/ws/feature-x',
|
|
12
|
+
createdAt: '2026-06-01T00:00:00.000Z',
|
|
13
|
+
};
|
|
14
|
+
const json = (body: unknown) => (route: Route) =>
|
|
15
|
+
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(body) });
|
|
16
|
+
|
|
17
|
+
await page.route('**/api/config', json({
|
|
18
|
+
exists: true,
|
|
19
|
+
config: { version: '0.2.19', devDir: 'C:/dev', workspacesDir: 'C:/ws', defaultAssistant: 'claude', scanDepth: 2 },
|
|
20
|
+
}));
|
|
21
|
+
await page.route('**/api/workspaces', json([feature]));
|
|
22
|
+
await page.route('**/api/workspaces/status', json({
|
|
23
|
+
'feature-x': { id: 'feature-x', branchName: 'feature-x', changedFiles: 3, dirtyRepos: 1, runningServices: 0, syncStatus: 'up-to-date', pendingValidation: false },
|
|
24
|
+
}));
|
|
25
|
+
await page.route('**/api/ai-detect', json([]));
|
|
26
|
+
await page.route('**/api/editor-detect', json([]));
|
|
27
|
+
await page.route('**/api/update-status', json({ updateAvailable: false, currentVersion: '0.2.19', latestVersion: '0.2.19' }));
|
|
28
|
+
await page.route('**/api/workflows/templates', json({ templates: [] }));
|
|
29
|
+
await page.route('**/api/repos', json([]));
|
|
30
|
+
await page.route('**/api/workspace/*/services', json({ services: [], orchestrationTools: [], runningState: [] }));
|
|
31
|
+
await page.route('**/api/workspace/*/changes', json({ changes: [] }));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test.describe('Redesigned NexusFlow shell', () => {
|
|
35
|
+
test('shows the dashboard overview with environment stats', async ({ page }) => {
|
|
36
|
+
await mockApi(page);
|
|
37
|
+
await page.goto('/');
|
|
38
|
+
|
|
39
|
+
await expect(page.getByRole('heading', { name: 'Overview' })).toBeVisible();
|
|
40
|
+
// Stat tiles + workspace row driven by the mocked status endpoint.
|
|
41
|
+
await expect(page.getByText('With uncommitted changes')).toBeVisible();
|
|
42
|
+
await expect(page.getByText('feature-x').first()).toBeVisible();
|
|
43
|
+
await expect(page.getByText('3 changed').first()).toBeVisible();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('navigates to the workspaces master-detail and opens a workspace via deep link', async ({ page }) => {
|
|
47
|
+
await mockApi(page);
|
|
48
|
+
|
|
49
|
+
// Sidebar navigation updates the route.
|
|
50
|
+
await page.goto('/');
|
|
51
|
+
await page.getByRole('button', { name: 'Workspaces' }).click();
|
|
52
|
+
await expect(page).toHaveURL(/#\/workspaces/);
|
|
53
|
+
await expect(page.getByRole('heading', { name: 'Workspaces' })).toBeVisible();
|
|
54
|
+
|
|
55
|
+
// Deep link selects a workspace and shows the detail tabs.
|
|
56
|
+
await page.goto('/#/workspaces/feature-x');
|
|
57
|
+
await expect(page.getByRole('heading', { name: 'feature-x' })).toBeVisible();
|
|
58
|
+
await expect(page.getByRole('tab', { name: 'Overview' })).toBeVisible();
|
|
59
|
+
await expect(page.getByText('Test feature workspace')).toBeVisible();
|
|
60
|
+
});
|
|
61
|
+
});
|
package/gui/e2e/wizard.spec.ts
CHANGED
|
@@ -224,8 +224,8 @@ test.describe('NexusFlow E2E GUI Tests', () => {
|
|
|
224
224
|
// 3. Open dashboard
|
|
225
225
|
await page.goto('/');
|
|
226
226
|
|
|
227
|
-
// 4. Click New Workspace in
|
|
228
|
-
await page.
|
|
227
|
+
// 4. Click New Workspace in the sidebar (scoped to avoid matching page-level buttons)
|
|
228
|
+
await page.getByRole('complementary').getByRole('button', { name: 'New workspace' }).click();
|
|
229
229
|
|
|
230
230
|
// 5. Fill Details
|
|
231
231
|
await page.getByPlaceholder('e.g., feature/oauth-authentication-flow').fill('feat-test-branch');
|
package/gui/package-lock.json
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"lucide-react": "^1.20.0",
|
|
13
13
|
"react": "^19.2.7",
|
|
14
14
|
"react-dom": "^19.2.7",
|
|
15
|
+
"react-router-dom": "^7.18.1",
|
|
15
16
|
"tailwindcss": "^4.3.1"
|
|
16
17
|
},
|
|
17
18
|
"devDependencies": {
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
"globals": "^17.6.0",
|
|
28
29
|
"typescript": "~6.0.2",
|
|
29
30
|
"typescript-eslint": "^8.61.1",
|
|
30
|
-
"vite": "^8.
|
|
31
|
+
"vite": "^8.1.2"
|
|
31
32
|
}
|
|
32
33
|
},
|
|
33
34
|
"node_modules/@babel/code-frame": {
|
|
@@ -1556,6 +1557,19 @@
|
|
|
1556
1557
|
"dev": true,
|
|
1557
1558
|
"license": "MIT"
|
|
1558
1559
|
},
|
|
1560
|
+
"node_modules/cookie": {
|
|
1561
|
+
"version": "1.1.1",
|
|
1562
|
+
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
|
1563
|
+
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
|
1564
|
+
"license": "MIT",
|
|
1565
|
+
"engines": {
|
|
1566
|
+
"node": ">=18"
|
|
1567
|
+
},
|
|
1568
|
+
"funding": {
|
|
1569
|
+
"type": "opencollective",
|
|
1570
|
+
"url": "https://opencollective.com/express"
|
|
1571
|
+
}
|
|
1572
|
+
},
|
|
1559
1573
|
"node_modules/cross-spawn": {
|
|
1560
1574
|
"version": "7.0.6",
|
|
1561
1575
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
|
@@ -2680,6 +2694,44 @@
|
|
|
2680
2694
|
"react": "^19.2.7"
|
|
2681
2695
|
}
|
|
2682
2696
|
},
|
|
2697
|
+
"node_modules/react-router": {
|
|
2698
|
+
"version": "7.18.1",
|
|
2699
|
+
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz",
|
|
2700
|
+
"integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==",
|
|
2701
|
+
"license": "MIT",
|
|
2702
|
+
"dependencies": {
|
|
2703
|
+
"cookie": "^1.0.1",
|
|
2704
|
+
"set-cookie-parser": "^2.6.0"
|
|
2705
|
+
},
|
|
2706
|
+
"engines": {
|
|
2707
|
+
"node": ">=20.0.0"
|
|
2708
|
+
},
|
|
2709
|
+
"peerDependencies": {
|
|
2710
|
+
"react": ">=18",
|
|
2711
|
+
"react-dom": ">=18"
|
|
2712
|
+
},
|
|
2713
|
+
"peerDependenciesMeta": {
|
|
2714
|
+
"react-dom": {
|
|
2715
|
+
"optional": true
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
},
|
|
2719
|
+
"node_modules/react-router-dom": {
|
|
2720
|
+
"version": "7.18.1",
|
|
2721
|
+
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz",
|
|
2722
|
+
"integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==",
|
|
2723
|
+
"license": "MIT",
|
|
2724
|
+
"dependencies": {
|
|
2725
|
+
"react-router": "7.18.1"
|
|
2726
|
+
},
|
|
2727
|
+
"engines": {
|
|
2728
|
+
"node": ">=20.0.0"
|
|
2729
|
+
},
|
|
2730
|
+
"peerDependencies": {
|
|
2731
|
+
"react": ">=18",
|
|
2732
|
+
"react-dom": ">=18"
|
|
2733
|
+
}
|
|
2734
|
+
},
|
|
2683
2735
|
"node_modules/rolldown": {
|
|
2684
2736
|
"version": "1.1.3",
|
|
2685
2737
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.3.tgz",
|
|
@@ -2729,6 +2781,12 @@
|
|
|
2729
2781
|
"semver": "bin/semver.js"
|
|
2730
2782
|
}
|
|
2731
2783
|
},
|
|
2784
|
+
"node_modules/set-cookie-parser": {
|
|
2785
|
+
"version": "2.7.2",
|
|
2786
|
+
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
|
2787
|
+
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
|
2788
|
+
"license": "MIT"
|
|
2789
|
+
},
|
|
2732
2790
|
"node_modules/shebang-command": {
|
|
2733
2791
|
"version": "2.0.0",
|
|
2734
2792
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
|
@@ -2916,9 +2974,9 @@
|
|
|
2916
2974
|
}
|
|
2917
2975
|
},
|
|
2918
2976
|
"node_modules/vite": {
|
|
2919
|
-
"version": "8.1.
|
|
2920
|
-
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.
|
|
2921
|
-
"integrity": "sha512-
|
|
2977
|
+
"version": "8.1.2",
|
|
2978
|
+
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.2.tgz",
|
|
2979
|
+
"integrity": "sha512-6YYPbRXTxx6bRXmOn7XdnQAy5DQNHhDgtjhDHI13oe4pY93kkcdGJWxpGwOm++/Wh0QpQhDrpIoVMrmrsI5AGQ==",
|
|
2922
2980
|
"license": "MIT",
|
|
2923
2981
|
"dependencies": {
|
|
2924
2982
|
"lightningcss": "^1.32.0",
|
package/gui/package.json
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"lucide-react": "^1.20.0",
|
|
17
17
|
"react": "^19.2.7",
|
|
18
18
|
"react-dom": "^19.2.7",
|
|
19
|
+
"react-router-dom": "^7.18.1",
|
|
19
20
|
"tailwindcss": "^4.3.1"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
|
@@ -31,6 +32,6 @@
|
|
|
31
32
|
"globals": "^17.6.0",
|
|
32
33
|
"typescript": "~6.0.2",
|
|
33
34
|
"typescript-eslint": "^8.61.1",
|
|
34
|
-
"vite": "^8.
|
|
35
|
+
"vite": "^8.1.2"
|
|
35
36
|
}
|
|
36
37
|
}
|