loadout-ai 0.2.2 → 0.3.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.
@@ -85,66 +85,89 @@ async function analyzeManagedUpdate(oldRoot, newRoot, unitIds) {
85
85
  };
86
86
  }
87
87
  /** Builds a read-only update plan from persisted installs and live GitHub snapshots. */
88
- export async function buildUpdatePlan(resolver = async (repository) => fetchRepositorySnapshot(repository)) {
88
+ export async function buildUpdatePlan(resolver = async (repository) => fetchRepositorySnapshot(repository, { timeoutMs: 30_000 }), options = {}) {
89
89
  const state = await readInstallState();
90
- return Promise.all(state.installs.map(async (record) => {
91
- const disabledAgents = (state.activations ?? [])
92
- .filter((activation) => activation.packageId === record.packageId &&
93
- activation.installationState === "installed" &&
94
- activation.activationState === "disabled")
95
- .map((activation) => activation.agent);
96
- const base = {
97
- packageId: record.packageId,
98
- repository: record.repository,
99
- installedCommit: record.resolvedCommit,
100
- targetAgents: record.targetAgents,
101
- ...(disabledAgents.length ? { disabledAgents } : {}),
102
- };
103
- if (!record.repository || !record.resolvedCommit) {
104
- return {
105
- ...base,
106
- status: "untracked",
107
- action: "Reinstall from the original source to begin update tracking.",
108
- };
109
- }
110
- try {
111
- const current = await resolver(record.repository);
112
- const same = current.commit.toLowerCase() === record.resolvedCommit.toLowerCase();
113
- let diff;
114
- let safetyFindings;
115
- let approvalRequired = false;
116
- if (!same && current.path) {
117
- const oldPath = repositoryCachePath(record.repository, record.resolvedCommit);
118
- const analysis = await analyzeManagedUpdate(oldPath, current.path, managedUnitIds(state, record.packageId));
119
- diff = analysis.diff;
120
- safetyFindings = analysis.safetyFindings;
121
- approvalRequired = analysis.approvalRequired;
122
- }
123
- return {
124
- ...base,
125
- availableCommit: current.commit,
126
- status: same ? "up-to-date" : "update-available",
127
- action: same
128
- ? "No action required."
129
- : disabledAgents.length
130
- ? `Enable ${record.packageId} for ${disabledAgents.join(", ")} before applying an update; planning remains read-only.`
131
- : approvalRequired
132
- ? `Approval required: review safety warnings before updating ${record.packageId}.`
133
- : `Run loadout update --package ${record.packageId} after review.`,
134
- ...(approvalRequired ? { approvalRequired: true } : {}),
135
- ...(safetyFindings?.length ? { safetyFindings } : {}),
136
- ...(diff ? { diff } : {}),
137
- };
138
- }
139
- catch (error) {
140
- return {
141
- ...base,
142
- status: "error",
143
- action: "Retry when GitHub is reachable; the installed version was not changed.",
144
- error: error instanceof Error ? error.message : String(error),
145
- };
90
+ const records = options.packageId
91
+ ? state.installs.filter((record) => record.packageId === options.packageId)
92
+ : state.installs;
93
+ const results = new Array(records.length);
94
+ let cursor = 0;
95
+ let completed = 0;
96
+ const workers = Array.from({
97
+ length: Math.min(Math.max(1, options.concurrency ?? 4), Math.max(1, records.length)),
98
+ }, async () => {
99
+ while (cursor < records.length) {
100
+ const index = cursor++;
101
+ const record = records[index];
102
+ results[index] = await (async () => {
103
+ const disabledAgents = (state.activations ?? [])
104
+ .filter((activation) => activation.packageId === record.packageId &&
105
+ activation.installationState === "installed" &&
106
+ activation.activationState === "disabled")
107
+ .map((activation) => activation.agent);
108
+ const base = {
109
+ packageId: record.packageId,
110
+ repository: record.repository,
111
+ installedCommit: record.resolvedCommit,
112
+ targetAgents: record.targetAgents,
113
+ ...(disabledAgents.length ? { disabledAgents } : {}),
114
+ };
115
+ if (!record.repository || !record.resolvedCommit) {
116
+ return {
117
+ ...base,
118
+ status: "untracked",
119
+ action: "Reinstall from the original source to begin update tracking.",
120
+ };
121
+ }
122
+ try {
123
+ const current = await resolver(record.repository);
124
+ const same = current.commit.toLowerCase() ===
125
+ record.resolvedCommit.toLowerCase();
126
+ let diff;
127
+ let safetyFindings;
128
+ let approvalRequired = false;
129
+ if (!same && current.path) {
130
+ const oldPath = repositoryCachePath(record.repository, record.resolvedCommit);
131
+ const analysis = await analyzeManagedUpdate(oldPath, current.path, managedUnitIds(state, record.packageId));
132
+ diff = analysis.diff;
133
+ safetyFindings = analysis.safetyFindings;
134
+ approvalRequired = analysis.approvalRequired;
135
+ }
136
+ return {
137
+ ...base,
138
+ availableCommit: current.commit,
139
+ status: same ? "up-to-date" : "update-available",
140
+ action: same
141
+ ? "No action required."
142
+ : disabledAgents.length
143
+ ? `Enable ${record.packageId} for ${disabledAgents.join(", ")} before applying an update; planning remains read-only.`
144
+ : approvalRequired
145
+ ? `Approval required: review safety warnings before updating ${record.packageId}.`
146
+ : `Run loadout update --package ${record.packageId} --yes after review.`,
147
+ ...(approvalRequired ? { approvalRequired: true } : {}),
148
+ ...(safetyFindings?.length ? { safetyFindings } : {}),
149
+ ...(diff ? { diff } : {}),
150
+ };
151
+ }
152
+ catch (error) {
153
+ return {
154
+ ...base,
155
+ status: "error",
156
+ action: "Retry when GitHub is reachable; the installed version was not changed.",
157
+ error: error instanceof Error ? error.message : String(error),
158
+ };
159
+ }
160
+ })();
161
+ completed += 1;
162
+ options.onProgress?.({
163
+ completed,
164
+ total: records.length,
165
+ packageId: record.packageId,
166
+ });
146
167
  }
147
- }));
168
+ });
169
+ await Promise.all(workers);
170
+ return results;
148
171
  }
149
172
  export function formatUpdatePlan(plans) {
150
173
  if (plans.length === 0)
@@ -160,6 +183,12 @@ export function formatUpdatePlan(plans) {
160
183
  })
161
184
  .join("\n");
162
185
  }
186
+ /** Updates safe enough for an explicit whole-profile `update --yes` apply. */
187
+ export function selectSafeAutomaticUpdates(plans) {
188
+ return plans.filter((plan) => plan.status === "update-available" &&
189
+ !plan.approvalRequired &&
190
+ !plan.disabledAgents?.length);
191
+ }
163
192
  function quarantineRoot() {
164
193
  return join(loadoutHome(), "quarantine");
165
194
  }
@@ -304,6 +304,18 @@ export const installStateSchema = z
304
304
  installs: z.array(installRecordSchema),
305
305
  mcpInstalls: z.array(mcpInstallRecordSchema).default([]),
306
306
  activations: z.array(managedActivationRecordSchema).default([]),
307
+ profile: z
308
+ .object({
309
+ mode: z.enum(["stable", "power", "maximum", "custom"]),
310
+ packageIds: z.array(text).optional(),
311
+ agents: z.array(agentIdSchema),
312
+ catalogPackages: z.array(z.object({
313
+ packageId: text,
314
+ reviewedCommit: optionalText,
315
+ })),
316
+ appliedAt: text,
317
+ })
318
+ .optional(),
307
319
  })
