magus-cli 7.12.2 → 7.14.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.
- package/README.md +21 -0
- package/package.json +4 -4
- package/src/__tests__/market-bases.test.ts +112 -0
- package/src/__tests__/market-config.test.ts +84 -0
- package/src/__tests__/market-conflicts.test.ts +175 -0
- package/src/__tests__/market-consent.test.ts +185 -0
- package/src/__tests__/market-content.test.ts +183 -0
- package/src/__tests__/market-delivery.test.ts +308 -0
- package/src/__tests__/market-fixture.ts +1167 -0
- package/src/__tests__/market-git.test.ts +130 -0
- package/src/__tests__/market-identity.test.ts +127 -0
- package/src/__tests__/market-install.test.ts +143 -0
- package/src/__tests__/market-layout.test.ts +206 -0
- package/src/__tests__/market-publish.test.ts +263 -0
- package/src/__tests__/market-r2-adapters.test.ts +190 -0
- package/src/__tests__/market-r2-company-cli.test.ts +350 -0
- package/src/__tests__/market-r2-company-unit.test.ts +224 -0
- package/src/__tests__/market-r2-local-items.test.ts +197 -0
- package/src/__tests__/market-r2-plugin-add.test.ts +470 -0
- package/src/__tests__/market-r2-plugin-publish.test.ts +568 -0
- package/src/__tests__/market-r2-routing.test.ts +127 -0
- package/src/__tests__/market-r2-skill-create-plugin.test.ts +315 -0
- package/src/__tests__/market-r2b-display-path.test.ts +180 -0
- package/src/__tests__/market-r2b-fit-row.test.ts +287 -0
- package/src/__tests__/market-r2b-local-items.test.ts +253 -0
- package/src/__tests__/market-r2b-plugin-add.test.ts +130 -0
- package/src/__tests__/market-readonly.test.ts +162 -0
- package/src/__tests__/market-repositories.test.ts +263 -0
- package/src/__tests__/market-results.test.ts +198 -0
- package/src/__tests__/market-router.test.ts +65 -0
- package/src/__tests__/market-safety.test.ts +357 -0
- package/src/__tests__/market-teammates.test.ts +304 -0
- package/src/cli/install.ts +64 -5
- package/src/cli/market-view.ts +329 -0
- package/src/cli/market.ts +664 -0
- package/src/cli/router.ts +69 -3
- package/src/cli/update-view.ts +56 -0
- package/src/cli/update.ts +111 -9
- package/src/data/marketplaces.ts +8 -2
- package/src/main.tsx +20 -2
- package/src/market-tui.tsx +21 -0
- package/src/services/active-closure.ts +71 -9
- package/src/services/claude-cli.ts +140 -10
- package/src/services/claude-settings.ts +19 -25
- package/src/services/doctor.ts +5 -5
- package/src/services/git-write.ts +615 -0
- package/src/services/github-cli.ts +309 -0
- package/src/services/local-marketplace.ts +112 -50
- package/src/services/market-bases.ts +191 -0
- package/src/services/market-change.ts +242 -0
- package/src/services/market-clone.ts +572 -0
- package/src/services/market-fs.ts +138 -0
- package/src/services/market-install.ts +172 -0
- package/src/services/market-layout.ts +311 -0
- package/src/services/market-local.ts +93 -0
- package/src/services/market-marker.ts +49 -0
- package/src/services/market-ops-content.ts +277 -0
- package/src/services/market-ops-plugin.ts +655 -0
- package/src/services/market-ops-publish.ts +1227 -0
- package/src/services/market-ops-repo.ts +916 -0
- package/src/services/market-ops-skill.ts +788 -0
- package/src/services/market-ops.ts +686 -0
- package/src/services/market-publish.ts +468 -0
- package/src/services/market-registry.ts +382 -0
- package/src/services/market-types.ts +268 -0
- package/src/services/marketplace-conflicts.ts +127 -0
- package/src/services/marketplace-refresh.ts +33 -3
- package/src/services/marketplace-sync.ts +253 -22
- package/src/services/plugin-dependencies.ts +17 -0
- package/src/services/plugin-manager.ts +1 -1
- package/src/services/plugin-mcp-config.ts +6 -9
- package/src/services/plugin-requires.ts +20 -8
- package/src/services/profile-absorb.ts +88 -3
- package/src/services/profile-adopt.ts +10 -1
- package/src/services/profiles.ts +64 -1
- package/src/services/resolver.ts +17 -5
- package/src/services/settings-manager.ts +2 -2
- package/src/services/skills-manager.ts +2 -2
- package/src/services/update-plan.ts +67 -8
- package/src/tui-runtime.tsx +142 -0
- package/src/tui.tsx +7 -124
- package/src/types/index.ts +7 -0
- package/src/ui/adapters/marketAdapter.ts +366 -0
- package/src/ui/adapters/pluginsAdapter.ts +45 -3
- package/src/ui/components/layout/ScreenLayout.tsx +43 -17
- package/src/ui/components/modals/ConfirmModal.tsx +145 -8
- package/src/ui/components/modals/HelpOverlay.tsx +10 -3
- package/src/ui/components/modals/ModalContainer.tsx +47 -5
- package/src/ui/components/primitives/ListCategoryRow.tsx +22 -2
- package/src/ui/components/primitives/ScopeBadge.tsx +28 -0
- package/src/ui/components/primitives/ScopeVersionLine.tsx +27 -0
- package/src/ui/components/primitives/SelectableRow.tsx +14 -1
- package/src/ui/components/primitives/ToneLine.tsx +25 -0
- package/src/ui/components/primitives/index.ts +5 -0
- package/src/ui/keymap.ts +21 -5
- package/src/ui/market/MarketApp.tsx +59 -0
- package/src/ui/market/MarketplaceScreen.tsx +280 -0
- package/src/ui/market/MarketplacesDetail.snowflake.tsx +161 -0
- package/src/ui/market/MarketplacesScreen.tsx +264 -0
- package/src/ui/market/PluginDetail.snowflake.tsx +61 -0
- package/src/ui/market/SkillDetail.snowflake.tsx +71 -0
- package/src/ui/market/marketKeymap.ts +243 -0
- package/src/ui/market/useMarketState.ts +694 -0
- package/src/ui/renderers/pluginRenderers.tsx +31 -3
- package/src/ui/renderers/pluginStatusText.ts +6 -0
- package/src/ui/screens/PluginsScreen.tsx +49 -5
- package/src/ui/screens/SkillsScreen.tsx +2 -2
- package/src/ui/state/AppContext.tsx +14 -2
- package/src/ui/state/types.ts +9 -2
- package/src/utils/config-dir.ts +29 -9
- package/src/utils/display-path.ts +33 -0
|
@@ -53,9 +53,11 @@ import type {
|
|
|
53
53
|
McpServerConfig,
|
|
54
54
|
ProfileManifest,
|
|
55
55
|
ProfileManifestEntry,
|
|
56
|
+
ProfileMarketplaceRef,
|
|
56
57
|
ProfileSkillRef,
|
|
57
58
|
} from "../types/index.js";
|
|
58
59
|
import { readManifest, writeManifest } from "./manifest.js";
|
|
60
|
+
import { registryLookup } from "./market-registry.js";
|
|
59
61
|
import { validateModelsConfig } from "./models-core.js";
|
|
60
62
|
import {
|
|
61
63
|
type ActivateOptions,
|
|
@@ -92,7 +94,7 @@ import {
|
|
|
92
94
|
skillFolder,
|
|
93
95
|
updateSkillIgnoreLines,
|
|
94
96
|
} from "./profile-skills.js";
|
|
95
|
-
import { resolveExtends, resolveProfile } from "./resolver.js";
|
|
97
|
+
import { EMPTY_LOOKUP, resolveExtends, resolveProfile } from "./resolver.js";
|
|
96
98
|
import { SettingsParseError } from "./settings-file.js";
|
|
97
99
|
import { trackedSkillFolders } from "./tracked-live-files.js";
|
|
98
100
|
|
|
@@ -105,7 +107,8 @@ export type AbsorbChange =
|
|
|
105
107
|
| { kind: "settings-changed" }
|
|
106
108
|
| { kind: "mcp-changed" }
|
|
107
109
|
| { kind: "models-changed" }
|
|
108
|
-
| { kind: "skill-released"; skill: string; folder: string }
|
|
110
|
+
| { kind: "skill-released"; skill: string; folder: string }
|
|
111
|
+
| { kind: "marketplace-added"; name: string; repo: string };
|
|
109
112
|
|
|
110
113
|
/** One line per change, naming the profile it went into. */
|
|
111
114
|
export function describeChange(change: AbsorbChange, profile: string): string {
|
|
@@ -124,9 +127,47 @@ export function describeChange(change: AbsorbChange, profile: string): string {
|
|
|
124
127
|
return `Updated model routing of profile "${profile}"`;
|
|
125
128
|
case "skill-released":
|
|
126
129
|
return `Skill "${change.skill}" was edited — it is now a custom skill in .claude/skills/${change.folder}; commit it`;
|
|
130
|
+
case "marketplace-added":
|
|
131
|
+
return `Profile "${profile}" records where ${change.name} comes from (${change.repo})`;
|
|
127
132
|
}
|
|
128
133
|
}
|
|
129
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Record in `next.marketplaces` where each enabled plugin's marketplace comes
|
|
137
|
+
* from, for every marketplace the entry does not name yet and `lookup` knows —
|
|
138
|
+
* this machine's registrations outside magus's defaults. Mutates `next`; each
|
|
139
|
+
* added name is pushed to `changes` when given. Without the record a teammate's
|
|
140
|
+
* `magus install` cannot register the marketplace, and the plugin fails to
|
|
141
|
+
* install there.
|
|
142
|
+
*/
|
|
143
|
+
function recordMarketplaces(
|
|
144
|
+
next: ProfileManifestEntry,
|
|
145
|
+
lookup: ReadonlyMap<string, ProfileMarketplaceRef>,
|
|
146
|
+
changes?: AbsorbChange[],
|
|
147
|
+
): void {
|
|
148
|
+
if (lookup.size === 0) return;
|
|
149
|
+
const declared = { ...(next.marketplaces ?? {}) };
|
|
150
|
+
const inherited = resolveExtends(next).marketplaces ?? {};
|
|
151
|
+
let added = false;
|
|
152
|
+
for (const [id, pin] of Object.entries(next.plugins ?? {})) {
|
|
153
|
+
if (!isOn(pin)) continue;
|
|
154
|
+
const at = id.lastIndexOf("@");
|
|
155
|
+
if (at <= 0) continue;
|
|
156
|
+
const name = id.slice(at + 1);
|
|
157
|
+
if (declared[name] || inherited[name]) continue;
|
|
158
|
+
const ref = lookup.get(name);
|
|
159
|
+
if (!ref) continue;
|
|
160
|
+
declared[name] = { ...ref };
|
|
161
|
+
added = true;
|
|
162
|
+
changes?.push({
|
|
163
|
+
kind: "marketplace-added",
|
|
164
|
+
name,
|
|
165
|
+
repo: ref.repo ?? ref.path ?? "",
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
if (added) next.marketplaces = declared;
|
|
169
|
+
}
|
|
170
|
+
|
|
130
171
|
// ─── The inverse of rendering ────────────────────────────────────────────────
|
|
131
172
|
|
|
132
173
|
/** The live config as absorption reads it. */
|
|
@@ -188,6 +229,7 @@ export function computeAbsorb(
|
|
|
188
229
|
live: LiveView,
|
|
189
230
|
otherPluginIds: ReadonlySet<string>,
|
|
190
231
|
derived: ReadonlySet<string> = new Set(),
|
|
232
|
+
lookup: ReadonlyMap<string, ProfileMarketplaceRef> = EMPTY_LOOKUP,
|
|
191
233
|
): { entry: ProfileManifestEntry; changes: AbsorbChange[] } {
|
|
192
234
|
const changes: AbsorbChange[] = [];
|
|
193
235
|
const next: ProfileManifestEntry = { ...entry };
|
|
@@ -240,6 +282,8 @@ export function computeAbsorb(
|
|
|
240
282
|
}
|
|
241
283
|
}
|
|
242
284
|
setOrDelete(next, "plugins", own);
|
|
285
|
+
// Where each plugin's marketplace comes from, when only this machine knows.
|
|
286
|
+
recordMarketplaces(next, lookup, changes);
|
|
243
287
|
|
|
244
288
|
// Settings: everything but enabledPlugins and machine-local keys, minus
|
|
245
289
|
// what `extends` supplies unchanged.
|
|
@@ -538,6 +582,20 @@ export function merge3(
|
|
|
538
582
|
),
|
|
539
583
|
),
|
|
540
584
|
);
|
|
585
|
+
// Where marketplaces come from, keyed by name: a name either side added is
|
|
586
|
+
// kept, and the same name with different refs on both sides is a conflict.
|
|
587
|
+
put(
|
|
588
|
+
"marketplaces",
|
|
589
|
+
nonEmpty(
|
|
590
|
+
merge3Entries(
|
|
591
|
+
base.marketplaces as Record<string, unknown> | undefined,
|
|
592
|
+
local.marketplaces as Record<string, unknown> | undefined,
|
|
593
|
+
remote.marketplaces as Record<string, unknown> | undefined,
|
|
594
|
+
"marketplaces",
|
|
595
|
+
conflicts,
|
|
596
|
+
),
|
|
597
|
+
),
|
|
598
|
+
);
|
|
541
599
|
// The routing config is validated as a unit, so it merges as one value.
|
|
542
600
|
let models = local.models;
|
|
543
601
|
if (!jsonEqual(local.models, remote.models)) {
|
|
@@ -746,6 +804,12 @@ function mergeAdditively(
|
|
|
746
804
|
};
|
|
747
805
|
}
|
|
748
806
|
if (everything.models) merged.models = everything.models;
|
|
807
|
+
// A marketplace `target` does not name is added; one it names keeps its ref.
|
|
808
|
+
const marketplaces = { ...(target.marketplaces ?? {}) };
|
|
809
|
+
for (const [name, ref] of Object.entries(everything.marketplaces ?? {})) {
|
|
810
|
+
if (!(name in marketplaces)) marketplaces[name] = ref;
|
|
811
|
+
}
|
|
812
|
+
if (Object.keys(marketplaces).length > 0) merged.marketplaces = marketplaces;
|
|
749
813
|
return merged;
|
|
750
814
|
}
|
|
751
815
|
|
|
@@ -804,6 +868,15 @@ function diffEntries(
|
|
|
804
868
|
if (!jsonEqual(before.models ?? null, after.models ?? null)) {
|
|
805
869
|
changes.push({ kind: "models-changed" });
|
|
806
870
|
}
|
|
871
|
+
const bm = before.marketplaces ?? {};
|
|
872
|
+
for (const [name, ref] of Object.entries(after.marketplaces ?? {})) {
|
|
873
|
+
if (jsonEqual(bm[name], ref)) continue;
|
|
874
|
+
changes.push({
|
|
875
|
+
kind: "marketplace-added",
|
|
876
|
+
name,
|
|
877
|
+
repo: ref.repo ?? ref.path ?? "",
|
|
878
|
+
});
|
|
879
|
+
}
|
|
807
880
|
return changes;
|
|
808
881
|
}
|
|
809
882
|
|
|
@@ -899,6 +972,7 @@ export async function absorbOrphanedProfile(
|
|
|
899
972
|
models: live.models,
|
|
900
973
|
};
|
|
901
974
|
|
|
975
|
+
const lookup = await registryLookup();
|
|
902
976
|
let merged: ProfileManifestEntry;
|
|
903
977
|
if (base) {
|
|
904
978
|
const local = computeAbsorb(
|
|
@@ -906,12 +980,19 @@ export async function absorbOrphanedProfile(
|
|
|
906
980
|
view,
|
|
907
981
|
union,
|
|
908
982
|
new Set(renderedBase?.dependencies?.derived ?? []),
|
|
983
|
+
lookup,
|
|
909
984
|
).entry;
|
|
910
985
|
const merge = merge3(base, local, targetEntry);
|
|
911
986
|
merged = merge.entry;
|
|
912
987
|
result.conflicts.push(...merge.conflicts);
|
|
913
988
|
} else {
|
|
914
|
-
const everything = computeAbsorb(
|
|
989
|
+
const everything = computeAbsorb(
|
|
990
|
+
{ name: orphan },
|
|
991
|
+
view,
|
|
992
|
+
union,
|
|
993
|
+
undefined,
|
|
994
|
+
lookup,
|
|
995
|
+
).entry;
|
|
915
996
|
merged = mergeAdditively(targetEntry, everything);
|
|
916
997
|
}
|
|
917
998
|
// A plugin the target pins keeps its pin when the edit merely enabled it.
|
|
@@ -1053,6 +1134,7 @@ export async function absorbOlderLayout(
|
|
|
1053
1134
|
},
|
|
1054
1135
|
await olderRenderIds(projectPath, named, rendered.allPluginIds),
|
|
1055
1136
|
new Set(rendered.dependencies?.derived ?? []),
|
|
1137
|
+
await registryLookup(),
|
|
1056
1138
|
);
|
|
1057
1139
|
const next = absorbed.entry;
|
|
1058
1140
|
const changes = diffEntries(entry, next);
|
|
@@ -1201,6 +1283,7 @@ export function absorbAdditively(
|
|
|
1201
1283
|
old: LiveView,
|
|
1202
1284
|
renderIds: ReadonlySet<string>,
|
|
1203
1285
|
derived: ReadonlySet<string> = new Set(),
|
|
1286
|
+
lookup: ReadonlyMap<string, ProfileMarketplaceRef> = EMPTY_LOOKUP,
|
|
1204
1287
|
): { entry: ProfileManifestEntry; conflicts: string[]; keptMore: boolean } {
|
|
1205
1288
|
const conflicts: string[] = [];
|
|
1206
1289
|
const flags = { keptMore: false };
|
|
@@ -1234,6 +1317,7 @@ export function absorbAdditively(
|
|
|
1234
1317
|
if (isOn(declared) && enabled[id] === undefined) flags.keptMore = true;
|
|
1235
1318
|
}
|
|
1236
1319
|
setOrDelete(next, "plugins", own);
|
|
1320
|
+
recordMarketplaces(next, lookup);
|
|
1237
1321
|
|
|
1238
1322
|
// Settings, recursively; `extends` keys compare against what they resolve to.
|
|
1239
1323
|
const { enabledPlugins: _drop, ...oldSettings } = withoutMachineKeys(
|
|
@@ -1463,6 +1547,7 @@ export async function reconcileActiveProfile(
|
|
|
1463
1547
|
marker.derived,
|
|
1464
1548
|
),
|
|
1465
1549
|
new Set(renderedBase.dependencies?.derived ?? []),
|
|
1550
|
+
await registryLookup(),
|
|
1466
1551
|
);
|
|
1467
1552
|
local = absorbed.entry;
|
|
1468
1553
|
result.changes.push(...absorbed.changes);
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
McpServerConfig,
|
|
18
18
|
ProfileManifest,
|
|
19
19
|
ProfileManifestEntry,
|
|
20
|
+
ProfileMarketplaceRef,
|
|
20
21
|
} from "../types/index.js";
|
|
21
22
|
import {
|
|
22
23
|
getEnabledPlugins,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
readSettings,
|
|
25
26
|
} from "./claude-settings.js";
|
|
26
27
|
import { emptyManifest } from "./manifest.js";
|
|
28
|
+
import { registryLookup } from "./market-registry.js";
|
|
27
29
|
import { withDerivedMarketplaces } from "./resolver.js";
|
|
28
30
|
|
|
29
31
|
/** The profile id used when adopting a project that has no manifest. */
|
|
@@ -62,6 +64,12 @@ export interface AdoptInput {
|
|
|
62
64
|
name?: string;
|
|
63
65
|
/** Injectable clock so the output is deterministic in tests. */
|
|
64
66
|
now?: string;
|
|
67
|
+
/**
|
|
68
|
+
* This machine's registered marketplaces outside magus's defaults
|
|
69
|
+
* ({@link registryLookup}), so a plugin from one of them records where its
|
|
70
|
+
* marketplace comes from and a teammate can register it.
|
|
71
|
+
*/
|
|
72
|
+
lookup?: ReadonlyMap<string, ProfileMarketplaceRef>;
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
/**
|
|
@@ -127,7 +135,7 @@ export function buildAdoptedProfile(input: AdoptInput): ProfileManifestEntry {
|
|
|
127
135
|
|
|
128
136
|
const entry: ProfileManifestEntry = {
|
|
129
137
|
name: input.name ?? "Default",
|
|
130
|
-
marketplaces: withDerivedMarketplaces(undefined, plugins),
|
|
138
|
+
marketplaces: withDerivedMarketplaces(undefined, plugins, input.lookup),
|
|
131
139
|
plugins,
|
|
132
140
|
createdAt: input.now ?? new Date().toISOString(),
|
|
133
141
|
};
|
|
@@ -205,6 +213,7 @@ export async function adoptCurrentProject(
|
|
|
205
213
|
settings,
|
|
206
214
|
mcpServers: mcp.mcpServers ?? {},
|
|
207
215
|
name,
|
|
216
|
+
lookup: await registryLookup(),
|
|
208
217
|
});
|
|
209
218
|
|
|
210
219
|
// Copy rather than mutate the caller's manifest: this function does not
|
package/src/services/profiles.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import fs from "fs-extra";
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
ProfileEntry,
|
|
6
|
+
ProfileManifest,
|
|
7
|
+
ProfileMarketplaceRef,
|
|
8
|
+
} from "../types/index.js";
|
|
5
9
|
import { readProfilesFile, writeJsonMinimal } from "./manifest.js";
|
|
10
|
+
import { registryLookup } from "./market-registry.js";
|
|
6
11
|
import { activeProfile } from "./profile-live.js";
|
|
7
12
|
|
|
8
13
|
const PROFILES_FILE = "profiles.json";
|
|
@@ -111,18 +116,27 @@ export async function listProfiles(
|
|
|
111
116
|
/**
|
|
112
117
|
* Save (create or overwrite) a profile.
|
|
113
118
|
* Returns the generated profile ID.
|
|
119
|
+
*
|
|
120
|
+
* `marketplaces` is written with the entry, and every plugin from a
|
|
121
|
+
* marketplace it does not name is looked up in this machine's registrations
|
|
122
|
+
* ({@link registryLookup}): a profile saved at PROJECT scope is committed, and
|
|
123
|
+
* a teammate's `magus install` can only register a marketplace the profile
|
|
124
|
+
* says where to find. magus's default marketplaces need no record.
|
|
114
125
|
*/
|
|
115
126
|
export async function saveProfile(
|
|
116
127
|
name: string,
|
|
117
128
|
plugins: Record<string, boolean>,
|
|
118
129
|
scope: "user" | "project",
|
|
119
130
|
projectPath?: string,
|
|
131
|
+
marketplaces?: Record<string, ProfileMarketplaceRef>,
|
|
120
132
|
): Promise<string> {
|
|
121
133
|
const data = await readProfiles(scope, projectPath);
|
|
122
134
|
const id = generateUniqueId(name, data.profiles);
|
|
123
135
|
const now = new Date().toISOString();
|
|
136
|
+
const recorded = await recordedMarketplaces(plugins, marketplaces);
|
|
124
137
|
data.profiles[id] = {
|
|
125
138
|
name,
|
|
139
|
+
...(Object.keys(recorded).length > 0 ? { marketplaces: recorded } : {}),
|
|
126
140
|
plugins,
|
|
127
141
|
createdAt: now,
|
|
128
142
|
updatedAt: now,
|
|
@@ -131,6 +145,27 @@ export async function saveProfile(
|
|
|
131
145
|
return id;
|
|
132
146
|
}
|
|
133
147
|
|
|
148
|
+
/**
|
|
149
|
+
* `given`, plus a ref from this machine's registrations for each plugin's
|
|
150
|
+
* marketplace that `given` does not name and magus does not know by default.
|
|
151
|
+
*/
|
|
152
|
+
async function recordedMarketplaces(
|
|
153
|
+
plugins: Record<string, unknown>,
|
|
154
|
+
given: Record<string, ProfileMarketplaceRef> | undefined,
|
|
155
|
+
): Promise<Record<string, ProfileMarketplaceRef>> {
|
|
156
|
+
const lookup = await registryLookup();
|
|
157
|
+
const out: Record<string, ProfileMarketplaceRef> = { ...(given ?? {}) };
|
|
158
|
+
for (const id of Object.keys(plugins)) {
|
|
159
|
+
const at = id.lastIndexOf("@");
|
|
160
|
+
if (at <= 0) continue;
|
|
161
|
+
const name = id.slice(at + 1);
|
|
162
|
+
if (out[name]) continue;
|
|
163
|
+
const ref = lookup.get(name);
|
|
164
|
+
if (ref) out[name] = { ...ref };
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
|
|
134
169
|
/**
|
|
135
170
|
* A profile's plugin map as Claude Code's `enabledPlugins` needs it.
|
|
136
171
|
*
|
|
@@ -240,9 +275,37 @@ export async function importProfileFromJson(
|
|
|
240
275
|
entry.plugins as Record<string, boolean>,
|
|
241
276
|
targetScope,
|
|
242
277
|
projectPath,
|
|
278
|
+
pastedMarketplaces(entry.marketplaces),
|
|
243
279
|
);
|
|
244
280
|
}
|
|
245
281
|
|
|
282
|
+
/**
|
|
283
|
+
* The pasted entry's `marketplaces`, keeping only refs of a shape a profile
|
|
284
|
+
* can hold: a `github` ref with a repo, or a `directory` ref with a path.
|
|
285
|
+
*/
|
|
286
|
+
function pastedMarketplaces(
|
|
287
|
+
value: unknown,
|
|
288
|
+
): Record<string, ProfileMarketplaceRef> | undefined {
|
|
289
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
const out: Record<string, ProfileMarketplaceRef> = {};
|
|
293
|
+
for (const [name, ref] of Object.entries(value as Record<string, unknown>)) {
|
|
294
|
+
if (!ref || typeof ref !== "object") continue;
|
|
295
|
+
const r = ref as Partial<ProfileMarketplaceRef>;
|
|
296
|
+
if (r.source === "github" && typeof r.repo === "string" && r.repo) {
|
|
297
|
+
out[name] = { source: "github", repo: r.repo };
|
|
298
|
+
} else if (
|
|
299
|
+
r.source === "directory" &&
|
|
300
|
+
typeof r.path === "string" &&
|
|
301
|
+
r.path
|
|
302
|
+
) {
|
|
303
|
+
out[name] = { source: "directory", path: r.path };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return out;
|
|
307
|
+
}
|
|
308
|
+
|
|
246
309
|
// ─── Internal helpers ──────────────────────────────────────────────────────────
|
|
247
310
|
|
|
248
311
|
function slugify(name: string): string {
|
package/src/services/resolver.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
DependencyReport,
|
|
24
24
|
ProfileManifest,
|
|
25
25
|
ProfileManifestEntry,
|
|
26
|
+
ProfileMarketplaceRef,
|
|
26
27
|
ResolvedBin,
|
|
27
28
|
ResolvedClosure,
|
|
28
29
|
SelectedBin,
|
|
@@ -479,13 +480,16 @@ async function resolveBins(
|
|
|
479
480
|
* never registered. `extends` made this the default failure, because it
|
|
480
481
|
* inherits plugins from a predefined profile and no marketplaces at all.
|
|
481
482
|
*
|
|
482
|
-
* Only marketplaces
|
|
483
|
-
*
|
|
483
|
+
* Only marketplaces with a known repo are derived, since a repo is needed to
|
|
484
|
+
* register one: magus's default marketplaces, and the names `lookup` answers
|
|
485
|
+
* (this machine's registrations, for a caller writing a profile a teammate
|
|
486
|
+
* will read). An unknown suffix is left alone for the manifest to declare.
|
|
484
487
|
* An explicit entry always wins: deriving must never override a pin.
|
|
485
488
|
*/
|
|
486
489
|
export function withDerivedMarketplaces(
|
|
487
490
|
declared: ProfileManifestEntry["marketplaces"],
|
|
488
|
-
plugins: Record<string,
|
|
491
|
+
plugins: Record<string, unknown>,
|
|
492
|
+
lookup: ReadonlyMap<string, ProfileMarketplaceRef> = EMPTY_LOOKUP,
|
|
489
493
|
): NonNullable<ProfileManifestEntry["marketplaces"]> {
|
|
490
494
|
const out = { ...(declared ?? {}) };
|
|
491
495
|
for (const id of Object.keys(plugins)) {
|
|
@@ -494,12 +498,20 @@ export function withDerivedMarketplaces(
|
|
|
494
498
|
const name = id.slice(at + 1);
|
|
495
499
|
if (out[name]) continue;
|
|
496
500
|
const known = getMarketplaceByName(name);
|
|
497
|
-
if (
|
|
498
|
-
|
|
501
|
+
if (known?.source.repo) {
|
|
502
|
+
out[name] = { source: "github", repo: known.source.repo };
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
const registered = lookup.get(name);
|
|
506
|
+
if (registered) out[name] = { ...registered };
|
|
499
507
|
}
|
|
500
508
|
return out;
|
|
501
509
|
}
|
|
502
510
|
|
|
511
|
+
/** No registrations to consult: derive from magus's default marketplaces only. */
|
|
512
|
+
export const EMPTY_LOOKUP: ReadonlyMap<string, ProfileMarketplaceRef> =
|
|
513
|
+
new Map();
|
|
514
|
+
|
|
503
515
|
/**
|
|
504
516
|
* The active profile's plugin pins, or null when there is nothing to read.
|
|
505
517
|
*
|
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { SettingDefinition } from "../data/settings-catalog.js";
|
|
4
4
|
import type { ClaudeSettings } from "../types/index.js";
|
|
5
|
+
import { requireClaudeConfigDir } from "../utils/config-dir.js";
|
|
5
6
|
import {
|
|
6
7
|
getGlobalSettingsPath,
|
|
7
8
|
getSettingsPath,
|
|
@@ -248,8 +249,7 @@ export async function discoverOutputStyles(
|
|
|
248
249
|
{ label: "Default", value: "" },
|
|
249
250
|
];
|
|
250
251
|
|
|
251
|
-
const
|
|
252
|
-
const cacheDir = path.join(homeDir, ".claude", "plugins", "cache");
|
|
252
|
+
const cacheDir = path.join(requireClaudeConfigDir(), "plugins", "cache");
|
|
253
253
|
|
|
254
254
|
// Collect enabled plugins from both scopes
|
|
255
255
|
const enabledPlugins = new Set<string>();
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import os from "node:os";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
import fs from "fs-extra";
|
|
4
3
|
import {
|
|
@@ -11,6 +10,7 @@ import type {
|
|
|
11
10
|
SkillInfo,
|
|
12
11
|
SkillSource,
|
|
13
12
|
} from "../types/index.js";
|
|
13
|
+
import { requireClaudeConfigDir } from "../utils/config-dir.js";
|
|
14
14
|
import {
|
|
15
15
|
getMarketplaceClonePath,
|
|
16
16
|
scanMcpServers,
|
|
@@ -33,7 +33,7 @@ const treeCache = new Map<string, TreeCacheEntry>();
|
|
|
33
33
|
// ─── Path helpers ──────────────────────────────────────────────────────────────
|
|
34
34
|
|
|
35
35
|
export function getUserSkillsDir(): string {
|
|
36
|
-
return path.join(
|
|
36
|
+
return path.join(requireClaudeConfigDir(), "skills");
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export function getProjectSkillsDir(projectPath?: string): string {
|
|
@@ -118,6 +118,12 @@ export type PluginReason =
|
|
|
118
118
|
| "marketplace-unread"
|
|
119
119
|
/** unknown: the catalog fetch failed ("latest" pins only). */
|
|
120
120
|
| "catalog-unverified"
|
|
121
|
+
/**
|
|
122
|
+
* unknown: its marketplace on this machine is not the one the profile
|
|
123
|
+
* names, or could not be compared with it. The Plugins screen's rows only;
|
|
124
|
+
* the plan leaves such a plugin out instead.
|
|
125
|
+
*/
|
|
126
|
+
| "marketplace-conflict"
|
|
121
127
|
/** current: listed, but its marketplace publishes no version to compare. */
|
|
122
128
|
| "no-published-version"
|
|
123
129
|
/** repair: files changed without a version bump. */
|
|
@@ -897,27 +903,37 @@ export function classifyPlugin(
|
|
|
897
903
|
* (`pluginsAnyProfileLists`). One neither pinned nor
|
|
898
904
|
* installed is planned only as a deprecated `remove`; see
|
|
899
905
|
* {@link onlyOtherProfilesList}.
|
|
906
|
+
* @param leaveOut plugins that get no item at all, whichever of the three
|
|
907
|
+
* sources would bring them in. `magus update` passes the
|
|
908
|
+
* plugins of every marketplace registered here from another
|
|
909
|
+
* repository than the profile names: what the catalog says
|
|
910
|
+
* about them describes the other repository, so planning an
|
|
911
|
+
* update, an install or a deprecated removal from it would
|
|
912
|
+
* act on the wrong source.
|
|
900
913
|
*/
|
|
901
914
|
export function planPluginUpdates(
|
|
902
915
|
pinned: Readonly<Record<string, string>>,
|
|
903
916
|
catalog: ReadonlyMap<string, PluginInfo> | readonly PluginInfo[],
|
|
904
917
|
requiredBy: RequiredBy = {},
|
|
905
918
|
listedByProfiles: readonly string[] = [],
|
|
919
|
+
leaveOut: (pluginId: string) => boolean = () => false,
|
|
906
920
|
): PluginUpdateItem[] {
|
|
907
921
|
const index = indexCatalog(catalog);
|
|
908
922
|
const plugins =
|
|
909
923
|
catalog instanceof Map
|
|
910
924
|
? [...catalog.values()]
|
|
911
925
|
: (catalog as readonly PluginInfo[]);
|
|
912
|
-
const pins = new Map(
|
|
926
|
+
const pins = new Map(
|
|
927
|
+
Object.entries(pinned).filter(([pluginId]) => !leaveOut(pluginId)),
|
|
928
|
+
);
|
|
913
929
|
for (const info of plugins) {
|
|
914
|
-
if (pins.has(info.id)) continue;
|
|
930
|
+
if (pins.has(info.id) || leaveOut(info.id)) continue;
|
|
915
931
|
const pin = pinForRow(info, pinned);
|
|
916
932
|
if (pin !== null) pins.set(info.id, pin);
|
|
917
933
|
}
|
|
918
934
|
const othersOnly = new Set<string>();
|
|
919
935
|
for (const pluginId of listedByProfiles) {
|
|
920
|
-
if (pins.has(pluginId)) continue;
|
|
936
|
+
if (pins.has(pluginId) || leaveOut(pluginId)) continue;
|
|
921
937
|
othersOnly.add(pluginId);
|
|
922
938
|
pins.set(pluginId, "latest");
|
|
923
939
|
}
|
|
@@ -982,19 +998,62 @@ export function pinForRow(
|
|
|
982
998
|
* SAME plan `magus update` runs, not a second classification. Computed once
|
|
983
999
|
* per data/pins change, and the one map feeds the row, the detail panel, the
|
|
984
1000
|
* header and the scope picker, so none of them can disagree.
|
|
1001
|
+
*
|
|
1002
|
+
* `leftOut` answers, for a plugin whose marketplace on this machine is not the
|
|
1003
|
+
* one the profile names, why it is left out; null for every other plugin. The
|
|
1004
|
+
* plan leaves those plugins out exactly as `magus update`'s does, and each
|
|
1005
|
+
* one's row gets a `marketplace-conflict` status carrying that answer, which
|
|
1006
|
+
* is no work for any apply.
|
|
985
1007
|
*/
|
|
986
1008
|
export function classifyPluginRows(
|
|
987
1009
|
plugins: readonly PluginInfo[],
|
|
988
1010
|
pins: Readonly<Record<string, string>>,
|
|
989
1011
|
requiredBy: RequiredBy = {},
|
|
990
1012
|
listedByProfiles: readonly string[] = [],
|
|
1013
|
+
leftOut: (pluginId: string) => string | null = () => null,
|
|
991
1014
|
): Map<string, PluginUpdateItem> {
|
|
992
|
-
|
|
993
|
-
planPluginUpdates(
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1015
|
+
const rows = new Map(
|
|
1016
|
+
planPluginUpdates(
|
|
1017
|
+
pins,
|
|
1018
|
+
plugins,
|
|
1019
|
+
requiredBy,
|
|
1020
|
+
listedByProfiles,
|
|
1021
|
+
(pluginId) => leftOut(pluginId) !== null,
|
|
1022
|
+
).map((i) => [i.pluginId, i]),
|
|
997
1023
|
);
|
|
1024
|
+
for (const info of plugins) {
|
|
1025
|
+
const note = leftOut(info.id);
|
|
1026
|
+
if (note === null) continue;
|
|
1027
|
+
rows.set(info.id, leftOutItem(info, pins[info.id] ?? "latest", note));
|
|
1028
|
+
}
|
|
1029
|
+
return rows;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* The status of a row the plan leaves out: no target, no scopes, nothing to
|
|
1034
|
+
* apply. Its marketplace's catalog is the other repository's, so no version it
|
|
1035
|
+
* offers is shown as available.
|
|
1036
|
+
*/
|
|
1037
|
+
function leftOutItem(
|
|
1038
|
+
info: PluginInfo,
|
|
1039
|
+
pinned: string,
|
|
1040
|
+
note: string,
|
|
1041
|
+
): PluginUpdateItem {
|
|
1042
|
+
return {
|
|
1043
|
+
pluginId: info.id,
|
|
1044
|
+
pinned,
|
|
1045
|
+
installed: highestInstalledVersion(info),
|
|
1046
|
+
loaded: info.loadedVersion ?? null,
|
|
1047
|
+
available: null,
|
|
1048
|
+
beforeVersions: Object.fromEntries(installedScopeVersions(info)) as Partial<
|
|
1049
|
+
Record<PluginScope, string>
|
|
1050
|
+
>,
|
|
1051
|
+
target: null,
|
|
1052
|
+
action: "unknown",
|
|
1053
|
+
reason: "marketplace-conflict",
|
|
1054
|
+
scopes: [],
|
|
1055
|
+
note,
|
|
1056
|
+
};
|
|
998
1057
|
}
|
|
999
1058
|
|
|
1000
1059
|
export interface PluginStateCounts {
|