loadout-ai 0.5.0 → 0.5.1

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 CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.5.1 - 2026-07-21
6
+
7
+ ### Fixed
8
+
9
+ - Remove disabled Maximum-library copies during package removal and complete
10
+ uninstall without inspecting or deleting unrelated skills that later occupy the
11
+ original agent path.
12
+ - Describe a disabled-only Maximum library as ready with nothing active, including
13
+ explicit active and disabled skill counts, instead of calling it a healthy active
14
+ installation.
15
+ - Name modified managed paths in blocked complete-uninstall previews so users can
16
+ review real drift before deciding whether to force removal.
17
+
18
+ ## 0.5.0 - 2026-07-21
19
+
5
20
  ### Added
6
21
 
7
22
  - Add the pinned MIT-licensed Obsidian Skills collection and recommend it only when an
package/MASTER_PLAN.md CHANGED
@@ -270,6 +270,13 @@ merged `codex/relatable-readme-hero` remote branch remains safe to delete after
270
270
  publish attempt. Every build now removes `dist` first on every platform, and the
271
271
  package smoke test rejects removed dashboard/demo JavaScript if it ever leaks
272
272
  back into the tarball. The failed OTP-gated attempt published nothing.
273
+ - [x] `P18-37 [TERRA]` Fix the founder-discovered disabled-library uninstall bug.
274
+ Removal now resolves each managed file to its disabled Maximum-library copy,
275
+ never an unmanaged skill that later occupies the original active path. A
276
+ regression test preserves the replacement bytes, the real founder state now
277
+ previews an unblocked removal of 29 packages/2,316 disabled records, and health
278
+ reports `library ready (nothing active)` with explicit counts. Release as
279
+ `0.5.1` before continuing the complete-uninstall acceptance step.
273
280
 
274
281
  ### Release 0.3 lifecycle hardening
275
282
 
package/README.md CHANGED
@@ -26,7 +26,7 @@
26
26
  </p>
27
27
 
28
28
  > [!IMPORTANT]
29
- > Installation is version-pinned so the code you test matches these docs. The commands below target `loadout-ai@0.5.0`; review the preview before every apply.
29
+ > Installation is version-pinned so the code you test matches these docs. The commands below target `loadout-ai@0.5.1`; review the preview before every apply.
30
30
 
31
31
  ## How it works
32
32
 
@@ -76,7 +76,7 @@ Skills, plugins, MCP servers, and agent settings tend to accumulate one experime
76
76
  You need Node.js 20 or newer and Git.
77
77
 
78
78
  ```bash
79
- npm install --global loadout-ai@0.5.0
79
+ npm install --global loadout-ai@0.5.1
80
80
  loadout --version
81
81
  loadout guide
82
82
  ```
package/dist/src/cli.js CHANGED
@@ -263,7 +263,7 @@ async function runSetup(options) {
263
263
  reader?.close();
264
264
  }
265
265
  }