308
320
  .passthrough();
309
321
  const lockedPackageSchema = z
package/docs/TESTING.md CHANGED
@@ -199,8 +199,20 @@ write to the detected real agent profiles. After npm publication, replace `npx .
199
199
 
200
200
  ## 8. Test credential-gated MCP configuration without exposing a key
201
201
 
202
- The Playwright recipe has no service credential. The GitHub read-only recipe does and
203
- must refuse `--yes` until its declared environment reference resolves:
202
+ List the reviewed recipes that need no separately billed AI/model API key:
203
+
204
+ ```bash
205
+ npx . mcp-recipe --no-key
206
+ ```
207
+
208
+ This includes Playwright, Chrome DevTools, and GitHub read-only. The first two have no
209
+ service credential. GitHub read-only needs a GitHub token and must refuse `--yes`
210
+ until its declared environment reference resolves. Use `--credential-free` for the
211
+ stricter zero-credential list:
212
+
213
+ ```bash
214
+ npx . mcp-recipe --credential-free
215
+ ```
204
216
 
205
217
  ```bash
206
218
  npx . mcp-recipe github-readonly --config "$TEST_HOME/mcp.json" --yes
@@ -230,3 +242,14 @@ npx . dashboard
230
242
  Open the printed loopback URL. CLI setup, updates, removal, discovery, and rollback all
