@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.
Files changed (74) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +23 -5
  3. package/bin/workspace.js +12 -0
  4. package/dist/commands/analyze-deps.js +5 -12
  5. package/dist/commands/archive.js +13 -18
  6. package/dist/commands/branch/create.js +5 -12
  7. package/dist/commands/branch/switch.js +14 -25
  8. package/dist/commands/cache/manager.js +13 -24
  9. package/dist/commands/cleanup.js +14 -26
  10. package/dist/commands/configure-claude.js +4 -8
  11. package/dist/commands/configure.js +38 -32
  12. package/dist/commands/dashboard/session-picker.js +19 -26
  13. package/dist/commands/dashboard/workspace-picker.js +19 -26
  14. package/dist/commands/delete.js +15 -30
  15. package/dist/commands/ghq-status.js +7 -12
  16. package/dist/commands/go.js +18 -27
  17. package/dist/commands/history.js +6 -13
  18. package/dist/commands/list.js +20 -29
  19. package/dist/commands/prune.js +196 -0
  20. package/dist/commands/remove-repo.js +21 -29
  21. package/dist/commands/save-context.js +5 -10
  22. package/dist/commands/sessions.js +19 -25
  23. package/dist/commands/suite/create.js +27 -53
  24. package/dist/commands/suite/delete.js +13 -25
  25. package/dist/commands/suite/export.js +20 -36
  26. package/dist/commands/suite/import.js +9 -20
  27. package/dist/commands/suite/use.js +9 -17
  28. package/dist/program.js +2 -0
  29. package/dist/utils/prompt.js +26 -0
  30. package/dist/utils/prompts.js +86 -118
  31. package/dist/utils/prune.js +70 -0
  32. package/package.json +3 -6
  33. package/scripts/release-notes.mjs +54 -0
  34. package/skills/config.md +17 -0
  35. package/skills/nemus/SKILL.md +7 -2
  36. package/skills/nemus/references/completion.md +22 -0
  37. package/skills/nemus/references/config.md +32 -0
  38. package/skills/nemus/references/prune.md +44 -0
  39. package/skills/nemus/references/reflect.md +43 -0
  40. package/skills/nemus/references/save-context.md +25 -0
  41. package/skills/prune-workspaces.md +23 -0
  42. package/skills/reflect.md +21 -0
  43. package/src/commands/analyze-deps.ts +5 -9
  44. package/src/commands/archive.ts +5 -7
  45. package/src/commands/branch/create.ts +5 -9
  46. package/src/commands/branch/switch.ts +14 -22
  47. package/src/commands/cache/manager.ts +13 -21
  48. package/src/commands/cleanup.ts +14 -23
  49. package/src/commands/configure-claude.ts +4 -5
  50. package/src/commands/configure.ts +35 -29
  51. package/src/commands/dashboard/session-picker.ts +6 -11
  52. package/src/commands/dashboard/workspace-picker.ts +6 -11
  53. package/src/commands/delete.test.ts +36 -42
  54. package/src/commands/delete.ts +15 -27
  55. package/src/commands/ghq-status.ts +5 -7
  56. package/src/commands/go.ts +18 -25
  57. package/src/commands/history.ts +6 -10
  58. package/src/commands/list.test.ts +19 -26
  59. package/src/commands/list.ts +24 -31
  60. package/src/commands/prune.ts +183 -0
  61. package/src/commands/remove-repo.ts +11 -17
  62. package/src/commands/save-context.ts +3 -5
  63. package/src/commands/sessions.ts +6 -10
  64. package/src/commands/suite/create.ts +28 -50
  65. package/src/commands/suite/delete.ts +14 -23
  66. package/src/commands/suite/export.ts +20 -33
  67. package/src/commands/suite/import.ts +9 -17
  68. package/src/commands/suite/use.ts +9 -14
  69. package/src/program.ts +2 -0
  70. package/src/utils/prompt.ts +16 -0
  71. package/src/utils/prompts.test.ts +70 -41
  72. package/src/utils/prompts.ts +98 -128
  73. package/src/utils/prune.test.ts +121 -0
  74. package/src/utils/prune.ts +109 -0
