@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
package/dist/utils/prompts.js
CHANGED
|
@@ -32,41 +32,31 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
-
};
|
|
38
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
36
|
exports.promptMultiWorkspaceSelection = exports.promptWorkspaceSelection = exports.confirmWorkspaceCreation = exports.promptWorkspaceName = exports.promptRepositorySelection = exports.promptInstanceSuffix = void 0;
|
|
40
|
-
const
|
|
41
|
-
const inquirer_autocomplete_prompt_1 = __importDefault(require("inquirer-autocomplete-prompt"));
|
|
37
|
+
const prompt_1 = require("./prompt");
|
|
42
38
|
const fuzzy = __importStar(require("fuzzy"));
|
|
43
39
|
const validation_1 = require("./validation");
|
|
44
40
|
const colors_1 = require("./colors");
|
|
45
|
-
// Register autocomplete prompt type
|
|
46
|
-
inquirer_1.default.registerPrompt('autocomplete', inquirer_autocomplete_prompt_1.default);
|
|
47
41
|
const promptInstanceSuffix = async (repoName, existingDirectoryNames) => {
|
|
48
|
-
const
|
|
49
|
-
{
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return `"${candidateName}" already exists. Choose a different suffix`;
|
|
65
|
-
}
|
|
66
|
-
return true;
|
|
67
|
-
},
|
|
42
|
+
const suffix = await (0, prompt_1.input)({
|
|
43
|
+
message: `"${repoName}" already exists. Enter a suffix for this instance:`,
|
|
44
|
+
validate: (input) => {
|
|
45
|
+
if (!input || input.trim().length === 0) {
|
|
46
|
+
return 'Suffix cannot be empty';
|
|
47
|
+
}
|
|
48
|
+
const trimmed = input.trim();
|
|
49
|
+
// Validate characters (alphanumeric, hyphens, underscores)
|
|
50
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(trimmed)) {
|
|
51
|
+
return 'Suffix can only contain letters, numbers, hyphens, and underscores';
|
|
52
|
+
}
|
|
53
|
+
const candidateName = `${repoName}-${trimmed}`;
|
|
54
|
+
if (existingDirectoryNames.includes(candidateName)) {
|
|
55
|
+
return `"${candidateName}" already exists. Choose a different suffix`;
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
68
58
|
},
|
|
69
|
-
|
|
59
|
+
});
|
|
70
60
|
return suffix.trim();
|
|
71
61
|
};
|
|
72
62
|
exports.promptInstanceSuffix = promptInstanceSuffix;
|
|
@@ -80,38 +70,34 @@ const promptRepositorySelection = async (repos, existingDirectoryNames = []) =>
|
|
|
80
70
|
console.log('Type "done" when finished selecting repositories.\n');
|
|
81
71
|
while (true) {
|
|
82
72
|
try {
|
|
83
|
-
const
|
|
84
|
-
{
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}));
|
|
110
|
-
return [doneOption, ...suggestions];
|
|
111
|
-
},
|
|
112
|
-
pageSize: 16,
|
|
73
|
+
const repoName = await (0, prompt_1.search)({
|
|
74
|
+
message: `Search and select repository (${(0, colors_1.colorize)(String(selectedEntries.length), 'cyan')} selected):`,
|
|
75
|
+
pageSize: 16,
|
|
76
|
+
source: async (term) => {
|
|
77
|
+
const searchInput = term || '';
|
|
78
|
+
// Always include done option
|
|
79
|
+
const doneOption = { name: (0, colors_1.colorize)('done - Finish selection', 'green'), value: 'done' };
|
|
80
|
+
// If no input or "done" typed, show done + top repos
|
|
81
|
+
if (!searchInput || searchInput.toLowerCase().startsWith('done')) {
|
|
82
|
+
return [
|
|
83
|
+
doneOption,
|
|
84
|
+
...repos.slice(0, 15).map(repo => ({
|
|
85
|
+
name: `${repo.name}${repo.description ? ` - ${(0, colors_1.colorize)(repo.description, 'gray')}` : ''}`,
|
|
86
|
+
value: repo.name,
|
|
87
|
+
}))
|
|
88
|
+
];
|
|
89
|
+
}
|
|
90
|
+
// Perform fuzzy search
|
|
91
|
+
const results = fuzzy.filter(searchInput, repos, {
|
|
92
|
+
extract: (repo) => `${repo.name} ${repo.description || ''}`,
|
|
93
|
+
});
|
|
94
|
+
const suggestions = results.slice(0, 15).map(result => ({
|
|
95
|
+
name: `${result.original.name}${result.original.description ? ` - ${(0, colors_1.colorize)(result.original.description, 'gray')}` : ''}`,
|
|
96
|
+
value: result.original.name,
|
|
97
|
+
}));
|
|
98
|
+
return [doneOption, ...suggestions];
|
|
113
99
|
},
|
|
114
|
-
|
|
100
|
+
});
|
|
115
101
|
if (repoName === 'done') {
|
|
116
102
|
if (selectedEntries.length === 0) {
|
|
117
103
|
console.log((0, colors_1.colorize)('\nYou must select at least one repository\n', 'yellow'));
|
|
@@ -153,22 +139,17 @@ const promptRepositorySelection = async (repos, existingDirectoryNames = []) =>
|
|
|
153
139
|
};
|
|
154
140
|
exports.promptRepositorySelection = promptRepositorySelection;
|
|
155
141
|
const promptWorkspaceName = async () => {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return validationResult;
|
|
165
|
-
}
|
|
166
|
-
return true;
|
|
167
|
-
},
|
|
168
|
-
filter: (input) => (0, validation_1.sanitizeWorkspaceName)(input),
|
|
142
|
+
const raw = await (0, prompt_1.input)({
|
|
143
|
+
message: 'Enter workspace name:',
|
|
144
|
+
validate: (value) => {
|
|
145
|
+
const validationResult = (0, validation_1.validateWorkspaceName)((0, validation_1.sanitizeWorkspaceName)(value));
|
|
146
|
+
if (validationResult !== true) {
|
|
147
|
+
return validationResult;
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
169
150
|
},
|
|
170
|
-
|
|
171
|
-
return
|
|
151
|
+
});
|
|
152
|
+
return (0, validation_1.sanitizeWorkspaceName)(raw);
|
|
172
153
|
};
|
|
173
154
|
exports.promptWorkspaceName = promptWorkspaceName;
|
|
174
155
|
const confirmWorkspaceCreation = async (workspaceName, repoCount, workspacePath) => {
|
|
@@ -179,14 +160,10 @@ const confirmWorkspaceCreation = async (workspaceName, repoCount, workspacePath)
|
|
|
179
160
|
console.log(`Location: ${(0, colors_1.colorize)(workspacePath, 'gray')}`);
|
|
180
161
|
console.log(`Repositories: ${(0, colors_1.colorize)(String(repoCount), 'yellow')}`);
|
|
181
162
|
console.log('='.repeat(60) + '\n');
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
message: 'Create workspace with these settings?',
|
|
187
|
-
default: true,
|
|
188
|
-
},
|
|
189
|
-
]);
|
|
163
|
+
const confirmed = await (0, prompt_1.confirm)({
|
|
164
|
+
message: 'Create workspace with these settings?',
|
|
165
|
+
default: true,
|
|
166
|
+
});
|
|
190
167
|
return confirmed;
|
|
191
168
|
};
|
|
192
169
|
exports.confirmWorkspaceCreation = confirmWorkspaceCreation;
|
|
@@ -199,17 +176,12 @@ const promptWorkspaceSelection = async (workspaces) => {
|
|
|
199
176
|
? `${ws.name} (${ws.metadata.repositories.length} repos)`
|
|
200
177
|
: ws.name,
|
|
201
178
|
value: ws.name,
|
|
202
|
-
short: ws.name,
|
|
203
179
|
}));
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
choices,
|
|
210
|
-
pageSize: 15,
|
|
211
|
-
},
|
|
212
|
-
]);
|
|
180
|
+
const workspaceName = await (0, prompt_1.select)({
|
|
181
|
+
message: 'Select workspace to update:',
|
|
182
|
+
choices,
|
|
183
|
+
pageSize: 15,
|
|
184
|
+
});
|
|
213
185
|
return workspaceName;
|
|
214
186
|
};
|
|
215
187
|
exports.promptWorkspaceSelection = promptWorkspaceSelection;
|
|
@@ -226,33 +198,29 @@ const promptMultiWorkspaceSelection = async (workspaces) => {
|
|
|
226
198
|
console.log((0, colors_1.colorize)('All workspaces selected.', 'yellow'));
|
|
227
199
|
break;
|
|
228
200
|
}
|
|
229
|
-
const
|
|
230
|
-
{
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}));
|
|
251
|
-
return [doneOption, ...suggestions];
|
|
252
|
-
},
|
|
253
|
-
pageSize: 16,
|
|
201
|
+
const workspaceName = await (0, prompt_1.search)({
|
|
202
|
+
message: `Search and select workspace (${(0, colors_1.colorize)(String(selectedNames.length), 'cyan')} selected):`,
|
|
203
|
+
pageSize: 16,
|
|
204
|
+
source: async (term) => {
|
|
205
|
+
const searchInput = term || '';
|
|
206
|
+
const doneOption = { name: (0, colors_1.colorize)('done - Finish selection', 'green'), value: 'done' };
|
|
207
|
+
if (!searchInput || searchInput.toLowerCase().startsWith('done')) {
|
|
208
|
+
return [
|
|
209
|
+
doneOption,
|
|
210
|
+
...availableNames.slice(0, 15).map(name => ({
|
|
211
|
+
name,
|
|
212
|
+
value: name,
|
|
213
|
+
}))
|
|
214
|
+
];
|
|
215
|
+
}
|
|
216
|
+
const results = fuzzy.filter(searchInput, availableNames);
|
|
217
|
+
const suggestions = results.slice(0, 15).map(result => ({
|
|
218
|
+
name: result.original,
|
|
219
|
+
value: result.original,
|
|
220
|
+
}));
|
|
221
|
+
return [doneOption, ...suggestions];
|
|
254
222
|
},
|
|
255
|
-
|
|
223
|
+
});
|
|
256
224
|
if (workspaceName === 'done') {
|
|
257
225
|
if (selectedNames.length === 0) {
|
|
258
226
|
console.log((0, colors_1.colorize)('\nYou must select at least one workspace\n', 'yellow'));
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toCandidate = toCandidate;
|
|
4
|
+
exports.isStale = isStale;
|
|
5
|
+
exports.protectionReason = protectionReason;
|
|
6
|
+
exports.planPrune = planPrune;
|
|
7
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
8
|
+
/** Build a dated candidate for a workspace. `now` is injected for testability. */
|
|
9
|
+
function toCandidate(ws, now) {
|
|
10
|
+
const referenceAt = ws.lastActiveAt > 0 ? ws.lastActiveAt : ws.createdAt;
|
|
11
|
+
const undatable = !(referenceAt > 0);
|
|
12
|
+
const ageDays = undatable ? 0 : Math.floor((now - referenceAt) / MS_PER_DAY);
|
|
13
|
+
return {
|
|
14
|
+
name: ws.name,
|
|
15
|
+
path: ws.path,
|
|
16
|
+
repoDirNames: ws.repoDirNames,
|
|
17
|
+
referenceAt,
|
|
18
|
+
fromSession: ws.lastActiveAt > 0,
|
|
19
|
+
ageDays,
|
|
20
|
+
undatable,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A workspace is a prune candidate when it is datable and its age meets the
|
|
25
|
+
* threshold. Undatable workspaces are never auto-selected — we won't delete
|
|
26
|
+
* something we can't put a date on. A future `referenceAt` (clock skew) yields
|
|
27
|
+
* a negative age and is therefore not stale.
|
|
28
|
+
*/
|
|
29
|
+
function isStale(c, days) {
|
|
30
|
+
return !c.undatable && c.ageDays >= days;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Why a stale workspace should be held back from deletion, or null if it's safe.
|
|
34
|
+
* Unsafe = any repo has uncommitted changes (`!clean`) or unpushed commits
|
|
35
|
+
* (`ahead > 0`). With `includeDirty`, nothing is held back. An empty workspace
|
|
36
|
+
* (no repos) is always safe.
|
|
37
|
+
*/
|
|
38
|
+
function protectionReason(statuses, includeDirty) {
|
|
39
|
+
if (includeDirty)
|
|
40
|
+
return null;
|
|
41
|
+
const dirty = statuses.filter((s) => !s.clean).length;
|
|
42
|
+
const unpushed = statuses.filter((s) => s.ahead > 0).length;
|
|
43
|
+
if (dirty === 0 && unpushed === 0)
|
|
44
|
+
return null;
|
|
45
|
+
const parts = [];
|
|
46
|
+
if (dirty > 0)
|
|
47
|
+
parts.push(`${dirty} repo${dirty === 1 ? '' : 's'} with uncommitted changes`);
|
|
48
|
+
if (unpushed > 0)
|
|
49
|
+
parts.push(`${unpushed} repo${unpushed === 1 ? '' : 's'} with unpushed commits`);
|
|
50
|
+
return parts.join(', ');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Partition stale candidates into prunable vs. protected, given a resolver that
|
|
54
|
+
* returns each workspace's per-repo git status. The resolver is only invoked
|
|
55
|
+
* for workspaces that actually have repos, so empty stale workspaces cost no git
|
|
56
|
+
* calls. Injecting the resolver keeps this function pure and unit-testable.
|
|
57
|
+
*/
|
|
58
|
+
async function planPrune(staleCandidates, getStatuses, includeDirty) {
|
|
59
|
+
const prunable = [];
|
|
60
|
+
const protectedList = [];
|
|
61
|
+
for (const c of staleCandidates) {
|
|
62
|
+
const statuses = c.repoDirNames.length > 0 ? await getStatuses(c) : [];
|
|
63
|
+
const reason = protectionReason(statuses, includeDirty);
|
|
64
|
+
if (reason)
|
|
65
|
+
protectedList.push({ candidate: c, reason });
|
|
66
|
+
else
|
|
67
|
+
prunable.push(c);
|
|
68
|
+
}
|
|
69
|
+
return { prunable, protected: protectedList };
|
|
70
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nemus-cli/nemus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"workspaces": [
|
|
5
5
|
"packages/*"
|
|
6
6
|
],
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"homepage": "https://github.com/me-public/nemus#readme",
|
|
31
31
|
"engines": {
|
|
32
|
-
"node": ">=22.
|
|
32
|
+
"node": ">=22.13.0",
|
|
33
33
|
"npm": ">=9.0.0"
|
|
34
34
|
},
|
|
35
35
|
"publishConfig": {
|
|
@@ -67,21 +67,18 @@
|
|
|
67
67
|
"postinstall": "node scripts/postinstall.js"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
|
+
"@inquirer/prompts": "^8.7.0",
|
|
70
71
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
71
72
|
"cli-progress": "^3.12.0",
|
|
72
73
|
"commander": "^15.0.0",
|
|
73
74
|
"fuzzy": "^0.1.3",
|
|
74
75
|
"ink": "^7.1.1",
|
|
75
|
-
"inquirer": "^8.0.0",
|
|
76
|
-
"inquirer-autocomplete-prompt": "^2.0.0",
|
|
77
76
|
"markdown-table": "^3.0.3",
|
|
78
77
|
"react": "^19.2.8",
|
|
79
78
|
"zod": "^4.3.6"
|
|
80
79
|
},
|
|
81
80
|
"devDependencies": {
|
|
82
81
|
"@types/cli-progress": "^3.11.0",
|
|
83
|
-
"@types/inquirer": "^9.0.3",
|
|
84
|
-
"@types/inquirer-autocomplete-prompt": "^2.0.0",
|
|
85
82
|
"@types/node": "^26.3.0",
|
|
86
83
|
"@types/react": "^19.2.2",
|
|
87
84
|
"ts-node": "^10.9.1",
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Emit GitHub Release notes for a given version by extracting that version's
|
|
3
|
+
// section from CHANGELOG.md (newest-first "Keep a Changelog" format), prepended
|
|
4
|
+
// with an install snippet and followed by a compare link to the previous tag.
|
|
5
|
+
//
|
|
6
|
+
// Usage: node scripts/release-notes.mjs <version> [repo]
|
|
7
|
+
// repo defaults to $GITHUB_REPOSITORY or "me-public/nemus".
|
|
8
|
+
//
|
|
9
|
+
// If the version has no CHANGELOG section, prints a minimal fallback (so a
|
|
10
|
+
// release is never blocked on a missing entry). Dependency-free; prints to
|
|
11
|
+
// stdout so the workflow can redirect it into `gh release --notes-file`.
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
|
|
14
|
+
const version = (process.argv[2] || '').trim();
|
|
15
|
+
const repo = (process.argv[3] || process.env.GITHUB_REPOSITORY || 'me-public/nemus').trim();
|
|
16
|
+
if (!version) {
|
|
17
|
+
process.stderr.write('usage: release-notes.mjs <version> [repo]\n');
|
|
18
|
+
process.exit(2);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const install = `\`\`\`bash\nnpm install -g @nemus-cli/nemus@${version}\n\`\`\``;
|
|
22
|
+
|
|
23
|
+
let body = '';
|
|
24
|
+
let prev = null;
|
|
25
|
+
try {
|
|
26
|
+
const changelog = readFileSync(new URL('../CHANGELOG.md', import.meta.url), 'utf8');
|
|
27
|
+
const lines = changelog.split('\n');
|
|
28
|
+
// Collect version headers in file order (newest first) with their line index.
|
|
29
|
+
const heads = [];
|
|
30
|
+
lines.forEach((line, i) => {
|
|
31
|
+
const m = line.match(/^## \[(\d+\.\d+\.\d+)\]/);
|
|
32
|
+
if (m) heads.push({ version: m[1], line: i });
|
|
33
|
+
});
|
|
34
|
+
const idx = heads.findIndex((h) => h.version === version);
|
|
35
|
+
if (idx !== -1) {
|
|
36
|
+
const start = heads[idx].line + 1;
|
|
37
|
+
const end = idx + 1 < heads.length ? heads[idx + 1].line : lines.length;
|
|
38
|
+
body = lines.slice(start, end).join('\n').trim();
|
|
39
|
+
// Newest-first: the NEXT header in the file is the previous release.
|
|
40
|
+
prev = idx + 1 < heads.length ? heads[idx + 1].version : null;
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// fall through to fallback
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const compare = prev
|
|
47
|
+
? `[\`v${prev}...v${version}\`](https://github.com/${repo}/compare/v${prev}...v${version})`
|
|
48
|
+
: `[\`v${version}\`](https://github.com/${repo}/releases/tag/v${version})`;
|
|
49
|
+
|
|
50
|
+
const parts = [`## Nemus v${version}`, '', install];
|
|
51
|
+
if (body) parts.push('', body);
|
|
52
|
+
parts.push('', '---', '', `**Full diff:** ${compare} · [Full changelog](https://github.com/${repo}/blob/main/CHANGELOG.md)`);
|
|
53
|
+
|
|
54
|
+
process.stdout.write(parts.join('\n') + '\n');
|
package/skills/config.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: config
|
|
3
|
+
description: Read and write Nemus configuration non-interactively (get/set/unset/list/path/edit)
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Non-interactive configuration in `~/.nemus/config.json` (no wizard):
|
|
7
|
+
```bash
|
|
8
|
+
nemus config list # all keys + resolved values (alias: ls; --json)
|
|
9
|
+
nemus config get <key> # print one value (--json)
|
|
10
|
+
nemus config set <key> <value> # set + validate + persist
|
|
11
|
+
nemus config unset <key> # revert a key to its default
|
|
12
|
+
nemus config path # print the config file path
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- Values are validated/coerced per key: booleans accept `true/false/yes/no/on/off/1/0`; enums (e.g. `cloneProtocol=ssh|https`) are checked. Invalid key/value exits non-zero.
|
|
16
|
+
- Common keys: `workspacesDir`, `githubOrg`, `cloneProtocol`, `aiAgent`, `primaryAgent`. Run `nemus config list` to see all.
|
|
17
|
+
- For a guided first-time setup, use `nemus configure` instead.
|
package/skills/nemus/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: nemus
|
|
3
3
|
description: Manage multi-repo development workspaces — create, sync, branch, diff, clone, archive, analyze dependencies, run commands across repos, check status, clean up, delete, list org repos, search repos, list suites, remove repo, cache, suite management, sessions, history, generate docs, configure, MCP server, AI prompt. Use when working with workspaces, repos, branches, git operations across multiple repos, or the `nemus` CLI tool. NEVER use `git clone` directly — always use `nemus update` to add repos and `nemus create` to create workspaces.
|
|
4
|
-
bashPattern: "\\bnemus\\s+(create|list|update|delete|sync|status|diff|run|go|doctor|analyze-deps|history|cleanup|remove-repo|archive|sessions|generate-docs|configure|configure-claude|ghq-status|tui|dashboard|dash|branch|suite|cache|mcp|--)\\b"
|
|
4
|
+
bashPattern: "\\bnemus\\s+(create|list|update|delete|prune|sync|status|diff|run|go|doctor|analyze-deps|reflect|retro|history|cleanup|remove-repo|archive|sessions|save-context|ctx|generate-docs|configure|config|configure-claude|ghq-status|completion|tui|dashboard|dash|branch|suite|cache|mcp|--)\\b"
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Nemus
|
|
@@ -33,6 +33,7 @@ Global flags: `-f/--force-refresh` (skip repo cache), `-y/--yes` (skip prompts),
|
|
|
33
33
|
| Create new workspace | [create-workspace](references/create-workspace.md) | `nemus create` | `c` |
|
|
34
34
|
| Add repos to workspace | [update-workspace](references/update-workspace.md) | `nemus update` | `u` |
|
|
35
35
|
| Delete workspace permanently | [delete-workspace](references/delete-workspace.md) | `nemus delete` | `d` |
|
|
36
|
+
| Prune inactive workspaces (safe) | [prune](references/prune.md) | `nemus prune` | — |
|
|
36
37
|
| Archive / unarchive workspace | [archive-workspace](references/archive-workspace.md) | `nemus archive` | `a` |
|
|
37
38
|
| List all workspaces | [list-workspaces](references/list-workspaces.md) | `nemus list` | `l` |
|
|
38
39
|
| Navigate to workspace | [go](references/go.md) | `nemus go [name]` | — |
|
|
@@ -88,14 +89,18 @@ Global flags: `-f/--force-refresh` (skip repo cache), `-y/--yes` (skip prompts),
|
|
|
88
89
|
| Resume Claude session | [sessions](references/sessions.md) | `nemus sessions` | `ses` |
|
|
89
90
|
| Agent management dashboard | [dashboard](references/dashboard.md) | `nemus dashboard` | `dash` |
|
|
90
91
|
| View operation history | [history](references/history.md) | `nemus history` | `h` |
|
|
92
|
+
| Save progress/context to a workspace | [save-context](references/save-context.md) | `nemus save-context` | `ctx` |
|
|
93
|
+
| Retrospective on recent sessions | [reflect](references/reflect.md) | `nemus reflect` | `retro` |
|
|
91
94
|
|
|
92
95
|
### Configuration & MCP
|
|
93
96
|
|
|
94
97
|
| Intent | Reference | CLI |
|
|
95
98
|
|---|---|---|
|
|
96
|
-
| Configure Nemus | [configure](references/configure.md) | `nemus configure` |
|
|
99
|
+
| Configure Nemus (interactive wizard) | [configure](references/configure.md) | `nemus configure` |
|
|
100
|
+
| Get/set config non-interactively | [config](references/config.md) | `nemus config get\|set\|list\|edit` |
|
|
97
101
|
| Configure Claude integration | [configure-claude](references/configure-claude.md) | `nemus configure-claude` |
|
|
98
102
|
| Check ghq status | [ghq-status](references/ghq-status.md) | `nemus ghq-status` |
|
|
103
|
+
| Shell completions (bash/zsh/fish) | [completion](references/completion.md) | `nemus completion <shell>` |
|
|
99
104
|
| Install / manage MCP server | [mcp](references/mcp.md) | `nemus mcp install\|status\|upgrade\|uninstall` |
|
|
100
105
|
|
|
101
106
|
### AI Assistant & TUI
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Shell Completions
|
|
2
|
+
|
|
3
|
+
`nemus completion <shell>` prints a completion script for `bash`, `zsh`, or
|
|
4
|
+
`fish`. It completes subcommands and, for workspace-scoped commands, live
|
|
5
|
+
workspace names (the script calls back into `nemus completion --workspaces`, so
|
|
6
|
+
completions stay fresh without regenerating). Registered for both `nemus` and
|
|
7
|
+
`nem`.
|
|
8
|
+
|
|
9
|
+
Install (pick your shell):
|
|
10
|
+
```bash
|
|
11
|
+
# bash
|
|
12
|
+
nemus completion bash > /etc/bash_completion.d/nemus # or: >> ~/.bashrc
|
|
13
|
+
|
|
14
|
+
# zsh — save on your $fpath as _nemus
|
|
15
|
+
nemus completion zsh > "${fpath[1]}/_nemus"
|
|
16
|
+
|
|
17
|
+
# fish
|
|
18
|
+
nemus completion fish > ~/.config/fish/completions/nemus.fish
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then restart the shell (or `source` the file). Requires a shell argument —
|
|
22
|
+
one of `bash|zsh|fish`.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Config — non-interactive configuration
|
|
2
|
+
|
|
3
|
+
`nemus config` reads and writes `~/.nemus/config.json` without the interactive
|
|
4
|
+
`nemus configure` wizard. Ideal for scripting and for setting one value.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
nemus config list # show all keys + resolved values (alias: ls)
|
|
8
|
+
nemus config list --json # machine-readable
|
|
9
|
+
nemus config get <key> # print one value (--json for structured)
|
|
10
|
+
nemus config set <key> <value> # set + validate + persist
|
|
11
|
+
nemus config unset <key> # remove a key (revert to default)
|
|
12
|
+
nemus config path # print the config file path
|
|
13
|
+
nemus config edit # open the file in $VISUAL/$EDITOR (needs a TTY)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Values are validated and coerced per key: booleans accept
|
|
17
|
+
`true/false/yes/no/on/off/1/0`; enums (e.g. `cloneProtocol` = `ssh|https`) are
|
|
18
|
+
checked. An unknown key or invalid value exits non-zero with a clear message.
|
|
19
|
+
|
|
20
|
+
Common keys: `workspacesDir`, `githubOrg`, `cloneProtocol`, `aiAgent`,
|
|
21
|
+
`primaryAgent`, `autoLaunchClaude`, `generateClaudeContext`, `installMcp`.
|
|
22
|
+
Run `nemus config list` to see them all.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# examples
|
|
26
|
+
nemus config set githubOrg acme
|
|
27
|
+
nemus config set cloneProtocol https
|
|
28
|
+
nemus config get workspacesDir --json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Pairs well with `--quiet` for scripts. For a guided first-time setup, use
|
|
32
|
+
`nemus configure` instead.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Prune Inactive Workspaces
|
|
2
|
+
|
|
3
|
+
Bulk-delete workspaces with no recent activity. **Safe by default** — it holds
|
|
4
|
+
back any workspace with uncommitted or unpushed work.
|
|
5
|
+
|
|
6
|
+
1. **Always preview first.** Show the user exactly what would be deleted (and
|
|
7
|
+
what is protected) before removing anything:
|
|
8
|
+
```bash
|
|
9
|
+
nemus prune --days <n> --dry-run
|
|
10
|
+
```
|
|
11
|
+
A workspace is *stale* when its most recent agent session — or, if it has no
|
|
12
|
+
session, its `createdAt` — is older than `--days` (default 30). Workspaces
|
|
13
|
+
with no date at all are never selected.
|
|
14
|
+
|
|
15
|
+
2. **Review the two lists.** `prune` prints:
|
|
16
|
+
- **Protected** — stale workspaces skipped because a repo has uncommitted
|
|
17
|
+
changes or unpushed commits (with the reason). These are NOT deleted.
|
|
18
|
+
- **Prunable** — stale workspaces that are safe to remove.
|
|
19
|
+
|
|
20
|
+
3. **Confirm with the user, then prune.** This permanently deletes the
|
|
21
|
+
workspace directories and every cloned repo inside them:
|
|
22
|
+
```bash
|
|
23
|
+
nemus prune --days <n> # prompts for confirmation (default: No)
|
|
24
|
+
nemus prune --days <n> --yes # non-interactive (only when the user is sure)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
4. **`--json`** gives a machine-readable plan (`{ prunable, protected }`) and,
|
|
28
|
+
like `--dry-run`, never deletes.
|
|
29
|
+
|
|
30
|
+
Deletions go through the same validated path as `nemus delete` (name allowlist +
|
|
31
|
+
path pinned inside the workspaces directory).
|
|
32
|
+
|
|
33
|
+
## Flags
|
|
34
|
+
|
|
35
|
+
| Flag | Short | Description |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `--days <n>` | `-d` | Stale after N days of inactivity (default 30) |
|
|
38
|
+
| `--dry-run` | | Show the plan without deleting anything |
|
|
39
|
+
| `--json` | | Output the plan as JSON (never deletes) |
|
|
40
|
+
| `--yes` | `-y` | Skip the confirmation prompt |
|
|
41
|
+
| `--include-dirty` | | Also prune workspaces with uncommitted/unpushed work (overrides the safety guard — use with care) |
|
|
42
|
+
|
|
43
|
+
> **Only** pass `--include-dirty` when the user has explicitly accepted losing
|
|
44
|
+
> uncommitted/unpushed work in the protected workspaces.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Reflect — retrospective on recent sessions
|
|
2
|
+
|
|
3
|
+
`nemus reflect` (alias `retro`) reads your recent workspaces' agent session
|
|
4
|
+
transcripts and asks *your own* configured agent (claude/pi/opencode — no API key
|
|
5
|
+
of Nemus's) to recommend concrete setup improvements: skills to add, missing
|
|
6
|
+
`AGENTS.md`/context rules, missing connectivity/smoke tests, and prompt/workflow
|
|
7
|
+
habits — each with a priority and an example.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
nemus reflect # analyze the most recent workspaces (default 10)
|
|
11
|
+
nemus reflect --limit 20 # widen the window
|
|
12
|
+
nemus reflect --workspace <name> # analyze a single workspace
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Output / sharing:
|
|
16
|
+
```bash
|
|
17
|
+
nemus reflect --json # structured report ({ ok:false, error } on failure)
|
|
18
|
+
nemus reflect --markdown > reflection.md # paste into an issue/PR
|
|
19
|
+
nemus reflect --group-by kind # group recommendations by kind (default: priority)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Review saved reports (each run is saved under `~/.nemus/reflect/` unless
|
|
23
|
+
`--no-save`):
|
|
24
|
+
```bash
|
|
25
|
+
nemus reflect history # list saved reports
|
|
26
|
+
nemus reflect show [id] # show one (id or id-prefix; defaults to latest)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Flags
|
|
30
|
+
|
|
31
|
+
| Flag | Description |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `--limit <n>` / `-n` | How many recent workspaces to analyze (default 10) |
|
|
34
|
+
| `--workspace <name>` / `-w` | Analyze a single workspace (ignores `--limit`) |
|
|
35
|
+
| `--json` | Structured JSON report to stdout |
|
|
36
|
+
| `--markdown` | Markdown report to stdout |
|
|
37
|
+
| `--group-by <how>` | `priority` (default) or `kind` |
|
|
38
|
+
| `--no-save` | Don't save the report to `~/.nemus/reflect/` |
|
|
39
|
+
| `--model` / `--thinking` | Judge model / pi thinking-level overrides |
|
|
40
|
+
| `--dry-run` | Print the assembled corpus + judge prompt without calling the agent |
|
|
41
|
+
|
|
42
|
+
Read-only and safe — it analyzes transcripts and prints advice; it changes no
|
|
43
|
+
repos. Use it to coach setup, not to modify anything.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Save Context
|
|
2
|
+
|
|
3
|
+
`nemus save-context` (alias `ctx`) writes a progress summary to the workspace's
|
|
4
|
+
`CONTEXT.md`, so work survives `/clear` or a new session. Read `CONTEXT.md` back
|
|
5
|
+
at the start of a session to resume. The workspace defaults to the current
|
|
6
|
+
directory; pass `-w` to target another.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
nemus save-context -m "…" # save to the current workspace
|
|
10
|
+
nemus save-context -w <name> -m "…" # target a specific workspace
|
|
11
|
+
nemus save-context # interactive: prompts for the summary
|
|
12
|
+
nemus save-context -f notes.md --append # read from a file, append (don't replace)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Use it to capture: what was done, what's in progress, key decisions, and the
|
|
16
|
+
next steps — before a context reset or when handing off.
|
|
17
|
+
|
|
18
|
+
## Flags
|
|
19
|
+
|
|
20
|
+
| Flag | Short | Description |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| `--workspace <name>` | `-w` | Workspace name (default: current directory) |
|
|
23
|
+
| `--message <text>` | `-m` | Summary text to save (skips the prompt) |
|
|
24
|
+
| `--file <path>` | `-f` | Read the summary from a file |
|
|
25
|
+
| `--append` | | Append to existing context instead of replacing |
|