231
243
  work without it. Browser automation is also optional and runs only when manually
232
244
  dispatched in CI; locally, use `npm run test:e2e:dashboard`.
245
+
246
+ ## Final cleanup test
247
+
248
+ ```bash
249
+ npx . uninstall
250
+ npx . uninstall --yes
251
+ ```
252
+
253
+ The first command is a dry run. The second removes only Loadout-managed agent files,
254
+ runtime tools, native jobs, state, snapshots, and cache; it leaves the locally invoked
255
+ package itself alone. Use `--remove-cli` only when testing a global npm install.
@@ -0,0 +1,176 @@
1
+ # Test Loadout on your own machine
2
+
3
+ This is the short, safe route through the product. Start in a normal terminal.
4
+ The first group only reads local state or prepares a preview; it does not replace
5
+ agent files. A preview can download reviewed source into Loadout's cache, but it
6
+ does not activate skills or change an agent configuration.
7
+
8
+ ## 1. Get oriented
9
+
10
+ ```bash
11
+ loadout guide
12
+ loadout library
13
+ loadout scan
14
+ loadout health
15
+ ```
16
+
17
+ `library` is the concise provenance view: it shows active skills and disabled
18
+ reviewed-library copies per agent. For the full source-package and upstream-repository
19
+ record for every skill, use `loadout library --all`. `scan` distinguishes
20
+ Loadout-managed skills from your own pre-existing skills. `health` checks local drift only; add
21
+ `--updates` only when you want it to contact the tracked public repositories.
22
+
23
+ ## 2. Explore without installing
24
+
25
+ ```bash
26
+ loadout catalog --json
27
+ loadout candidate list --limit 10
28
+ loadout recommend --project .
29
+ loadout optimize --project .
30
+ loadout tool
31
+ loadout tool graphify
32
+ ```
33
+
34
+ Run the project commands from the project you care about, or replace `.` with its
35
+ absolute path. `optimize` is still a preview until `--yes` is supplied. `tool
36
+ graphify` is also a preview; Graphify is a reviewed runtime tool and does not
37
+ need an OpenAI or Anthropic API key for its code-only install.
38
+
39
+ ## 3. Open the optional dashboard
40
+
41
+ ```bash
42
+ loadout dashboard
43
+ ```
44
+
45
+ Open the `http://127.0.0.1:PORT` address it prints. It never listens on the
46
+ network. The dashboard shows status, health, installed packages, updates, local
47
+ project recommendations, profiles, and the catalog. Its Apply and Undo buttons
48
+ require an in-page preview, acknowledgement, and a private local session token.
49
+ Stop the server with `Control-C`.
50
+
51
+ ## 4. Preview and install a profile
52
+
53
+ Use this order. Each setup command previews first; interactive setup asks for
54
+ confirmation before changing files. A mutation creates a snapshot first.
55
+
56
+ ```bash
57
+ # Recommended everyday skills
58
+ loadout setup --mode stable --agents codex,claude-code
59
+
60
+ # Broader daily-use selection (50 curated skill directories)
61
+ loadout setup --mode power --agents codex,claude-code
62
+
63
+ # Download the broad reviewed library while keeping the active set controlled
64
+ loadout setup --mode maximum --agents codex,claude-code
65
+ ```
66
+
67
+ At the API-access question, choose `None` unless you separately pay for a
68
+ provider API. A ChatGPT Plus or Claude Pro subscription is not an API key. Core
69
+ skill profiles do not require one; credentialed MCP and runtime operations stay
70
+ explicit.
71
+
72
+ For unattended use only after reviewing a preview, the equivalent is:
73
+
74
+ ```bash
75
+ loadout setup --mode stable --agents codex,claude-code --yes
76
+ ```
77
+
78
+ Do not add `--approve-risk` unless the displayed preview identifies a specific
79
+ reviewed finding and you understand it.
80
+
81
+ ## 5. Test a change and recover
82
+
83
+ ```bash
84
+ # Preview Graphify, then install only if the preview looks right
85
+ loadout tool graphify --agents codex,claude-code
86
+ loadout tool graphify --agents codex,claude-code --yes --approve-risk
87
+
88
+ # Check the exact current state
89
+ loadout library
90
+ loadout health
91
+
92
+ # Restore a prior snapshot if you do not like the result
93
+ loadout rollback --list
94
+ loadout rollback
95
+
96
+ # Or remove only Graphify and restore its pre-install agent state
97
+ loadout tool graphify --remove --agents codex,claude-code --yes --approve-risk
98
+ ```
99
+
100
+ `rollback` restores a whole Loadout snapshot. The tool-specific remove command
101
+ is narrower and is preferable when you only want to undo that one runtime tool.
102
+
103
+ ## 6. Test daily discovery and updates
104
+
105
+ ```bash
106
+ loadout alerts
107
+ loadout update
108
+ loadout update --package superpowers
109
+ loadout autopilot
110
+ ```
111
+
112
+ `update --package` checks only the named tracked package. The default is a
113
+ read-only diff and safety plan. Apply only after review:
114
+
115
+ ```bash
116
+ loadout update --package superpowers --apply
117
+ loadout update --yes
118
+ ```
119
+
120
+ `autopilot` previews two native daily read-only jobs (updates and discovery).
121
+ Enable or remove both explicitly:
122
+
123
+ ```bash
124
+ loadout autopilot --time 09:00 --yes
125
+ loadout autopilot --remove --yes
126
+ ```
127
+
128
+ The daily update job re-evaluates your saved Stable, Power, or Maximum profile and
129
+ checks every managed package, but never supplies `--yes`. Daily discovery can add
130
+ interesting repositories to the review queue; it cannot silently promote or install
131
+ them.
132
+
133
+ ## 7. Test MCP choices without a model API key
134
+
135
+ ```bash
136
+ loadout mcp-recipe --no-key
137
+ ```
138
+
139
+ Expect Playwright MCP, Chrome DevTools MCP, and GitHub read-only. None requires a
140
+ separately billed AI/model API key. GitHub read-only still discloses that it needs a
141
+ GitHub token; use `loadout mcp-recipe --credential-free` to exclude every service
142
+ credential too. Browser configuration and real connection testing remain explicit.
143
+ Graphify is a separate runtime tool, not an MCP server.
144
+
145
+ ## 8. Preview complete cleanup
146
+
147
+ ```bash
148
+ loadout uninstall
149
+ ```
150
+
151
+ Read the package, runtime, scheduler, and state summary. The preview changes nothing.
152
+ At the very end of testing, remove all Loadout-managed data while keeping the CLI:
153
+
154
+ ```bash
155
+ loadout uninstall --yes
156
+ ```
157
+
158
+ To remove the npm command too, use `loadout uninstall --yes --remove-cli`. Complete
159
+ cleanup deliberately deletes Loadout's snapshots, so it is the last lifecycle test.
160
+
161
+ ## 9. Advanced surface
162
+
163
+ The first help screen deliberately focuses on daily use. Existing advanced
164
+ commands have not been removed:
165
+
166
+ ```bash
167
+ loadout advanced
168
+ loadout candidate --help
169
+ loadout mcp-recipe --help
170
+ loadout <command> --help
171
+ ```
172
+
173
+ Avoid running registry publishing, signing, sandbox, credential, or arbitrary
174
+ MCP configuration commands on your main profile as part of routine user testing.
175
+ They are package-author or integration workflows, not required to use Loadout's
176
+ core product.
@@ -0,0 +1,42 @@
1
+ # Loadout 0.3 Product-Hardening Plan
2
+
3
+ ## Goal
4
+
5
+ Ship a user-testable CLI release with one-command cleanup, honest whole-profile update
6
+ checks, explicit no-key MCP discovery, and a verified local dashboard.
7
+
8
+ ## Public contracts
9
+
10
+ 1. `loadout uninstall` is a preview. `loadout uninstall --yes` removes scheduled
11
+ Loadout jobs, runtime tools, managed agent files, disabled library copies, cache,
12
+ snapshots, and state while preserving unmanaged or modified files. `--remove-cli`
13
+ additionally runs the global npm uninstall.
14
+ 2. `loadout update` checks every tracked package and compares the last installed
15
+ Stable/Power/Maximum profile with the current trusted catalog. It never mutates by
16
+ default.
17
+ 3. `loadout update --yes` reapplies trusted profile changes and applies all
18
+ statically-safe active package updates; blocked, disabled, or failed updates are
19
+ reported and skipped. `--package <id> --yes` remains the precise path.
20
+ 4. Stable, Power, and Maximum are re-evaluated against the trusted catalog whenever
21
+ `update` runs. Discovery can nominate new candidates daily, but candidates cannot
22
+ enter a profile until immutable source, license, compatibility, and safety review
23
+ are recorded.
24
+ 5. `loadout mcp-recipe --no-key` lists reviewed recipes requiring no separately
25
+ billed AI/model API key. It includes GitHub read-only while separately disclosing
26
+ the GitHub token. `--credential-free` is the stricter zero-credential filter.
27
+ 6. `loadout dashboard` remains loopback-only and is verified by the browser E2E test.
28
+
29
+ ## Implementation sequence
30
+
31
+ - [x] Add failing tests for uninstall preview/application and CLI help.
32
+ - [x] Implement uninstall orchestration with dependency injection and path guards.
33
+ - [x] Add failing tests for persisted profile state and profile drift reporting.
34
+ - [x] Record setup mode in snapshot-restorable install state and include it in update.
35
+ - [x] Add failing tests for bulk safe update selection and clear skipped results.
36
+ - [x] Implement `update --yes`, retaining `--apply` as a compatible alias.
37
+ - [x] Add and test Chrome DevTools as a pinned no-key MCP recipe.
38
+ - [x] Add `mcp-recipe --no-key` and beginner-readable recipe output.
39
+ - [x] Update README, test guide, master plan, changelog, completion, and version.
40
+ - [ ] Run unit, lint, type, evidence, package, CLI E2E, performance, dashboard E2E,
41
+ and npm dry-run gates.
42
+ - [ ] Review, commit, merge to main, push, tag, and publish to npm.
@@ -0,0 +1,86 @@
1
+ # CLI UX Polish Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use `executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Make Loadout's everyday CLI path understandable for a first-time user while preserving its existing safe, advanced capabilities.
6
+
7
+ **Architecture:** Keep the CLI as the primary interface. Add one concise, read-only `guide` command and a focused help footer; retain advanced commands but remove them from the first-screen help. Fix JSON output and package-scoped update previews as explicit public CLI contracts. Document a safe test journey and keep the dashboard as an optional local companion.
8
+
9
+ **Tech Stack:** Node.js 20+, TypeScript, Commander, Vitest, framework-free local dashboard.
10
+
11
+ ## Global Constraints
12
+
13
+ - Never mutate a user's agent configuration from a default or preview command.
14
+ - Existing command names remain valid even if hidden from first-screen help.
15
+ - Machine-readable commands must emit valid JSON when `--json` is accepted.
16
+ - Do not require an API key or GitHub account for the core test journey.
17
+ - Every behavior change gets a test before its production code.
18
+
19
+ ---
20
+
21
+ ### Task 1: Define the beginner CLI contract
22
+
23
+ **Files:**
24
+
25
+ - Modify: `tests/cli-help.test.ts`
26
+ - Modify: `src/cli.ts`
27
+
28
+ **Interfaces:**
29
+
30
+ - Produces: `loadout guide`, a read-only command that explains setup, discovery, project recommendations, recovery, dashboard, and help.
31
+
32
+ - [x] **Step 1: Write failing CLI contract tests** for `loadout guide`, a focused top-level help footer, and retained access to an advanced command.
33
+ - [x] **Step 2: Run** `npm test -- tests/cli-help.test.ts` **and confirm the new assertions fail.**
34
+ - [x] **Step 3: Implement** `guide`, hide maintainer-only commands from first-screen help, and add a short help footer that points to the guide.
35
+ - [x] **Step 4: Run** `npm test -- tests/cli-help.test.ts` **and confirm it passes.**
36
+ - [ ] **Step 5: Commit** the focused CLI discoverability change.
37
+
38
+ ### Task 2: Repair machine-readable and scoped preview contracts
39
+
40
+ **Files:**
41
+
42
+ - Modify: `tests/cli-help.test.ts`
43
+ - Modify: `tests/update.test.ts`
44
+ - Modify: `src/cli.ts`
45
+ - Modify: `src/core/update.ts`
46
+
47
+ **Interfaces:**
48
+
49
+ - `loadout catalog --json` returns a JSON array.
50
+ - `loadout update --package <id>` plans only that managed package and rejects an unknown installed package clearly.
51
+
52
+ - [x] **Step 1: Write failing tests** for catalog JSON and package-scoped update planning.
53
+ - [x] **Step 2: Run the focused test files** and confirm the assertions fail for the existing implementation.
54
+ - [x] **Step 3: Implement the smallest compatible fixes.**
55
+ - [x] **Step 4: Run the focused tests** and confirm they pass.
56
+ - [ ] **Step 5: Commit** the contract fixes separately.
57
+
58
+ ### Task 3: Record user testing and current product scope
59
+
60
+ **Files:**
61
+
62
+ - Create: `docs/USER_TEST_GUIDE.md`
63
+ - Modify: `MASTER_PLAN.md`
64
+
65
+ **Interfaces:**
66
+
67
+ - The guide gives a real-profile-safe order of commands, says which commands change state, and gives rollback instructions.
68
+ - `MASTER_PLAN.md` begins with the one authoritative unfinished-work section and moves stale immediate tasks out of the active path.
69
+
70
+ - [x] **Step 1: Add a concise user testing guide** for daily use, discovery, optional dashboard, safe mutation, rollback, and advanced validation.
71
+ - [x] **Step 2: Add the current remaining work section** and mark historic, non-essential ideas as deferred rather than pretending they are active product requirements.
72
+ - [x] **Step 3: Run markdown and CLI smoke checks** to make sure command names match the real surface.
73
+ - [ ] **Step 4: Commit** documentation separately.
74
+
75
+ ### Task 4: Validate the actual user journey
76
+
77
+ **Files:**
78
+
79
+ - Test: `tests/cli-help.test.ts`
80
+ - Test: `tests/update.test.ts`
81
+ - Test: `tests/dashboard.test.ts`
82
+
83
+ - [x] **Step 1: Build and run** the CLI guide, catalog JSON, health, library, project recommendation preview, and dashboard endpoint checks without changing agent files.
84
+ - [ ] **Step 2: Run** `npm run verify:full` **and inspect every failure if any.**
85
+ - [ ] **Step 3: Review the diff for accidental profile/cache/secrets changes.**
86
+ - [ ] **Step 4: Commit, then present the exact install/test/recovery commands to the user.**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loadout-ai",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "Universal upgrade manager for AI coding agents",