@@ -4,12 +4,12 @@ const {
4
4
  mockRm,
5
5
  mockListWorkspaces,
6
6
  mockPromptMultiWorkspaceSelection,
7
- mockPrompt,
7
+ mockConfirm,
8
8
  } = vi.hoisted(() => ({
9
9
  mockRm: vi.fn(),
10
10
  mockListWorkspaces: vi.fn(),
11
11
  mockPromptMultiWorkspaceSelection: vi.fn(),
12
- mockPrompt: vi.fn(),
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('inquirer', () => ({
43
- default: {
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
- mockPrompt
86
- .mockResolvedValueOnce({ confirmed: true }) // confirm deletion
87
- .mockResolvedValueOnce({ deleteMore: false }); // don't delete more
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
- mockPrompt
103
- .mockResolvedValueOnce({ confirmed: true })
104
- .mockResolvedValueOnce({ deleteMore: false });
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
- mockPrompt
121
- .mockResolvedValueOnce({ confirmed: false }) // cancel deletion
122
- .mockResolvedValueOnce({ deleteMore: false }); // don't delete more
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(mockPrompt).toHaveBeenCalledTimes(2);
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
- mockPrompt
143
- .mockResolvedValueOnce({ confirmed: true }) // confirm 1st deletion
144
- .mockResolvedValueOnce({ deleteMore: true }) // delete more
145
- .mockResolvedValueOnce({ confirmed: true }); // confirm 2nd deletion
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
- mockPrompt.mockResolvedValueOnce({ confirmed: true });
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(mockPrompt).toHaveBeenCalledTimes(1); // only the confirm prompt
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
- mockPrompt
184
- .mockResolvedValueOnce({ confirmed: true })
185
- .mockResolvedValueOnce({ deleteMore: false });
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
- mockPrompt.mockResolvedValueOnce({ confirmed: true });
198
+ mockConfirm.mockResolvedValueOnce(true);
201
199
 
202
200
  await main();
203
201
 
204
- expect(mockPrompt).toHaveBeenCalledWith(
205
- expect.arrayContaining([
206
- expect.objectContaining({
207
- message: 'Delete workspace ws-alpha?',
208
- default: true,
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
- mockPrompt.mockResolvedValueOnce({ confirmed: true });
216
+ mockConfirm.mockResolvedValueOnce(true);
221
217
 
222
218
  await main();
223
219
 
224
- expect(mockPrompt).toHaveBeenCalledWith(
225
- expect.arrayContaining([
226
- expect.objectContaining({
227
- message: 'Delete these 3 workspaces?',
228
- default: true,
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
- mockPrompt.mockResolvedValueOnce({ confirmed: true });
236
+ mockConfirm.mockResolvedValueOnce(true);
243
237
 
244
238
  await main();
245
239
 
@@ -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 inquirer from 'inquirer';
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 { confirmed } = await inquirer.prompt([
80
- {
81
- type: 'confirm',
82
- name: 'confirmed',
83
- message: targets.length === 1
84
- ? `Delete workspace ${targets[0].name}?`
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 { confirmed } = await inquirer.prompt([
151
- {
152
- type: 'confirm',
153
- name: 'confirmed',
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 { deleteMore } = await inquirer.prompt([
179
- {
180
- type: 'confirm',
181
- name: 'deleteMore',
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 inquirer from 'inquirer';
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 { showRepos } = await inquirer.prompt([{
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 { showInfo } = await inquirer.prompt([{
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
  }
@@ -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 inquirer from 'inquirer';
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 { workspaceName } = await inquirer.prompt([
72
- {
73
- type: 'autocomplete',
74
- name: 'workspaceName',
75
- message: 'Select workspace to navigate to:',
76
- source: async (_answersSoFar: any, input: string | undefined) => {
77
- const searchInput = input || '';
78
- const source = items.map(item => ({
79
- name: `${item.name} ${item.lastActiveLabel ? colorize(item.lastActiveLabel, 'dim') : colorize('no session', 'dim')} ${colorize(`${item.repoCount} repos`, 'dim')}`,
80
- value: item.name,
81
- }));
82
- if (!searchInput) return source;
83
- const results = fuzzy.filter(searchInput, items, { extract: (item) => item.name });
84
- return results.map(result => ({
85
- name: `${result.original.name} ${result.original.lastActiveLabel ? colorize(result.original.lastActiveLabel, 'dim') : colorize('no session', 'dim')} ${colorize(`${result.original.repoCount} repos`, 'dim')}`,
86
- value: result.original.name,
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
  }
@@ -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 inquirer from 'inquirer';
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 { confirm } = await inquirer.prompt([
92
- {
93
- type: 'confirm',
94
- name: 'confirm',
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 (confirm) {
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
- mockPrompt,
6
+ mockSearch,
7
7
  mockWriteFile,
8
8
  } = vi.hoisted(() => ({
9
9
  mockListWorkspaces: vi.fn(),
10
10
  mockGetWorkspaceSessions: vi.fn(),
11
- mockPrompt: vi.fn(),
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('inquirer', () => ({
37
- default: {
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
- mockPrompt.mockResolvedValueOnce({ selected: 'ws-a' });
87
+ mockSearch.mockResolvedValueOnce('ws-a');
95
88
 
96
89
  await main();
97
90
 
98
- expect(mockPrompt).toHaveBeenCalledTimes(1);
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(mockPrompt).not.toHaveBeenCalled();
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(mockPrompt).not.toHaveBeenCalled();
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
- mockPrompt.mockResolvedValueOnce({ selected: 'my-workspace' });
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
- mockPrompt.mockResolvedValueOnce({ selected: 'my-workspace' });
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
- mockPrompt.mockResolvedValueOnce({ selected: 'my-workspace' });
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
- mockPrompt.mockResolvedValueOnce({ selected: 'old-ws' });
169
+ mockSearch.mockResolvedValueOnce('old-ws');
177
170
 
178
171
  await main();
179
172
 
180
- expect(mockPrompt).toHaveBeenCalledTimes(1);
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
- mockPrompt.mockResolvedValueOnce({ selected: 'ws-a' });
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(mockPrompt).not.toHaveBeenCalled();
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(mockPrompt).not.toHaveBeenCalled();
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
- mockPrompt.mockResolvedValueOnce({ selected: 'has-session' });
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 = mockPrompt.mock.calls[0][0][0];
236
+ const promptCall = mockSearch.mock.calls[0][0];
244
237
  const source = promptCall.source;
245
- const items = await source(null, '');
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
  });
@@ -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 inquirer from 'inquirer';
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 { selected } = await inquirer.prompt([
140
- {
141
- type: 'autocomplete',
142
- name: 'selected',
143
- message: 'Select workspace to open:',
144
- source: async (_answersSoFar: any, input: string | undefined) => {
145
- const searchInput = input || '';
146
-
147
- const source = items.map(item => ({
148
- name: `${item.name} ${item.lastActiveLabel ? colorize(item.lastActiveLabel, 'dim') : colorize('no session', 'dim')} ${colorize(`${item.repoCount} repos`, 'dim')}`,
149
- value: item.name,
150
- }));
151
-
152
- if (!searchInput) return source;
153
-
154
- const results = fuzzy.filter(searchInput, items, {
155
- extract: (item) => item.name,
156
- });
157
-
158
- return results.map(result => ({
159
- name: `${result.original.name} ${result.original.lastActiveLabel ? colorize(result.original.lastActiveLabel, 'dim') : colorize('no session', 'dim')} ${colorize(`${result.original.repoCount} repos`, 'dim')}`,
160
- value: result.original.name,
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) {