@nemus-cli/nemus 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +23 -5
- package/bin/workspace.js +12 -0
- package/dist/commands/analyze-deps.js +5 -12
- package/dist/commands/archive.js +13 -18
- package/dist/commands/branch/create.js +5 -12
- package/dist/commands/branch/switch.js +14 -25
- package/dist/commands/cache/manager.js +13 -24
- package/dist/commands/cleanup.js +14 -26
- package/dist/commands/configure-claude.js +4 -8
- package/dist/commands/configure.js +38 -32
- package/dist/commands/dashboard/session-picker.js +19 -26
- package/dist/commands/dashboard/workspace-picker.js +19 -26
- package/dist/commands/delete.js +15 -30
- package/dist/commands/ghq-status.js +7 -12
- package/dist/commands/go.js +18 -27
- package/dist/commands/history.js +6 -13
- package/dist/commands/list.js +20 -29
- package/dist/commands/prune.js +196 -0
- package/dist/commands/remove-repo.js +21 -29
- package/dist/commands/save-context.js +5 -10
- package/dist/commands/sessions.js +19 -25
- package/dist/commands/suite/create.js +27 -53
- package/dist/commands/suite/delete.js +13 -25
- package/dist/commands/suite/export.js +20 -36
- package/dist/commands/suite/import.js +9 -20
- package/dist/commands/suite/use.js +9 -17
- package/dist/program.js +2 -0
- package/dist/utils/prompt.js +26 -0
- package/dist/utils/prompts.js +86 -118
- package/dist/utils/prune.js +70 -0
- package/package.json +3 -6
- package/scripts/release-notes.mjs +54 -0
- package/skills/config.md +17 -0
- package/skills/nemus/SKILL.md +7 -2
- package/skills/nemus/references/completion.md +22 -0
- package/skills/nemus/references/config.md +32 -0
- package/skills/nemus/references/prune.md +44 -0
- package/skills/nemus/references/reflect.md +43 -0
- package/skills/nemus/references/save-context.md +25 -0
- package/skills/prune-workspaces.md +23 -0
- package/skills/reflect.md +21 -0
- package/src/commands/analyze-deps.ts +5 -9
- package/src/commands/archive.ts +5 -7
- package/src/commands/branch/create.ts +5 -9
- package/src/commands/branch/switch.ts +14 -22
- package/src/commands/cache/manager.ts +13 -21
- package/src/commands/cleanup.ts +14 -23
- package/src/commands/configure-claude.ts +4 -5
- package/src/commands/configure.ts +35 -29
- package/src/commands/dashboard/session-picker.ts +6 -11
- package/src/commands/dashboard/workspace-picker.ts +6 -11
- package/src/commands/delete.test.ts +36 -42
- package/src/commands/delete.ts +15 -27
- package/src/commands/ghq-status.ts +5 -7
- package/src/commands/go.ts +18 -25
- package/src/commands/history.ts +6 -10
- package/src/commands/list.test.ts +19 -26
- package/src/commands/list.ts +24 -31
- package/src/commands/prune.ts +183 -0
- package/src/commands/remove-repo.ts +11 -17
- package/src/commands/save-context.ts +3 -5
- package/src/commands/sessions.ts +6 -10
- package/src/commands/suite/create.ts +28 -50
- package/src/commands/suite/delete.ts +14 -23
- package/src/commands/suite/export.ts +20 -33
- package/src/commands/suite/import.ts +9 -17
- package/src/commands/suite/use.ts +9 -14
- package/src/program.ts +2 -0
- package/src/utils/prompt.ts +16 -0
- package/src/utils/prompts.test.ts +70 -41
- package/src/utils/prompts.ts +98 -128
- package/src/utils/prune.test.ts +121 -0
- package/src/utils/prune.ts +109 -0
|
@@ -4,12 +4,12 @@ const {
|
|
|
4
4
|
mockRm,
|
|
5
5
|
mockListWorkspaces,
|
|
6
6
|
mockPromptMultiWorkspaceSelection,
|
|
7
|
-
|
|
7
|
+
mockConfirm,
|
|
8
8
|
} = vi.hoisted(() => ({
|
|
9
9
|
mockRm: vi.fn(),
|
|
10
10
|
mockListWorkspaces: vi.fn(),
|
|
11
11
|
mockPromptMultiWorkspaceSelection: vi.fn(),
|
|
12
|
-
|
|
12
|
+
mockConfirm: vi.fn(),
|
|
13
13
|
}));
|
|
14
14
|
|
|
15
15
|
vi.mock('fs/promises', () => ({
|
|
@@ -39,10 +39,8 @@ vi.mock('../utils/colors', () => ({
|
|
|
39
39
|
colorize: (text: string) => text,
|
|
40
40
|
}));
|
|
41
41
|
|
|
42
|
-
vi.mock('
|
|
43
|
-
|
|
44
|
-
prompt: mockPrompt,
|
|
45
|
-
},
|
|
42
|
+
vi.mock('../utils/prompt', () => ({
|
|
43
|
+
confirm: mockConfirm,
|
|
46
44
|
}));
|
|
47
45
|
|
|
48
46
|
import { main } from './delete';
|
|
@@ -82,9 +80,9 @@ describe('delete-workspace main', () => {
|
|
|
82
80
|
|
|
83
81
|
mockPromptMultiWorkspaceSelection.mockResolvedValueOnce(['ws-a']);
|
|
84
82
|
|
|
85
|
-
|
|
86
|
-
.mockResolvedValueOnce(
|
|
87
|
-
.mockResolvedValueOnce(
|
|
83
|
+
mockConfirm
|
|
84
|
+
.mockResolvedValueOnce(true) // confirm deletion
|
|
85
|
+
.mockResolvedValueOnce(false); // don't delete more
|
|
88
86
|
|
|
89
87
|
await main();
|
|
90
88
|
|
|
@@ -99,9 +97,9 @@ describe('delete-workspace main', () => {
|
|
|
99
97
|
|
|
100
98
|
mockPromptMultiWorkspaceSelection.mockResolvedValueOnce(['ws-a', 'ws-b']);
|
|
101
99
|
|
|
102
|
-
|
|
103
|
-
.mockResolvedValueOnce(
|
|
104
|
-
.mockResolvedValueOnce(
|
|
100
|
+
mockConfirm
|
|
101
|
+
.mockResolvedValueOnce(true)
|
|
102
|
+
.mockResolvedValueOnce(false);
|
|
105
103
|
|
|
106
104
|
await main();
|
|
107
105
|
|
|
@@ -117,15 +115,15 @@ describe('delete-workspace main', () => {
|
|
|
117
115
|
|
|
118
116
|
mockPromptMultiWorkspaceSelection.mockResolvedValueOnce(['ws-a']);
|
|
119
117
|
|
|
120
|
-
|
|
121
|
-
.mockResolvedValueOnce(
|
|
122
|
-
.mockResolvedValueOnce(
|
|
118
|
+
mockConfirm
|
|
119
|
+
.mockResolvedValueOnce(false) // cancel deletion
|
|
120
|
+
.mockResolvedValueOnce(false); // don't delete more
|
|
123
121
|
|
|
124
122
|
await main();
|
|
125
123
|
|
|
126
124
|
expect(mockRm).not.toHaveBeenCalled();
|
|
127
125
|
// Should still ask "delete more?" — not exit immediately
|
|
128
|
-
expect(
|
|
126
|
+
expect(mockConfirm).toHaveBeenCalledTimes(2);
|
|
129
127
|
});
|
|
130
128
|
|
|
131
129
|
it('loops when user wants to delete more', async () => {
|
|
@@ -139,10 +137,10 @@ describe('delete-workspace main', () => {
|
|
|
139
137
|
.mockResolvedValueOnce(['ws-a'])
|
|
140
138
|
.mockResolvedValueOnce(['ws-b']);
|
|
141
139
|
|
|
142
|
-
|
|
143
|
-
.mockResolvedValueOnce(
|
|
144
|
-
.mockResolvedValueOnce(
|
|
145
|
-
.mockResolvedValueOnce(
|
|
140
|
+
mockConfirm
|
|
141
|
+
.mockResolvedValueOnce(true) // confirm 1st deletion
|
|
142
|
+
.mockResolvedValueOnce(true) // delete more
|
|
143
|
+
.mockResolvedValueOnce(true); // confirm 2nd deletion
|
|
146
144
|
|
|
147
145
|
await main();
|
|
148
146
|
|
|
@@ -159,14 +157,14 @@ describe('delete-workspace main', () => {
|
|
|
159
157
|
.mockResolvedValueOnce([]); // none left after deletion
|
|
160
158
|
|
|
161
159
|
mockPromptMultiWorkspaceSelection.mockResolvedValueOnce(['ws-only']);
|
|
162
|
-
|
|
160
|
+
mockConfirm.mockResolvedValueOnce(true);
|
|
163
161
|
|
|
164
162
|
await main();
|
|
165
163
|
|
|
166
164
|
expect(mockRm).toHaveBeenCalledTimes(1);
|
|
167
165
|
expect(logInfo).toHaveBeenCalledWith('No more workspaces remaining');
|
|
168
166
|
// Should NOT have asked "delete more?" since there are none left
|
|
169
|
-
expect(
|
|
167
|
+
expect(mockConfirm).toHaveBeenCalledTimes(1); // only the confirm prompt
|
|
170
168
|
});
|
|
171
169
|
|
|
172
170
|
it('handles individual deletion failure without stopping others', async () => {
|
|
@@ -180,9 +178,9 @@ describe('delete-workspace main', () => {
|
|
|
180
178
|
.mockRejectedValueOnce(new Error('permission denied')) // ws-a fails
|
|
181
179
|
.mockResolvedValueOnce(undefined); // ws-b succeeds
|
|
182
180
|
|
|
183
|
-
|
|
184
|
-
.mockResolvedValueOnce(
|
|
185
|
-
.mockResolvedValueOnce(
|
|
181
|
+
mockConfirm
|
|
182
|
+
.mockResolvedValueOnce(true)
|
|
183
|
+
.mockResolvedValueOnce(false);
|
|
186
184
|
|
|
187
185
|
await main();
|
|
188
186
|
|
|
@@ -197,17 +195,15 @@ describe('delete-workspace main', () => {
|
|
|
197
195
|
.mockResolvedValueOnce([]);
|
|
198
196
|
|
|
199
197
|
mockPromptMultiWorkspaceSelection.mockResolvedValueOnce(['ws-alpha']);
|
|
200
|
-
|
|
198
|
+
mockConfirm.mockResolvedValueOnce(true);
|
|
201
199
|
|
|
202
200
|
await main();
|
|
203
201
|
|
|
204
|
-
expect(
|
|
205
|
-
expect.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}),
|
|
210
|
-
])
|
|
202
|
+
expect(mockConfirm).toHaveBeenCalledWith(
|
|
203
|
+
expect.objectContaining({
|
|
204
|
+
message: 'Delete workspace ws-alpha?',
|
|
205
|
+
default: true,
|
|
206
|
+
})
|
|
211
207
|
);
|
|
212
208
|
});
|
|
213
209
|
|
|
@@ -217,17 +213,15 @@ describe('delete-workspace main', () => {
|
|
|
217
213
|
.mockResolvedValueOnce([]);
|
|
218
214
|
|
|
219
215
|
mockPromptMultiWorkspaceSelection.mockResolvedValueOnce(['ws-a', 'ws-b', 'ws-c']);
|
|
220
|
-
|
|
216
|
+
mockConfirm.mockResolvedValueOnce(true);
|
|
221
217
|
|
|
222
218
|
await main();
|
|
223
219
|
|
|
224
|
-
expect(
|
|
225
|
-
expect.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}),
|
|
230
|
-
])
|
|
220
|
+
expect(mockConfirm).toHaveBeenCalledWith(
|
|
221
|
+
expect.objectContaining({
|
|
222
|
+
message: 'Delete these 3 workspaces?',
|
|
223
|
+
default: true,
|
|
224
|
+
})
|
|
231
225
|
);
|
|
232
226
|
});
|
|
233
227
|
|
|
@@ -239,7 +233,7 @@ describe('delete-workspace main', () => {
|
|
|
239
233
|
.mockResolvedValueOnce([]);
|
|
240
234
|
|
|
241
235
|
mockPromptMultiWorkspaceSelection.mockResolvedValueOnce(['ws-a']);
|
|
242
|
-
|
|
236
|
+
mockConfirm.mockResolvedValueOnce(true);
|
|
243
237
|
|
|
244
238
|
await main();
|
|
245
239
|
|
package/src/commands/delete.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { listWorkspaces } from '../utils/workspace-meta';
|
|
|
5
5
|
import { promptMultiWorkspaceSelection } from '../utils/prompts';
|
|
6
6
|
import { logInfo, logSuccess, logError, logWarning } from '../utils/logger';
|
|
7
7
|
import { colorize } from '../utils/colors';
|
|
8
|
-
import
|
|
8
|
+
import { confirm } from '../utils/prompt';
|
|
9
9
|
import { getGlobalOpts, parseList } from '../utils/command-helpers';
|
|
10
10
|
|
|
11
11
|
export function registerDeleteCommand(parent: Command) {
|
|
@@ -76,16 +76,12 @@ async function handleDelete(opts: {
|
|
|
76
76
|
logWarning('This will permanently delete all cloned repositories in the selected workspaces!');
|
|
77
77
|
|
|
78
78
|
if (!opts.yes) {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
: `Delete these ${targets.length} workspaces?`,
|
|
86
|
-
default: true,
|
|
87
|
-
},
|
|
88
|
-
]);
|
|
79
|
+
const confirmed = await confirm({
|
|
80
|
+
message: targets.length === 1
|
|
81
|
+
? `Delete workspace ${targets[0].name}?`
|
|
82
|
+
: `Delete these ${targets.length} workspaces?`,
|
|
83
|
+
default: true,
|
|
84
|
+
});
|
|
89
85
|
if (!confirmed) {
|
|
90
86
|
logInfo('Deletion cancelled');
|
|
91
87
|
process.exit(0);
|
|
@@ -147,14 +143,10 @@ async function handleDelete(opts: {
|
|
|
147
143
|
? `Delete workspace ${resolved[0].name}?`
|
|
148
144
|
: `Delete these ${resolved.length} workspaces?`;
|
|
149
145
|
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
message: confirmMessage,
|
|
155
|
-
default: true,
|
|
156
|
-
},
|
|
157
|
-
]);
|
|
146
|
+
const confirmed = await confirm({
|
|
147
|
+
message: confirmMessage,
|
|
148
|
+
default: true,
|
|
149
|
+
});
|
|
158
150
|
|
|
159
151
|
if (confirmed) {
|
|
160
152
|
for (const { name, path: workspacePath } of resolved) {
|
|
@@ -175,14 +167,10 @@ async function handleDelete(opts: {
|
|
|
175
167
|
break;
|
|
176
168
|
}
|
|
177
169
|
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
message: 'Delete more workspaces?',
|
|
183
|
-
default: false,
|
|
184
|
-
},
|
|
185
|
-
]);
|
|
170
|
+
const deleteMore = await confirm({
|
|
171
|
+
message: 'Delete more workspaces?',
|
|
172
|
+
default: false,
|
|
173
|
+
});
|
|
186
174
|
|
|
187
175
|
if (!deleteMore) {
|
|
188
176
|
break;
|
|
@@ -2,7 +2,7 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { getGhqStatus, displayGhqInfo, ghqList } from '../utils/ghq-integration';
|
|
3
3
|
import { logSuccess, logWarning, logError } from '../utils/logger';
|
|
4
4
|
import { colorize } from '../utils/colors';
|
|
5
|
-
import
|
|
5
|
+
import { confirm } from '../utils/prompt';
|
|
6
6
|
|
|
7
7
|
export function registerGhqStatusCommand(parent: Command) {
|
|
8
8
|
parent
|
|
@@ -30,10 +30,9 @@ async function handleGhqStatus() {
|
|
|
30
30
|
console.log('');
|
|
31
31
|
|
|
32
32
|
if (status.repoCount && status.repoCount > 0) {
|
|
33
|
-
const
|
|
34
|
-
type: 'confirm', name: 'showRepos',
|
|
33
|
+
const showRepos = await confirm({
|
|
35
34
|
message: `View all ${status.repoCount} managed repositories?`, default: false,
|
|
36
|
-
}
|
|
35
|
+
});
|
|
37
36
|
|
|
38
37
|
if (showRepos) {
|
|
39
38
|
const repos = await ghqList();
|
|
@@ -48,10 +47,9 @@ async function handleGhqStatus() {
|
|
|
48
47
|
console.log('');
|
|
49
48
|
console.log('The workspace manager will use direct git clones.');
|
|
50
49
|
|
|
51
|
-
const
|
|
52
|
-
type: 'confirm', name: 'showInfo',
|
|
50
|
+
const showInfo = await confirm({
|
|
53
51
|
message: 'Show ghq installation information?', default: true,
|
|
54
|
-
}
|
|
52
|
+
});
|
|
55
53
|
|
|
56
54
|
if (showInfo) { displayGhqInfo(); }
|
|
57
55
|
}
|
package/src/commands/go.ts
CHANGED
|
@@ -7,12 +7,9 @@ import { listWorkspaces } from '../utils/workspace-meta';
|
|
|
7
7
|
import { getWorkspaceSessions } from '../utils/claude-sessions';
|
|
8
8
|
import { logError } from '../utils/logger';
|
|
9
9
|
import { colorize } from '../utils/colors';
|
|
10
|
-
import
|
|
11
|
-
import autocompletePrompt from 'inquirer-autocomplete-prompt';
|
|
10
|
+
import { search } from '../utils/prompt';
|
|
12
11
|
import * as fuzzy from 'fuzzy';
|
|
13
12
|
|
|
14
|
-
inquirer.registerPrompt('autocomplete', autocompletePrompt);
|
|
15
|
-
|
|
16
13
|
const TEMP_FILE = path.join(os.homedir(), '.workspace-last-go');
|
|
17
14
|
const RESUME_FLAG_FILE = path.join(os.homedir(), '.workspace-resume-session');
|
|
18
15
|
|
|
@@ -68,27 +65,23 @@ async function handleGo(workspaceArg?: string) {
|
|
|
68
65
|
return 0;
|
|
69
66
|
});
|
|
70
67
|
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
source
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
},
|
|
89
|
-
pageSize: 15,
|
|
90
|
-
} as any,
|
|
91
|
-
]);
|
|
68
|
+
const workspaceName = await search<string>({
|
|
69
|
+
message: 'Select workspace to navigate to:',
|
|
70
|
+
pageSize: 15,
|
|
71
|
+
source: async (term: string | undefined) => {
|
|
72
|
+
const searchInput = term || '';
|
|
73
|
+
const source = items.map(item => ({
|
|
74
|
+
name: `${item.name} ${item.lastActiveLabel ? colorize(item.lastActiveLabel, 'dim') : colorize('no session', 'dim')} ${colorize(`${item.repoCount} repos`, 'dim')}`,
|
|
75
|
+
value: item.name,
|
|
76
|
+
}));
|
|
77
|
+
if (!searchInput) return source;
|
|
78
|
+
const results = fuzzy.filter(searchInput, items, { extract: (item) => item.name });
|
|
79
|
+
return results.map(result => ({
|
|
80
|
+
name: `${result.original.name} ${result.original.lastActiveLabel ? colorize(result.original.lastActiveLabel, 'dim') : colorize('no session', 'dim')} ${colorize(`${result.original.repoCount} repos`, 'dim')}`,
|
|
81
|
+
value: result.original.name,
|
|
82
|
+
}));
|
|
83
|
+
},
|
|
84
|
+
});
|
|
92
85
|
|
|
93
86
|
selectedWorkspace = workspaceName;
|
|
94
87
|
}
|
package/src/commands/history.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { readHistory, getOperationStats, filterHistory, clearHistory } from '../utils/history';
|
|
3
3
|
import { logError, logInfo, logStep, logSuccess } from '../utils/logger';
|
|
4
4
|
import { colorize } from '../utils/colors';
|
|
5
|
-
import
|
|
5
|
+
import { confirm } from '../utils/prompt';
|
|
6
6
|
import { getGlobalOpts } from '../utils/command-helpers';
|
|
7
7
|
|
|
8
8
|
const displayHistoryTable = (records: Array<{ timestamp: string; command: string; workspace?: string; duration: number; success: boolean; error?: string }>) => {
|
|
@@ -88,16 +88,12 @@ export function registerHistoryCommand(parent: Command) {
|
|
|
88
88
|
return;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
message: 'Are you sure you want to clear operation history?',
|
|
96
|
-
default: false,
|
|
97
|
-
},
|
|
98
|
-
]);
|
|
91
|
+
const confirmed = await confirm({
|
|
92
|
+
message: 'Are you sure you want to clear operation history?',
|
|
93
|
+
default: false,
|
|
94
|
+
});
|
|
99
95
|
|
|
100
|
-
if (
|
|
96
|
+
if (confirmed) {
|
|
101
97
|
await clearHistory();
|
|
102
98
|
logSuccess('Operation history cleared');
|
|
103
99
|
} else {
|
|
@@ -3,12 +3,12 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
|
3
3
|
const {
|
|
4
4
|
mockListWorkspaces,
|
|
5
5
|
mockGetWorkspaceSessions,
|
|
6
|
-
|
|
6
|
+
mockSearch,
|
|
7
7
|
mockWriteFile,
|
|
8
8
|
} = vi.hoisted(() => ({
|
|
9
9
|
mockListWorkspaces: vi.fn(),
|
|
10
10
|
mockGetWorkspaceSessions: vi.fn(),
|
|
11
|
-
|
|
11
|
+
mockSearch: vi.fn(),
|
|
12
12
|
mockWriteFile: vi.fn(),
|
|
13
13
|
}));
|
|
14
14
|
|
|
@@ -33,15 +33,8 @@ vi.mock('fs/promises', () => ({
|
|
|
33
33
|
writeFile: mockWriteFile,
|
|
34
34
|
}));
|
|
35
35
|
|
|
36
|
-
vi.mock('
|
|
37
|
-
|
|
38
|
-
prompt: mockPrompt,
|
|
39
|
-
registerPrompt: vi.fn(),
|
|
40
|
-
},
|
|
41
|
-
}));
|
|
42
|
-
|
|
43
|
-
vi.mock('inquirer-autocomplete-prompt', () => ({
|
|
44
|
-
default: vi.fn(),
|
|
36
|
+
vi.mock('../utils/prompt', () => ({
|
|
37
|
+
search: mockSearch,
|
|
45
38
|
}));
|
|
46
39
|
|
|
47
40
|
vi.mock('fuzzy', () => ({
|
|
@@ -91,11 +84,11 @@ describe('list-workspaces main', () => {
|
|
|
91
84
|
|
|
92
85
|
it('lists workspaces and prompts for selection', async () => {
|
|
93
86
|
mockListWorkspaces.mockResolvedValueOnce(makeWorkspaceList('ws-a', 'ws-b'));
|
|
94
|
-
|
|
87
|
+
mockSearch.mockResolvedValueOnce('ws-a');
|
|
95
88
|
|
|
96
89
|
await main();
|
|
97
90
|
|
|
98
|
-
expect(
|
|
91
|
+
expect(mockSearch).toHaveBeenCalledTimes(1);
|
|
99
92
|
expect(mockWriteFile).toHaveBeenCalledWith(
|
|
100
93
|
expect.stringContaining('.workspace-last-go'),
|
|
101
94
|
'/test-workspaces/ws-a',
|
|
@@ -109,7 +102,7 @@ describe('list-workspaces main', () => {
|
|
|
109
102
|
await main();
|
|
110
103
|
|
|
111
104
|
expect(logInfo).toHaveBeenCalledWith('No workspaces found');
|
|
112
|
-
expect(
|
|
105
|
+
expect(mockSearch).not.toHaveBeenCalled();
|
|
113
106
|
});
|
|
114
107
|
|
|
115
108
|
it('does not prompt when no archived workspaces exist', async () => {
|
|
@@ -119,12 +112,12 @@ describe('list-workspaces main', () => {
|
|
|
119
112
|
await main();
|
|
120
113
|
|
|
121
114
|
expect(logInfo).toHaveBeenCalledWith('No archived workspaces found');
|
|
122
|
-
expect(
|
|
115
|
+
expect(mockSearch).not.toHaveBeenCalled();
|
|
123
116
|
});
|
|
124
117
|
|
|
125
118
|
it('writes temp file on selection', async () => {
|
|
126
119
|
mockListWorkspaces.mockResolvedValueOnce(makeWorkspaceList('my-workspace'));
|
|
127
|
-
|
|
120
|
+
mockSearch.mockResolvedValueOnce('my-workspace');
|
|
128
121
|
|
|
129
122
|
await main();
|
|
130
123
|
|
|
@@ -145,7 +138,7 @@ describe('list-workspaces main', () => {
|
|
|
145
138
|
lastActiveLabel: '2m ago',
|
|
146
139
|
agentType: 'pi',
|
|
147
140
|
}]);
|
|
148
|
-
|
|
141
|
+
mockSearch.mockResolvedValueOnce('my-workspace');
|
|
149
142
|
|
|
150
143
|
await main();
|
|
151
144
|
|
|
@@ -159,7 +152,7 @@ describe('list-workspaces main', () => {
|
|
|
159
152
|
it('does not write resume flag when workspace has no session', async () => {
|
|
160
153
|
mockListWorkspaces.mockResolvedValueOnce(makeWorkspaceList('my-workspace'));
|
|
161
154
|
mockGetWorkspaceSessions.mockResolvedValueOnce([]);
|
|
162
|
-
|
|
155
|
+
mockSearch.mockResolvedValueOnce('my-workspace');
|
|
163
156
|
|
|
164
157
|
await main();
|
|
165
158
|
|
|
@@ -173,11 +166,11 @@ describe('list-workspaces main', () => {
|
|
|
173
166
|
process.argv = ['node', 'list-workspaces.js', '--archived'];
|
|
174
167
|
const archived = makeArchivedWorkspaceList('old-ws');
|
|
175
168
|
mockListWorkspaces.mockResolvedValueOnce(archived);
|
|
176
|
-
|
|
169
|
+
mockSearch.mockResolvedValueOnce('old-ws');
|
|
177
170
|
|
|
178
171
|
await main();
|
|
179
172
|
|
|
180
|
-
expect(
|
|
173
|
+
expect(mockSearch).toHaveBeenCalledTimes(1);
|
|
181
174
|
expect(mockWriteFile).toHaveBeenCalledWith(
|
|
182
175
|
expect.stringContaining('.workspace-last-go'),
|
|
183
176
|
'/test-workspaces/old-ws',
|
|
@@ -188,7 +181,7 @@ describe('list-workspaces main', () => {
|
|
|
188
181
|
it('displays workspace name in the list output', async () => {
|
|
189
182
|
const logSpy = vi.spyOn(console, 'log');
|
|
190
183
|
mockListWorkspaces.mockResolvedValueOnce(makeWorkspaceList('ws-a'));
|
|
191
|
-
|
|
184
|
+
mockSearch.mockResolvedValueOnce('ws-a');
|
|
192
185
|
|
|
193
186
|
await main();
|
|
194
187
|
|
|
@@ -203,7 +196,7 @@ describe('list-workspaces main', () => {
|
|
|
203
196
|
|
|
204
197
|
await main();
|
|
205
198
|
|
|
206
|
-
expect(
|
|
199
|
+
expect(mockSearch).not.toHaveBeenCalled();
|
|
207
200
|
expect(writeSpy).toHaveBeenCalledTimes(1);
|
|
208
201
|
const payload = JSON.parse(writeSpy.mock.calls[0][0] as string);
|
|
209
202
|
expect(payload.count).toBe(2);
|
|
@@ -220,7 +213,7 @@ describe('list-workspaces main', () => {
|
|
|
220
213
|
await main();
|
|
221
214
|
|
|
222
215
|
expect(logInfo).not.toHaveBeenCalled();
|
|
223
|
-
expect(
|
|
216
|
+
expect(mockSearch).not.toHaveBeenCalled();
|
|
224
217
|
const payload = JSON.parse(writeSpy.mock.calls[0][0] as string);
|
|
225
218
|
expect(payload).toEqual({ archived: false, count: 0, workspaces: [] });
|
|
226
219
|
writeSpy.mockRestore();
|
|
@@ -235,14 +228,14 @@ describe('list-workspaces main', () => {
|
|
|
235
228
|
lastActiveAt: new Date(),
|
|
236
229
|
lastActiveLabel: '5m ago',
|
|
237
230
|
}]);
|
|
238
|
-
|
|
231
|
+
mockSearch.mockResolvedValueOnce('has-session');
|
|
239
232
|
|
|
240
233
|
await main();
|
|
241
234
|
|
|
242
235
|
// The prompt source function should show has-session first
|
|
243
|
-
const promptCall =
|
|
236
|
+
const promptCall = mockSearch.mock.calls[0][0];
|
|
244
237
|
const source = promptCall.source;
|
|
245
|
-
const items = await source(
|
|
238
|
+
const items = await source('');
|
|
246
239
|
expect(items[0].value).toBe('has-session');
|
|
247
240
|
expect(items[1].value).toBe('no-session');
|
|
248
241
|
});
|
package/src/commands/list.ts
CHANGED
|
@@ -7,12 +7,9 @@ import { getWorkspaceSessions } from '../utils/claude-sessions';
|
|
|
7
7
|
import { logInfo, logError } from '../utils/logger';
|
|
8
8
|
import { outputJson } from '../utils/output';
|
|
9
9
|
import { colorize } from '../utils/colors';
|
|
10
|
-
import
|
|
11
|
-
import autocompletePrompt from 'inquirer-autocomplete-prompt';
|
|
10
|
+
import { search } from '../utils/prompt';
|
|
12
11
|
import * as fuzzy from 'fuzzy';
|
|
13
12
|
|
|
14
|
-
inquirer.registerPrompt('autocomplete', autocompletePrompt);
|
|
15
|
-
|
|
16
13
|
const TEMP_FILE = path.join(os.homedir(), '.workspace-last-go');
|
|
17
14
|
const RESUME_FLAG_FILE = path.join(os.homedir(), '.workspace-resume-session');
|
|
18
15
|
|
|
@@ -136,33 +133,29 @@ async function handleList(opts: { archived?: boolean; json?: boolean }) {
|
|
|
136
133
|
console.log('');
|
|
137
134
|
|
|
138
135
|
if (items.length > 0 && (process.stdout.isTTY || process.env.VITEST === 'true')) {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
},
|
|
163
|
-
pageSize: 15,
|
|
164
|
-
} as any,
|
|
165
|
-
]);
|
|
136
|
+
const selected = await search<string>({
|
|
137
|
+
message: 'Select workspace to open:',
|
|
138
|
+
pageSize: 15,
|
|
139
|
+
source: async (term: string | undefined) => {
|
|
140
|
+
const searchInput = term || '';
|
|
141
|
+
|
|
142
|
+
const source = items.map(item => ({
|
|
143
|
+
name: `${item.name} ${item.lastActiveLabel ? colorize(item.lastActiveLabel, 'dim') : colorize('no session', 'dim')} ${colorize(`${item.repoCount} repos`, 'dim')}`,
|
|
144
|
+
value: item.name,
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
if (!searchInput) return source;
|
|
148
|
+
|
|
149
|
+
const results = fuzzy.filter(searchInput, items, {
|
|
150
|
+
extract: (item) => item.name,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return results.map(result => ({
|
|
154
|
+
name: `${result.original.name} ${result.original.lastActiveLabel ? colorize(result.original.lastActiveLabel, 'dim') : colorize('no session', 'dim')} ${colorize(`${result.original.repoCount} repos`, 'dim')}`,
|
|
155
|
+
value: result.original.name,
|
|
156
|
+
}));
|
|
157
|
+
},
|
|
158
|
+
});
|
|
166
159
|
|
|
167
160
|
const selectedItem = items.find(i => i.name === selected);
|
|
168
161
|
if (selectedItem) {
|