@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
@@ -0,0 +1,183 @@
1
+ import { Command } from 'commander';
2
+ import * as fs from 'fs/promises';
3
+ import { safeWorkspacePath } from '../utils/validation';
4
+ import { listWorkspaces } from '../utils/workspace-meta';
5
+ import { getWorkspaceSessions } from '../utils/claude-sessions';
6
+ import { getAllReposStatus } from '../utils/git-status';
7
+ import { logInfo, logSuccess, logError, logWarning, logStep } from '../utils/logger';
8
+ import { colorize } from '../utils/colors';
9
+ import { confirm } from '../utils/prompt';
10
+ import { getGlobalOpts } from '../utils/command-helpers';
11
+ import { outputJson, outputJsonError } from '../utils/output';
12
+ import {
13
+ toCandidate,
14
+ isStale,
15
+ planPrune,
16
+ type WorkspaceForPrune,
17
+ type PruneCandidate,
18
+ } from '../utils/prune';
19
+
20
+ const DEFAULT_DAYS = 30;
21
+
22
+ export function registerPruneCommand(parent: Command) {
23
+ parent
24
+ .command('prune')
25
+ .description('Delete workspaces with no recent activity (safe by default)')
26
+ .option('-d, --days <n>', `Consider a workspace stale after N days of inactivity (default ${DEFAULT_DAYS})`)
27
+ .option('--include-dirty', 'Also prune workspaces with uncommitted/unpushed changes (default: protected)')
28
+ .option('--dry-run', 'Show what would be pruned without deleting anything')
29
+ .option('-y, --yes', 'Skip the confirmation prompt')
30
+ .option('--json', 'Output the prune plan as JSON (never deletes)')
31
+ .action(async (opts, cmd) => {
32
+ const globalOpts = getGlobalOpts(cmd);
33
+ await handlePrune({ ...opts, ...globalOpts });
34
+ });
35
+ }
36
+
37
+ function parseDays(raw: unknown): number | null {
38
+ if (raw === undefined) return DEFAULT_DAYS;
39
+ const n = Number(raw);
40
+ if (!Number.isFinite(n) || n < 0) return null;
41
+ return Math.floor(n);
42
+ }
43
+
44
+ export async function handlePrune(opts: {
45
+ days?: string;
46
+ includeDirty?: boolean;
47
+ dryRun?: boolean;
48
+ yes?: boolean;
49
+ json?: boolean;
50
+ }) {
51
+ const json = !!opts.json;
52
+ const days = parseDays(opts.days);
53
+ if (days === null) {
54
+ if (json) outputJsonError('--days must be a non-negative number');
55
+ else logError('--days must be a non-negative number');
56
+ process.exit(1);
57
+ }
58
+
59
+ try {
60
+ const [workspaces, sessions] = await Promise.all([listWorkspaces(), getWorkspaceSessions()]);
61
+ const sessionMap = new Map(sessions.map((s) => [s.workspaceName, s]));
62
+ const now = Date.now();
63
+
64
+ const candidates: PruneCandidate[] = workspaces.map((ws) => {
65
+ const session = sessionMap.get(ws.name);
66
+ const createdRaw = ws.metadata?.createdAt ? Date.parse(ws.metadata.createdAt) : NaN;
67
+ const forPrune: WorkspaceForPrune = {
68
+ name: ws.name,
69
+ path: ws.path,
70
+ repoDirNames: (ws.metadata?.repositories ?? []).map((r) => r.directoryName),
71
+ lastActiveAt: session ? session.lastActiveAt.getTime() : 0,
72
+ createdAt: Number.isFinite(createdRaw) ? createdRaw : 0,
73
+ };
74
+ return toCandidate(forPrune, now);
75
+ });
76
+
77
+ const stale = candidates.filter((c) => isStale(c, days));
78
+
79
+ if (stale.length === 0) {
80
+ if (json) {
81
+ outputJson({ ok: true, days, prunable: [], protected: [], scanned: workspaces.length });
82
+ } else {
83
+ logInfo(`No workspaces inactive for ${days}+ days (scanned ${workspaces.length}).`);
84
+ }
85
+ return;
86
+ }
87
+
88
+ // Compute the plan. The git safety check only runs for stale workspaces.
89
+ const plan = await planPrune(
90
+ stale,
91
+ (c) => getAllReposStatus(c.path, c.repoDirNames, 3),
92
+ !!opts.includeDirty,
93
+ );
94
+
95
+ if (json) {
96
+ outputJson({
97
+ ok: true,
98
+ days,
99
+ scanned: workspaces.length,
100
+ prunable: plan.prunable.map((c) => ({ name: c.name, path: c.path, ageDays: c.ageDays, repos: c.repoDirNames.length })),
101
+ protected: plan.protected.map((p) => ({ name: p.candidate.name, ageDays: p.candidate.ageDays, reason: p.reason })),
102
+ });
103
+ return;
104
+ }
105
+
106
+ // Human report.
107
+ console.log('\n' + '='.repeat(60));
108
+ console.log(colorize(`Prune — workspaces inactive for ${days}+ days`, 'bright'));
109
+ console.log('='.repeat(60) + '\n');
110
+
111
+ if (plan.protected.length > 0) {
112
+ logWarning(`Protected (${plan.protected.length}) — skipped due to unsaved work:`);
113
+ for (const p of plan.protected) {
114
+ console.log(` ${colorize('•', 'yellow')} ${colorize(p.candidate.name, 'cyan')} — ${p.reason} ${colorize(`(${ageLabel(p.candidate)})`, 'gray')}`);
115
+ }
116
+ console.log('');
117
+ }
118
+
119
+ if (plan.prunable.length === 0) {
120
+ logInfo('Nothing safe to prune.');
121
+ if (plan.protected.length > 0) logInfo('Re-run with --include-dirty to include the protected ones (careful).');
122
+ return;
123
+ }
124
+
125
+ console.log(`${colorize('Prunable', 'bright')} (${plan.prunable.length}):`);
126
+ for (const c of plan.prunable) {
127
+ const repoLabel = c.repoDirNames.length === 1 ? '1 repo' : `${c.repoDirNames.length} repos`;
128
+ console.log(` ${colorize('✗', 'red')} ${colorize(c.name, 'cyan')} ${colorize(`(${ageLabel(c)}, ${repoLabel})`, 'gray')}`);
129
+ }
130
+ console.log('');
131
+
132
+ if (opts.dryRun) {
133
+ logInfo(`Dry run — nothing deleted. ${plan.prunable.length} workspace(s) would be pruned.`);
134
+ return;
135
+ }
136
+
137
+ logWarning('This permanently deletes the selected workspaces and every cloned repo inside them!');
138
+
139
+ if (!opts.yes) {
140
+ const confirmed = await confirm({
141
+ message: plan.prunable.length === 1
142
+ ? `Prune workspace ${plan.prunable[0].name}?`
143
+ : `Prune these ${plan.prunable.length} workspaces?`,
144
+ default: false,
145
+ });
146
+ if (!confirmed) {
147
+ logInfo('Prune cancelled');
148
+ return;
149
+ }
150
+ }
151
+
152
+ let deleted = 0;
153
+ for (const c of plan.prunable) {
154
+ let target: string;
155
+ try {
156
+ // Re-validate through the same choke point delete uses: enforces the
157
+ // name allowlist and pins the path inside WORKSPACES_DIR.
158
+ target = safeWorkspacePath(c.name);
159
+ } catch (error) {
160
+ logError(error instanceof Error ? error.message : `Invalid workspace name "${c.name}"`);
161
+ continue;
162
+ }
163
+ try {
164
+ await fs.rm(target, { recursive: true, force: true });
165
+ logSuccess(`Pruned "${colorize(c.name, 'cyan')}"`);
166
+ deleted++;
167
+ } catch (error) {
168
+ logError(`Failed to prune "${c.name}"`);
169
+ if (error instanceof Error) logError(error.message);
170
+ }
171
+ }
172
+ logStep(`Pruned ${deleted} of ${plan.prunable.length} workspace(s).`);
173
+ } catch (error) {
174
+ if (json) outputJsonError(error instanceof Error ? error.message : 'prune failed');
175
+ else logError(error instanceof Error ? error.message : 'prune failed');
176
+ process.exit(1);
177
+ }
178
+ }
179
+
180
+ function ageLabel(c: PruneCandidate): string {
181
+ const base = c.ageDays === 1 ? '1 day' : `${c.ageDays} days`;
182
+ return c.fromSession ? `${base} since last session` : `${base} since created, no sessions`;
183
+ }
@@ -6,13 +6,10 @@ import { loadMetadata, saveMetadata } from '../utils/workspace-meta';
6
6
  import { generateClaudeContext } from '../utils/claude-integration';