266
- const LOADOUT_VERSION = "0.5.0";
266
+ const LOADOUT_VERSION = "0.5.1";
267
267
  function durableSchedulerLauncher() {
268
268
  return [
269
269
  join(dirname(process.execPath), process.platform === "win32" ? "npx.cmd" : "npx"),
@@ -100,18 +100,26 @@ export async function buildHealthReport(options = {}) {
100
100
  fix: "Check connectivity and retry.",
101
101
  });
102
102
  const configured = state.installs.length > 0 || (state.mcpInstalls ?? []).length > 0;
103
+ const activeSkills = (state.activations ?? []).filter((entry) => entry.installationState === "installed" &&
104
+ entry.activationState === "active").length;
105
+ const disabledSkills = (state.activations ?? []).filter((entry) => entry.installationState === "installed" &&
106
+ entry.activationState === "disabled").length;
103
107
  const status = findings.some((finding) => finding.level === "error")
104
108
  ? "unhealthy"
105
109
  : !configured
106
110
  ? "not-configured"
107
111
  : findings.some((finding) => finding.level === "warning")
108
112
  ? "attention"
109
- : "healthy";
113
+ : activeSkills === 0 && disabledSkills > 0
114
+ ? "library-only"
115
+ : "healthy";
110
116
  return {
111
117
  status,
112
118
  generatedAt: new Date().toISOString(),
113
119
  agents,
114
120
  installedPackages: state.installs.length,
121
+ activeSkills,
122
+ disabledSkills,
115
123
  updatesChecked: Boolean(options.updates || options.checkUpdates),
116
124
  updatesAvailable: available.length,
117
125
  driftedFiles: drifted.length,
@@ -120,7 +128,7 @@ export async function buildHealthReport(options = {}) {
120
128
  };
121
129
  }
122
130
  export function formatHealthReport(report) {
123
- const icon = report.status === "not-configured"
131
+ const icon = report.status === "not-configured" || report.status === "library-only"
124
132
  ? "•"
125
133
  : report.status === "healthy"
126
134
  ? "✓"
@@ -128,8 +136,8 @@ export function formatHealthReport(report) {
128
136
  ? "!"
129
137
  : "✗";
130
138
  const lines = [
131
- `${icon} Loadout health: ${report.status === "not-configured" ? "not configured" : report.status}`,
132
- `Packages: ${report.installedPackages} installed, ${report.updatesChecked ? `${report.updatesAvailable} update(s)` : "updates not checked (use --updates)"}, ${report.driftedFiles} drifted file(s), ${report.driftedMcpServers} drifted MCP server(s)`,
139
+ `${icon} Loadout health: ${report.status === "not-configured" ? "not configured" : report.status === "library-only" ? "library ready (nothing active)" : report.status}`,
140
+ `Packages: ${report.installedPackages} managed; skills: ${report.activeSkills ?? 0} active, ${report.disabledSkills ?? 0} disabled; ${report.updatesChecked ? `${report.updatesAvailable} update(s)` : "updates not checked (use --updates)"}; ${report.driftedFiles} drifted file(s), ${report.driftedMcpServers} drifted MCP server(s)`,
133
141
  ];
134
142
  for (const finding of report.findings)
135
143
  lines.push(`${finding.level === "ok" ? "✓" : finding.level === "error" ? "✗" : finding.level === "warning" ? "!" : "•"} ${finding.message}${finding.fix ? ` ${finding.fix}` : ""}`);
@@ -3,6 +3,7 @@ import { readFile, rm } from "node:fs/promises";
3
3
  import { forgetInstall, installStatePath, readInstallState } from "./state.js";
4
4
  import { writeMcpConfigPlan } from "./mcp.js";
5
5
  import { runMutationTransaction } from "./transaction.js";
6
+ import { managedFileReadPath } from "./active-set.js";
6
7
  import { codexMcpServerFingerprint, removeCodexMcpServerBlock, writeCodexMcpConfigContent, } from "./codex-mcp.js";
7
8
  export async function planRemove(packageId) {
8
9
  const state = await readInstallState();
@@ -11,19 +12,20 @@ export async function planRemove(packageId) {
11
12
  if (!record && !trackedMcp.length)
12
13
  throw new Error(`Package is not managed by Loadout: ${packageId}`);
13
14
  const files = await Promise.all((record?.files ?? []).map(async (file) => {
15
+ const managedPath = managedFileReadPath(packageId, file.path, state.activations ?? []);
14
16
  try {
15
17
  const digest = createHash("sha256")
16
- .update(await readFile(file.path))
18
+ .update(await readFile(managedPath))
17
19
  .digest("hex");
18
20
  return {
19
- path: file.path,
21
+ path: managedPath,
20
22
  status: digest === file.sha256
21
23
  ? "unchanged"
22
24
  : "modified",
23
25
  };
24
26
  }
25
27
  catch {
26
- return { path: file.path, status: "missing" };
28
+ return { path: managedPath, status: "missing" };
27
29
  }
28
30
  }));
29
31
  const modified = files.filter((file) => file.status === "modified");
@@ -113,6 +113,9 @@ export async function uninstallGlobalCli() {
113
113
  });
114
114
  }
115
115
  export function formatUninstallPlan(plan) {
116
+ const modifiedPaths = plan.packages.flatMap((entry) => entry.files
117
+ .filter((file) => file.status === "modified")
118
+ .map((file) => file.path));
116
119
  return [
117
120
  "Complete Loadout uninstall preview",
118
121
  "",
@@ -122,7 +125,14 @@ export function formatUninstallPlan(plan) {
122
125
  `Daily jobs to remove: ${plan.schedulers.map((item) => item.job).join(", ") || "none"}`,
123
126
  `State and cache: ${plan.stateHome}`,
124
127
  ...(plan.blocked
125
- ? ["", "BLOCKED: managed files were changed outside Loadout."]
128
+ ? [
129
+ "",
130
+ "BLOCKED: managed files were changed outside Loadout.",
131
+ ...modifiedPaths.slice(0, 10).map((path) => ` Modified: ${path}`),
132
+ ...(modifiedPaths.length > 10
133
+ ? [` …and ${modifiedPaths.length - 10} more`]
134
+ : []),
135
+ ]
126
136
  : []),
127
137
  "",
128
138
  ...plan.warnings.map((warning) => `Warning: ${warning}`),
@@ -183,7 +183,7 @@ cleanup deliberately deletes Loadout's snapshots, so it is the last lifecycle te
183
183
  ## Troubleshooting and recovery
184
184
 
185
185
  - **`loadout` is not found after installation:** confirm `npm install --global
186
- loadout-ai@0.5.0` completed, run `hash -r`, and confirm npm's global binary
186
+ loadout-ai@0.5.1` completed, run `hash -r`, and confirm npm's global binary
187
187
  directory is on `PATH`. For a source checkout, run `npm run build` and `npm link`.
188
188
  - **A preview asks for `--approve-risk`:** read the reported scripts, domains,
189
189
  credentials, binaries, or instruction findings. If you accept that specific plan,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loadout-ai",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "Universal upgrade manager for AI coding agents",