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
package/src/cli/router.ts
CHANGED
|
@@ -28,6 +28,12 @@ export interface RouteOutcome {
|
|
|
28
28
|
launchTui: boolean;
|
|
29
29
|
/** From --theme, validated. Only meaningful when launchTui is true. */
|
|
30
30
|
theme?: ThemeMode;
|
|
31
|
+
/**
|
|
32
|
+
* Which interactive app to launch when `launchTui` is true: the dependency
|
|
33
|
+
* TUI (bare `magus`, the default) or the company marketplace screens
|
|
34
|
+
* (`magus edit`).
|
|
35
|
+
*/
|
|
36
|
+
app?: "main" | "market";
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
export type ThemeFlagParse =
|
|
@@ -87,6 +93,21 @@ const HANDLED = (exitCode: number): RouteOutcome => ({
|
|
|
87
93
|
});
|
|
88
94
|
const LAUNCH_TUI: RouteOutcome = { handled: false, launchTui: true };
|
|
89
95
|
|
|
96
|
+
/** `magus edit --help`. */
|
|
97
|
+
function editHelp(): string {
|
|
98
|
+
return `magus edit — open the company marketplace screens
|
|
99
|
+
|
|
100
|
+
Usage: magus edit
|
|
101
|
+
|
|
102
|
+
Lists the company marketplaces on this machine, their plugins and skills, and
|
|
103
|
+
creates, connects and changes them: publishes edited skills (one skill, or a
|
|
104
|
+
whole plugin as one new version), and adds this project's and your USER skills
|
|
105
|
+
and plugin folders to a marketplace. Needs a terminal.
|
|
106
|
+
|
|
107
|
+
\`magus market --help\` lists the commands that do the same without one.
|
|
108
|
+
`;
|
|
109
|
+
}
|
|
110
|
+
|
|
90
111
|
/**
|
|
91
112
|
* Every name the switch below handles.
|
|
92
113
|
*
|
|
@@ -101,6 +122,8 @@ const SUBCOMMANDS = new Set([
|
|
|
101
122
|
"profile",
|
|
102
123
|
"models",
|
|
103
124
|
"doctor",
|
|
125
|
+
"market",
|
|
126
|
+
"edit",
|
|
104
127
|
]);
|
|
105
128
|
|
|
106
129
|
/**
|
|
@@ -122,8 +145,12 @@ export async function route(
|
|
|
122
145
|
*/
|
|
123
146
|
themeEnv?: ThemeEnv,
|
|
124
147
|
): Promise<RouteOutcome> {
|
|
125
|
-
// Top-level flags take precedence over any command token
|
|
126
|
-
|
|
148
|
+
// Top-level flags take precedence over any command token — for every
|
|
149
|
+
// command but `market` and `edit`, which answer --help and --version
|
|
150
|
+
// themselves (and write nothing doing so). `magus upgrade --help` must still
|
|
151
|
+
// print help rather than upgrade, so this exception is exactly argv[0].
|
|
152
|
+
const marketFirst = argv[0] === "market" || argv[0] === "edit";
|
|
153
|
+
if (!marketFirst && (argv.includes("--version") || argv.includes("-v"))) {
|
|
127
154
|
console.log(`magus v${version}`);
|
|
128
155
|
const result = await checkForUpdates();
|
|
129
156
|
if (result.updateAvailable) {
|
|
@@ -133,7 +160,7 @@ export async function route(
|
|
|
133
160
|
return HANDLED(0);
|
|
134
161
|
}
|
|
135
162
|
|
|
136
|
-
if (argv.includes("--help") || argv.includes("-h")) {
|
|
163
|
+
if (!marketFirst && (argv.includes("--help") || argv.includes("-h"))) {
|
|
137
164
|
printHelp(version);
|
|
138
165
|
return HANDLED(0);
|
|
139
166
|
}
|
|
@@ -216,6 +243,32 @@ export async function route(
|
|
|
216
243
|
return HANDLED(await runModelsCommand(rest));
|
|
217
244
|
case "doctor":
|
|
218
245
|
return HANDLED(await runDoctorCommand(rest));
|
|
246
|
+
case "market": {
|
|
247
|
+
const { runMarketCommand } = await import("./market.js");
|
|
248
|
+
return HANDLED(await runMarketCommand(rest, version));
|
|
249
|
+
}
|
|
250
|
+
case "edit": {
|
|
251
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
252
|
+
console.log(editHelp());
|
|
253
|
+
return HANDLED(0);
|
|
254
|
+
}
|
|
255
|
+
if (rest.includes("--version") || rest.includes("-v")) {
|
|
256
|
+
console.log(`magus v${version}`);
|
|
257
|
+
return HANDLED(0);
|
|
258
|
+
}
|
|
259
|
+
if (rest.length > 0) {
|
|
260
|
+
console.error(
|
|
261
|
+
"magus edit: takes no arguments — the commands are `magus market <verb>` (`magus market --help` lists them)",
|
|
262
|
+
);
|
|
263
|
+
return HANDLED(2);
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
handled: false,
|
|
267
|
+
launchTui: true,
|
|
268
|
+
app: "market",
|
|
269
|
+
...(themeFlag.theme === undefined ? {} : { theme: themeFlag.theme }),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
219
272
|
default:
|
|
220
273
|
// Bare `magus` or an unrecognized token → interactive TUI.
|
|
221
274
|
return themeFlag.theme === undefined
|
|
@@ -303,6 +356,19 @@ applied by a PreToolUse hook magus registers at user scope.
|
|
|
303
356
|
models status What is routed now, plus any drift from the config
|
|
304
357
|
models off Stop routing; subagents inherit the session model again
|
|
305
358
|
|
|
359
|
+
Company marketplaces — your team's own Claude Code plugin marketplace on
|
|
360
|
+
GitHub, listed first in magus. Every change prints its plan and asks first.
|
|
361
|
+
edit Open the company marketplace screens
|
|
362
|
+
market list Company marketplaces on this machine, with their plugins
|
|
363
|
+
market create Create one on GitHub; market connect use an existing one
|
|
364
|
+
market remove <m> Remove one from this machine (the repository stays)
|
|
365
|
+
market plugin create|delete <p@m>; market plugin add <path>
|
|
366
|
+
market skill add|edit|remove <skill>
|
|
367
|
+
market status How your local copies of company skills stand
|
|
368
|
+
market publish <skill> Send a local skill's edits back to its plugin
|
|
369
|
+
market publish --plugin <p@m> Every edited skill of a plugin, one version
|
|
370
|
+
market --help Every market command, its flags and exit codes
|
|
371
|
+
|
|
306
372
|
Other commands:
|
|
307
373
|
upgrade Update magus ITSELF to the latest version
|
|
308
374
|
|
package/src/cli/update-view.ts
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* own word, so the report survives NO_COLOR, a pipe, and colour blindness.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
+
import type { RegistrationResult } from "../services/marketplace-sync.js";
|
|
29
30
|
import { brewUnpinCommand } from "../services/toolchain.js";
|
|
30
31
|
import { binCommandFor } from "../services/update-engine.js";
|
|
31
32
|
import {
|
|
@@ -935,6 +936,61 @@ export function note(text: string): string {
|
|
|
935
936
|
return `${PAD} ${dim(text)}`;
|
|
936
937
|
}
|
|
937
938
|
|
|
939
|
+
// -- marketplace registration ------------------------------------------------
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* What registering the profile's marketplaces did, for `install` and
|
|
943
|
+
* `update` alike: each marketplace added, the one-time notice for a company
|
|
944
|
+
* marketplace trusted from the profile, then each conflict and failure with
|
|
945
|
+
* its fix. Empty when nothing happened.
|
|
946
|
+
*/
|
|
947
|
+
export function registrationReport(result: RegistrationResult): string[] {
|
|
948
|
+
const lines: string[] = [];
|
|
949
|
+
for (const { name, source } of result.added) {
|
|
950
|
+
lines.push(ok(`${bold(name)} added to this machine ${dim(`(${source})`)}`));
|
|
951
|
+
}
|
|
952
|
+
for (const name of result.trusted) {
|
|
953
|
+
lines.push(
|
|
954
|
+
info(
|
|
955
|
+
`${bold(name)} is now a company marketplace on this machine, from this project's profile`,
|
|
956
|
+
),
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
for (const problem of [...result.conflicts, ...result.failed]) {
|
|
960
|
+
lines.push(fail(`${bold(problem.name)}: ${problem.message}`));
|
|
961
|
+
if (problem.fix) lines.push(note(problem.fix));
|
|
962
|
+
}
|
|
963
|
+
return lines;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/** The plugins left out of this run because their marketplace's source conflicts. */
|
|
967
|
+
export function conflictSkipLine(name: string, plugins: string[]): string {
|
|
968
|
+
return warn(
|
|
969
|
+
`Left out ${plugins.map((p) => bold(p)).join(", ")} — ${name} on this machine is not the one this project's profile names`,
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* `magus install` stopped before activating `profile`: it turns on plugins
|
|
975
|
+
* from a marketplace whose registration here is another repository's.
|
|
976
|
+
*/
|
|
977
|
+
export function conflictStopLine(
|
|
978
|
+
profile: string,
|
|
979
|
+
name: string,
|
|
980
|
+
plugins: string[],
|
|
981
|
+
): string {
|
|
982
|
+
return fail(
|
|
983
|
+
`Did not activate ${bold(profile)}: it turns on ${plugins.map((p) => bold(p)).join(", ")}, and ${name} on this machine is not the one this project's profile names — nothing was installed`,
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/** A read-only run names each marketplace a real run would add, and adds none. */
|
|
988
|
+
export function notRegisteredYetLine(name: string): string {
|
|
989
|
+
return info(
|
|
990
|
+
`${bold(name)} is not on this machine yet — ${bold("magus update")} adds it`,
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
|
|
938
994
|
// -- unverified items ----------------------------------------------------------
|
|
939
995
|
//
|
|
940
996
|
// A plugin is `unknown` for several distinct reasons — its marketplace publishes
|
package/src/cli/update.ts
CHANGED
|
@@ -39,11 +39,25 @@ import { isClaudeAvailable } from "../services/claude-cli.js";
|
|
|
39
39
|
import { clearContentDriftCache } from "../services/content-drift.js";
|
|
40
40
|
import { refusedPinWhy } from "../services/deps-plan.js";
|
|
41
41
|
import { validateManifest } from "../services/manifest.js";
|
|
42
|
+
import {
|
|
43
|
+
conflictFreeOptions,
|
|
44
|
+
fromConflicted,
|
|
45
|
+
leaveOutConflicted,
|
|
46
|
+
} from "../services/marketplace-conflicts.js";
|
|
42
47
|
import {
|
|
43
48
|
refreshRegisteredMarketplaces,
|
|
44
49
|
reportRegisteredMarketplaces,
|
|
45
50
|
} from "../services/marketplace-refresh.js";
|
|
46
|
-
import {
|
|
51
|
+
import {
|
|
52
|
+
findMarketplaceConflicts,
|
|
53
|
+
isMarketplaceRegistered,
|
|
54
|
+
registerClosureMarketplaces,
|
|
55
|
+
} from "../services/marketplace-sync.js";
|
|
56
|
+
import {
|
|
57
|
+
type PluginDependencyReader,
|
|
58
|
+
readPluginDependencies,
|
|
59
|
+
readUpdatedPluginDependencies,
|
|
60
|
+
} from "../services/plugin-dependencies.js";
|
|
47
61
|
import {
|
|
48
62
|
type PluginInfo,
|
|
49
63
|
clearMarketplaceCache,
|
|
@@ -99,14 +113,17 @@ import {
|
|
|
99
113
|
type Step,
|
|
100
114
|
applyFrame,
|
|
101
115
|
applyRow,
|
|
116
|
+
conflictSkipLine,
|
|
102
117
|
fail as failLine,
|
|
103
118
|
header,
|
|
104
119
|
info,
|
|
120
|
+
notRegisteredYetLine,
|
|
105
121
|
note as noteLine,
|
|
106
122
|
nothingToApplyLines,
|
|
107
123
|
ok as okLine,
|
|
108
124
|
pendingHeadline,
|
|
109
125
|
planReport,
|
|
126
|
+
registrationReport,
|
|
110
127
|
stepFrame,
|
|
111
128
|
stepRecord,
|
|
112
129
|
summaryLine,
|
|
@@ -334,7 +351,7 @@ async function updateProfile(
|
|
|
334
351
|
/** Notices worth keeping, flushed after the checklist collapses. */
|
|
335
352
|
const notices: string[] = [];
|
|
336
353
|
|
|
337
|
-
|
|
354
|
+
let closure = await runStep(steps.profile, async (say) => {
|
|
338
355
|
say(`resolving "${targetId}"`);
|
|
339
356
|
const resolved = await resolveProfile(current, targetId);
|
|
340
357
|
steps.profile.detail = [
|
|
@@ -369,14 +386,66 @@ async function updateProfile(
|
|
|
369
386
|
// profile's plugins, plus whatever is installed at USER scope (the refresher
|
|
370
387
|
// reads that itself). And each clone at most once an hour, machine-wide.
|
|
371
388
|
const readOnly = flags.dryRun || flags.check;
|
|
372
|
-
|
|
389
|
+
/** Marketplaces whose registration here is not the one the profile names. */
|
|
390
|
+
const conflicted = new Set<string>();
|
|
391
|
+
/**
|
|
392
|
+
* The conflicts a real run would report, found without registering or
|
|
393
|
+
* trusting anything, so a run that registers nothing still leaves out the
|
|
394
|
+
* same plugins as one that does.
|
|
395
|
+
*/
|
|
396
|
+
const detectConflicts = async () => {
|
|
397
|
+
const conflicts = await findMarketplaceConflicts(
|
|
398
|
+
closure.marketplaces,
|
|
399
|
+
projectPath,
|
|
400
|
+
);
|
|
401
|
+
notices.push(
|
|
402
|
+
...registrationReport({ added: [], trusted: [], conflicts, failed: [] }),
|
|
403
|
+
);
|
|
404
|
+
for (const c of conflicts) conflicted.add(c.name);
|
|
405
|
+
};
|
|
373
406
|
const refresh = await runStep(steps.marketplaces, async (say) => {
|
|
374
407
|
if (readOnly) {
|
|
408
|
+
await detectConflicts();
|
|
409
|
+
// Registering is a write, so a read-only run names what it would add.
|
|
410
|
+
// A conflicted one is never added, so it is not named.
|
|
411
|
+
for (const name of Object.keys(closure.marketplaces)) {
|
|
412
|
+
if (conflicted.has(name)) continue;
|
|
413
|
+
if (!(await isMarketplaceRegistered(name))) {
|
|
414
|
+
notices.push(notRegisteredYetLine(name));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
375
417
|
say("read-only — selecting, not pulling");
|
|
376
418
|
// Report-only, NOT a skip: the selection is a pure read, so a read-only
|
|
377
419
|
// run can still say exactly what a real run would pull.
|
|
378
|
-
return reportRegisteredMarketplaces([], {
|
|
420
|
+
return reportRegisteredMarketplaces([], {
|
|
421
|
+
pluginIds: Object.keys(closure.plugins),
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
// Register what the profile names before anything is planned: without
|
|
425
|
+
// the marketplace a plugin from it is "not found", and its dependencies
|
|
426
|
+
// cannot be read. A marketplace added here holds plugin.json files the
|
|
427
|
+
// first resolve could not see, so the profile is resolved again.
|
|
428
|
+
// Without `claude` nothing can be registered; the apply phase says so.
|
|
429
|
+
if (await isClaudeAvailable()) {
|
|
430
|
+
say("registering marketplaces the profile names");
|
|
431
|
+
const registration = await registerClosureMarketplaces(
|
|
432
|
+
closure.marketplaces,
|
|
433
|
+
projectPath,
|
|
434
|
+
);
|
|
435
|
+
notices.push(...registrationReport(registration));
|
|
436
|
+
for (const c of registration.conflicts) conflicted.add(c.name);
|
|
437
|
+
if (registration.added.length > 0) {
|
|
438
|
+
clearMarketplaceCache();
|
|
439
|
+
closure = await resolveProfile(
|
|
440
|
+
current,
|
|
441
|
+
targetId,
|
|
442
|
+
conflictFreeOptions(conflicted),
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
await detectConflicts();
|
|
379
447
|
}
|
|
448
|
+
const pluginIds = Object.keys(closure.plugins);
|
|
380
449
|
let settled = 0;
|
|
381
450
|
return refreshRegisteredMarketplaces(
|
|
382
451
|
[],
|
|
@@ -428,6 +497,40 @@ async function updateProfile(
|
|
|
428
497
|
clearMarketplaceCache();
|
|
429
498
|
clearContentDriftCache();
|
|
430
499
|
|
|
500
|
+
// A pulled clone may carry a new publish that adds a dependency or a tool
|
|
501
|
+
// requirement to a plugin already listed: resolve again, so this run
|
|
502
|
+
// installs those too rather than the next one.
|
|
503
|
+
// A conflicted marketplace resolves again too: the first resolve read its
|
|
504
|
+
// plugins' dependencies and tool requirements from the other repository.
|
|
505
|
+
if (conflicted.size > 0 || (!readOnly && refresh.refreshed.length > 0)) {
|
|
506
|
+
// A plugin that follows its marketplace's latest is moved to the
|
|
507
|
+
// clone's version by this run, so its dependencies are that version's:
|
|
508
|
+
// read the clone first. A pinned plugin keeps the installed copy's.
|
|
509
|
+
const specs = closure.plugins;
|
|
510
|
+
const readDependencies: PluginDependencyReader | undefined = readOnly
|
|
511
|
+
? undefined
|
|
512
|
+
: (id, at) => {
|
|
513
|
+
const spec = specs[id];
|
|
514
|
+
return spec === undefined || spec === "latest"
|
|
515
|
+
? readUpdatedPluginDependencies(id, at)
|
|
516
|
+
: readPluginDependencies(id, at);
|
|
517
|
+
};
|
|
518
|
+
closure = await resolveProfile(
|
|
519
|
+
current,
|
|
520
|
+
targetId,
|
|
521
|
+
conflictFreeOptions(conflicted, readDependencies),
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// A plugin whose marketplace here is not the profile's is left out of this
|
|
526
|
+
// run's work, with the tools only it declares: installing either would take
|
|
527
|
+
// it from the wrong repository. The same filter `magus install` uses.
|
|
528
|
+
const permitted = leaveOutConflicted(closure, conflicted);
|
|
529
|
+
closure = permitted.closure;
|
|
530
|
+
for (const l of permitted.leftOut) {
|
|
531
|
+
notices.push(conflictSkipLine(l.marketplace, l.plugins));
|
|
532
|
+
}
|
|
533
|
+
|
|
431
534
|
const catalog = await runStep(steps.catalog, async (say) => {
|
|
432
535
|
say("reading installed and available plugins");
|
|
433
536
|
const map = new Map<string, PluginInfo>(
|
|
@@ -527,11 +630,15 @@ async function updateProfile(
|
|
|
527
630
|
// Every profile's plugins, not only this one's: a deprecated plugin a
|
|
528
631
|
// profile that is not active lists is still a row the render keeps, and
|
|
529
632
|
// is planned for removal.
|
|
633
|
+
// The catalog lists a conflicted marketplace's installed plugins, and
|
|
634
|
+
// other profiles may list more of them: the planner leaves out every
|
|
635
|
+
// one, not only those `leaveOutConflicted` took out of the closure.
|
|
530
636
|
plugins: planPluginUpdates(
|
|
531
637
|
closure.plugins,
|
|
532
638
|
catalog,
|
|
533
639
|
requiredBy,
|
|
534
640
|
pluginsAnyProfileLists(current),
|
|
641
|
+
(pluginId) => fromConflicted(pluginId, conflicted),
|
|
535
642
|
),
|
|
536
643
|
bins: planBinUpdates(tools, toolFacts),
|
|
537
644
|
skills: planSkillUpdates(skillRefs, installedSkills),
|
|
@@ -648,11 +755,6 @@ async function updateProfile(
|
|
|
648
755
|
return 1;
|
|
649
756
|
}
|
|
650
757
|
|
|
651
|
-
// Register any marketplace the profile names but this machine does not know.
|
|
652
|
-
// Without this an install fails with "not found in marketplace" for anything
|
|
653
|
-
// outside magus's built-in list, which is every private or third-party one.
|
|
654
|
-
await registerClosureMarketplaces(closure.marketplaces);
|
|
655
|
-
|
|
656
758
|
// ---- apply phase, under an animated meter --------------------------------
|
|
657
759
|
|
|
658
760
|
const nameWidth = Math.max(12, ...workItems.map((n) => width(n)));
|
package/src/data/marketplaces.ts
CHANGED
|
@@ -249,6 +249,8 @@ export function getAllMarketplaces(
|
|
|
249
249
|
featured: defaultMp?.featured,
|
|
250
250
|
deprecated: defaultMp?.deprecated,
|
|
251
251
|
owned: defaultMp?.owned,
|
|
252
|
+
...(local.company ? { company: true } : {}),
|
|
253
|
+
...(local.ownerName ? { ownerName: local.ownerName } : {}),
|
|
252
254
|
});
|
|
253
255
|
}
|
|
254
256
|
}
|
|
@@ -262,9 +264,13 @@ export function getAllMarketplaces(
|
|
|
262
264
|
}
|
|
263
265
|
}
|
|
264
266
|
|
|
265
|
-
// Sort:
|
|
267
|
+
// Sort: company marketplaces first, then Magus, deprecated last, then
|
|
268
|
+
// alphabetically
|
|
266
269
|
return Array.from(all.values()).sort((a, b) => {
|
|
267
|
-
//
|
|
270
|
+
// This machine's company marketplaces lead
|
|
271
|
+
if (a.company && !b.company) return -1;
|
|
272
|
+
if (!a.company && b.company) return 1;
|
|
273
|
+
// Magus (MadAppGang) next
|
|
268
274
|
const aIsMag = a.source.repo?.toLowerCase().includes("madappgang/");
|
|
269
275
|
const bIsMag = b.source.repo?.toLowerCase().includes("madappgang/");
|
|
270
276
|
if (aIsMag && !bIsMag) return -1;
|
package/src/main.tsx
CHANGED
|
@@ -12,8 +12,8 @@ export const VERSION = (pkg as { version: string }).version;
|
|
|
12
12
|
//
|
|
13
13
|
// NOTHING in this file may statically import @opentui/* or ./ui/*. `magus
|
|
14
14
|
// hook …` runs on every matching Claude Code tool call, and a static import
|
|
15
|
-
// would load the whole TUI module graph before the hook could answer. The
|
|
16
|
-
// (./tui.js), the router (./cli/router.js, which reaches @opentui through
|
|
15
|
+
// would load the whole TUI module graph before the hook could answer. The TUIs
|
|
16
|
+
// (./tui.js and ./market-tui.js), the router (./cli/router.js, which reaches @opentui through
|
|
17
17
|
// cli/update.ts → ui/theme.ts) and the hook itself are all imported on demand,
|
|
18
18
|
// after the branch that decides which one is needed.
|
|
19
19
|
// Pinned by __tests__/hook-import-policy.test.ts.
|
|
@@ -94,6 +94,24 @@ async function main(): Promise<void> {
|
|
|
94
94
|
const isInteractive =
|
|
95
95
|
process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
96
96
|
|
|
97
|
+
if (outcome.app === "market") {
|
|
98
|
+
if (!isInteractive) {
|
|
99
|
+
console.error(
|
|
100
|
+
"magus edit opens an interactive screen, which needs a terminal. `magus market --help` lists the commands that run without one.",
|
|
101
|
+
);
|
|
102
|
+
process.exit(2);
|
|
103
|
+
}
|
|
104
|
+
// The market screens, imported as late as the main TUI below and for
|
|
105
|
+
// the same reason.
|
|
106
|
+
const { startMarketTui } = await import("./market-tui.js");
|
|
107
|
+
await startMarketTui({
|
|
108
|
+
theme: outcome.theme,
|
|
109
|
+
env: themeEnv,
|
|
110
|
+
isInteractive,
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
97
115
|
// Last possible moment: this is the import that pulls in @opentui and the
|
|
98
116
|
// whole ui/ tree. Everything above runs without them.
|
|
99
117
|
const { startTui } = await import("./tui.js");
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The interactive screens behind `magus edit`: the company
|
|
3
|
+
* marketplaces on this machine, and the plugins and skills of one of them.
|
|
4
|
+
*
|
|
5
|
+
* Imported dynamically from `main.tsx`, like `tui.tsx`, so no other command
|
|
6
|
+
* loads `@opentui/*` or `src/ui/`. Opening it writes nothing; every change it
|
|
7
|
+
* makes is a plan the user confirmed, applied by the same functions the
|
|
8
|
+
* `magus market` verbs run.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { type MountTuiOptions, mountTui } from "./tui-runtime.js";
|
|
12
|
+
import { MarketApp } from "./ui/market/MarketApp.js";
|
|
13
|
+
|
|
14
|
+
export type StartMarketTuiOptions = MountTuiOptions;
|
|
15
|
+
|
|
16
|
+
/** Start the market TUI. Never resolves — it runs until the user quits. */
|
|
17
|
+
export async function startMarketTui(
|
|
18
|
+
opts: StartMarketTuiOptions,
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
await mountTui(opts, (quit) => <MarketApp onExit={quit} />);
|
|
21
|
+
}
|
|
@@ -14,14 +14,25 @@
|
|
|
14
14
|
* NOTHING HERE PRINTS — it runs under the CLI's live region and inside the TUI.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { getMarketplaceByName } from "../data/marketplaces.js";
|
|
17
18
|
import type {
|
|
18
19
|
ProfileManifest,
|
|
19
20
|
ResolvedClosure,
|
|
20
21
|
SelectedBin,
|
|
21
22
|
} from "../types/index.js";
|
|
22
23
|
import { readManifest, validateManifest } from "./manifest.js";
|
|
24
|
+
import {
|
|
25
|
+
type MarketplaceCheck,
|
|
26
|
+
NOTHING_LEFT_OUT,
|
|
27
|
+
conflictFreeOptions,
|
|
28
|
+
} from "./marketplace-conflicts.js";
|
|
29
|
+
import { findMarketplaceConflicts } from "./marketplace-sync.js";
|
|
23
30
|
import { activeProfile } from "./profile-live.js";
|
|
24
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
type ResolveOptions,
|
|
33
|
+
pluginsAnyProfileLists,
|
|
34
|
+
resolveProfile,
|
|
35
|
+
} from "./resolver.js";
|
|
25
36
|
|
|
26
37
|
/**
|
|
27
38
|
* The profile to use: the explicit one, else the active one, else the only one.
|
|
@@ -51,6 +62,13 @@ export interface ActiveClosure {
|
|
|
51
62
|
* set `magus update` passes it.
|
|
52
63
|
*/
|
|
53
64
|
listedByProfiles: string[];
|
|
65
|
+
/**
|
|
66
|
+
* The profile's marketplaces whose plugins are left out of updates here —
|
|
67
|
+
* the same comparison `magus update` makes, read without changing
|
|
68
|
+
* anything. When this machine's side could not be read, every marketplace
|
|
69
|
+
* the comparison covers is `unchecked`.
|
|
70
|
+
*/
|
|
71
|
+
marketplaces: MarketplaceCheck;
|
|
54
72
|
}
|
|
55
73
|
|
|
56
74
|
export interface ActiveClosureDeps {
|
|
@@ -59,16 +77,19 @@ export interface ActiveClosureDeps {
|
|
|
59
77
|
resolveProfile(
|
|
60
78
|
manifest: ProfileManifest,
|
|
61
79
|
id: string,
|
|
62
|
-
opts?:
|
|
63
|
-
binResolver?: (pluginId: string) => Promise<SelectedBin[]>;
|
|
64
|
-
},
|
|
80
|
+
opts?: ResolveOptions,
|
|
65
81
|
): Promise<ResolvedClosure>;
|
|
82
|
+
findMarketplaceConflicts(
|
|
83
|
+
marketplaces: ResolvedClosure["marketplaces"],
|
|
84
|
+
projectRoot?: string,
|
|
85
|
+
): Promise<Array<{ name: string }>>;
|
|
66
86
|
}
|
|
67
87
|
|
|
68
88
|
const REAL_DEPS: ActiveClosureDeps = {
|
|
69
89
|
readManifest,
|
|
70
90
|
activeProfile,
|
|
71
91
|
resolveProfile,
|
|
92
|
+
findMarketplaceConflicts,
|
|
72
93
|
};
|
|
73
94
|
|
|
74
95
|
export interface LoadActiveClosureOptions {
|
|
@@ -111,21 +132,62 @@ export async function loadActiveClosure(
|
|
|
111
132
|
);
|
|
112
133
|
if (profileId === null) return null;
|
|
113
134
|
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
135
|
+
const bins: ResolveOptions =
|
|
136
|
+
opts.resolveBins === false ? { binResolver: NO_BINS } : {};
|
|
137
|
+
let closure = await deps.resolveProfile(manifest, profileId, bins);
|
|
138
|
+
const marketplaces = await checkMarketplaces(deps, closure, projectPath);
|
|
139
|
+
// A conflicted plugin's dependencies and tools were read from the other
|
|
140
|
+
// repository: resolve again without them, as `magus update` does.
|
|
141
|
+
const leftOut = new Set([
|
|
142
|
+
...marketplaces.conflicted,
|
|
143
|
+
...marketplaces.unchecked,
|
|
144
|
+
]);
|
|
145
|
+
if (leftOut.size > 0) {
|
|
146
|
+
closure = await deps.resolveProfile(manifest, profileId, {
|
|
147
|
+
...conflictFreeOptions(leftOut),
|
|
148
|
+
...bins,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
119
151
|
return {
|
|
120
152
|
profileId,
|
|
121
153
|
closure,
|
|
122
154
|
listedByProfiles: pluginsAnyProfileLists(manifest),
|
|
155
|
+
marketplaces,
|
|
123
156
|
};
|
|
124
157
|
} catch {
|
|
125
158
|
return null;
|
|
126
159
|
}
|
|
127
160
|
}
|
|
128
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Which of the closure's marketplaces differ from this machine's. A failed
|
|
164
|
+
* read is an answer too: every marketplace the comparison covers (any that is
|
|
165
|
+
* not one of magus's defaults) is then unchecked, and left out.
|
|
166
|
+
*/
|
|
167
|
+
async function checkMarketplaces(
|
|
168
|
+
deps: ActiveClosureDeps,
|
|
169
|
+
closure: ResolvedClosure,
|
|
170
|
+
projectPath: string,
|
|
171
|
+
): Promise<MarketplaceCheck> {
|
|
172
|
+
const names = Object.keys(closure.marketplaces);
|
|
173
|
+
if (names.length === 0) return NOTHING_LEFT_OUT;
|
|
174
|
+
try {
|
|
175
|
+
const conflicts = await deps.findMarketplaceConflicts(
|
|
176
|
+
closure.marketplaces,
|
|
177
|
+
projectPath,
|
|
178
|
+
);
|
|
179
|
+
return {
|
|
180
|
+
conflicted: new Set(conflicts.map((c) => c.name)),
|
|
181
|
+
unchecked: new Set(),
|
|
182
|
+
};
|
|
183
|
+
} catch {
|
|
184
|
+
return {
|
|
185
|
+
conflicted: new Set(),
|
|
186
|
+
unchecked: new Set(names.filter((n) => !getMarketplaceByName(n))),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
129
191
|
/**
|
|
130
192
|
* The active profile's plugin pins — `pluginId -> exact version | "latest"` — and
|
|
131
193
|
* `{}` when no profile resolves.
|