7
7
  import { logInfo, logSuccess, logError, logStep } from '../utils/logger';
8
8
  import { colorize } from '../utils/colors';
9
- import inquirer from 'inquirer';
10
- import autocompletePrompt from 'inquirer-autocomplete-prompt';
9
+ import { confirm, search } from '../utils/prompt';
11
10
  import * as fuzzy from 'fuzzy';
12
11
  import { getGlobalOpts, resolveWorkspace, parseList } from '../utils/command-helpers';
13
12
 
14
- inquirer.registerPrompt('autocomplete', autocompletePrompt);
15
-
16
13
  export function registerRemoveRepoCommand(parent: Command) {
17
14
  parent
18
15
  .command('remove-repo')
@@ -75,10 +72,9 @@ async function handleRemoveRepo(opts: {
75
72
  await fs.rm(workspacePath, { recursive: true, force: true });
76
73
  logSuccess(`Workspace "${workspaceName}" deleted.`);
77
74
  } else {
78
- const { deleteWorkspace } = await inquirer.prompt([{
79
- type: 'confirm', name: 'deleteWorkspace',
75
+ const deleteWorkspace = await confirm({
80
76
  message: `Delete the entire workspace folder "${workspaceName}"?`, default: false,
81
- }]);
77
+ });
82
78
  if (deleteWorkspace) {
83
79
  await fs.rm(workspacePath, { recursive: true, force: true });
84
80
  logSuccess(`Workspace "${workspaceName}" deleted.`);
@@ -115,11 +111,11 @@ async function handleRemoveRepo(opts: {
115
111
  }
116
112
 
117
113
  try {
118
- const { repoDir } = await inquirer.prompt([{
119
- type: 'autocomplete', name: 'repoDir',
114
+ const repoDir = await search<string>({
120
115
  message: `Search and select repository (${colorize(String(selectedDirs.length), 'cyan')} selected):`,
121
- source: async (_answersSoFar: any, input: string | undefined) => {
122
- const searchInput = input || '';
116
+ pageSize: 16,
117
+ source: async (term: string | undefined) => {
118
+ const searchInput = term || '';
123
119
  const doneOption = { name: colorize('done - Finish selection', 'green'), value: 'done' };
124
120
  if (!searchInput || searchInput.toLowerCase().startsWith('done')) {
125
121
  return [doneOption, ...available.map(e => ({ name: e.displayName, value: e.dirName }))];
@@ -127,8 +123,7 @@ async function handleRemoveRepo(opts: {
127
123
  const results = fuzzy.filter(searchInput, available, { extract: (e) => e.displayName });
128
124
  return [doneOption, ...results.map(result => ({ name: result.original.displayName, value: result.original.dirName }))];
129
125
  },
130
- pageSize: 16,
131
- } as any]);
126
+ });
132
127
 
133
128
  if (repoDir === 'done') {
134
129
  if (selectedDirs.length === 0) { console.log(colorize('\nYou must select at least one repository\n', 'yellow')); continue; }
@@ -147,11 +142,10 @@ async function handleRemoveRepo(opts: {
147
142
  console.log('');
148
143
 
149
144
  if (!opts.yes) {
150
- const { confirm } = await inquirer.prompt([{
151
- type: 'confirm', name: 'confirm',
145
+ const confirmed = await confirm({
152
146
  message: `Remove ${selectedDirs.length} instance(s)? This will delete the directories.`, default: false,
153
- }]);
154
- if (!confirm) { logInfo('Removal cancelled'); process.exit(0); }
147
+ });
148
+ if (!confirmed) { logInfo('Removal cancelled'); process.exit(0); }
155
149
  }
156
150
 
157
151
  logStep(4, 4, 'Removing instances...');
@@ -6,7 +6,7 @@ import { logInfo, logSuccess, logError } from '../utils/logger';
6
6
  import { colorize } from '../utils/colors';
7
7
  import { loadMetadata, saveMetadata, listWorkspaces } from '../utils/workspace-meta';
8
8
  import { formatContextFile, appendToContextFile } from '../utils/context-file';
9
- import inquirer from 'inquirer';
9
+ import { select } from '../utils/prompt';
10
10
 
11
11
  const CONTEXT_FILENAME = 'CONTEXT.md';
12
12
 
@@ -66,12 +66,10 @@ async function resolveWorkspacePath(workspaceName?: string): Promise<{ name: str
66
66
  logError('No workspaces found');
67
67
  return null;
68
68
  }
69
- const { selected } = await inquirer.prompt([{
70
- type: 'list',
71
- name: 'selected',
69
+ const selected = await select({
72
70
  message: 'Select workspace:',
73
71
  choices: workspaces.map(w => ({ name: w.name, value: w.name })),
74
- }]);
72
+ });
75
73
  return { name: selected, path: path.join(WORKSPACES_DIR, selected) };
76
74
  }
77
75
 
@@ -7,12 +7,9 @@ import { listWorkspaces } from '../utils/workspace-meta';
7
7
  import { logError, logInfo } from '../utils/logger';
8
8
  import { outputJson, outputJsonError } 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
 
@@ -85,11 +82,11 @@ async function handleSessions(opts: { json?: boolean } = {}) {
85
82
  }
86
83
  console.log('');
87
84
 
88
- const { selected } = await inquirer.prompt([{
89
- type: 'autocomplete', name: 'selected',
85
+ const selected = await search<WorkspaceSession>({
90
86
  message: 'Select workspace to resume:',
91
- source: async (_answersSoFar: any, input: string | undefined) => {
92
- const searchInput = input || '';
87
+ pageSize: 15,
88
+ source: async (term: string | undefined) => {
89
+ const searchInput = term || '';
93
90
  const source = items.map(item => ({
94
91
  name: `${item.session.workspaceName} ${colorize(item.session.lastActiveLabel, 'dim')} ${colorize(item.repoLabel, 'dim')}`,
95
92
  value: item.session,
@@ -101,8 +98,7 @@ async function handleSessions(opts: { json?: boolean } = {}) {
101
98
  value: result.original.session,
102
99
  }));
103
100
  },
104
- pageSize: 15,
105
- } as any]);
101
+ });
106
102
 
107
103
  const session = selected as WorkspaceSession;
108
104
  await fs.writeFile(TEMP_FILE, session.workspacePath, 'utf-8');
@@ -7,37 +7,27 @@ import { logInfo, logSuccess, logError, logStep } from '../../utils/logger';
7
7
  import { colorize } from '../../utils/colors';
8
8
  import { saveSuite, getSuite, validateSuiteName } from '../../utils/suite';
9
9
  import { SuiteEntry, WorkspaceSuite, PostCloneHook } from '../../types';
10
- import inquirer from 'inquirer';
10
+ import { confirm, input, select } from '../../utils/prompt';
11
11
 
12
12
  async function promptSuiteDetails(defaultName?: string): Promise<{ name: string; description: string }> {
13
- const { name, description } = await inquirer.prompt([
14
- {
15
- type: 'input',
16
- name: 'name',
17
- message: 'Suite name:',
18
- default: defaultName,
19
- validate: (input: string) => validateSuiteName(input.trim()),
20
- filter: (input: string) => input.trim(),
21
- },
22
- {
23
- type: 'input',
24
- name: 'description',
25
- message: 'Suite description (optional):',
26
- },
27
- ]);
13
+ const name = (await input({
14
+ message: 'Suite name:',
15
+ default: defaultName,
16
+ validate: (input: string) => validateSuiteName(input.trim()),
17
+ })).trim();
18
+
19
+ const description = await input({
20
+ message: 'Suite description (optional):',
21
+ });
28
22
 
29
23
  return { name, description: description || '' };
30
24
  }
31
25
 
32
26
  async function promptPostCloneHooks(): Promise<PostCloneHook[] | undefined> {
33
- const { addHooks } = await inquirer.prompt([
34
- {
35
- type: 'confirm',
36
- name: 'addHooks',
37
- message: 'Add post-clone hooks?',
38
- default: false,
39
- },
40
- ]);
27
+ const addHooks = await confirm({
28
+ message: 'Add post-clone hooks?',
29
+ default: false,
30
+ });
41
31
 
42
32
  if (!addHooks) return undefined;
43
33
 
@@ -45,13 +35,9 @@ async function promptPostCloneHooks(): Promise<PostCloneHook[] | undefined> {
45
35
  console.log('Enter commands one at a time. Type "done" when finished.\n');
46
36
 
47
37
  while (true) {
48
- const { command } = await inquirer.prompt([
49
- {
50
- type: 'input',
51
- name: 'command',
52
- message: `Command ${commands.length + 1} (or "done"):`,
53
- },
54
- ]);
38
+ const command = await input({
39
+ message: `Command ${commands.length + 1} (or "done"):`,
40
+ });
55
41
 
56
42
  if (command.trim().toLowerCase() === 'done') break;
57
43
  if (command.trim()) {
@@ -71,14 +57,10 @@ async function checkOverwrite(name: string): Promise<boolean> {
71
57
  const existing = await getSuite(name);
72
58
  if (!existing) return true;
73
59
 
74
- const { overwrite } = await inquirer.prompt([
75
- {
76
- type: 'confirm',
77
- name: 'overwrite',
78
- message: `Suite "${name}" already exists (${existing.entries.length} repos). Overwrite?`,
79
- default: false,
80
- },
81
- ]);
60
+ const overwrite = await confirm({
61
+ message: `Suite "${name}" already exists (${existing.entries.length} repos). Overwrite?`,
62
+ default: false,
63
+ });
82
64
 
83
65
  return overwrite;
84
66
  }
@@ -211,17 +193,13 @@ export async function main() {
211
193
  console.log('='.repeat(60) + '\n');
212
194
 
213
195
  try {
214
- const { mode } = await inquirer.prompt([
215
- {
216
- type: 'list',
217
- name: 'mode',
218
- message: 'How would you like to create the suite?',
219
- choices: [
220
- { name: 'Pick repositories interactively', value: 'interactive' },
221
- { name: 'Save from an existing workspace', value: 'workspace' },
222
- ],
223
- },
224
- ]);
196
+ const mode = await select({
197
+ message: 'How would you like to create the suite?',
198
+ choices: [
199
+ { name: 'Pick repositories interactively', value: 'interactive' },
200
+ { name: 'Save from an existing workspace', value: 'workspace' },
201
+ ],
202
+ });
225
203
 
226
204
  if (mode === 'interactive') {
227
205
  await createInteractive();
@@ -3,7 +3,7 @@
3
3
  import { listSuites, deleteSuite } from '../../utils/suite';
4
4
  import { logInfo, logSuccess, logError } from '../../utils/logger';
5
5
  import { colorize } from '../../utils/colors';
6
- import inquirer from 'inquirer';
6
+ import { confirm, select } from '../../utils/prompt';
7
7
 
8
8
  export async function main() {
9
9
  console.log('\n' + '='.repeat(60));
@@ -18,28 +18,19 @@ export async function main() {
18
18
  process.exit(0);
19
19
  }
20
20
 
21
- const { suiteName } = await inquirer.prompt([
22
- {
23
- type: 'list',
24
- name: 'suiteName',
25
- message: 'Select a suite to delete:',
26
- choices: suites.map(s => ({
27
- name: `${s.name} (${s.entries.length} repos)${s.description ? ` - ${s.description}` : ''}`,
28
- value: s.name,
29
- short: s.name,
30
- })),
31
- pageSize: 15,
32
- },
33
- ]);
34
-
35
- const { confirmed } = await inquirer.prompt([
36
- {
37
- type: 'confirm',
38
- name: 'confirmed',
39
- message: `Are you sure you want to delete suite "${suiteName}"?`,
40
- default: false,
41
- },
42
- ]);
21
+ const suiteName = await select({
22
+ message: 'Select a suite to delete:',
23
+ choices: suites.map(s => ({
24
+ name: `${s.name} (${s.entries.length} repos)${s.description ? ` - ${s.description}` : ''}`,
25
+ value: s.name,
26
+ })),
27
+ pageSize: 15,
28
+ });
29
+
30
+ const confirmed = await confirm({
31
+ message: `Are you sure you want to delete suite "${suiteName}"?`,
32
+ default: false,
33
+ });
43
34
 
44
35
  if (!confirmed) {
45
36
  logInfo('Deletion cancelled');
@@ -5,7 +5,7 @@ import * as path from 'path';
5
5
  import { listSuites, exportSuite, exportAllSuites } from '../../utils/suite';
6
6
  import { logInfo, logSuccess, logError } from '../../utils/logger';
7
7
  import { colorize } from '../../utils/colors';
8
- import inquirer from 'inquirer';
8
+ import { input, select } from '../../utils/prompt';
9
9
 
10
10
  export async function main(opts?: { file?: string }) {
11
11
  console.log('\n' + '='.repeat(60));
@@ -20,35 +20,26 @@ export async function main(opts?: { file?: string }) {
20
20
  process.exit(0);
21
21
  }
22
22
 
23
- const { mode } = await inquirer.prompt([
24
- {
25
- type: 'list',
26
- name: 'mode',
27
- message: 'What would you like to export?',
28
- choices: [
29
- { name: 'A single suite', value: 'single' },
30
- { name: 'All suites', value: 'all' },
31
- ],
32
- },
33
- ]);
23
+ const mode = await select({
24
+ message: 'What would you like to export?',
25
+ choices: [
26
+ { name: 'A single suite', value: 'single' },
27
+ { name: 'All suites', value: 'all' },
28
+ ],
29
+ });
34
30
 
35
31
  let data;
36
32
  let defaultFilename: string;
37
33
 
38
34
  if (mode === 'single') {
39
- const { suiteName } = await inquirer.prompt([
40
- {
41
- type: 'list',
42
- name: 'suiteName',
43
- message: 'Select a suite to export:',
44
- choices: suites.map(s => ({
45
- name: `${s.name} (${s.entries.length} repos)${s.description ? ` - ${s.description}` : ''}`,
46
- value: s.name,
47
- short: s.name,
48
- })),
49
- pageSize: 15,
50
- },
51
- ]);
35
+ const suiteName = await select({
36
+ message: 'Select a suite to export:',
37
+ choices: suites.map(s => ({
38
+ name: `${s.name} (${s.entries.length} repos)${s.description ? ` - ${s.description}` : ''}`,
39
+ value: s.name,
40
+ })),
41
+ pageSize: 15,
42
+ });
52
43
 
53
44
  data = await exportSuite(suiteName);
54
45
  if (!data) {
@@ -68,14 +59,10 @@ export async function main(opts?: { file?: string }) {
68
59
  if (outputArg && !outputArg.startsWith('-')) {
69
60
  outputPath = path.resolve(outputArg);
70
61
  } else {
71
- const { filePath } = await inquirer.prompt([
72
- {
73
- type: 'input',
74
- name: 'filePath',
75
- message: 'Output file path:',
76
- default: `./${defaultFilename}`,
77
- },
78
- ]);
62
+ const filePath = await input({
63
+ message: 'Output file path:',
64
+ default: `./${defaultFilename}`,
65
+ });
79
66
  outputPath = path.resolve(filePath);
80
67
  }
81
68
 
@@ -6,7 +6,7 @@ import { importSuites } from '../../utils/suite';
6
6
  import { logInfo, logSuccess, logError, logWarning } from '../../utils/logger';
7
7
  import { colorize } from '../../utils/colors';
8
8
  import { SuitesStore } from '../../types';
9
- import inquirer from 'inquirer';
9
+ import { confirm, input } from '../../utils/prompt';
10
10
 
11
11
  export async function main(opts?: { file?: string }) {
12
12
  console.log('\n' + '='.repeat(60));
@@ -20,14 +20,10 @@ export async function main(opts?: { file?: string }) {
20
20
  if (fileArg && !fileArg.startsWith('-')) {
21
21
  filePath = path.resolve(fileArg);
22
22
  } else {
23
- const { inputPath } = await inquirer.prompt([
24
- {
25
- type: 'input',
26
- name: 'inputPath',
27
- message: 'Path to suite JSON file:',
28
- validate: (input: string) => input.trim().length > 0 || 'File path is required',
29
- },
30
- ]);
23
+ const inputPath = await input({
24
+ message: 'Path to suite JSON file:',
25
+ validate: (input: string) => input.trim().length > 0 || 'File path is required',
26
+ });
31
27
  filePath = path.resolve(inputPath);
32
28
  }
33
29
 
@@ -61,14 +57,10 @@ export async function main(opts?: { file?: string }) {
61
57
  }
62
58
  console.log('');
63
59
 
64
- const { overwrite } = await inquirer.prompt([
65
- {
66
- type: 'confirm',
67
- name: 'overwrite',
68
- message: 'Overwrite existing suites with the same name?',
69
- default: false,
70
- },
71
- ]);
60
+ const overwrite = await confirm({
61
+ message: 'Overwrite existing suites with the same name?',
62
+ default: false,
63
+ });
72
64
 
73
65
  const result = await importSuites(data, overwrite);
74
66
 
@@ -13,7 +13,7 @@ import { colorize } from '../../utils/colors';
13
13
  import { listSuites } from '../../utils/suite';
14
14
  import { runPostCloneHooks } from '../../utils/hooks';
15
15
  import { GitHubRepo } from '../../types';
16
- import inquirer from 'inquirer';
16
+ import { select } from '../../utils/prompt';
17
17
  import { validateWorkspaceName, checkWorkspaceExists, sanitizeWorkspaceName, resolveWorkspaceNameConflict } from '../../utils/validation';
18
18
 
19
19
  export interface SuiteUseOpts {
@@ -64,19 +64,14 @@ export async function main(opts?: SuiteUseOpts) {
64
64
  }
65
65
  selectedSuite = found;
66
66
  } else {
67
- const { suite } = await inquirer.prompt([
68
- {
69
- type: 'list',
70
- name: 'suite',
71
- message: 'Select a suite:',
72
- choices: suites.map(s => ({
73
- name: `${s.name} (${s.entries.length} repos)${s.description ? ` - ${s.description}` : ''}`,
74
- value: s,
75
- short: s.name,
76
- })),
77
- pageSize: 15,
78
- },
79
- ]);
67
+ const suite = await select({
68
+ message: 'Select a suite:',
69
+ choices: suites.map(s => ({
70
+ name: `${s.name} (${s.entries.length} repos)${s.description ? ` - ${s.description}` : ''}`,
71
+ value: s,
72
+ })),
73
+ pageSize: 15,
74
+ });
80
75
  selectedSuite = suite;
81
76
  }
82
77
 
package/src/program.ts CHANGED
@@ -39,6 +39,7 @@ import { registerCreateCommand } from './commands/create';
39
39
  import { registerListCommand } from './commands/list';
40
40
  import { registerUpdateCommand } from './commands/update';
41
41
  import { registerDeleteCommand } from './commands/delete';
42
+ import { registerPruneCommand } from './commands/prune';
42
43
  import { registerSyncCommand } from './commands/sync';
43
44
  import { registerStatusCommand } from './commands/status';
44
45
  import { registerDiffCommand } from './commands/diff';
@@ -66,6 +67,7 @@ registerCreateCommand(program);
66
67
  registerListCommand(program);
67
68
  registerUpdateCommand(program);
68
69
  registerDeleteCommand(program);
70
+ registerPruneCommand(program);
69
71
  registerSyncCommand(program);
70
72
  registerStatusCommand(program);
71
73
  registerDiffCommand(program);