@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
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
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
|
|
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
|
|
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
|
|
package/src/commands/sessions.ts
CHANGED
|
@@ -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
|
|
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
|
|
89
|
-
type: 'autocomplete', name: 'selected',
|
|
85
|
+
const selected = await search<WorkspaceSession>({
|
|
90
86
|
message: 'Select workspace to resume:',
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
|
10
|
+
import { confirm, input, select } from '../../utils/prompt';
|
|
11
11
|
|
|
12
12
|
async function promptSuiteDetails(defaultName?: string): Promise<{ name: string; description: string }> {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
49
|
-
{
|
|
50
|
-
|
|
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
|
|
75
|
-
{
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
name: '
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
name: '
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
name: '
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
name: '
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
name: '
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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);
|