@ouro.bot/cli 0.1.0-alpha.272 → 0.1.0-alpha.273
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.json
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.273",
|
|
6
|
+
"changes": [
|
|
7
|
+
"feat(version-manager): auto-prune old CLI versions during activate. The user observed `~/.ouro-cli/versions/` accumulating every CLI version they'd ever installed (alpha.85 from 2026-03-20 onward, ~100MB+ of dead node_modules trees) because nothing ever GCed. New `pruneOldVersions(retain=5, deps?)` walks `~/.ouro-cli/versions/`, sorts by alpha-suffix numerically, and deletes everything outside the retention window — except always preserves (a) the currently-active version (CurrentVersion symlink target), and (b) the previous version (previous symlink target), so `ouro rollback` stays one command away. Wired into `cli-defaults.ts` via `ensureCurrentVersionInstalled` and `activateCliVersion` — every successful version activation self-prunes. New helpers `compareCliVersions(a, b)` and `selectVersionsToPrune(installed, protected, retain)` are pure and exported for direct unit coverage. 9 new tests covering version comparison, retention selection, current/previous protection, partial-failure handling, missing-symlink fallback, and non-directory entry filtering.",
|
|
8
|
+
"test(contract): new OURO_DAEMON_INSTANTIATION_ALLOWLIST in test-isolation.contract.test.ts flags any new test file that constructs `new OuroDaemon(...)` outside the 11 grandfathered files. Constructing a real daemon and calling start() runs killOrphanProcesses() and writePidfile() against the production pidfile at ~/.ouro-cli/daemon.pids. The runtime guards added in #346 short-circuit those functions under vitest, but if a future change to start() adds a NEW production-state side-effect, the existing 11 tests would silently exercise it. The contract test forces conscious review of each new file taking this shape — same defense-in-depth pattern as BYPASS_USE_ALLOWLIST in alpha.265."
|
|
9
|
+
]
|
|
10
|
+
},
|
|
4
11
|
{
|
|
5
12
|
"version": "0.1.0-alpha.272",
|
|
6
13
|
"changes": [
|
|
@@ -494,6 +494,14 @@ function createDefaultOuroCliDeps(socketPath = socket_client_1.DEFAULT_DAEMON_SO
|
|
|
494
494
|
(0, ouro_version_manager_1.installVersion)(version, {});
|
|
495
495
|
}
|
|
496
496
|
(0, ouro_version_manager_1.activateVersion)(version, {});
|
|
497
|
+
// Self-prune: every successful version activation cleans up old
|
|
498
|
+
// versions outside the retention window. Without this, every CLI
|
|
499
|
+
// version ever installed accumulates indefinitely under
|
|
500
|
+
// ~/.ouro-cli/versions/ — the user observed installs going back to
|
|
501
|
+
// alpha.85 from March 20 (~100MB of dead node_modules trees).
|
|
502
|
+
// pruneOldVersions keeps the 5 most recent + the active + the
|
|
503
|
+
// previous version, so rollback stays one command away.
|
|
504
|
+
(0, ouro_version_manager_1.pruneOldVersions)(undefined, {});
|
|
497
505
|
},
|
|
498
506
|
/* v8 ignore stop */
|
|
499
507
|
/* v8 ignore start -- CLI version management defaults: integration code @preserve */
|
|
@@ -508,7 +516,14 @@ function createDefaultOuroCliDeps(socketPath = socket_client_1.DEFAULT_DAEMON_SO
|
|
|
508
516
|
});
|
|
509
517
|
},
|
|
510
518
|
installCliVersion: async (version) => { (0, ouro_version_manager_1.installVersion)(version, {}); },
|
|
511
|
-
activateCliVersion: (version) => {
|
|
519
|
+
activateCliVersion: (version) => {
|
|
520
|
+
(0, ouro_version_manager_1.activateVersion)(version, {});
|
|
521
|
+
// Same self-prune as ensureCurrentVersionInstalled — fires from the
|
|
522
|
+
// checkForCliUpdate path 1 (in-process update detected → activate
|
|
523
|
+
// → re-exec). Without this, the path 1 code path never triggers
|
|
524
|
+
// a prune.
|
|
525
|
+
(0, ouro_version_manager_1.pruneOldVersions)(undefined, {});
|
|
526
|
+
},
|
|
512
527
|
getCurrentCliVersion: () => (0, ouro_version_manager_1.getCurrentVersion)({}),
|
|
513
528
|
getPreviousCliVersion: () => (0, ouro_version_manager_1.getPreviousVersion)({}),
|
|
514
529
|
listCliVersions: () => (0, ouro_version_manager_1.listInstalledVersions)({}),
|
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DEFAULT_RETAIN_VERSIONS = void 0;
|
|
36
37
|
exports.getOuroCliHome = getOuroCliHome;
|
|
37
38
|
exports.getCurrentVersion = getCurrentVersion;
|
|
38
39
|
exports.getPreviousVersion = getPreviousVersion;
|
|
@@ -40,11 +41,16 @@ exports.buildChangelogCommand = buildChangelogCommand;
|
|
|
40
41
|
exports.listInstalledVersions = listInstalledVersions;
|
|
41
42
|
exports.installVersion = installVersion;
|
|
42
43
|
exports.activateVersion = activateVersion;
|
|
44
|
+
exports.compareCliVersions = compareCliVersions;
|
|
45
|
+
exports.selectVersionsToPrune = selectVersionsToPrune;
|
|
46
|
+
exports.pruneOldVersions = pruneOldVersions;
|
|
43
47
|
exports.ensureLayout = ensureLayout;
|
|
44
48
|
const fs = __importStar(require("fs"));
|
|
45
49
|
const os = __importStar(require("os"));
|
|
46
50
|
const path = __importStar(require("path"));
|
|
47
51
|
const runtime_1 = require("../../nerves/runtime");
|
|
52
|
+
/** Maximum number of installed CLI versions to retain after pruning. */
|
|
53
|
+
exports.DEFAULT_RETAIN_VERSIONS = 5;
|
|
48
54
|
function getOuroCliHome(homeDir) {
|
|
49
55
|
/* v8 ignore next -- dep default: tests always inject @preserve */
|
|
50
56
|
const home = homeDir ?? os.homedir();
|
|
@@ -155,6 +161,124 @@ function activateVersion(version, deps) {
|
|
|
155
161
|
}
|
|
156
162
|
symlinkSync(newTarget, currentVersionPath);
|
|
157
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Compare two version strings of the form `0.1.0-alpha.{n}`. Returns
|
|
166
|
+
* positive when `a` is newer, negative when `b` is newer, 0 when equal.
|
|
167
|
+
*
|
|
168
|
+
* The harness only ships `0.1.0-alpha.{n}` versions today; this comparator
|
|
169
|
+
* extracts the numeric tail and falls back to a lexicographic compare for
|
|
170
|
+
* any version that doesn't match the pattern (so unexpected version
|
|
171
|
+
* formats sort consistently rather than throwing).
|
|
172
|
+
*/
|
|
173
|
+
function compareCliVersions(a, b) {
|
|
174
|
+
const aMatch = /alpha\.(\d+)/.exec(a);
|
|
175
|
+
const bMatch = /alpha\.(\d+)/.exec(b);
|
|
176
|
+
if (aMatch && bMatch) {
|
|
177
|
+
const aN = parseInt(aMatch[1], 10);
|
|
178
|
+
const bN = parseInt(bMatch[1], 10);
|
|
179
|
+
return aN - bN;
|
|
180
|
+
}
|
|
181
|
+
// Fallback: lexicographic. Both-mismatched and one-mismatched cases land
|
|
182
|
+
// here. Predictable, even if not strictly semver-correct.
|
|
183
|
+
if (a < b)
|
|
184
|
+
return -1;
|
|
185
|
+
if (a > b)
|
|
186
|
+
return 1;
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Identify which installed CLI versions can be safely deleted, given a
|
|
191
|
+
* retention policy. Pure: takes the full version list and the protected
|
|
192
|
+
* (current/previous) versions, returns the versions to delete.
|
|
193
|
+
*
|
|
194
|
+
* Retention rules:
|
|
195
|
+
* - Always keep the N most recent versions (default 5).
|
|
196
|
+
* - Always keep the currently-active version (CurrentVersion symlink target).
|
|
197
|
+
* - Always keep the previous version (previous symlink target) so rollback
|
|
198
|
+
* stays one command away.
|
|
199
|
+
* - Delete everything else.
|
|
200
|
+
*
|
|
201
|
+
* Exported so tests can pin the policy without shelling out to a real fs.
|
|
202
|
+
*/
|
|
203
|
+
function selectVersionsToPrune(installedVersions, protectedVersions, retain = exports.DEFAULT_RETAIN_VERSIONS) {
|
|
204
|
+
if (installedVersions.length <= retain)
|
|
205
|
+
return [];
|
|
206
|
+
const sorted = [...installedVersions].sort(compareCliVersions).reverse();
|
|
207
|
+
const keepers = new Set(sorted.slice(0, retain));
|
|
208
|
+
if (protectedVersions.current)
|
|
209
|
+
keepers.add(protectedVersions.current);
|
|
210
|
+
if (protectedVersions.previous)
|
|
211
|
+
keepers.add(protectedVersions.previous);
|
|
212
|
+
return installedVersions.filter((v) => !keepers.has(v));
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Prune installed CLI versions according to the retention policy. Removes
|
|
216
|
+
* `~/.ouro-cli/versions/{version}/` directories for versions outside the
|
|
217
|
+
* retention window. Best-effort: per-version delete failures are logged
|
|
218
|
+
* via nerves but don't propagate.
|
|
219
|
+
*
|
|
220
|
+
* Called from the activate path (cli-defaults.ts) so that every successful
|
|
221
|
+
* `ouro up` self-prunes. The user observed `~/.ouro-cli/versions/` going
|
|
222
|
+
* back to alpha.85 from March 20 — every CLI version they'd ever installed
|
|
223
|
+
* was still on disk because nothing ever GCed.
|
|
224
|
+
*/
|
|
225
|
+
function pruneOldVersions(retain = exports.DEFAULT_RETAIN_VERSIONS, deps = {}) {
|
|
226
|
+
const cliHome = getOuroCliHome(deps.homeDir);
|
|
227
|
+
/* v8 ignore start -- dep defaults: tests always inject @preserve */
|
|
228
|
+
const readdirSync = deps.readdirSync ?? ((p, opts) => fs.readdirSync(p, opts));
|
|
229
|
+
const readlinkSync = deps.readlinkSync ?? fs.readlinkSync;
|
|
230
|
+
const rmSync = deps.rmSync ?? fs.rmSync;
|
|
231
|
+
/* v8 ignore stop */
|
|
232
|
+
const versionsDir = path.join(cliHome, "versions");
|
|
233
|
+
let installed;
|
|
234
|
+
try {
|
|
235
|
+
installed = readdirSync(versionsDir, { withFileTypes: true })
|
|
236
|
+
.filter((e) => e.isDirectory())
|
|
237
|
+
.map((e) => e.name);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return { kept: [], deleted: [], failed: [] };
|
|
241
|
+
}
|
|
242
|
+
const current = (() => {
|
|
243
|
+
try {
|
|
244
|
+
return path.basename(readlinkSync(path.join(cliHome, "CurrentVersion")));
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
})();
|
|
250
|
+
const previous = (() => {
|
|
251
|
+
try {
|
|
252
|
+
return path.basename(readlinkSync(path.join(cliHome, "previous")));
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
})();
|
|
258
|
+
const toDelete = selectVersionsToPrune(installed, { current, previous }, retain);
|
|
259
|
+
const deleted = [];
|
|
260
|
+
const failed = [];
|
|
261
|
+
for (const version of toDelete) {
|
|
262
|
+
const versionDir = path.join(versionsDir, version);
|
|
263
|
+
try {
|
|
264
|
+
rmSync(versionDir, { recursive: true, force: true });
|
|
265
|
+
deleted.push(version);
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
failed.push({
|
|
269
|
+
version,
|
|
270
|
+
error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
(0, runtime_1.emitNervesEvent)({
|
|
275
|
+
component: "daemon",
|
|
276
|
+
event: "daemon.cli_versions_pruned",
|
|
277
|
+
message: `pruned ${deleted.length} old CLI versions`,
|
|
278
|
+
meta: { retain, deleted, failed: failed.length, kept: installed.filter((v) => !deleted.includes(v)) },
|
|
279
|
+
});
|
|
280
|
+
return { kept: installed.filter((v) => !deleted.includes(v)), deleted, failed };
|
|
281
|
+
}
|
|
158
282
|
function ensureLayout(deps) {
|
|
159
283
|
const cliHome = getOuroCliHome(deps.homeDir);
|
|
160
284
|
/* v8 ignore next -- dep default: tests always inject @preserve */
|