magus-cli 0.0.0-placeholder → 7.0.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 +309 -0
- package/bin/magus.js +170 -0
- package/package.json +67 -3
- package/scripts/build-binaries.ts +113 -0
- package/scripts/capture-builtin.ts +412 -0
- package/scripts/check-optional-deps.ts +49 -0
- package/scripts/test-isolated.ts +49 -0
- package/scripts/verify-binary.ts +95 -0
- package/scripts/verify-community-registry.ts +272 -0
- package/src/__tests__/active-closure.test.ts +184 -0
- package/src/__tests__/alias-adopt.test.ts +365 -0
- package/src/__tests__/alias-parser.test.ts +417 -0
- package/src/__tests__/alias-shell-writer.test.ts +681 -0
- package/src/__tests__/alias-store.test.ts +163 -0
- package/src/__tests__/binary-signing.test.ts +120 -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__/cache-provenance.test.ts +84 -0
- package/src/__tests__/capture-builtin.test.ts +92 -0
- package/src/__tests__/catalog-cache-store.test.ts +309 -0
- package/src/__tests__/catalog-notice.test.ts +155 -0
- package/src/__tests__/cli-ansi.test.ts +297 -0
- package/src/__tests__/cli-apply-seams.test.ts +288 -0
- package/src/__tests__/cli-live.test.ts +391 -0
- package/src/__tests__/cli-router.test.ts +240 -0
- package/src/__tests__/cli-tool-commands.test.ts +239 -0
- package/src/__tests__/cli-tools-adapter.test.ts +294 -0
- package/src/__tests__/cli-update-view.test.ts +445 -0
- package/src/__tests__/community-fetch.test.ts +545 -0
- package/src/__tests__/community-registry.test.ts +269 -0
- package/src/__tests__/community-staleness.test.ts +720 -0
- package/src/__tests__/component-badge.test.ts +98 -0
- package/src/__tests__/component-icons.test.ts +94 -0
- package/src/__tests__/content-drift.test.ts +353 -0
- package/src/__tests__/conventions-integration.test.ts +830 -0
- package/src/__tests__/conventions-manager.test.ts +1236 -0
- package/src/__tests__/doctor-bins.test.ts +43 -0
- package/src/__tests__/doctor.test.ts +463 -0
- package/src/__tests__/dotenv.test.ts +195 -0
- package/src/__tests__/enabled-not-installed.test.ts +94 -0
- package/src/__tests__/file-locking.test.ts +208 -0
- 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__/footer-hints.test.ts +40 -0
- package/src/__tests__/format-command.test.ts +78 -0
- package/src/__tests__/git-worktree.test.ts +108 -0
- package/src/__tests__/github-budget.test.ts +206 -0
- package/src/__tests__/gitignore-check.test.ts +71 -0
- package/src/__tests__/gitignore-detector.test.ts +176 -0
- package/src/__tests__/gitignore-fixer.test.ts +317 -0
- package/src/__tests__/gitignore-resolver.test.ts +193 -0
- package/src/__tests__/gitignore-service.test.ts +137 -0
- package/src/__tests__/go-module.test.ts +43 -0
- package/src/__tests__/hook-import-policy.test.ts +90 -0
- package/src/__tests__/hook-process.test.ts +256 -0
- package/src/__tests__/hook-registration.test.ts +224 -0
- package/src/__tests__/install-command.test.ts +81 -0
- package/src/__tests__/install-plan.test.ts +104 -0
- package/src/__tests__/launcher-signal.test.ts +308 -0
- package/src/__tests__/local-marketplace-scan.test.ts +174 -0
- package/src/__tests__/manifest.test.ts +281 -0
- package/src/__tests__/markdown-renderer.test.ts +142 -0
- package/src/__tests__/marketplace-badge.test.ts +140 -0
- package/src/__tests__/marketplace-refresh.test.ts +335 -0
- package/src/__tests__/marketplace-version.test.ts +84 -0
- package/src/__tests__/marketplaces.test.ts +128 -0
- package/src/__tests__/mate-availability.test.ts +156 -0
- package/src/__tests__/mate-catalog.test.ts +295 -0
- package/src/__tests__/mcp-registry.test.ts +158 -0
- package/src/__tests__/model-visuals.test.tsx +2466 -0
- package/src/__tests__/models-adapter.test.ts +332 -0
- package/src/__tests__/models-cli.test.ts +273 -0
- package/src/__tests__/models-core.test.ts +802 -0
- package/src/__tests__/models-manager.test.ts +500 -0
- package/src/__tests__/models-presets-marketplace.test.ts +168 -0
- package/src/__tests__/models-screen-state.test.ts +315 -0
- package/src/__tests__/moved-marketplace.test.ts +86 -0
- package/src/__tests__/open-file.test.ts +59 -0
- package/src/__tests__/owned-marketplaces.test.ts +93 -0
- package/src/__tests__/plugin-checkout.test.ts +66 -0
- package/src/__tests__/plugin-cli-argv.test.ts +109 -0
- package/src/__tests__/plugin-contents.test.ts +371 -0
- package/src/__tests__/plugin-manager-fallback.test.ts +320 -0
- package/src/__tests__/plugin-releases.test.ts +98 -0
- package/src/__tests__/plugin-requires.test.ts +187 -0
- package/src/__tests__/plugin-row-text.test.ts +336 -0
- package/src/__tests__/plugin-setup.test.ts +1016 -0
- package/src/__tests__/profile-adopt.test.ts +177 -0
- package/src/__tests__/profile-command.test.ts +110 -0
- package/src/__tests__/profile-materializer.test.ts +238 -0
- package/src/__tests__/profile-sync.test.ts +154 -0
- package/src/__tests__/registry-version-resolution.test.ts +88 -0
- package/src/__tests__/resolve-executable.test.ts +42 -0
- package/src/__tests__/resolver.test.ts +351 -0
- package/src/__tests__/scope-action.test.ts +134 -0
- package/src/__tests__/scope-squares.test.tsx +164 -0
- package/src/__tests__/selection-scope.test.ts +100 -0
- package/src/__tests__/settings-file.test.ts +179 -0
- package/src/__tests__/shell-script-callers.test.ts +92 -0
- package/src/__tests__/skill-install-files.test.ts +136 -0
- package/src/__tests__/skills-catalog-client.test.ts +48 -0
- package/src/__tests__/style-wrap.test.ts +220 -0
- package/src/__tests__/styles-manager.test.ts +1098 -0
- package/src/__tests__/styles-origins.test.ts +414 -0
- package/src/__tests__/styles-screen-state.test.ts +457 -0
- package/src/__tests__/styles-status-line.test.ts +72 -0
- package/src/__tests__/styles-sync.test.ts +452 -0
- package/src/__tests__/symlink-manager.test.ts +180 -0
- package/src/__tests__/tabbar-layout.test.ts +100 -0
- package/src/__tests__/terminology-filler.test.ts +214 -0
- package/src/__tests__/theme-adaptive-colors.test.ts +354 -0
- package/src/__tests__/theme-env.test.ts +281 -0
- package/src/__tests__/theme-resolve.test.ts +380 -0
- package/src/__tests__/tool-probe.test.ts +1400 -0
- package/src/__tests__/toolchain.test.ts +232 -0
- package/src/__tests__/ui-version-writers.test.ts +88 -0
- package/src/__tests__/update-apply.test.ts +637 -0
- package/src/__tests__/update-plan.test.ts +1373 -0
- package/src/__tests__/uppercase-keybindings.test.ts +101 -0
- package/src/__tests__/useGitignoreModal.test.ts +79 -0
- package/src/__tests__/version-check-refresh.test.ts +45 -0
- package/src/__tests__/version-snapshot.test.ts +212 -0
- package/src/__tests__/version-unknown-state.test.ts +388 -0
- package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
- package/src/cli/ansi.ts +520 -0
- package/src/cli/bootstrap.ts +134 -0
- package/src/cli/doctor.ts +218 -0
- package/src/cli/hook.ts +129 -0
- package/src/cli/install.ts +382 -0
- package/src/cli/live.ts +374 -0
- package/src/cli/models.ts +297 -0
- package/src/cli/profile.ts +197 -0
- package/src/cli/prompt.ts +32 -0
- package/src/cli/router.ts +207 -0
- package/src/cli/update-view.ts +526 -0
- package/src/cli/update.ts +613 -0
- package/src/cli/upgrade.ts +97 -0
- package/src/data/alias-flags.ts +301 -0
- package/src/data/cli-tools.ts +146 -0
- package/src/data/community-styles.ts +531 -0
- package/src/data/gitignore-defaults.ts +33 -0
- package/src/data/gitignore-reasons.ts +93 -0
- package/src/data/gitignore-templates.ts +25 -0
- package/src/data/marketplaces.ts +278 -0
- package/src/data/mcp-servers.ts +51 -0
- package/src/data/models-presets.ts +329 -0
- package/src/data/predefined-profiles.ts +252 -0
- package/src/data/settings-catalog.ts +714 -0
- package/src/data/skill-repos.ts +91 -0
- package/src/data/styles/asd-ste100.md +38 -0
- package/src/data/styles/calibrated.md +24 -0
- package/src/data/styles/direct.md +23 -0
- package/src/data/styles/evidence-first.md +22 -0
- package/src/data/styles/explanatory.md +23 -0
- package/src/data/styles/index.ts +57 -0
- package/src/data/styles/no-slop.md +32 -0
- package/src/data/styles/plain-language.md +23 -0
- package/src/data/styles/structured.md +30 -0
- package/src/data/styles/terminology.md +28 -0
- package/src/data/styles/terse.md +20 -0
- package/src/main.tsx +110 -0
- package/src/markdown.d.ts +13 -0
- package/src/opentui.d.ts +203 -0
- package/src/services/active-closure.ts +117 -0
- package/src/services/alias-settings.ts +60 -0
- package/src/services/alias-shell-writer.ts +1244 -0
- package/src/services/alias-store.ts +172 -0
- package/src/services/binary-signing.ts +78 -0
- package/src/services/catalog-cache-store.ts +316 -0
- package/src/services/claude-cli.ts +304 -0
- package/src/services/claude-runner.ts +31 -0
- package/src/services/claude-settings.ts +1589 -0
- package/src/services/cli-tool-commands.ts +133 -0
- package/src/services/community-fetcher.ts +90 -0
- package/src/services/community-styles.ts +1194 -0
- package/src/services/content-drift.ts +208 -0
- package/src/services/conventions-manager.ts +945 -0
- package/src/services/doctor-bins.ts +63 -0
- package/src/services/doctor.ts +570 -0
- package/src/services/dotenv.ts +181 -0
- package/src/services/git-worktree.ts +129 -0
- package/src/services/github-budget.ts +274 -0
- package/src/services/gitignore-check.ts +63 -0
- package/src/services/gitignore-detector.ts +167 -0
- package/src/services/gitignore-fixer.ts +280 -0
- package/src/services/gitignore-resolver.ts +208 -0
- package/src/services/gitignore-service.ts +167 -0
- package/src/services/hook-registration.ts +218 -0
- package/src/services/install-plan.ts +107 -0
- package/src/services/local-marketplace.ts +587 -0
- package/src/services/manifest.ts +250 -0
- package/src/services/marketplace-catalog-git.ts +173 -0
- package/src/services/marketplace-catalog.ts +129 -0
- package/src/services/marketplace-fetcher.ts +400 -0
- package/src/services/marketplace-refresh.ts +253 -0
- package/src/services/marketplace-sync.ts +145 -0
- package/src/services/mate-availability.ts +133 -0
- package/src/services/mate-catalog.ts +265 -0
- package/src/services/mcp-registry.ts +171 -0
- package/src/services/models-core.ts +969 -0
- package/src/services/models-manager.ts +606 -0
- package/src/services/plugin-checkout.ts +186 -0
- package/src/services/plugin-manager.ts +878 -0
- package/src/services/plugin-mcp-config.ts +242 -0
- package/src/services/plugin-releases.ts +47 -0
- package/src/services/plugin-requires.ts +227 -0
- package/src/services/plugin-setup.ts +965 -0
- package/src/services/profile-adopt.ts +206 -0
- package/src/services/profile-materializer.ts +133 -0
- package/src/services/profile-sync.ts +158 -0
- package/src/services/profiles.ts +225 -0
- package/src/services/resolver.ts +344 -0
- package/src/services/settings-file.ts +69 -0
- package/src/services/settings-manager.ts +307 -0
- package/src/services/skills-manager.ts +1111 -0
- package/src/services/skillsmp-client.ts +193 -0
- package/src/services/styles-manager.ts +1294 -0
- package/src/services/symlink-manager.ts +189 -0
- package/src/services/terminology-filler.ts +266 -0
- package/src/services/tool-probe.ts +1637 -0
- package/src/services/toolchain.ts +431 -0
- package/src/services/update-engine.ts +691 -0
- package/src/services/update-plan.ts +863 -0
- package/src/services/version-check.ts +127 -0
- package/src/services/version-snapshot.ts +189 -0
- package/src/tui.tsx +112 -0
- package/src/types/bun.d.ts +27 -0
- package/src/types/gitignore.ts +68 -0
- package/src/types/index.ts +447 -0
- package/src/ui/App.tsx +538 -0
- package/src/ui/adapters/catalogNotice.ts +122 -0
- package/src/ui/adapters/cliToolsAdapter.ts +176 -0
- package/src/ui/adapters/modelsAdapter.ts +213 -0
- package/src/ui/adapters/pluginsAdapter.ts +413 -0
- package/src/ui/adapters/settingsAdapter.ts +168 -0
- package/src/ui/adapters/skillsAdapter.ts +229 -0
- package/src/ui/adapters/stylesAdapter.ts +403 -0
- package/src/ui/components/CategoryHeader.tsx +64 -0
- package/src/ui/components/EmptyFilterState.tsx +32 -0
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScopeIndicator.tsx +63 -0
- package/src/ui/components/ScrollableList.tsx +95 -0
- package/src/ui/components/SearchInput.tsx +57 -0
- package/src/ui/components/StyledText.tsx +75 -0
- package/src/ui/components/TabBar.tsx +101 -0
- package/src/ui/components/layout/FooterHints.tsx +78 -0
- package/src/ui/components/layout/Panel.tsx +63 -0
- package/src/ui/components/layout/ProgressBar.tsx +44 -0
- package/src/ui/components/layout/ScopeTabs.tsx +54 -0
- package/src/ui/components/layout/ScreenLayout.tsx +321 -0
- package/src/ui/components/layout/index.ts +9 -0
- package/src/ui/components/modals/ConfirmModal.tsx +62 -0
- package/src/ui/components/modals/InputModal.tsx +75 -0
- package/src/ui/components/modals/LoadingModal.tsx +51 -0
- package/src/ui/components/modals/MessageModal.tsx +66 -0
- package/src/ui/components/modals/ModalContainer.tsx +159 -0
- package/src/ui/components/modals/SelectModal.tsx +90 -0
- package/src/ui/components/modals/index.ts +6 -0
- package/src/ui/components/primitives/ActionHints.tsx +44 -0
- package/src/ui/components/primitives/DetailSection.tsx +23 -0
- package/src/ui/components/primitives/KeyValueLine.tsx +26 -0
- package/src/ui/components/primitives/ListCategoryRow.tsx +45 -0
- package/src/ui/components/primitives/MetaText.tsx +40 -0
- package/src/ui/components/primitives/ScopeDetail.tsx +67 -0
- package/src/ui/components/primitives/ScopeSquares.tsx +58 -0
- package/src/ui/components/primitives/SelectableRow.tsx +30 -0
- package/src/ui/components/primitives/index.ts +9 -0
- package/src/ui/hooks/index.ts +2 -0
- package/src/ui/hooks/useGitignoreModal.ts +95 -0
- package/src/ui/hooks/useKeyboard.ts +24 -0
- package/src/ui/hooks/useKeyboardHandler.ts +63 -0
- package/src/ui/registry.ts +33 -0
- package/src/ui/renderers/cliToolRenderers.tsx +326 -0
- package/src/ui/renderers/gitignoreRenderers.tsx +80 -0
- package/src/ui/renderers/markdownRenderer.tsx +302 -0
- package/src/ui/renderers/mcpRenderers.tsx +149 -0
- package/src/ui/renderers/modelRenderers.tsx +1309 -0
- package/src/ui/renderers/modelVisuals.tsx +1402 -0
- package/src/ui/renderers/pluginRenderers.tsx +1089 -0
- package/src/ui/renderers/pluginRowText.ts +70 -0
- package/src/ui/renderers/pluginStatusText.ts +56 -0
- package/src/ui/renderers/profileRenderers.tsx +406 -0
- package/src/ui/renderers/settingsRenderers.tsx +218 -0
- package/src/ui/renderers/skillRenderers.tsx +479 -0
- package/src/ui/renderers/styleRenderers.tsx +813 -0
- package/src/ui/screens/AliasScreen.tsx +1558 -0
- package/src/ui/screens/CliToolsScreen.tsx +367 -0
- package/src/ui/screens/EnvVarsScreen.tsx +221 -0
- package/src/ui/screens/GitignoreScreen.tsx +562 -0
- package/src/ui/screens/McpRegistryScreen.tsx +366 -0
- package/src/ui/screens/McpScreen.tsx +256 -0
- package/src/ui/screens/ModelsScreen.tsx +542 -0
- package/src/ui/screens/PluginsScreen.tsx +1464 -0
- package/src/ui/screens/ProfilesScreen.tsx +438 -0
- package/src/ui/screens/SkillsScreen.tsx +853 -0
- package/src/ui/screens/StylesScreen.tsx +1064 -0
- package/src/ui/screens/index.ts +11 -0
- package/src/ui/state/AppContext.tsx +233 -0
- package/src/ui/state/DimensionsContext.tsx +154 -0
- package/src/ui/state/reducer.ts +810 -0
- package/src/ui/state/types.ts +503 -0
- package/src/ui/theme-mode.ts +352 -0
- package/src/ui/theme-resolve.ts +147 -0
- package/src/ui/theme.ts +175 -0
- package/src/utils/clipboard.ts +58 -0
- package/src/utils/command-utils.ts +144 -0
- package/src/utils/config-dir.ts +45 -0
- package/src/utils/file-locking.ts +146 -0
- package/src/utils/fuzzy-search.ts +138 -0
- package/src/utils/go-module.ts +36 -0
- package/src/utils/open-file.ts +84 -0
- package/src/utils/run.ts +67 -0
- package/src/utils/string-utils.ts +88 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Team profile manifest (.claude/profiles.json) — read, migrate, validate.
|
|
3
|
+
*
|
|
4
|
+
* This is the v2 view over the same project-scoped profiles.json that the TUI's
|
|
5
|
+
* v1 saved-profiles code (services/profiles.ts) reads. v1 files migrate forward
|
|
6
|
+
* transparently: a v1 profile (`{name, plugins: Record<string,boolean>}`) is a
|
|
7
|
+
* structural subset of a v2 manifest entry, so migration is a version bump.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import fs from "fs-extra";
|
|
12
|
+
import type { ProfileManifest, ProfileManifestEntry } from "../types/index.js";
|
|
13
|
+
import { validateModelsConfig } from "./models-core.js";
|
|
14
|
+
|
|
15
|
+
const PROFILES_FILE = "profiles.json";
|
|
16
|
+
|
|
17
|
+
/** Project-scoped manifest path: <projectPath|cwd>/.claude/profiles.json */
|
|
18
|
+
export function getManifestPath(projectPath?: string): string {
|
|
19
|
+
const base = projectPath ?? process.cwd();
|
|
20
|
+
return path.join(base, ".claude", PROFILES_FILE);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** An empty, valid v2 manifest. */
|
|
24
|
+
export function emptyManifest(): ProfileManifest {
|
|
25
|
+
return { version: 2, profiles: {} };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Normalize any parsed profiles.json (v1 or v2) into a v2 manifest.
|
|
30
|
+
* Unknown/missing shapes degrade to an empty manifest rather than throwing —
|
|
31
|
+
* validation is the caller's job via {@link validateManifest}.
|
|
32
|
+
*/
|
|
33
|
+
export function migrateToV2(raw: unknown): ProfileManifest {
|
|
34
|
+
if (!raw || typeof raw !== "object") return emptyManifest();
|
|
35
|
+
const obj = raw as Record<string, unknown>;
|
|
36
|
+
|
|
37
|
+
const profilesRaw =
|
|
38
|
+
obj.profiles && typeof obj.profiles === "object"
|
|
39
|
+
? (obj.profiles as Record<string, unknown>)
|
|
40
|
+
: {};
|
|
41
|
+
|
|
42
|
+
const profiles: Record<string, ProfileManifestEntry> = {};
|
|
43
|
+
for (const [id, entry] of Object.entries(profilesRaw)) {
|
|
44
|
+
if (entry && typeof entry === "object") {
|
|
45
|
+
// v1 and v2 entries are both accepted as-is; v1's plugins map
|
|
46
|
+
// (Record<string, boolean>) is valid under v2's widened type.
|
|
47
|
+
profiles[id] = entry as ProfileManifestEntry;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const manifest: ProfileManifest = { version: 2, profiles };
|
|
52
|
+
// Only carry strictVersions when enabled, so a committed manifest that
|
|
53
|
+
// doesn't opt in stays minimal (no spurious `"strictVersions": false`).
|
|
54
|
+
if (obj.strictVersions === true) manifest.strictVersions = true;
|
|
55
|
+
return manifest;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Read and migrate the project manifest. Returns an empty v2 manifest if the
|
|
60
|
+
* file is absent or unreadable.
|
|
61
|
+
*/
|
|
62
|
+
export async function readManifest(
|
|
63
|
+
projectPath?: string,
|
|
64
|
+
): Promise<ProfileManifest> {
|
|
65
|
+
const filePath = getManifestPath(projectPath);
|
|
66
|
+
try {
|
|
67
|
+
if (await fs.pathExists(filePath)) {
|
|
68
|
+
return migrateToV2(await fs.readJson(filePath));
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
// Corrupt/unreadable — fall through to empty.
|
|
72
|
+
}
|
|
73
|
+
return emptyManifest();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Write the manifest back to disk (used by `profile sync`, tests). */
|
|
77
|
+
export async function writeManifest(
|
|
78
|
+
manifest: ProfileManifest,
|
|
79
|
+
projectPath?: string,
|
|
80
|
+
): Promise<void> {
|
|
81
|
+
const filePath = getManifestPath(projectPath);
|
|
82
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
83
|
+
await fs.writeJson(filePath, manifest, { spaces: 2 });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ManifestValidationError {
|
|
87
|
+
/** Dotted path to the offending field, e.g. "profiles.frontend.plugins". */
|
|
88
|
+
path: string;
|
|
89
|
+
message: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Validate a manifest structurally. Returns an empty array when valid.
|
|
94
|
+
* Strict enough to back `install --check`; does not touch the network or FS.
|
|
95
|
+
*/
|
|
96
|
+
export function validateManifest(
|
|
97
|
+
manifest: ProfileManifest,
|
|
98
|
+
): ManifestValidationError[] {
|
|
99
|
+
const errors: ManifestValidationError[] = [];
|
|
100
|
+
|
|
101
|
+
if (manifest.version !== 2) {
|
|
102
|
+
errors.push({
|
|
103
|
+
path: "version",
|
|
104
|
+
message: `expected version 2, got ${JSON.stringify(manifest.version)}`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const ids = Object.keys(manifest.profiles ?? {});
|
|
109
|
+
if (ids.length === 0) {
|
|
110
|
+
errors.push({ path: "profiles", message: "no profiles defined" });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const [id, entry] of Object.entries(manifest.profiles ?? {})) {
|
|
114
|
+
const base = `profiles.${id}`;
|
|
115
|
+
if (!entry || typeof entry !== "object") {
|
|
116
|
+
errors.push({ path: base, message: "profile must be an object" });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (!entry.name || typeof entry.name !== "string") {
|
|
120
|
+
errors.push({ path: `${base}.name`, message: "missing name" });
|
|
121
|
+
}
|
|
122
|
+
if (entry.plugins !== undefined) {
|
|
123
|
+
for (const [pluginId, ver] of Object.entries(entry.plugins)) {
|
|
124
|
+
if (typeof ver !== "string" && typeof ver !== "boolean") {
|
|
125
|
+
errors.push({
|
|
126
|
+
path: `${base}.plugins.${pluginId}`,
|
|
127
|
+
message: "version must be a string or boolean",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (!pluginId.includes("@")) {
|
|
131
|
+
errors.push({
|
|
132
|
+
path: `${base}.plugins.${pluginId}`,
|
|
133
|
+
message: "plugin id must be in the form name@marketplace",
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (entry.marketplaces !== undefined) {
|
|
139
|
+
for (const [mpName, ref] of Object.entries(entry.marketplaces)) {
|
|
140
|
+
if (ref.source === "github" && !ref.repo) {
|
|
141
|
+
errors.push({
|
|
142
|
+
path: `${base}.marketplaces.${mpName}`,
|
|
143
|
+
message: "github marketplace requires `repo`",
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (ref.source === "directory" && !ref.path) {
|
|
147
|
+
errors.push({
|
|
148
|
+
path: `${base}.marketplaces.${mpName}`,
|
|
149
|
+
message: "directory marketplace requires `path`",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (entry.skills !== undefined) {
|
|
155
|
+
entry.skills.forEach((s, i) => {
|
|
156
|
+
if (!s.name || !s.repo || !s.path) {
|
|
157
|
+
errors.push({
|
|
158
|
+
path: `${base}.skills[${i}]`,
|
|
159
|
+
message: "skill requires name, repo, and path",
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
// Model routing is the one field here whose bad value does not degrade:
|
|
165
|
+
// MEASURED (benches/agent-model-routing, `bogus-model` 5/5), a model the
|
|
166
|
+
// Agent tool does not accept refuses the spawn outright, so EVERY subagent
|
|
167
|
+
// dispatch fails for everyone who pulled the manifest. This is the gate
|
|
168
|
+
// that stops a hand-edited entry reaching a session.
|
|
169
|
+
if (entry.models !== undefined) {
|
|
170
|
+
for (const e of validateModelsConfig(entry.models)) {
|
|
171
|
+
errors.push({
|
|
172
|
+
path: e.path ? `${base}.models.${e.path}` : `${base}.models`,
|
|
173
|
+
message: e.message,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return errors;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Read/modify/write ONE profile entry in the committed manifest.
|
|
184
|
+
*
|
|
185
|
+
* Returns false when there is nothing to record against (no profile named, no
|
|
186
|
+
* manifest file, or no entry for that profile) — all three are normal states,
|
|
187
|
+
* and `mutate` is not called.
|
|
188
|
+
*
|
|
189
|
+
* THROWS when the manifest exists and cannot be read as JSON. `readManifest`
|
|
190
|
+
* degrades a corrupt file to an empty manifest, which here would mean silently
|
|
191
|
+
* recording nothing and reporting success — the caller has just written a live
|
|
192
|
+
* setting the next `magus install` will revert, and would never learn.
|
|
193
|
+
*/
|
|
194
|
+
export async function updateProfileEntry(
|
|
195
|
+
projectPath: string,
|
|
196
|
+
profile: string | null,
|
|
197
|
+
mutate: (entry: ProfileManifestEntry) => void,
|
|
198
|
+
): Promise<boolean> {
|
|
199
|
+
if (!profile) return false;
|
|
200
|
+
const filePath = getManifestPath(projectPath);
|
|
201
|
+
if (!(await fs.pathExists(filePath))) return false;
|
|
202
|
+
|
|
203
|
+
// Read directly rather than through readManifest: its `catch` is exactly
|
|
204
|
+
// what turns a corrupt file into a silent no-op.
|
|
205
|
+
let raw: unknown;
|
|
206
|
+
try {
|
|
207
|
+
raw = await fs.readJson(filePath);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`${filePath} is not valid JSON, refusing to write to it: ${String(error)}`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const manifest = migrateToV2(raw);
|
|
215
|
+
const entry = manifest.profiles[profile];
|
|
216
|
+
if (!entry) return false;
|
|
217
|
+
|
|
218
|
+
mutate(entry);
|
|
219
|
+
entry.updatedAt = new Date().toISOString();
|
|
220
|
+
await writeManifest(manifest, projectPath);
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Merge `patch` into `profiles.<profile>.settings`, optionally removing the
|
|
226
|
+
* keys named in `unset`.
|
|
227
|
+
*
|
|
228
|
+
* Why this exists: `.claude/settings.json` is a symlink into the active
|
|
229
|
+
* profile's materialized directory, so a live write lands in the right place —
|
|
230
|
+
* but `materializeProfile` REWRITES that file from the manifest on every
|
|
231
|
+
* `magus install`, so the live write alone silently regresses. The manifest
|
|
232
|
+
* is the durable half of every settings write magus makes.
|
|
233
|
+
*/
|
|
234
|
+
export async function mergeManifestSettings(
|
|
235
|
+
projectPath: string,
|
|
236
|
+
profile: string | null,
|
|
237
|
+
patch: Record<string, unknown>,
|
|
238
|
+
opts: { unset?: readonly string[] } = {},
|
|
239
|
+
): Promise<boolean> {
|
|
240
|
+
return updateProfileEntry(projectPath, profile, (entry) => {
|
|
241
|
+
const settings = { ...(entry.settings ?? {}), ...patch };
|
|
242
|
+
for (const key of opts.unset ?? []) {
|
|
243
|
+
// The key must be ABSENT, not present-and-undefined: an `= undefined`
|
|
244
|
+
// assignment still answers `in` checks, so the next call would read it
|
|
245
|
+
// as already handled and skip work it still owes.
|
|
246
|
+
delete settings[key];
|
|
247
|
+
}
|
|
248
|
+
entry.settings = settings;
|
|
249
|
+
});
|
|
250
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* marketplace-catalog-git.ts — read a marketplace's CURRENT catalog over git,
|
|
3
|
+
* without touching the checked-out clone.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists
|
|
6
|
+
* ---------------
|
|
7
|
+
* `autoUpdate: false` is deliberate policy here: plugins are not updated
|
|
8
|
+
* automatically, the UI merely shows that an update is available. That freezes the
|
|
9
|
+
* clone at `~/.claude/plugins/marketplaces/<name>`, which is correct — the clone
|
|
10
|
+
* is the INSTALL SOURCE, and pinning it is the point.
|
|
11
|
+
*
|
|
12
|
+
* The mistake was using that same frozen clone as the CATALOG — the answer to
|
|
13
|
+
* "what version exists upstream". Those are two different jobs riding one
|
|
14
|
+
* artifact, and pinning the first silently blinded the second: the clone said
|
|
15
|
+
* terminal 4.1.4, the installed copy was 4.1.4, so the screen reported up to date
|
|
16
|
+
* while 4.2.0 had shipped.
|
|
17
|
+
*
|
|
18
|
+
* `git fetch` updates `refs/remotes/origin/*` and nothing else. The working tree
|
|
19
|
+
* does not move, HEAD does not move, so installs stay pinned exactly as the policy
|
|
20
|
+
* intends — and `git show origin/<branch>:.claude-plugin/marketplace.json` then
|
|
21
|
+
* reads the real upstream catalog. Verified 2026-08-17 against the magus clone:
|
|
22
|
+
* catalog read as 9.0.3 / terminal 4.2.0 while HEAD stayed at c45ddee.
|
|
23
|
+
*
|
|
24
|
+
* It also sidesteps the reason the HTTP path was failing. `raw.githubusercontent.com`
|
|
25
|
+
* was returning 429; git talks the smart-transport protocol over SSH or HTTPS and
|
|
26
|
+
* draws on neither that bucket nor the REST budget. So the version check keeps
|
|
27
|
+
* working precisely when the HTTP catalog cannot.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { spawn } from "node:child_process";
|
|
31
|
+
import fs from "node:fs";
|
|
32
|
+
import path from "node:path";
|
|
33
|
+
import { claudeConfigDirOrNull } from "../utils/config-dir.js";
|
|
34
|
+
import {
|
|
35
|
+
type MarketplacePlugin,
|
|
36
|
+
normalizeCatalogJson,
|
|
37
|
+
} from "./marketplace-catalog.js";
|
|
38
|
+
|
|
39
|
+
/** Network reach; a wedged fetch must never hold up the screen. */
|
|
40
|
+
const FETCH_TIMEOUT_MS = 15_000;
|
|
41
|
+
/** Local object reads are fast or broken. */
|
|
42
|
+
const LOCAL_TIMEOUT_MS = 5_000;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolved per call, honouring CLAUDE_CONFIG_DIR — the same override Claude Code
|
|
46
|
+
* uses, and the same approach content-drift.ts takes for the same directory.
|
|
47
|
+
*
|
|
48
|
+
* Returns null when a test has not chosen a config dir, so an unisolated test
|
|
49
|
+
* cannot make this function perform a live `git fetch` against the operator's real
|
|
50
|
+
* marketplace clones. See utils/config-dir.ts.
|
|
51
|
+
*/
|
|
52
|
+
function cloneDir(name: string): string | null {
|
|
53
|
+
const configDir = claudeConfigDirOrNull();
|
|
54
|
+
if (!configDir) return null;
|
|
55
|
+
return path.join(configDir, "plugins", "marketplaces", name);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function git(
|
|
59
|
+
cwd: string,
|
|
60
|
+
args: string[],
|
|
61
|
+
timeoutMs: number,
|
|
62
|
+
): Promise<{ ok: boolean; out: string }> {
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
const child = spawn("git", args, {
|
|
65
|
+
cwd,
|
|
66
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
67
|
+
timeout: timeoutMs,
|
|
68
|
+
killSignal: "SIGKILL",
|
|
69
|
+
env: {
|
|
70
|
+
...process.env,
|
|
71
|
+
// Never let git stop for credentials: this runs inside a TUI with no
|
|
72
|
+
// way to answer a prompt, and a private marketplace we cannot read
|
|
73
|
+
// should degrade to "unknown", not hang the screen forever.
|
|
74
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
75
|
+
GIT_ASKPASS: "echo",
|
|
76
|
+
SSH_ASKPASS: "echo",
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
let out = "";
|
|
80
|
+
child.stdout.on("data", (d: Buffer | string) => {
|
|
81
|
+
out += String(d);
|
|
82
|
+
});
|
|
83
|
+
child.on("error", () => resolve({ ok: false, out: "" }));
|
|
84
|
+
child.on("close", (code: number | null) =>
|
|
85
|
+
resolve({ ok: code === 0, out }),
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Session cache. A catalog cannot change under us mid-session unless the user
|
|
92
|
+
* explicitly refreshes, and each miss costs a network round trip per marketplace.
|
|
93
|
+
*/
|
|
94
|
+
const catalogCache = new Map<string, MarketplacePlugin[] | null>();
|
|
95
|
+
|
|
96
|
+
export function clearGitCatalogCache(): void {
|
|
97
|
+
catalogCache.clear();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The branch `origin/HEAD` points at, falling back to `main`. */
|
|
101
|
+
async function defaultBranch(dir: string): Promise<string> {
|
|
102
|
+
const ref = await git(
|
|
103
|
+
dir,
|
|
104
|
+
["symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
|
|
105
|
+
LOCAL_TIMEOUT_MS,
|
|
106
|
+
);
|
|
107
|
+
const name = ref.out.trim();
|
|
108
|
+
// "origin/main" → "main"
|
|
109
|
+
if (ref.ok && name.startsWith("origin/")) return name.slice("origin/".length);
|
|
110
|
+
return "main";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The marketplace's upstream catalog, or null when it cannot be read honestly.
|
|
115
|
+
*
|
|
116
|
+
* Null covers: no clone on disk, not a git repo, no reachable remote, fetch
|
|
117
|
+
* refused, or a catalog that does not parse. Every one of those must stay
|
|
118
|
+
* distinguishable from "an empty catalog" — collapsing them is the bug this
|
|
119
|
+
* module was written to help fix.
|
|
120
|
+
*/
|
|
121
|
+
export async function readCatalogFromRemote(
|
|
122
|
+
marketplaceName: string,
|
|
123
|
+
): Promise<MarketplacePlugin[] | null> {
|
|
124
|
+
if (catalogCache.has(marketplaceName)) {
|
|
125
|
+
return catalogCache.get(marketplaceName) ?? null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const dir = cloneDir(marketplaceName);
|
|
129
|
+
if (!dir || !fs.existsSync(path.join(dir, ".git"))) {
|
|
130
|
+
catalogCache.set(marketplaceName, null);
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const branch = await defaultBranch(dir);
|
|
135
|
+
|
|
136
|
+
// Refs only. No merge, no checkout, no working-tree change — this is what
|
|
137
|
+
// makes the read compatible with `autoUpdate: false`.
|
|
138
|
+
const fetched = await git(
|
|
139
|
+
dir,
|
|
140
|
+
["fetch", "--quiet", "origin", branch],
|
|
141
|
+
FETCH_TIMEOUT_MS,
|
|
142
|
+
);
|
|
143
|
+
if (!fetched.ok) {
|
|
144
|
+
// A failed fetch may still leave a usable-but-older origin ref from a
|
|
145
|
+
// previous fetch. That is strictly better than the working tree, but it is
|
|
146
|
+
// not current, so it is not offered here — the caller's clone fallback
|
|
147
|
+
// already covers "possibly stale", and labels it as such.
|
|
148
|
+
catalogCache.set(marketplaceName, null);
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const shown = await git(
|
|
153
|
+
dir,
|
|
154
|
+
["show", `origin/${branch}:.claude-plugin/marketplace.json`],
|
|
155
|
+
LOCAL_TIMEOUT_MS,
|
|
156
|
+
);
|
|
157
|
+
if (!shown.ok) {
|
|
158
|
+
catalogCache.set(marketplaceName, null);
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const plugins = normalizeCatalogJson(
|
|
164
|
+
marketplaceName,
|
|
165
|
+
JSON.parse(shown.out),
|
|
166
|
+
);
|
|
167
|
+
catalogCache.set(marketplaceName, plugins);
|
|
168
|
+
return plugins;
|
|
169
|
+
} catch {
|
|
170
|
+
catalogCache.set(marketplaceName, null);
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* marketplace-catalog.ts — the catalog shape and its parser, transport-agnostic.
|
|
3
|
+
*
|
|
4
|
+
* A marketplace catalog can arrive two ways: over HTTP from
|
|
5
|
+
* raw.githubusercontent.com (`marketplace-fetcher.ts`) or out of a git remote ref
|
|
6
|
+
* (`marketplace-catalog-git.ts`). Both must produce byte-identical results — a
|
|
7
|
+
* catalog that parsed differently depending on how it was fetched would make
|
|
8
|
+
* version comparisons depend on the transport, which is the same class of bug as
|
|
9
|
+
* the one that made a failed fetch read as "up to date".
|
|
10
|
+
*
|
|
11
|
+
* This module exists so the two transports share one parser without importing
|
|
12
|
+
* each other. The fetcher needs the git reader as a fallback and the git reader
|
|
13
|
+
* needs the parser, so putting the parser in either one would make them mutually
|
|
14
|
+
* dependent.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { PluginRelease } from "../types/index.js";
|
|
18
|
+
import { normalizeReleases } from "./plugin-releases.js";
|
|
19
|
+
|
|
20
|
+
export interface MarketplacePlugin {
|
|
21
|
+
name: string;
|
|
22
|
+
version: string | null;
|
|
23
|
+
description: string;
|
|
24
|
+
category?: string;
|
|
25
|
+
author?: { name: string; email?: string };
|
|
26
|
+
homepage?: string;
|
|
27
|
+
tags?: string[];
|
|
28
|
+
releases?: PluginRelease[];
|
|
29
|
+
/**
|
|
30
|
+
* The plugin's directory inside the marketplace repo, as declared. "" means
|
|
31
|
+
* the repo root, which is how every single-plugin marketplace publishes
|
|
32
|
+
* ("source": "./").
|
|
33
|
+
*/
|
|
34
|
+
source?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Set only when the plugin declares an object source naming a repo other
|
|
37
|
+
* than the marketplace's own. Its files live there, not in the marketplace.
|
|
38
|
+
*/
|
|
39
|
+
sourceRepo?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Session-level cache for each marketplace's declared version (from
|
|
43
|
+
// marketplace.json's top-level `metadata.version`), so the TUI can surface a
|
|
44
|
+
// marketplace's version without re-fetching. Only populated for marketplaces that
|
|
45
|
+
// actually declare one.
|
|
46
|
+
const marketplaceVersionCache = new Map<string, string>();
|
|
47
|
+
|
|
48
|
+
export function clearMarketplaceVersionCache(): void {
|
|
49
|
+
marketplaceVersionCache.clear();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Returns the version a marketplace declared in its `metadata.version`, if one
|
|
54
|
+
* was seen during a successful read this session. Returns `undefined` for
|
|
55
|
+
* marketplaces that declare no version (or haven't been read yet).
|
|
56
|
+
*/
|
|
57
|
+
export function getMarketplaceVersion(
|
|
58
|
+
marketplaceName: string,
|
|
59
|
+
): string | undefined {
|
|
60
|
+
return marketplaceVersionCache.get(marketplaceName);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface RawPlugin {
|
|
64
|
+
name: string;
|
|
65
|
+
version?: string | null;
|
|
66
|
+
description?: string;
|
|
67
|
+
category?: string;
|
|
68
|
+
author?: { name: string; email?: string };
|
|
69
|
+
homepage?: string;
|
|
70
|
+
tags?: string[];
|
|
71
|
+
releases?: unknown;
|
|
72
|
+
source?: string | { source?: string; repo?: string };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Split a manifest `source` into a repo-relative directory and, when the plugin
|
|
77
|
+
* points at a different repo than the marketplace, that repo.
|
|
78
|
+
*
|
|
79
|
+
* Both forms are in use: a string path ("./", "./plugins/dev") and an object
|
|
80
|
+
* ({"source":"github","repo":"owner/name"}), which addyosmani/agent-skills
|
|
81
|
+
* publishes. The object form has no path — the plugin is the whole repo.
|
|
82
|
+
*/
|
|
83
|
+
function splitSource(source: RawPlugin["source"]): {
|
|
84
|
+
source?: string;
|
|
85
|
+
sourceRepo?: string;
|
|
86
|
+
} {
|
|
87
|
+
if (typeof source === "string") {
|
|
88
|
+
return { source: source.replace(/^\.\//, "").replace(/\/+$/, "") };
|
|
89
|
+
}
|
|
90
|
+
if (source && typeof source === "object" && typeof source.repo === "string") {
|
|
91
|
+
return { source: "", sourceRepo: source.repo };
|
|
92
|
+
}
|
|
93
|
+
return {};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Parse a `marketplace.json` payload into plugin entries. */
|
|
97
|
+
export function normalizeCatalogJson(
|
|
98
|
+
marketplaceName: string,
|
|
99
|
+
raw: unknown,
|
|
100
|
+
): MarketplacePlugin[] {
|
|
101
|
+
const data = (raw ?? {}) as {
|
|
102
|
+
plugins?: RawPlugin[];
|
|
103
|
+
metadata?: { version?: string };
|
|
104
|
+
};
|
|
105
|
+
const plugins: MarketplacePlugin[] = [];
|
|
106
|
+
|
|
107
|
+
const version = data.metadata?.version;
|
|
108
|
+
if (typeof version === "string" && version.length > 0) {
|
|
109
|
+
marketplaceVersionCache.set(marketplaceName, version);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (data.plugins && Array.isArray(data.plugins)) {
|
|
113
|
+
for (const plugin of data.plugins) {
|
|
114
|
+
plugins.push({
|
|
115
|
+
name: plugin.name,
|
|
116
|
+
version: plugin.version || null,
|
|
117
|
+
description: plugin.description || "",
|
|
118
|
+
category: plugin.category,
|
|
119
|
+
author: plugin.author,
|
|
120
|
+
homepage: plugin.homepage,
|
|
121
|
+
tags: plugin.tags,
|
|
122
|
+
releases: normalizeReleases(plugin.releases),
|
|
123
|
+
...splitSource(plugin.source),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return plugins;
|
|
129
|
+
}
|