nexus-prime 7.1.1 → 7.2.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.
|
@@ -293,7 +293,7 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
293
293
|
detailLevel === 'debug' && bootstrap.tokenOptimization?.autoApplied && bootstrap.tokenOptimization?.plan
|
|
294
294
|
? `Auto token plan\n\`\`\`txt\n${bootstrap.tokenOptimization.plan}\n\`\`\``
|
|
295
295
|
: '',
|
|
296
|
-
|
|
296
|
+
formatJsonDetails('Structured details', payload),
|
|
297
297
|
hctx.formatProtocolChecklist(),
|
|
298
298
|
].filter(Boolean).join('\n\n'),
|
|
299
299
|
}],
|
|
@@ -499,6 +499,7 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
499
499
|
`Verification: ${verifiedWorkers}/${execution.workerResults.length} worker(s) verified`,
|
|
500
500
|
`Tokens: saved ${Number(execution.tokenTelemetry?.savedTokens || 0).toLocaleString()} · compression ${Number(execution.tokenTelemetry?.compressionPct || 0)}%`,
|
|
501
501
|
autoTokenApplyNote || null,
|
|
502
|
+
`Payload ref: ${workspace.stateKey} · ${detailLevel}`,
|
|
502
503
|
...(detailLevel === 'debug' ? [
|
|
503
504
|
`Specialists: ${execution.plannerState?.selectedSpecialists.map((specialist) => specialist.name).slice(0, 4).join(', ') || 'none selected'}`,
|
|
504
505
|
`Assets: ${(execution.activeSkills || []).length} skills · ${(execution.activeWorkflows || []).length} workflows · ${(runtimeUsage.artifactSelectionAudit?.selected?.length || 0)} audited selections`,
|
|
@@ -512,7 +513,7 @@ export async function handleOrchestrationGroup(toolName, hctx, request, args, ct
|
|
|
512
513
|
] : []),
|
|
513
514
|
]),
|
|
514
515
|
detailLevel === 'debug' && execution.result ? `Result\n\`\`\`\n${execution.result}\n\`\`\`` : `Result preview\n\`\`\`\n${truncateText(execution.result || summarizeExecution(execution), 900)}\n\`\`\``,
|
|
515
|
-
|
|
516
|
+
formatJsonDetails('Structured details', payload),
|
|
516
517
|
hctx.formatRemainingProtocolSteps(),
|
|
517
518
|
].filter(Boolean).join('\n\n'),
|
|
518
519
|
}],
|
package/dist/cli/cleanup.js
CHANGED
|
@@ -1,22 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* nexus cleanup — bound the growth of worktree + runs directories.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* can
|
|
7
|
-
*
|
|
8
|
-
* Defaults chosen to be loose enough that a busy day of work doesn't trip them:
|
|
9
|
-
* - TTL: 12h per worktree, 6h per run
|
|
10
|
-
* - maxCount: 32 worktrees, 64 runs
|
|
11
|
-
* - maxBytes: 2 GiB across worktrees, 1 GiB across runs
|
|
12
|
-
*
|
|
13
|
-
* Override with env: NEXUS_WORKTREE_TTL_MS, NEXUS_WORKTREE_MAX_COUNT,
|
|
14
|
-
* NEXUS_WORKTREE_MAX_BYTES, NEXUS_RUNS_TTL_MS, NEXUS_RUNS_MAX_COUNT,
|
|
15
|
-
* NEXUS_RUNS_MAX_BYTES.
|
|
4
|
+
* This never touches durable memory. It enforces aggressive defaults because
|
|
5
|
+
* transient Nexus state must never surprise users with 10GB+ growth. Heavy
|
|
6
|
+
* artifacts can still be retained by raising env budgets explicitly.
|
|
16
7
|
*/
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as os from 'os';
|
|
17
10
|
import * as path from 'path';
|
|
18
11
|
import { formatBytes, sweepDirectory, sweepOrphanWorktrees, } from '../install/fs-purge.js';
|
|
19
|
-
import { getNexusStateDir, getWorktreeRoots, getRuntimeTmpRoots, } from '../install/state-locator.js';
|
|
12
|
+
import { getNexusStateDir, getWorktreeRoots, getRuntimeTmpRoots, NEXUS_TMP_GLOB_PREFIXES, } from '../install/state-locator.js';
|
|
20
13
|
function parseIntEnv(name, fallback) {
|
|
21
14
|
const raw = process.env[name];
|
|
22
15
|
const parsed = Number.parseInt(raw ?? '', 10);
|
|
@@ -24,18 +17,42 @@ function parseIntEnv(name, fallback) {
|
|
|
24
17
|
}
|
|
25
18
|
export function resolveWorktreeBudget() {
|
|
26
19
|
return {
|
|
27
|
-
ttlMs: parseIntEnv('NEXUS_WORKTREE_TTL_MS',
|
|
28
|
-
maxCount: parseIntEnv('NEXUS_WORKTREE_MAX_COUNT',
|
|
29
|
-
maxBytes: parseIntEnv('NEXUS_WORKTREE_MAX_BYTES',
|
|
20
|
+
ttlMs: parseIntEnv('NEXUS_WORKTREE_TTL_MS', 60 * 60 * 1000),
|
|
21
|
+
maxCount: parseIntEnv('NEXUS_WORKTREE_MAX_COUNT', 8),
|
|
22
|
+
maxBytes: parseIntEnv('NEXUS_WORKTREE_MAX_BYTES', 512 * 1024 * 1024),
|
|
30
23
|
};
|
|
31
24
|
}
|
|
32
25
|
export function resolveRunsBudget() {
|
|
33
26
|
return {
|
|
34
|
-
ttlMs: parseIntEnv('NEXUS_RUNS_TTL_MS',
|
|
35
|
-
maxCount: parseIntEnv('NEXUS_RUNS_MAX_COUNT',
|
|
36
|
-
maxBytes: parseIntEnv('NEXUS_RUNS_MAX_BYTES',
|
|
27
|
+
ttlMs: parseIntEnv('NEXUS_RUNS_TTL_MS', 30 * 60 * 1000),
|
|
28
|
+
maxCount: parseIntEnv('NEXUS_RUNS_MAX_COUNT', 16),
|
|
29
|
+
maxBytes: parseIntEnv('NEXUS_RUNS_MAX_BYTES', 256 * 1024 * 1024),
|
|
37
30
|
};
|
|
38
31
|
}
|
|
32
|
+
function collectLegacyTmpRoots() {
|
|
33
|
+
const tmpRoot = os.tmpdir();
|
|
34
|
+
let entries = [];
|
|
35
|
+
try {
|
|
36
|
+
entries = fs.readdirSync(tmpRoot);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const roots = new Set();
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
if (!NEXUS_TMP_GLOB_PREFIXES.some((prefix) => entry.startsWith(prefix)))
|
|
44
|
+
continue;
|
|
45
|
+
const fullPath = path.join(tmpRoot, entry);
|
|
46
|
+
try {
|
|
47
|
+
if (fs.statSync(fullPath).isDirectory())
|
|
48
|
+
roots.add(fullPath);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// ignore races with concurrently removed tmp dirs
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return [...roots].sort();
|
|
55
|
+
}
|
|
39
56
|
export async function runCleanup(options) {
|
|
40
57
|
const dryRun = options.dryRun || !options.fix;
|
|
41
58
|
const repoRoot = options.repoRoot ?? process.cwd();
|
|
@@ -51,7 +68,6 @@ export async function runCleanup(options) {
|
|
|
51
68
|
};
|
|
52
69
|
const label = dryRun ? '[dry-run]' : '[cleanup]';
|
|
53
70
|
console.log(`\n${label} Nexus cleanup — repo: ${repoRoot}`);
|
|
54
|
-
// Worktree roots
|
|
55
71
|
for (const root of getWorktreeRoots()) {
|
|
56
72
|
const sweep = sweepDirectory(root, {
|
|
57
73
|
maxAgeMs: worktreeBudget.ttlMs,
|
|
@@ -65,6 +81,7 @@ export async function runCleanup(options) {
|
|
|
65
81
|
report.totalEntriesRemoved += sweep.removed.length;
|
|
66
82
|
if (sweep.entriesBefore > 0 || sweep.removed.length > 0) {
|
|
67
83
|
console.log(` worktree: ${root}`);
|
|
84
|
+
console.log(` budget: ttl=${Math.round(worktreeBudget.ttlMs / 60000)}m, max=${worktreeBudget.maxCount}, bytes=${formatBytes(worktreeBudget.maxBytes)}`);
|
|
68
85
|
console.log(` before: ${sweep.entriesBefore} entries, ${formatBytes(sweep.totalBytesBefore)}`);
|
|
69
86
|
console.log(` removed: ${sweep.removed.length} entries, ${formatBytes(sweep.removed.reduce((s, r) => s + r.bytes, 0))}`);
|
|
70
87
|
for (const r of sweep.removed) {
|
|
@@ -72,8 +89,12 @@ export async function runCleanup(options) {
|
|
|
72
89
|
}
|
|
73
90
|
}
|
|
74
91
|
}
|
|
75
|
-
|
|
76
|
-
|
|
92
|
+
const runsRoots = new Set([
|
|
93
|
+
path.join(getNexusStateDir(), 'runs'),
|
|
94
|
+
...getRuntimeTmpRoots(),
|
|
95
|
+
...collectLegacyTmpRoots(),
|
|
96
|
+
]);
|
|
97
|
+
for (const root of runsRoots) {
|
|
77
98
|
const sweep = sweepDirectory(root, {
|
|
78
99
|
maxAgeMs: runsBudget.ttlMs,
|
|
79
100
|
maxCount: runsBudget.maxCount,
|
|
@@ -86,11 +107,11 @@ export async function runCleanup(options) {
|
|
|
86
107
|
report.totalEntriesRemoved += sweep.removed.length;
|
|
87
108
|
if (sweep.entriesBefore > 0 || sweep.removed.length > 0) {
|
|
88
109
|
console.log(` runs: ${root}`);
|
|
110
|
+
console.log(` budget: ttl=${Math.round(runsBudget.ttlMs / 60000)}m, max=${runsBudget.maxCount}, bytes=${formatBytes(runsBudget.maxBytes)}`);
|
|
89
111
|
console.log(` before: ${sweep.entriesBefore} entries, ${formatBytes(sweep.totalBytesBefore)}`);
|
|
90
112
|
console.log(` removed: ${sweep.removed.length} entries, ${formatBytes(sweep.removed.reduce((s, r) => s + r.bytes, 0))}`);
|
|
91
113
|
}
|
|
92
114
|
}
|
|
93
|
-
// Orphan worktrees (dir exists on disk but `git worktree list` forgot about it)
|
|
94
115
|
try {
|
|
95
116
|
const orphanReport = await sweepOrphanWorktrees(repoRoot, { dryRun });
|
|
96
117
|
report.orphans.paths = orphanReport.ops.filter((op) => op.kind !== 'missing').map((op) => op.path);
|
package/dist/cli/uninstall.d.ts
CHANGED
|
@@ -2,17 +2,12 @@ import { loadManifest } from '../install/manifest.js';
|
|
|
2
2
|
import { type StopAllNexusProcessesOptions } from '../install/process-cleanup.js';
|
|
3
3
|
export interface UninstallOptions {
|
|
4
4
|
dryRun: boolean;
|
|
5
|
-
/** Stop daemons and unregister all known clients. Accepted for CLI clarity; uninstall already does this by default. */
|
|
6
5
|
all?: boolean;
|
|
7
|
-
/** Preferred purge flag. */
|
|
8
6
|
purge?: boolean;
|
|
9
|
-
/** Back-compat alias for purge. */
|
|
10
7
|
purgeAll?: boolean;
|
|
11
8
|
yes: boolean;
|
|
12
9
|
workspaceRoot?: string;
|
|
13
|
-
/** Skip interactive prompt in tests. */
|
|
14
10
|
_forceYes?: boolean;
|
|
15
|
-
/** Test-only process cleanup injection; production callers should not set this. */
|
|
16
11
|
_processCleanup?: StopAllNexusProcessesOptions;
|
|
17
12
|
}
|
|
18
13
|
export interface UninstallReport {
|
package/dist/cli/uninstall.js
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* nexus uninstall — reverse every mutation Nexus Prime made to this machine.
|
|
3
3
|
*
|
|
4
|
-
* `--dry-run`
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* The manifest is authoritative where present. Heuristics fill the gap when
|
|
10
|
-
* the manifest is missing (e.g. legacy installs). Every destructive step has
|
|
11
|
-
* a dry-run preview, and leftovers are always reported.
|
|
4
|
+
* `--dry-run` walks registrations, processes, markers, and filesystem paths.
|
|
5
|
+
* Default uninstall unregisters clients, stops processes, clears markers, removes
|
|
6
|
+
* transient scratch, and clears the install manifest. `--purge` also deletes
|
|
7
|
+
* durable state such as memory, Synapse, Architects, logs, caches, and worktrees.
|
|
12
8
|
*/
|
|
13
9
|
import * as fs from 'fs';
|
|
14
10
|
import * as readline from 'readline';
|
|
@@ -73,7 +69,6 @@ export async function runUninstall(options) {
|
|
|
73
69
|
}
|
|
74
70
|
const label = dryRun ? '[dry-run]' : '[uninstall]';
|
|
75
71
|
console.log(`\n${label} Nexus Prime uninstall — workspace: ${workspaceRoot}`);
|
|
76
|
-
// Step 1 — reverse IDE registrations before stopping processes so clients do not immediately respawn MCP sessions.
|
|
77
72
|
const manifest = loadManifest();
|
|
78
73
|
const unregisterReport = unregisterNexusRegistrations({ workspaceRoot, dryRun, manifest });
|
|
79
74
|
const removedRegs = unregisterReport.reports.filter((r) => r.removed);
|
|
@@ -83,13 +78,11 @@ export async function runUninstall(options) {
|
|
|
83
78
|
for (const r of removedRegs) {
|
|
84
79
|
console.log(` - ${r.filePath} :: ${r.jsonPath.join('.') || '(root)'}`);
|
|
85
80
|
}
|
|
86
|
-
// Step 2 — stop Nexus MCP/daemon processes and remove lockfiles.
|
|
87
81
|
const processReport = await stopAllNexusProcesses({ ...options._processCleanup, dryRun });
|
|
88
82
|
report.daemonsKilled = processReport.daemonsKilled;
|
|
89
83
|
report.lockfilesRemoved = processReport.lockfilesRemoved;
|
|
90
84
|
report.errors.push(...processReport.errors);
|
|
91
85
|
console.log(` ${dryRun ? '◦' : '✓'} processes: ${report.daemonsKilled} process(es), ${report.lockfilesRemoved} lockfile(s)`);
|
|
92
|
-
// Step 3 — remove setup markers.
|
|
93
86
|
const markers = new Set([setupMarkerPath, ...manifest.setupMarkers]);
|
|
94
87
|
for (const marker of markers) {
|
|
95
88
|
if (!fs.existsSync(marker))
|
|
@@ -107,7 +100,6 @@ export async function runUninstall(options) {
|
|
|
107
100
|
}
|
|
108
101
|
}
|
|
109
102
|
}
|
|
110
|
-
// Step 4 — purge filesystem artifacts.
|
|
111
103
|
const extras = manifest.paths.map((e) => ({ path: e.path, scope: e.scope }));
|
|
112
104
|
let stateReport = null;
|
|
113
105
|
let tmpReport = null;
|
|
@@ -116,7 +108,6 @@ export async function runUninstall(options) {
|
|
|
116
108
|
tmpReport = purgeTmpRoots({ dryRun });
|
|
117
109
|
}
|
|
118
110
|
else {
|
|
119
|
-
// Default (no --purge-all): only transient tmp dirs. Persistent state stays.
|
|
120
111
|
tmpReport = purgeTmpRoots({ dryRun });
|
|
121
112
|
}
|
|
122
113
|
const totals = [stateReport, tmpReport]
|
|
@@ -137,11 +128,9 @@ export async function runUninstall(options) {
|
|
|
137
128
|
console.log(` - ${op.path} (${formatBytes(op.bytes)})`);
|
|
138
129
|
}
|
|
139
130
|
}
|
|
140
|
-
|
|
141
|
-
if (!dryRun && purgeAll) {
|
|
131
|
+
if (!dryRun) {
|
|
142
132
|
clearManifest(installManifestPath);
|
|
143
133
|
}
|
|
144
|
-
// Step 6 — leftover audit.
|
|
145
134
|
report.leftovers = collectLeftovers(nexusStateDir);
|
|
146
135
|
if (report.leftovers.length > 0) {
|
|
147
136
|
console.log(` ! leftovers (still present in state dir):`);
|
|
@@ -156,7 +145,8 @@ export async function runUninstall(options) {
|
|
|
156
145
|
report.ok = report.errors.length === 0 && (dryRun || (purgeAll ? report.leftovers.length === 0 : true));
|
|
157
146
|
console.log(`\n${label} done. ${report.ok ? 'OK' : 'completed with issues'}.`);
|
|
158
147
|
if (!purgeAll && !dryRun) {
|
|
159
|
-
console.log(' Persistent
|
|
148
|
+
console.log(' Persistent memory preserved. All registrations, processes, manifest, and transient scratch were removed.');
|
|
149
|
+
console.log(' Re-run with --purge --yes to delete durable memory/Synapse/Architects state too.');
|
|
160
150
|
}
|
|
161
151
|
return report;
|
|
162
152
|
}
|
|
@@ -6,7 +6,7 @@ import { SessionDNAManager } from './session-dna.js';
|
|
|
6
6
|
import { clearBootstrapReceipt, readBootstrapReceipt } from './bootstrap/bootstrap-registry.js';
|
|
7
7
|
import { doctorGitWorktrees } from './worktree-health.js';
|
|
8
8
|
import { sweepDirectory, sweepOrphanWorktrees } from '../install/fs-purge.js';
|
|
9
|
-
import { getWorktreeRoots } from '../install/state-locator.js';
|
|
9
|
+
import { getRuntimeTmpRoots, getWorktreeRoots } from '../install/state-locator.js';
|
|
10
10
|
import { resolveWorktreeBudget, resolveRunsBudget } from '../cli/cleanup.js';
|
|
11
11
|
function isOlderThan(target, maxAgeMs) {
|
|
12
12
|
try {
|
|
@@ -59,15 +59,12 @@ function cleanupTmpDirEntries(rootDir, opts) {
|
|
|
59
59
|
export async function runStartupHygiene(input) {
|
|
60
60
|
const mode = input.mode ?? 'stale';
|
|
61
61
|
const stateDir = resolveNexusStateDir();
|
|
62
|
-
// 1) Runtime registry snapshots (dashboard + operators) — safe to prune stale/dead entries.
|
|
63
62
|
const registryDir = path.join(stateDir, 'runtime-registry');
|
|
64
63
|
const registryBefore = fs.existsSync(registryDir) ? fs.readdirSync(registryDir).filter((f) => f.endsWith('.json')).length : 0;
|
|
65
64
|
const registry = new RuntimeRegistry(stateDir);
|
|
66
65
|
registry.pruneStalePublic();
|
|
67
66
|
const registryAfter = fs.existsSync(registryDir) ? fs.readdirSync(registryDir).filter((f) => f.endsWith('.json')).length : 0;
|
|
68
|
-
// 2) SessionDNA stale locks + orphan session JSONs (best-effort).
|
|
69
67
|
const cleanedSessionLocks = SessionDNAManager.cleanStale(path.join(stateDir, 'sessions'));
|
|
70
|
-
// 3) Bootstrap receipt — safe to clear when stale/invalid (never deletes installed configs).
|
|
71
68
|
let clearedBootstrapReceipt = false;
|
|
72
69
|
if (mode === 'fresh') {
|
|
73
70
|
clearBootstrapReceipt(input.workspaceStateRoot);
|
|
@@ -75,40 +72,32 @@ export async function runStartupHygiene(input) {
|
|
|
75
72
|
}
|
|
76
73
|
else {
|
|
77
74
|
const receipt = readBootstrapReceipt(input.workspaceStateRoot);
|
|
78
|
-
// If the receipt is stale or corrupt, readBootstrapReceipt returns null; clear the file to avoid confusing future probes.
|
|
79
75
|
if (receipt === null) {
|
|
80
76
|
clearBootstrapReceipt(input.workspaceStateRoot);
|
|
81
77
|
clearedBootstrapReceipt = true;
|
|
82
78
|
}
|
|
83
79
|
}
|
|
84
|
-
// 4) Git worktree metadata — only prunes Nexus-owned broken/stale entries.
|
|
85
80
|
let worktreeDoctorOverall;
|
|
86
81
|
try {
|
|
87
82
|
const health = await doctorGitWorktrees(input.repoRoot);
|
|
88
83
|
worktreeDoctorOverall = health.overall;
|
|
89
84
|
}
|
|
90
85
|
catch {
|
|
91
|
-
//
|
|
86
|
+
// hygiene must not block startup
|
|
92
87
|
}
|
|
93
|
-
// 5) Temp artifacts (transient only). Never touches memory DB / vault.
|
|
94
88
|
const tmpDir = os.tmpdir();
|
|
95
|
-
const maxAgeMs =
|
|
89
|
+
const maxAgeMs = mode === 'fresh' ? 0 : 30 * 60 * 1000;
|
|
96
90
|
let cleanedTmpEntries = 0;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
cleanedTmpEntries += cleanupTmpDirEntries(path.join(tmpDir, 'nexus-prime-runtime-hooks'), { mode, maxAgeMs });
|
|
102
|
-
cleanedTmpEntries += cleanupTmpDirEntries(path.join(tmpDir, 'nexus-prime-runtime-automations'), { mode, maxAgeMs });
|
|
103
|
-
cleanedTmpEntries += cleanupTmpDirEntries(path.join(tmpDir, 'nexus-prime-sessions'), { mode, maxAgeMs });
|
|
91
|
+
const transientRoots = [...getRuntimeTmpRoots(), ...getWorktreeRoots()];
|
|
92
|
+
for (const root of transientRoots) {
|
|
93
|
+
cleanedTmpEntries += cleanupTmpDirEntries(root, { mode, maxAgeMs });
|
|
94
|
+
}
|
|
104
95
|
cleanedTmpEntries += cleanupTmpDirEntries(tmpDir, { mode, maxAgeMs, matchPrefix: 'nexus-prime-feature-registry-' });
|
|
96
|
+
cleanedTmpEntries += cleanupTmpDirEntries(tmpDir, { mode, maxAgeMs, matchPrefix: 'nexus-reset-test-' });
|
|
105
97
|
if (mode === 'fresh') {
|
|
106
|
-
// Optional fresh wipe of purely transient runtime UI/event buffers.
|
|
107
98
|
cleanedTmpEntries += safeUnlink(path.join(stateDir, 'events.jsonl')) ? 1 : 0;
|
|
108
99
|
cleanedTmpEntries += safeUnlink(path.join(stateDir, 'dashboard-state.json')) ? 1 : 0;
|
|
109
100
|
}
|
|
110
|
-
// 6) Bounded worktree sweep — enforce TTL / count / bytes budgets so growth
|
|
111
|
-
// stays bounded even when users never run `nexus cleanup`.
|
|
112
101
|
const worktreeBudget = resolveWorktreeBudget();
|
|
113
102
|
let boundedWorktreeSweeps = 0;
|
|
114
103
|
for (const root of getWorktreeRoots()) {
|
|
@@ -123,34 +112,33 @@ export async function runStartupHygiene(input) {
|
|
|
123
112
|
boundedWorktreeSweeps += sweep.removed.length;
|
|
124
113
|
}
|
|
125
114
|
catch {
|
|
126
|
-
//
|
|
115
|
+
// best-effort
|
|
127
116
|
}
|
|
128
117
|
}
|
|
129
|
-
// 7) Orphan worktree sweep — dirs exist on disk but git forgot them.
|
|
130
118
|
let orphanWorktreesRemoved = 0;
|
|
131
119
|
try {
|
|
132
120
|
const orphans = await sweepOrphanWorktrees(input.repoRoot, { dryRun: false });
|
|
133
121
|
orphanWorktreesRemoved = orphans.ops.filter((op) => op.removed).length;
|
|
134
122
|
}
|
|
135
123
|
catch {
|
|
136
|
-
// ignore
|
|
124
|
+
// ignore
|
|
137
125
|
}
|
|
138
|
-
// 8) Bounded runs sweep.
|
|
139
126
|
const runsBudget = resolveRunsBudget();
|
|
140
127
|
let boundedRunsSweeps = 0;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
128
|
+
for (const runsRoot of [path.join(input.workspaceStateRoot, 'runs'), ...getRuntimeTmpRoots()]) {
|
|
129
|
+
try {
|
|
130
|
+
const sweep = sweepDirectory(runsRoot, {
|
|
131
|
+
maxAgeMs: runsBudget.ttlMs,
|
|
132
|
+
maxCount: runsBudget.maxCount,
|
|
133
|
+
maxBytes: runsBudget.maxBytes,
|
|
134
|
+
dryRun: false,
|
|
135
|
+
keepRoot: true,
|
|
136
|
+
});
|
|
137
|
+
boundedRunsSweeps += sweep.removed.length;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// best-effort
|
|
141
|
+
}
|
|
154
142
|
}
|
|
155
143
|
return {
|
|
156
144
|
mode,
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* process-cleanup — stop every Nexus-owned process without touching anything else.
|
|
3
3
|
*
|
|
4
|
-
* Wraps
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Wraps lockfile cleanup and adds a process-table sweep for legacy launch
|
|
5
|
+
* shapes. Older installs can be started by npx/npm, by MCP clients as
|
|
6
|
+
* `node .../dist/index.js`, or by dashboard/daemon commands that never wrote a
|
|
7
|
+
* current lockfile. Uninstall must catch all of those, otherwise users think
|
|
8
|
+
* Nexus was removed while stale workers keep writing state.
|
|
7
9
|
*/
|
|
8
10
|
import * as fs from 'fs';
|
|
9
11
|
import { spawnSync } from 'child_process';
|
|
@@ -36,30 +38,43 @@ export function killProcessTree(pid) {
|
|
|
36
38
|
function sleep(ms) {
|
|
37
39
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
38
40
|
}
|
|
41
|
+
function normalizeCommand(command) {
|
|
42
|
+
return command.replace(/\s+/g, ' ').trim();
|
|
43
|
+
}
|
|
44
|
+
function looksLikeNexusEntrypoint(command) {
|
|
45
|
+
const normalized = normalizeCommand(command);
|
|
46
|
+
return /\bnexus-prime(?:\s|$)/.test(normalized)
|
|
47
|
+
|| /(?:^|[\s"'])npx(?:\.cmd)?(?:\s+--yes)?\s+nexus-prime(?:\s|$)/.test(normalized)
|
|
48
|
+
|| /(?:^|[\s"'])npm(?:\.cmd)?\s+(?:exec|x)\s+(?:--\s+)?nexus-prime(?:\s|$)/.test(normalized)
|
|
49
|
+
|| /[\\/]nexus-prime[\\/](?:dist[\\/])?(?:index|cli)\.js(?:\s|$)/.test(normalized)
|
|
50
|
+
|| /[\\/]node_modules[\\/]\.bin[\\/]nexus-prime(?:\s|$)/.test(normalized);
|
|
51
|
+
}
|
|
52
|
+
function isBenignNexusCommand(command) {
|
|
53
|
+
const normalized = normalizeCommand(command);
|
|
54
|
+
return /\b(npm|pnpm|yarn)\s+(?:install|add|remove|uninstall|view|info|show)\b/.test(normalized)
|
|
55
|
+
|| /\b(vitest|jest|tsc|tsx)\b.*\bsrc[\\/]install[\\/]process-cleanup\.ts\b/.test(normalized);
|
|
56
|
+
}
|
|
39
57
|
function isNexusProcessCommand(command) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
58
|
+
const normalized = normalizeCommand(command);
|
|
59
|
+
if (!looksLikeNexusEntrypoint(normalized) || isBenignNexusCommand(normalized))
|
|
60
|
+
return false;
|
|
61
|
+
// Runtime surfaces that can keep writing state / holding dashboard ports.
|
|
62
|
+
if (/\b(mcp|daemon\s+__serve|dashboard|server|serve|start|orchestrate|bootstrap)\b/.test(normalized)) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
// MCP clients often launch the compiled entrypoint without a visible subcommand.
|
|
66
|
+
return /[\\/]nexus-prime[\\/]dist[\\/](?:index|cli)\.js(?:\s|$)/.test(normalized)
|
|
67
|
+
|| /[\\/]node_modules[\\/]\.bin[\\/]nexus-prime(?:\s|$)/.test(normalized);
|
|
43
68
|
}
|
|
44
|
-
|
|
45
|
-
if (process.platform === 'win32') {
|
|
46
|
-
return [];
|
|
47
|
-
}
|
|
48
|
-
const result = spawnSync('ps', ['-axo', 'pid=,command='], {
|
|
49
|
-
encoding: 'utf8',
|
|
50
|
-
timeout: 5_000,
|
|
51
|
-
});
|
|
52
|
-
if (result.status !== 0) {
|
|
53
|
-
return [];
|
|
54
|
-
}
|
|
69
|
+
function parseProcessLines(stdout) {
|
|
55
70
|
const candidates = [];
|
|
56
|
-
for (const line of (
|
|
57
|
-
const match = line.match(/^\s*(\d+)\s+(.+)$/);
|
|
71
|
+
for (const line of (stdout ?? '').split(/\r?\n/)) {
|
|
72
|
+
const match = line.match(/^\s*(\d+)\s+(.+)$/) ?? line.match(/^\s*(\d+)\t(.+)$/);
|
|
58
73
|
if (!match)
|
|
59
74
|
continue;
|
|
60
75
|
const pid = Number(match[1]);
|
|
61
76
|
const command = match[2];
|
|
62
|
-
if (pid === process.pid || !Number.isInteger(pid) || pid <=
|
|
77
|
+
if (pid === process.pid || pid === process.ppid || !Number.isInteger(pid) || pid <= 1)
|
|
63
78
|
continue;
|
|
64
79
|
if (!isNexusProcessCommand(command))
|
|
65
80
|
continue;
|
|
@@ -67,6 +82,28 @@ export function listNexusProcessPids() {
|
|
|
67
82
|
}
|
|
68
83
|
return candidates;
|
|
69
84
|
}
|
|
85
|
+
export function listNexusProcessPids() {
|
|
86
|
+
if (process.platform === 'win32') {
|
|
87
|
+
const result = spawnSync('powershell.exe', [
|
|
88
|
+
'-NoProfile',
|
|
89
|
+
'-Command',
|
|
90
|
+
"Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match 'nexus-prime|nexus-prime.*dist|node_modules.*nexus-prime' } | ForEach-Object { \"$($_.ProcessId)`t$($_.CommandLine)\" }",
|
|
91
|
+
], {
|
|
92
|
+
encoding: 'utf8',
|
|
93
|
+
timeout: 5_000,
|
|
94
|
+
windowsHide: true,
|
|
95
|
+
});
|
|
96
|
+
return result.status === 0 ? parseProcessLines(result.stdout ?? '') : [];
|
|
97
|
+
}
|
|
98
|
+
const result = spawnSync('ps', ['-axo', 'pid=,command='], {
|
|
99
|
+
encoding: 'utf8',
|
|
100
|
+
timeout: 5_000,
|
|
101
|
+
});
|
|
102
|
+
if (result.status !== 0) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
return parseProcessLines(result.stdout ?? '');
|
|
106
|
+
}
|
|
70
107
|
async function stopPid(pid, options) {
|
|
71
108
|
if (options.dryRun) {
|
|
72
109
|
return { stopped: true };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexus-prime",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.2.0",
|
|
4
4
|
"description": "Local-first MCP control plane for coding agents with bootstrap-orchestrate execution, memory fabric, token budgeting, and worktree-backed swarms",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|