@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,109 @@
|
|
|
1
|
+
// Pure logic for `nemus prune` — deciding which workspaces are stale and which
|
|
2
|
+
// are unsafe to delete. Kept free of I/O so it can be unit-tested exhaustively;
|
|
3
|
+
// the command layer (src/commands/prune.ts) does the filesystem/git work and
|
|
4
|
+
// feeds the results in here.
|
|
5
|
+
import type { GitStatus } from '../types';
|
|
6
|
+
|
|
7
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
8
|
+
|
|
9
|
+
export interface WorkspaceForPrune {
|
|
10
|
+
name: string;
|
|
11
|
+
path: string;
|
|
12
|
+
/** Repo directory names inside the workspace (for the git safety check). */
|
|
13
|
+
repoDirNames: string[];
|
|
14
|
+
/** Epoch ms of the most recent agent session, or 0 if none. */
|
|
15
|
+
lastActiveAt: number;
|
|
16
|
+
/** Epoch ms parsed from metadata.createdAt, or 0 if absent/unparseable. */
|
|
17
|
+
createdAt: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PruneCandidate {
|
|
21
|
+
name: string;
|
|
22
|
+
path: string;
|
|
23
|
+
repoDirNames: string[];
|
|
24
|
+
/** The timestamp staleness is measured from (lastActive, else createdAt). */
|
|
25
|
+
referenceAt: number;
|
|
26
|
+
/** Whether the reference came from a real session (vs. createdAt fallback). */
|
|
27
|
+
fromSession: boolean;
|
|
28
|
+
/** Whole days since referenceAt (floored). */
|
|
29
|
+
ageDays: number;
|
|
30
|
+
/** True when we have no date at all to judge age. */
|
|
31
|
+
undatable: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ProtectedWorkspace {
|
|
35
|
+
candidate: PruneCandidate;
|
|
36
|
+
reason: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PrunePlan {
|
|
40
|
+
/** Stale + safe to delete. */
|
|
41
|
+
prunable: PruneCandidate[];
|
|
42
|
+
/** Stale but held back (uncommitted/unpushed work), unless includeDirty. */
|
|
43
|
+
protected: ProtectedWorkspace[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Build a dated candidate for a workspace. `now` is injected for testability. */
|
|
47
|
+
export function toCandidate(ws: WorkspaceForPrune, now: number): PruneCandidate {
|
|
48
|
+
const referenceAt = ws.lastActiveAt > 0 ? ws.lastActiveAt : ws.createdAt;
|
|
49
|
+
const undatable = !(referenceAt > 0);
|
|
50
|
+
const ageDays = undatable ? 0 : Math.floor((now - referenceAt) / MS_PER_DAY);
|
|
51
|
+
return {
|
|
52
|
+
name: ws.name,
|
|
53
|
+
path: ws.path,
|
|
54
|
+
repoDirNames: ws.repoDirNames,
|
|
55
|
+
referenceAt,
|
|
56
|
+
fromSession: ws.lastActiveAt > 0,
|
|
57
|
+
ageDays,
|
|
58
|
+
undatable,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A workspace is a prune candidate when it is datable and its age meets the
|
|
64
|
+
* threshold. Undatable workspaces are never auto-selected — we won't delete
|
|
65
|
+
* something we can't put a date on. A future `referenceAt` (clock skew) yields
|
|
66
|
+
* a negative age and is therefore not stale.
|
|
67
|
+
*/
|
|
68
|
+
export function isStale(c: PruneCandidate, days: number): boolean {
|
|
69
|
+
return !c.undatable && c.ageDays >= days;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Why a stale workspace should be held back from deletion, or null if it's safe.
|
|
74
|
+
* Unsafe = any repo has uncommitted changes (`!clean`) or unpushed commits
|
|
75
|
+
* (`ahead > 0`). With `includeDirty`, nothing is held back. An empty workspace
|
|
76
|
+
* (no repos) is always safe.
|
|
77
|
+
*/
|
|
78
|
+
export function protectionReason(statuses: GitStatus[], includeDirty: boolean): string | null {
|
|
79
|
+
if (includeDirty) return null;
|
|
80
|
+
const dirty = statuses.filter((s) => !s.clean).length;
|
|
81
|
+
const unpushed = statuses.filter((s) => s.ahead > 0).length;
|
|
82
|
+
if (dirty === 0 && unpushed === 0) return null;
|
|
83
|
+
const parts: string[] = [];
|
|
84
|
+
if (dirty > 0) parts.push(`${dirty} repo${dirty === 1 ? '' : 's'} with uncommitted changes`);
|
|
85
|
+
if (unpushed > 0) parts.push(`${unpushed} repo${unpushed === 1 ? '' : 's'} with unpushed commits`);
|
|
86
|
+
return parts.join(', ');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Partition stale candidates into prunable vs. protected, given a resolver that
|
|
91
|
+
* returns each workspace's per-repo git status. The resolver is only invoked
|
|
92
|
+
* for workspaces that actually have repos, so empty stale workspaces cost no git
|
|
93
|
+
* calls. Injecting the resolver keeps this function pure and unit-testable.
|
|
94
|
+
*/
|
|
95
|
+
export async function planPrune(
|
|
96
|
+
staleCandidates: PruneCandidate[],
|
|
97
|
+
getStatuses: (c: PruneCandidate) => Promise<GitStatus[]>,
|
|
98
|
+
includeDirty: boolean,
|
|
99
|
+
): Promise<PrunePlan> {
|
|
100
|
+
const prunable: PruneCandidate[] = [];
|
|
101
|
+
const protectedList: ProtectedWorkspace[] = [];
|
|
102
|
+
for (const c of staleCandidates) {
|
|
103
|
+
const statuses = c.repoDirNames.length > 0 ? await getStatuses(c) : [];
|
|
104
|
+
const reason = protectionReason(statuses, includeDirty);
|
|
105
|
+
if (reason) protectedList.push({ candidate: c, reason });
|
|
106
|
+
else prunable.push(c);
|
|
107
|
+
}
|
|
108
|
+
return { prunable, protected: protectedList };
|
|
109
|
+
}
|