claudeup 6.5.1 → 6.7.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/package.json +4 -4
- package/src/__tests__/active-closure.test.ts +184 -0
- package/src/__tests__/blackbox-active-closure.test.ts +119 -0
- package/src/__tests__/blackbox-convergence.test.ts +334 -0
- package/src/__tests__/blackbox-parity.test.ts +684 -0
- package/src/__tests__/blackbox-plugin-state.test.ts +523 -0
- package/src/__tests__/blackbox-tool-state.test.ts +669 -0
- package/src/__tests__/cli-apply-seams.test.ts +2 -0
- package/src/__tests__/cli-tool-commands.test.ts +1 -1
- package/src/__tests__/cli-tools-adapter.test.ts +294 -0
- package/src/__tests__/cli-update-view.test.ts +142 -2
- package/src/__tests__/content-drift.test.ts +60 -0
- package/src/__tests__/conventions-manager.test.ts +45 -45
- package/src/__tests__/enabled-not-installed.test.ts +2 -3
- package/src/__tests__/fixtures/tool-probe/brew-list-versions-tmux.txt +2 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-behind-homebrew-spec.txt +29 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-tmux-current.txt +7 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-unknown-formula.txt +6 -0
- package/src/__tests__/fixtures/tool-probe/go-version-m-tmux-mcp.txt +27 -0
- package/src/__tests__/fixtures/tool-probe/npm-dist-tags-missing-scoped.txt +4 -0
- package/src/__tests__/fixtures/tool-probe/npm-dist-tags-openai-codex.txt +4 -0
- package/src/__tests__/fixtures/tool-probe/pypi-cowsay.txt +3 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-browser-use.toml +10 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-git.toml +8 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-pinned.toml +8 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-graphifyy.toml +7 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-mcp.toml +6 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-serena-agent.toml +9 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated-pinned.txt +5 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated.txt +11 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list.txt +22 -0
- package/src/__tests__/{gitignore-prerun.test.ts → gitignore-check.test.ts} +8 -6
- package/src/__tests__/models-presets-marketplace.test.ts +141 -0
- package/src/__tests__/plugin-cli-argv.test.ts +1 -1
- package/src/__tests__/plugin-manager-fallback.test.ts +3 -5
- package/src/__tests__/plugin-row-text.test.ts +336 -0
- package/src/__tests__/resolver.test.ts +3 -3
- package/src/__tests__/scope-action.test.ts +69 -19
- package/src/__tests__/shell-script-callers.test.ts +8 -8
- package/src/__tests__/tool-probe.test.ts +1400 -0
- package/src/__tests__/update-apply.test.ts +283 -1
- package/src/__tests__/update-plan.test.ts +773 -31
- package/src/__tests__/version-unknown-state.test.ts +388 -0
- package/src/cli/router.ts +0 -5
- package/src/cli/update-view.ts +92 -12
- package/src/cli/update.ts +114 -38
- package/src/data/models-presets.ts +10 -11
- package/src/data/predefined-profiles.ts +7 -7
- package/src/services/active-closure.ts +117 -0
- package/src/services/catalog-cache-store.ts +6 -2
- package/src/services/claude-settings.ts +2 -174
- package/src/services/community-styles.ts +1 -1
- package/src/services/{gitignore-prerun.ts → gitignore-check.ts} +14 -10
- package/src/services/gitignore-fixer.ts +1 -1
- package/src/services/marketplace-refresh.ts +30 -0
- package/src/services/plugin-manager.ts +200 -283
- package/src/services/resolver.ts +7 -2
- package/src/services/tool-probe.ts +1637 -0
- package/src/services/toolchain.ts +97 -0
- package/src/services/update-engine.ts +220 -32
- package/src/services/update-plan.ts +640 -160
- package/src/services/version-snapshot.ts +1 -1
- package/src/types/bun.d.ts +6 -0
- package/src/ui/App.tsx +2 -27
- package/src/ui/adapters/cliToolsAdapter.ts +176 -0
- package/src/ui/adapters/pluginsAdapter.ts +57 -0
- package/src/ui/components/modals/ModalContainer.tsx +0 -26
- package/src/ui/hooks/index.ts +0 -1
- package/src/ui/hooks/useGitignoreModal.ts +2 -3
- package/src/ui/renderers/cliToolRenderers.tsx +108 -67
- package/src/ui/renderers/pluginRenderers.tsx +157 -98
- package/src/ui/renderers/pluginRowText.ts +70 -0
- package/src/ui/renderers/pluginStatusText.ts +56 -0
- package/src/ui/screens/CliToolsScreen.tsx +154 -280
- package/src/ui/screens/PluginsScreen.tsx +156 -140
- package/src/ui/state/types.ts +0 -7
- package/src/__tests__/dual-write-prevention.test.ts +0 -380
- package/src/__tests__/gap-fill-versions.test.ts +0 -389
- package/src/__tests__/plugin-version-check.test.ts +0 -762
- package/src/cli/claude.ts +0 -21
- package/src/prerunner/index.ts +0 -500
- package/src/services/plugin-version-check.ts +0 -360
- package/src/services/update-cache.ts +0 -78
- package/src/ui/components/modals/VersionMismatchModal.tsx +0 -138
- package/src/ui/hooks/useMismatchModal.ts +0 -89
package/src/cli/update.ts
CHANGED
|
@@ -34,11 +34,14 @@
|
|
|
34
34
|
* `cli/update-view.ts` owns every string. Nothing in this file formats.
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
|
+
import { selectProfileId } from "../services/active-closure.js";
|
|
37
38
|
import { isClaudeAvailable } from "../services/claude-cli.js";
|
|
38
39
|
import { clearContentDriftCache } from "../services/content-drift.js";
|
|
39
|
-
import { checkBinaries } from "../services/doctor-bins.js";
|
|
40
40
|
import { validateManifest } from "../services/manifest.js";
|
|
41
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
refreshRegisteredMarketplaces,
|
|
43
|
+
reportRegisteredMarketplaces,
|
|
44
|
+
} from "../services/marketplace-refresh.js";
|
|
42
45
|
import { registerClosureMarketplaces } from "../services/marketplace-sync.js";
|
|
43
46
|
import {
|
|
44
47
|
type PluginInfo,
|
|
@@ -48,6 +51,7 @@ import {
|
|
|
48
51
|
import { resolveProfile } from "../services/resolver.js";
|
|
49
52
|
import { getInstalledSkillNames } from "../services/skills-manager.js";
|
|
50
53
|
import { activeProfile } from "../services/symlink-manager.js";
|
|
54
|
+
import { REAL_TOOL_PROBE_IO, probeTools } from "../services/tool-probe.js";
|
|
51
55
|
// The apply half lives in services/ so the TUI runs the SAME code — see the
|
|
52
56
|
// header of update-engine.ts for what drifted while it did not.
|
|
53
57
|
import {
|
|
@@ -77,11 +81,15 @@ import {
|
|
|
77
81
|
header,
|
|
78
82
|
info,
|
|
79
83
|
note as noteLine,
|
|
84
|
+
nothingToApplyNotice,
|
|
80
85
|
ok as okLine,
|
|
81
86
|
planReport,
|
|
82
87
|
stepFrame,
|
|
83
88
|
stepRecord,
|
|
84
89
|
summaryLine,
|
|
90
|
+
uncheckedGateNotice,
|
|
91
|
+
unknownPluginsNotice,
|
|
92
|
+
unknownToolsNotice,
|
|
85
93
|
warn as warnLine,
|
|
86
94
|
} from "./update-view.js";
|
|
87
95
|
|
|
@@ -195,10 +203,13 @@ export async function runUpdateCommand(
|
|
|
195
203
|
|
|
196
204
|
// Active profile, not the union — "update my dependencies" means the set in
|
|
197
205
|
// use. A single-profile manifest needs no activation to be unambiguous.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
206
|
+
// Chosen through the same function the TUI uses, so both judge a plugin
|
|
207
|
+
// against the same pin.
|
|
208
|
+
const targetId = selectProfileId(
|
|
209
|
+
profileIds,
|
|
210
|
+
flags.profile,
|
|
211
|
+
await activeProfile(projectPath),
|
|
212
|
+
);
|
|
202
213
|
if (!targetId) {
|
|
203
214
|
console.error(
|
|
204
215
|
failLine(
|
|
@@ -246,7 +257,22 @@ export async function runUpdateCommand(
|
|
|
246
257
|
// clones first, then drop every cached answer derived from the old HEAD —
|
|
247
258
|
// skipping this is how a clone sits days behind while every plugin reads as
|
|
248
259
|
// up to date.
|
|
260
|
+
// ...but NOT under --dry-run or --check. Both promise to write nothing, and
|
|
261
|
+
// `git pull --ff-only` on a clone under ~/.claude/plugins/marketplaces/ is a
|
|
262
|
+
// write — it advances the user's marketplace to a new commit. The help text
|
|
263
|
+
// said "Writes nothing, exits 0" while the run printed "1 refreshed" in its
|
|
264
|
+
// own output, which is where this was caught. A read-only run plans against
|
|
265
|
+
// the clone as it stands; the notice below says so, so an unrefreshed
|
|
266
|
+
// catalog is never mistaken for a current one.
|
|
249
267
|
const refresh = await runStep(steps.marketplaces, async (say) => {
|
|
268
|
+
if (flags.dryRun || flags.check) {
|
|
269
|
+
say("read-only — selecting, not pulling");
|
|
270
|
+
// Report-only, NOT a skip: the autoUpdate-disabled verdict is a pure
|
|
271
|
+
// read and is the one diagnostic a read-only run must not lose.
|
|
272
|
+
const report = await reportRegisteredMarketplaces();
|
|
273
|
+
steps.marketplaces.detail = `not refreshed (read-only) · ${report.skipped.length} eligible`;
|
|
274
|
+
return report;
|
|
275
|
+
}
|
|
250
276
|
let settled = 0;
|
|
251
277
|
return refreshRegisteredMarketplaces([], {
|
|
252
278
|
onStart: (names) => {
|
|
@@ -311,26 +337,39 @@ export async function runUpdateCommand(
|
|
|
311
337
|
return map;
|
|
312
338
|
});
|
|
313
339
|
|
|
314
|
-
|
|
340
|
+
// The tool probe: installed version, owner, and the owning installer's own
|
|
341
|
+
// "behind?" answer — the same probe the TUI's CLI Tools screen uses. It used
|
|
342
|
+
// to be a presence check, which is why every present unpinned tool planned
|
|
343
|
+
// as an upgrade on every run.
|
|
344
|
+
const toolFacts = await runStep(steps.binaries, async (say) => {
|
|
315
345
|
if (closure.bins.length === 0) {
|
|
316
346
|
steps.binaries.detail = "none declared";
|
|
317
347
|
return [];
|
|
318
348
|
}
|
|
319
349
|
steps.binaries.progress = { done: 0, total: closure.bins.length };
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
(
|
|
324
|
-
steps.binaries.progress = { done:
|
|
325
|
-
say(name);
|
|
350
|
+
const facts = await probeTools(closure.bins, REAL_TOOL_PROBE_IO, {
|
|
351
|
+
// `settled`, not `index`: results arrive in completion order, and a
|
|
352
|
+
// meter driven by index would jump backwards.
|
|
353
|
+
onResult: (f, { settled, total }) => {
|
|
354
|
+
steps.binaries.progress = { done: settled, total };
|
|
355
|
+
say(f.name);
|
|
326
356
|
},
|
|
327
|
-
);
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
357
|
+
});
|
|
358
|
+
const items = planBinUpdates(closure.bins, facts);
|
|
359
|
+
const n = (a: string) => items.filter((i) => i.action === a).length;
|
|
360
|
+
const behind = n("upgrade");
|
|
361
|
+
const unknown = n("unknown");
|
|
362
|
+
const missing = n("install");
|
|
363
|
+
steps.binaries.detail = [
|
|
364
|
+
`${items.length} checked`,
|
|
365
|
+
behind > 0 ? `${behind} behind` : "",
|
|
366
|
+
unknown > 0 ? `${unknown} unknown` : "",
|
|
367
|
+
missing > 0 ? `${missing} missing` : "",
|
|
368
|
+
]
|
|
369
|
+
.filter(Boolean)
|
|
370
|
+
.join(" · ");
|
|
371
|
+
if (missing + unknown > 0) steps.binaries.state = "warn";
|
|
372
|
+
return facts;
|
|
334
373
|
});
|
|
335
374
|
|
|
336
375
|
const installedSkills = await runStep(steps.skills, async (say) => {
|
|
@@ -353,7 +392,7 @@ export async function runUpdateCommand(
|
|
|
353
392
|
const plan: UpdatePlan = {
|
|
354
393
|
profileId: targetId,
|
|
355
394
|
plugins: planPluginUpdates(closure.plugins, catalog),
|
|
356
|
-
bins: planBinUpdates(closure.bins,
|
|
395
|
+
bins: planBinUpdates(closure.bins, toolFacts),
|
|
357
396
|
skills: planSkillUpdates(closure.skills, installedSkills),
|
|
358
397
|
binSpecs: new Map(closure.bins.map((b) => [b.name, b])),
|
|
359
398
|
};
|
|
@@ -361,23 +400,34 @@ export async function runUpdateCommand(
|
|
|
361
400
|
for (const line of planReport(plan)) console.log(line);
|
|
362
401
|
|
|
363
402
|
const counts = summarizePlan(plan);
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
403
|
+
const unknownToolNames = plan.bins
|
|
404
|
+
.filter((b) => b.action === "unknown")
|
|
405
|
+
.map((b) => b.name);
|
|
406
|
+
const toolUnknownLine = () =>
|
|
407
|
+
unknownToolsNotice(counts.unknownTools, unknownToolNames);
|
|
408
|
+
if (counts.unknownPlugins > 0 || counts.unknownTools > 0) console.log("");
|
|
409
|
+
if (counts.unknownPlugins > 0) {
|
|
410
|
+
console.log(unknownPluginsNotice(counts.unknownPlugins));
|
|
371
411
|
}
|
|
412
|
+
if (counts.unknownTools > 0) console.log(toolUnknownLine());
|
|
372
413
|
|
|
373
414
|
// `--check` gates on planIsBehind, not planHasWork. An already-installed
|
|
374
415
|
// skill is re-fetched on every apply because its manifest ref carries no
|
|
375
|
-
// content hash,
|
|
376
|
-
//
|
|
377
|
-
//
|
|
416
|
+
// content hash, which is no evidence that anything is out of date — counting
|
|
417
|
+
// it made the gate exit 1 forever for any profile declaring a single skill.
|
|
418
|
+
//
|
|
419
|
+
// An `unknown` does not fail the gate, for plugins and tools alike (see
|
|
420
|
+
// planIsBehind). The same argument applies to both: a plugin whose
|
|
421
|
+
// marketplace publishes no version, or a tool another npm owns, is unknown
|
|
422
|
+
// permanently, so failing on it is the skill case again — this profile's 5
|
|
423
|
+
// unversioned official plugins made `--check` exit 1 on every run. A tool
|
|
424
|
+
// or plugin something says is BEHIND still does fail it. Because a failed
|
|
425
|
+
// check now passes silently by exit code, the unchecked count is printed
|
|
426
|
+
// beside the verdict; the per-kind notices and each row's reason are above.
|
|
378
427
|
if (flags.check) {
|
|
379
428
|
console.log("");
|
|
380
429
|
if (!planIsBehind(plan)) {
|
|
430
|
+
if (counts.unknown > 0) console.log(uncheckedGateNotice(counts.unknown));
|
|
381
431
|
console.log(okLine("Nothing missing and nothing behind."));
|
|
382
432
|
return 0;
|
|
383
433
|
}
|
|
@@ -406,12 +456,11 @@ export async function runUpdateCommand(
|
|
|
406
456
|
// fine". But nothing is APPLIED for an unknown — the apply loop skips it —
|
|
407
457
|
// so reporting on `planHasWork` alone claimed a run had happened when it
|
|
408
458
|
// had not. Say what is actually true instead.
|
|
409
|
-
if (counts.
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
);
|
|
459
|
+
if (counts.unknownPlugins + counts.unknownTools > 0) {
|
|
460
|
+
if (counts.unknownPlugins > 0) {
|
|
461
|
+
console.log(nothingToApplyNotice(counts.unknownPlugins));
|
|
462
|
+
}
|
|
463
|
+
if (counts.unknownTools > 0) console.log(toolUnknownLine());
|
|
415
464
|
return 0;
|
|
416
465
|
}
|
|
417
466
|
console.log(okLine("Everything is up to date."));
|
|
@@ -466,6 +515,8 @@ export async function runUpdateCommand(
|
|
|
466
515
|
};
|
|
467
516
|
|
|
468
517
|
const applyLive = new LiveRegion();
|
|
518
|
+
/** The last detail each item finished with — the reason, for an unchanged one. */
|
|
519
|
+
const finishedDetail = new Map<string, string>();
|
|
469
520
|
const reporter: ApplyReporter = {
|
|
470
521
|
begin: (name) => {
|
|
471
522
|
state.current = name;
|
|
@@ -473,12 +524,17 @@ export async function runUpdateCommand(
|
|
|
473
524
|
},
|
|
474
525
|
finish: (succeeded, name, detail, ms) => {
|
|
475
526
|
state.done++;
|
|
527
|
+
finishedDetail.set(name, detail);
|
|
476
528
|
applyLive.note(
|
|
477
529
|
applyRow(
|
|
478
530
|
succeeded,
|
|
479
531
|
name,
|
|
480
532
|
nameWidth,
|
|
481
|
-
succeeded
|
|
533
|
+
!succeeded
|
|
534
|
+
? fg(brand.danger, detail)
|
|
535
|
+
: detail.startsWith("no change — ")
|
|
536
|
+
? fg(brand.warning, detail)
|
|
537
|
+
: dim(detail),
|
|
482
538
|
ms,
|
|
483
539
|
),
|
|
484
540
|
);
|
|
@@ -501,9 +557,29 @@ export async function runUpdateCommand(
|
|
|
501
557
|
counts.unknown,
|
|
502
558
|
failed.length,
|
|
503
559
|
Date.now() - startedAt,
|
|
560
|
+
outcome.unchanged.length,
|
|
504
561
|
))
|
|
505
562
|
console.log(line);
|
|
506
563
|
|
|
564
|
+
// Exited 0, read back unmoved. Reported, never fatal — the same precedent
|
|
565
|
+
// as the pin mismatches below: this is a standing property of the machine
|
|
566
|
+
// (a stale tap, a shadowing PATH entry), not something this run did wrong,
|
|
567
|
+
// and it will be replanned. What it must never be is counted as "changed".
|
|
568
|
+
if (outcome.unchanged.length > 0) {
|
|
569
|
+
console.log(
|
|
570
|
+
warnLine(
|
|
571
|
+
`${bold(String(outcome.unchanged.length))} item(s) ran but did not change:`,
|
|
572
|
+
),
|
|
573
|
+
);
|
|
574
|
+
for (const name of outcome.unchanged) {
|
|
575
|
+
const detail = (finishedDetail.get(name) ?? "").replace(
|
|
576
|
+
/^no change — /,
|
|
577
|
+
"",
|
|
578
|
+
);
|
|
579
|
+
console.log(noteLine(`${name}: ${detail}`));
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
507
583
|
// An exact pin the installer could not deliver. Reported, never fatal: only
|
|
508
584
|
// the newest published version is installable, so this is a standing property
|
|
509
585
|
// of the manifest rather than something this run did wrong, and failing here
|
|
@@ -40,19 +40,18 @@ export const DEFAULT_AGENT_GRADES: Record<string, Grade> = {
|
|
|
40
40
|
// what is true; both were on `smart` because "writes an important document" got confused
|
|
41
41
|
// with "makes an expensive judgement".
|
|
42
42
|
"dev:spec-writer": "normal",
|
|
43
|
-
"dev:
|
|
43
|
+
"dev:aggregator": "normal",
|
|
44
44
|
|
|
45
45
|
// Reading a lot is not analysing.
|
|
46
46
|
//
|
|
47
|
-
// `
|
|
47
|
+
// `analyze` is read-only: it locates implementations, traces a path and reports
|
|
48
48
|
// file:line. That is high-volume retrieval with light inference — the same shape as
|
|
49
49
|
// `Explore`, with more structure in the output. It sat on `smart` because "investigates"
|
|
50
50
|
// sounds like thinking; what it actually does is look.
|
|
51
|
-
"code-
|
|
51
|
+
"code-search:analyze": "cheap",
|
|
52
52
|
|
|
53
|
-
// Search
|
|
53
|
+
// Search and bookkeeping: bounded, mechanical, high-volume.
|
|
54
54
|
Explore: "cheap",
|
|
55
|
-
"dev:scribe": "cheap",
|
|
56
55
|
"dev:stack-detector": "cheap",
|
|
57
56
|
"dev:docs": "cheap",
|
|
58
57
|
"terminal:tui-navigator": "cheap",
|
|
@@ -69,7 +68,7 @@ export const DEFAULT_AGENT_GRADES: Record<string, Grade> = {
|
|
|
69
68
|
* answers to "what will this preset cost me", and the lump sum shows neither.
|
|
70
69
|
*
|
|
71
70
|
* Read out of `plugins/dev/commands/{dev,debug,investigate}.md` — agents only. Those files
|
|
72
|
-
* also name skills (`dev:context-detection`, `code-
|
|
71
|
+
* also name skills (`dev:context-detection`, `code-search:investigate`) which are not
|
|
73
72
|
* dispatched through the Agent tool and so are not routed at all.
|
|
74
73
|
*/
|
|
75
74
|
export const WORKFLOWS: { name: string; agents: string[] }[] = [
|
|
@@ -79,7 +78,7 @@ export const WORKFLOWS: { name: string; agents: string[] }[] = [
|
|
|
79
78
|
"dev:stack-detector",
|
|
80
79
|
"dev:architect",
|
|
81
80
|
"dev:developer",
|
|
82
|
-
"dev:
|
|
81
|
+
"dev:qa-engineer",
|
|
83
82
|
"dev:reviewer",
|
|
84
83
|
],
|
|
85
84
|
},
|
|
@@ -87,14 +86,14 @@ export const WORKFLOWS: { name: string; agents: string[] }[] = [
|
|
|
87
86
|
name: "debug",
|
|
88
87
|
agents: [
|
|
89
88
|
"dev:stack-detector",
|
|
90
|
-
"code-
|
|
89
|
+
"code-search:analyze",
|
|
91
90
|
"dev:debugger",
|
|
92
91
|
"dev:developer",
|
|
93
92
|
],
|
|
94
93
|
},
|
|
95
94
|
{
|
|
96
95
|
name: "investigate",
|
|
97
|
-
agents: ["code-
|
|
96
|
+
agents: ["code-search:analyze", "dev:researcher"],
|
|
98
97
|
},
|
|
99
98
|
];
|
|
100
99
|
|
|
@@ -102,7 +101,7 @@ export const WORKFLOWS: { name: string; agents: string[] }[] = [
|
|
|
102
101
|
* Agents whose model is a CAPABILITY choice, not a spend choice.
|
|
103
102
|
*
|
|
104
103
|
* The three tiers answer "how much model", and that is the wrong question for an agent that
|
|
105
|
-
* reads images. `designer:
|
|
104
|
+
* reads images. `designer:review` compares a rendered screen against a reference and
|
|
106
105
|
* `designer:ui` audits a screenshot for usability: what they need is the model that SEES
|
|
107
106
|
* best, which is not a point on a cheap-to-smart line and does not move when the preset
|
|
108
107
|
* changes.
|
|
@@ -112,7 +111,7 @@ export const WORKFLOWS: { name: string; agents: string[] }[] = [
|
|
|
112
111
|
* preset's `fallback`, which is a defensible default and an honest one; pinning them to a
|
|
113
112
|
* model id guessed here is neither.
|
|
114
113
|
*/
|
|
115
|
-
export const VISION_AGENTS = ["designer:
|
|
114
|
+
export const VISION_AGENTS = ["designer:review", "designer:ui"] as const;
|
|
116
115
|
|
|
117
116
|
function preset(
|
|
118
117
|
name: string,
|
|
@@ -52,7 +52,7 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
|
|
|
52
52
|
magusPlugins: [
|
|
53
53
|
"setup",
|
|
54
54
|
"multimodel",
|
|
55
|
-
"code-
|
|
55
|
+
"code-search",
|
|
56
56
|
"dev",
|
|
57
57
|
"browser-use",
|
|
58
58
|
"designer",
|
|
@@ -90,7 +90,7 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
|
|
|
90
90
|
icon: "★",
|
|
91
91
|
magusPlugins: [
|
|
92
92
|
"dev",
|
|
93
|
-
"code-
|
|
93
|
+
"code-search",
|
|
94
94
|
"terminal",
|
|
95
95
|
"setup",
|
|
96
96
|
"designer",
|
|
@@ -129,7 +129,7 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
|
|
|
129
129
|
name: "Backend Forge",
|
|
130
130
|
description: "API development, debugging, code quality, data workflows",
|
|
131
131
|
icon: "★",
|
|
132
|
-
magusPlugins: ["dev", "code-
|
|
132
|
+
magusPlugins: ["dev", "code-search", "terminal", "setup", "multimodel"],
|
|
133
133
|
anthropicPlugins: [
|
|
134
134
|
"feature-dev",
|
|
135
135
|
"code-review",
|
|
@@ -159,7 +159,7 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
|
|
|
159
159
|
icon: "★",
|
|
160
160
|
magusPlugins: [
|
|
161
161
|
"dev",
|
|
162
|
-
"code-
|
|
162
|
+
"code-search",
|
|
163
163
|
"terminal",
|
|
164
164
|
"setup",
|
|
165
165
|
"multimodel",
|
|
@@ -194,11 +194,11 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
|
|
|
194
194
|
icon: "★",
|
|
195
195
|
magusPlugins: [
|
|
196
196
|
"dev",
|
|
197
|
-
"code-
|
|
197
|
+
"code-search",
|
|
198
198
|
"terminal",
|
|
199
199
|
"setup",
|
|
200
200
|
"browser-use",
|
|
201
|
-
"image
|
|
201
|
+
"image@magus-marketing",
|
|
202
202
|
"video-editing@magus-marketing",
|
|
203
203
|
],
|
|
204
204
|
anthropicPlugins: [
|
|
@@ -226,7 +226,7 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
|
|
|
226
226
|
description:
|
|
227
227
|
"Planning, code review, coordination, and broad repo visibility",
|
|
228
228
|
icon: "★",
|
|
229
|
-
magusPlugins: ["dev", "code-
|
|
229
|
+
magusPlugins: ["dev", "code-search", "terminal", "setup", "multimodel"],
|
|
230
230
|
anthropicPlugins: [
|
|
231
231
|
"code-review",
|
|
232
232
|
"pr-review-toolkit",
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which profile is in effect for a project, and what it resolves to.
|
|
3
|
+
*
|
|
4
|
+
* `claudeup update` and the TUI must judge a plugin against the SAME pin, or
|
|
5
|
+
* the same plugin in the same project reads differently in each. They used to
|
|
6
|
+
* pick the profile in different ways — the CLI inline, the TUI not at all (it
|
|
7
|
+
* judged everything against "latest") — so an exact pin could never agree.
|
|
8
|
+
* Both now choose through {@link selectProfileId} over the same manifest and
|
|
9
|
+
* the same active-profile symlink.
|
|
10
|
+
*
|
|
11
|
+
* Read-only. Unlike the CLI's `ensureManifest`, nothing here bootstraps a
|
|
12
|
+
* manifest: a screen opening must never author a file a team commits.
|
|
13
|
+
*
|
|
14
|
+
* NOTHING HERE PRINTS — it runs under the CLI's live region and inside the TUI.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
BinRequirement,
|
|
19
|
+
ProfileManifest,
|
|
20
|
+
ResolvedClosure,
|
|
21
|
+
} from "../types/index.js";
|
|
22
|
+
import { readManifest, validateManifest } from "./manifest.js";
|
|
23
|
+
import { resolveProfile } from "./resolver.js";
|
|
24
|
+
import { activeProfile } from "./symlink-manager.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The profile to use: the explicit one, else the active one, else the only one.
|
|
28
|
+
*
|
|
29
|
+
* Each candidate counts only if the manifest defines it. A stale active-profile
|
|
30
|
+
* symlink naming a profile that was since removed would otherwise be chosen and
|
|
31
|
+
* then fail to resolve, even when the manifest has exactly one profile and the
|
|
32
|
+
* answer is unambiguous.
|
|
33
|
+
*/
|
|
34
|
+
export function selectProfileId(
|
|
35
|
+
profileIds: readonly string[],
|
|
36
|
+
explicit: string | undefined,
|
|
37
|
+
active: string | null,
|
|
38
|
+
): string | null {
|
|
39
|
+
if (explicit !== undefined && profileIds.includes(explicit)) return explicit;
|
|
40
|
+
if (active !== null && profileIds.includes(active)) return active;
|
|
41
|
+
if (profileIds.length === 1) return profileIds[0] ?? null;
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ActiveClosure {
|
|
46
|
+
profileId: string;
|
|
47
|
+
closure: ResolvedClosure;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ActiveClosureDeps {
|
|
51
|
+
readManifest(projectPath: string): Promise<ProfileManifest>;
|
|
52
|
+
activeProfile(projectPath: string): Promise<string | null>;
|
|
53
|
+
resolveProfile(
|
|
54
|
+
manifest: ProfileManifest,
|
|
55
|
+
id: string,
|
|
56
|
+
opts?: {
|
|
57
|
+
binResolver?: (pluginId: string) => Promise<BinRequirement[]>;
|
|
58
|
+
},
|
|
59
|
+
): Promise<ResolvedClosure>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const REAL_DEPS: ActiveClosureDeps = {
|
|
63
|
+
readManifest,
|
|
64
|
+
activeProfile,
|
|
65
|
+
resolveProfile,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export interface LoadActiveClosureOptions {
|
|
69
|
+
/**
|
|
70
|
+
* false → `resolveProfile` gets a resolver that returns no requirements, so
|
|
71
|
+
* no plugin's `plugin.json` is read and `closure.bins` holds only the
|
|
72
|
+
* profile's own `cliTools` entries. Default true.
|
|
73
|
+
*
|
|
74
|
+
* The Plugins screen needs only `closure.plugins`. The default resolver
|
|
75
|
+
* locates and reads every declared plugin's `plugin.json`, which is work the
|
|
76
|
+
* screen would pay on every fetch for a field it never reads.
|
|
77
|
+
*/
|
|
78
|
+
resolveBins?: boolean;
|
|
79
|
+
deps?: Partial<ActiveClosureDeps>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const NO_BINS = async (): Promise<BinRequirement[]> => [];
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The active profile's closure for a project.
|
|
86
|
+
*
|
|
87
|
+
* Null when there is no manifest, an invalid one, or no profile that can be
|
|
88
|
+
* selected without an explicit choice. Never throws: a caller that gets null
|
|
89
|
+
* judges against "latest" and says so, which is better than a screen that fails
|
|
90
|
+
* to load over a profile it only needed for pins.
|
|
91
|
+
*/
|
|
92
|
+
export async function loadActiveClosure(
|
|
93
|
+
projectPath: string,
|
|
94
|
+
opts: LoadActiveClosureOptions = {},
|
|
95
|
+
): Promise<ActiveClosure | null> {
|
|
96
|
+
const deps: ActiveClosureDeps = { ...REAL_DEPS, ...opts.deps };
|
|
97
|
+
try {
|
|
98
|
+
const manifest = await deps.readManifest(projectPath);
|
|
99
|
+
if (validateManifest(manifest).length > 0) return null;
|
|
100
|
+
|
|
101
|
+
const profileId = selectProfileId(
|
|
102
|
+
Object.keys(manifest.profiles),
|
|
103
|
+
undefined,
|
|
104
|
+
await deps.activeProfile(projectPath),
|
|
105
|
+
);
|
|
106
|
+
if (profileId === null) return null;
|
|
107
|
+
|
|
108
|
+
const closure = await deps.resolveProfile(
|
|
109
|
+
manifest,
|
|
110
|
+
profileId,
|
|
111
|
+
opts.resolveBins === false ? { binResolver: NO_BINS } : {},
|
|
112
|
+
);
|
|
113
|
+
return { profileId, closure };
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -35,8 +35,12 @@ import type { MarketplacePlugin } from "./marketplace-catalog.js";
|
|
|
35
35
|
/**
|
|
36
36
|
* How long a cached catalog is served before we ask again.
|
|
37
37
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
38
|
+
* This is the ONLY refresh rhythm claudeup has, and it is why the TUI and
|
|
39
|
+
* `claudeup update` can disagree about the same plugin for up to an hour:
|
|
40
|
+
* `update` fast-forwards the clones and clears this cache before planning,
|
|
41
|
+
* while the TUI serves whatever is here. That is not a bug in either path, but
|
|
42
|
+
* anything that reports "no update" off a cached catalog has to say the answer
|
|
43
|
+
* is up to an hour old.
|
|
40
44
|
*/
|
|
41
45
|
export const CATALOG_TTL_MS = 60 * 60 * 1000;
|
|
42
46
|
|