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
|
@@ -160,7 +160,7 @@ async function recoverMissingMarketplace(
|
|
|
160
160
|
const defaultMp = defaultMarketplaces.find((m) => m.name === marketplace);
|
|
161
161
|
if (!defaultMp?.source.repo) return false;
|
|
162
162
|
|
|
163
|
-
await addMarketplace(defaultMp.source.repo);
|
|
163
|
+
await addMarketplace(defaultMp.source.repo, "user");
|
|
164
164
|
return true;
|
|
165
165
|
}
|
|
166
166
|
|
|
@@ -180,7 +180,7 @@ async function execPluginCommand(
|
|
|
180
180
|
): Promise<void> {
|
|
181
181
|
const argv = ["plugin", verb, pluginId, "--scope", scope];
|
|
182
182
|
try {
|
|
183
|
-
await
|
|
183
|
+
await runPluginVerb(verb, pluginId, scope, undefined, timeoutMs);
|
|
184
184
|
} catch (error) {
|
|
185
185
|
const msg = error instanceof Error ? error.message : String(error);
|
|
186
186
|
if (msg.includes("not found in marketplace")) {
|
|
@@ -387,19 +387,149 @@ export async function updatePlugin(
|
|
|
387
387
|
}
|
|
388
388
|
|
|
389
389
|
/**
|
|
390
|
-
*
|
|
391
|
-
*
|
|
390
|
+
* Register a marketplace: `claude plugin marketplace add <source> --scope
|
|
391
|
+
* <scope>`. `source` is anything Claude Code accepts — `owner/repo`, an https
|
|
392
|
+
* git URL, or a path to a marketplace directory. The scope is required so no
|
|
393
|
+
* caller declares one by accident. Longer timeout: this clones.
|
|
394
|
+
*
|
|
395
|
+
* There is deliberately no wrapper for `claude plugin marketplace update`: it
|
|
396
|
+
* deletes the clone before re-cloning and can leave it gone for good. A clone
|
|
397
|
+
* is refreshed with `fastForwardCloneIfPresent` instead.
|
|
398
|
+
*/
|
|
399
|
+
export async function addMarketplace(
|
|
400
|
+
source: string,
|
|
401
|
+
scope: PluginScope,
|
|
402
|
+
): Promise<void> {
|
|
403
|
+
await execClaude(
|
|
404
|
+
["plugin", "marketplace", "add", source, "--scope", scope],
|
|
405
|
+
60000,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Remove a marketplace from every settings scope: `claude plugin marketplace
|
|
411
|
+
* remove <name>`. Claude Code uninstalls every plugin it provided.
|
|
392
412
|
*/
|
|
393
|
-
export async function
|
|
394
|
-
await execClaude(["plugin", "marketplace", "
|
|
413
|
+
export async function removeMarketplace(name: string): Promise<void> {
|
|
414
|
+
await execClaude(["plugin", "marketplace", "remove", name], 60000);
|
|
395
415
|
}
|
|
396
416
|
|
|
397
417
|
/**
|
|
398
|
-
*
|
|
399
|
-
*
|
|
418
|
+
* Run `claude plugin install|update <id> --scope <scope>` and nothing else.
|
|
419
|
+
*
|
|
420
|
+
* No recovery on "not found in marketplace": the recovery behind
|
|
421
|
+
* {@link installPlugin} and {@link updatePlugin} edits
|
|
422
|
+
* `known_marketplaces.json` directly, so a caller that must change Claude
|
|
423
|
+
* Code's state only through its CLI runs this and reports the error.
|
|
424
|
+
*
|
|
425
|
+
* `cwd` is where Claude Code resolves project and local scope; it defaults to
|
|
426
|
+
* this process's working directory.
|
|
400
427
|
*/
|
|
401
|
-
export async function
|
|
402
|
-
|
|
428
|
+
export async function runPluginVerb(
|
|
429
|
+
verb: "install" | "update",
|
|
430
|
+
pluginId: string,
|
|
431
|
+
scope: PluginScope,
|
|
432
|
+
cwd?: string,
|
|
433
|
+
timeoutMs = 60000,
|
|
434
|
+
): Promise<void> {
|
|
435
|
+
await execClaude(
|
|
436
|
+
["plugin", verb, pluginId, "--scope", scope],
|
|
437
|
+
timeoutMs,
|
|
438
|
+
cwd,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** One row of `claude plugin marketplace list --json`. */
|
|
443
|
+
export interface CliMarketplace {
|
|
444
|
+
name: string;
|
|
445
|
+
source: string;
|
|
446
|
+
repo?: string;
|
|
447
|
+
url?: string;
|
|
448
|
+
path?: string;
|
|
449
|
+
installLocation?: string;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** `claude plugin marketplace list --json`. */
|
|
453
|
+
export async function listMarketplaces(): Promise<CliMarketplace[]> {
|
|
454
|
+
const out = await execClaude(["plugin", "marketplace", "list", "--json"]);
|
|
455
|
+
return parseJsonArray<CliMarketplace>(out, "marketplace list").filter(
|
|
456
|
+
(m) => typeof m?.name === "string",
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** One row of `claude plugin list --json`. */
|
|
461
|
+
export interface CliPlugin {
|
|
462
|
+
id: string;
|
|
463
|
+
version?: string;
|
|
464
|
+
scope: PluginScope;
|
|
465
|
+
enabled?: boolean;
|
|
466
|
+
installPath?: string;
|
|
467
|
+
projectPath?: string;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** `claude plugin list --json`, run in `cwd` when given. */
|
|
471
|
+
export async function listPlugins(cwd?: string): Promise<CliPlugin[]> {
|
|
472
|
+
const out = await execClaude(["plugin", "list", "--json"], 30000, cwd);
|
|
473
|
+
return parseJsonArray<CliPlugin>(out, "plugin list").filter(
|
|
474
|
+
(p) => typeof p?.id === "string",
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function parseJsonArray<T>(out: string, what: string): T[] {
|
|
479
|
+
let parsed: unknown;
|
|
480
|
+
try {
|
|
481
|
+
parsed = JSON.parse(out || "[]");
|
|
482
|
+
} catch (error) {
|
|
483
|
+
throw new Error(
|
|
484
|
+
`claude ${what} --json printed something that is not JSON`,
|
|
485
|
+
{ cause: error },
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
if (!Array.isArray(parsed)) {
|
|
489
|
+
throw new Error(`claude ${what} --json did not print a list`);
|
|
490
|
+
}
|
|
491
|
+
return parsed as T[];
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** What `claude plugin validate` said about a directory. */
|
|
495
|
+
export interface ValidateResult {
|
|
496
|
+
ok: boolean;
|
|
497
|
+
output: string;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* `claude plugin validate <dir>`, plain — never `--strict`, which fails on the
|
|
502
|
+
* company marker and on a marketplace with no plugins yet. The exit code is
|
|
503
|
+
* the gate; the output is kept for the report either way.
|
|
504
|
+
*/
|
|
505
|
+
export async function validatePath(dir: string): Promise<ValidateResult> {
|
|
506
|
+
try {
|
|
507
|
+
const output = await execClaude(["plugin", "validate", dir], 60000);
|
|
508
|
+
return { ok: true, output };
|
|
509
|
+
} catch (error) {
|
|
510
|
+
return {
|
|
511
|
+
ok: false,
|
|
512
|
+
output: error instanceof Error ? error.message : String(error),
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* The next step when `claude plugin marketplace add` failed on GitHub
|
|
519
|
+
* credentials, or null when it failed for another reason. Claude Code clones
|
|
520
|
+
* with plain git, so a gh sign-in alone is not enough: git must also use gh's
|
|
521
|
+
* credentials, which `gh auth setup-git` arranges.
|
|
522
|
+
*/
|
|
523
|
+
export function marketplaceAddHint(error: unknown): string | null {
|
|
524
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
525
|
+
if (
|
|
526
|
+
/terminal prompts disabled|could not read (Username|Password)|Authentication failed|Invalid username or password|Permission denied \(publickey|Repository not found|returned error: 40[13]/i.test(
|
|
527
|
+
message,
|
|
528
|
+
)
|
|
529
|
+
) {
|
|
530
|
+
return "Run `gh auth login`, then `gh auth setup-git`, and try again.";
|
|
531
|
+
}
|
|
532
|
+
return null;
|
|
403
533
|
}
|
|
404
534
|
|
|
405
535
|
/**
|
|
@@ -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 semver from "semver";
|
|
@@ -11,6 +10,7 @@ import type {
|
|
|
11
10
|
MarketplaceSource,
|
|
12
11
|
McpServerConfig,
|
|
13
12
|
} from "../types/index.js";
|
|
13
|
+
import { requireClaudeConfigDir } from "../utils/config-dir.js";
|
|
14
14
|
import { parsePluginId } from "../utils/string-utils.js";
|
|
15
15
|
import { repoHeadSha } from "./content-drift.js";
|
|
16
16
|
import { inheritablePaths } from "./git-worktree.js";
|
|
@@ -61,16 +61,14 @@ export function getClaudeDir(projectPath?: string): string {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* (measured; Node follows the change). Reading `HOME` at call time is what lets
|
|
70
|
-
* a caller — a test above all — point the global settings somewhere else.
|
|
64
|
+
* Claude Code's USER config directory, resolved per call through
|
|
65
|
+
* {@link requireClaudeConfigDir}: `CLAUDE_CONFIG_DIR` when set, else
|
|
66
|
+
* `<HOME>/.claude` with `HOME` read at call time. Claude Code keeps USER
|
|
67
|
+
* `settings.json` there as well as its plugin state, so both come from one
|
|
68
|
+
* root and a caller — a test above all — moves them together.
|
|
71
69
|
*/
|
|
72
70
|
export function getGlobalClaudeDir(): string {
|
|
73
|
-
return
|
|
71
|
+
return requireClaudeConfigDir();
|
|
74
72
|
}
|
|
75
73
|
|
|
76
74
|
export async function ensureClaudeDir(projectPath?: string): Promise<string> {
|
|
@@ -1070,13 +1068,13 @@ export async function discoverAllGlobalMarketplaces(): Promise<
|
|
|
1070
1068
|
* path it writes still names the operator's real home.
|
|
1071
1069
|
*/
|
|
1072
1070
|
export const pluginsDir = (): string =>
|
|
1073
|
-
path.join(
|
|
1071
|
+
path.join(requireClaudeConfigDir(), "plugins");
|
|
1074
1072
|
|
|
1075
1073
|
const installedPluginsFile = (root: string = pluginsDir()): string =>
|
|
1076
1074
|
path.join(root, "installed_plugins.json");
|
|
1077
1075
|
|
|
1078
1076
|
const knownMarketplacesFile = (): string =>
|
|
1079
|
-
path.join(
|
|
1077
|
+
path.join(pluginsDir(), "known_marketplaces.json");
|
|
1080
1078
|
|
|
1081
1079
|
interface KnownMarketplaceEntry {
|
|
1082
1080
|
source: { source: string; path?: string; repo?: string };
|
|
@@ -1160,12 +1158,7 @@ export async function recoverMarketplaceSettings(): Promise<MarketplaceRecoveryR
|
|
|
1160
1158
|
if (entry.source?.source === "directory") {
|
|
1161
1159
|
const defaultMp = defaultMarketplaces.find((m) => m.name === name);
|
|
1162
1160
|
if (defaultMp?.source.repo) {
|
|
1163
|
-
const marketplacesDir = path.join(
|
|
1164
|
-
os.homedir(),
|
|
1165
|
-
".claude",
|
|
1166
|
-
"plugins",
|
|
1167
|
-
"marketplaces",
|
|
1168
|
-
);
|
|
1161
|
+
const marketplacesDir = path.join(pluginsDir(), "marketplaces");
|
|
1169
1162
|
entry.source = {
|
|
1170
1163
|
source: "github",
|
|
1171
1164
|
repo: defaultMp.source.repo,
|
|
@@ -1209,14 +1202,15 @@ async function readKnownMarketplaces(): Promise<KnownMarketplaces> {
|
|
|
1209
1202
|
*/
|
|
1210
1203
|
export async function isMarketplaceRegistered(name: string): Promise<boolean> {
|
|
1211
1204
|
const known = await readKnownMarketplaces();
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
"
|
|
1218
|
-
|
|
1219
|
-
|
|
1205
|
+
const entry = known[name];
|
|
1206
|
+
if (!entry) return false;
|
|
1207
|
+
// A `directory` marketplace is read where it is: it has no clone under
|
|
1208
|
+
// plugins/marketplaces, and its own folder is what must exist.
|
|
1209
|
+
const where =
|
|
1210
|
+
entry.source.source === "directory" && entry.source.path
|
|
1211
|
+
? entry.source.path
|
|
1212
|
+
: path.join(pluginsDir(), "marketplaces", name);
|
|
1213
|
+
return fs.pathExists(where);
|
|
1220
1214
|
}
|
|
1221
1215
|
|
|
1222
1216
|
/**
|
package/src/services/doctor.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { existsSync } from "node:fs";
|
|
16
16
|
import { join } from "node:path";
|
|
17
17
|
import type { PluginConventions } from "../types/index.js";
|
|
18
|
+
import { requireClaudeConfigDir } from "../utils/config-dir.js";
|
|
18
19
|
import { parsePluginId } from "../utils/string-utils.js";
|
|
19
20
|
import { readInstalledPluginsRegistry } from "./claude-settings.js";
|
|
20
21
|
import {
|
|
@@ -70,10 +71,9 @@ export type DoctorLogger = (
|
|
|
70
71
|
// INTERNAL HELPERS
|
|
71
72
|
// ============================================================
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const MARKETPLACES_DIR = join(homedir(), ".claude", "plugins", "marketplaces");
|
|
74
|
+
/** Claude Code's marketplace clones, resolved per call. */
|
|
75
|
+
const marketplacesDir = (): string =>
|
|
76
|
+
join(requireClaudeConfigDir(), "plugins", "marketplaces");
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
79
|
* Resolve a plugin ID to its filesystem source path.
|
|
@@ -92,7 +92,7 @@ async function resolvePluginPath(pluginId: string): Promise<string | null> {
|
|
|
92
92
|
if (!localPlugin?.source || typeof localPlugin.source !== "string")
|
|
93
93
|
return null;
|
|
94
94
|
|
|
95
|
-
const marketplacePath = join(
|
|
95
|
+
const marketplacePath = join(marketplacesDir(), mpName);
|
|
96
96
|
const pluginPath = join(
|
|
97
97
|
marketplacePath,
|
|
98
98
|
localPlugin.source.replace("./", ""),
|