magus-cli 0.0.0-placeholder → 7.1.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 +725 -0
- package/src/__tests__/blackbox-plugin-state.test.ts +545 -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 +524 -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 +248 -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 +340 -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__/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 +345 -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 +310 -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 +1396 -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 +403 -0
- package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
- package/src/cli/ansi.ts +528 -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 +264 -0
- package/src/cli/update-view.ts +717 -0
- package/src/cli/update.ts +600 -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 +1564 -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 +657 -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 +245 -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 +847 -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 +377 -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 +820 -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 +535 -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 +1423 -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 +440 -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 +149 -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,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple fuzzy search utility for filtering lists
|
|
3
|
+
* Uses a scoring algorithm that rewards:
|
|
4
|
+
* - Consecutive character matches
|
|
5
|
+
* - Matches at word boundaries
|
|
6
|
+
* - Matches at start of string
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface FuzzyMatch<T> {
|
|
10
|
+
item: T;
|
|
11
|
+
score: number;
|
|
12
|
+
matches: number[]; // indices of matched characters
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Calculate fuzzy match score for a query against a target string
|
|
17
|
+
* Returns null if no match, otherwise returns score and match indices
|
|
18
|
+
*/
|
|
19
|
+
export function fuzzyMatch(
|
|
20
|
+
query: string,
|
|
21
|
+
target: string,
|
|
22
|
+
): { score: number; matches: number[] } | null {
|
|
23
|
+
if (!query) return { score: 1, matches: [] };
|
|
24
|
+
|
|
25
|
+
const queryLower = query.toLowerCase();
|
|
26
|
+
const targetLower = target.toLowerCase();
|
|
27
|
+
|
|
28
|
+
let queryIdx = 0;
|
|
29
|
+
let score = 0;
|
|
30
|
+
const matches: number[] = [];
|
|
31
|
+
let prevMatchIdx = -1;
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < targetLower.length && queryIdx < queryLower.length; i++) {
|
|
34
|
+
if (targetLower[i] === queryLower[queryIdx]) {
|
|
35
|
+
matches.push(i);
|
|
36
|
+
|
|
37
|
+
// Bonus for consecutive matches
|
|
38
|
+
if (prevMatchIdx === i - 1) {
|
|
39
|
+
score += 5;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Bonus for matching at word boundary
|
|
43
|
+
if (i === 0 || /[\s\-_.]/.test(target[i - 1])) {
|
|
44
|
+
score += 10;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Bonus for matching at start
|
|
48
|
+
if (i === 0) {
|
|
49
|
+
score += 15;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Base score for each match
|
|
53
|
+
score += 1;
|
|
54
|
+
|
|
55
|
+
prevMatchIdx = i;
|
|
56
|
+
queryIdx++;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// All query characters must be found
|
|
61
|
+
if (queryIdx !== queryLower.length) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Penalty for longer strings (prefer shorter, more precise matches)
|
|
66
|
+
score -= Math.floor(target.length / 10);
|
|
67
|
+
|
|
68
|
+
return { score, matches };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Filter and sort items by fuzzy match score
|
|
73
|
+
*/
|
|
74
|
+
export function fuzzyFilter<T>(
|
|
75
|
+
items: T[],
|
|
76
|
+
query: string,
|
|
77
|
+
getText: (item: T) => string,
|
|
78
|
+
): FuzzyMatch<T>[] {
|
|
79
|
+
if (!query.trim()) {
|
|
80
|
+
return items.map((item) => ({ item, score: 1, matches: [] }));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const results: FuzzyMatch<T>[] = [];
|
|
84
|
+
|
|
85
|
+
for (const item of items) {
|
|
86
|
+
const text = getText(item);
|
|
87
|
+
const match = fuzzyMatch(query, text);
|
|
88
|
+
|
|
89
|
+
if (match) {
|
|
90
|
+
results.push({
|
|
91
|
+
item,
|
|
92
|
+
score: match.score,
|
|
93
|
+
matches: match.matches,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Sort by score descending
|
|
99
|
+
return results.sort((a, b) => b.score - a.score);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Highlight matched characters in a string
|
|
104
|
+
* Returns array of { text, highlighted } segments
|
|
105
|
+
*/
|
|
106
|
+
export function highlightMatches(
|
|
107
|
+
text: string,
|
|
108
|
+
matches: number[],
|
|
109
|
+
): Array<{ text: string; highlighted: boolean }> {
|
|
110
|
+
if (matches.length === 0) {
|
|
111
|
+
return [{ text, highlighted: false }];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const matchSet = new Set(matches);
|
|
115
|
+
const segments: Array<{ text: string; highlighted: boolean }> = [];
|
|
116
|
+
let currentSegment = "";
|
|
117
|
+
let isHighlighted = matchSet.has(0);
|
|
118
|
+
|
|
119
|
+
for (let i = 0; i < text.length; i++) {
|
|
120
|
+
const shouldHighlight = matchSet.has(i);
|
|
121
|
+
|
|
122
|
+
if (shouldHighlight !== isHighlighted) {
|
|
123
|
+
if (currentSegment) {
|
|
124
|
+
segments.push({ text: currentSegment, highlighted: isHighlighted });
|
|
125
|
+
}
|
|
126
|
+
currentSegment = text[i];
|
|
127
|
+
isHighlighted = shouldHighlight;
|
|
128
|
+
} else {
|
|
129
|
+
currentSegment += text[i];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (currentSegment) {
|
|
134
|
+
segments.push({ text: currentSegment, highlighted: isHighlighted });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return segments;
|
|
138
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Go module path → installed binary name.
|
|
3
|
+
*
|
|
4
|
+
* `go install` names the binary after the last path element of the module
|
|
5
|
+
* path, EXCEPT when that element is a major-version suffix: the module
|
|
6
|
+
* `github.com/MadAppGang/tmux-mcp/v2` installs `tmux-mcp`, not `v2`. Taking a
|
|
7
|
+
* plain basename therefore points every later check at a binary that never
|
|
8
|
+
* exists — presence probes fail, so the module is reinstalled on every run,
|
|
9
|
+
* doctor reports it missing, and the pin comparison never runs at all.
|
|
10
|
+
*
|
|
11
|
+
* Only the Go rule lives here. npm and cargo share the `name@version` syntax
|
|
12
|
+
* but not the suffix convention — an npm scoped package can legitimately be
|
|
13
|
+
* called `@scope/v2` — so this must not be folded into the generic spec parser.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Trailing major-version suffix per the Go modules spec: `/vN` with N >= 2.
|
|
18
|
+
* Anchored to the END of the path with nothing after `vN`, so a segment that
|
|
19
|
+
* merely starts with "v2" (`github.com/x/v2tool`) is a real name, not a suffix.
|
|
20
|
+
* v0 and v1 are never written as a path suffix. The alternation, not a plain
|
|
21
|
+
* `[2-9][0-9]*`, because that shorthand rejects v10–v19 — a first digit of 1
|
|
22
|
+
* is only excluded when it is the ONLY digit.
|
|
23
|
+
*/
|
|
24
|
+
const MAJOR_VERSION_SUFFIX = /\/v(?:[2-9]|[1-9][0-9]+)$/;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The binary `go install <modulePath>` produces.
|
|
28
|
+
*
|
|
29
|
+
* Takes the MODULE PATH — the part before any `@version` — so callers that
|
|
30
|
+
* hold a full `path@version` spec split it first. An empty or single-segment
|
|
31
|
+
* input is returned unchanged.
|
|
32
|
+
*/
|
|
33
|
+
export function goBinaryName(modulePath: string): string {
|
|
34
|
+
const withoutSuffix = modulePath.replace(MAJOR_VERSION_SUFFIX, "");
|
|
35
|
+
return withoutSuffix.split("/").pop() || withoutSuffix;
|
|
36
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand a file or URL to the operating system's default application.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists rather than launching `$EDITOR`: magus owns the terminal.
|
|
5
|
+
* Suspending an OpenTUI app to give the TTY to a full-screen editor and then
|
|
6
|
+
* restoring raw mode, the alternate screen buffer and the cursor is a reliable
|
|
7
|
+
* way to leave the terminal wedged if anything goes wrong in between. Handing
|
|
8
|
+
* the path to the OS launches a separate process that never touches our TTY.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
|
|
13
|
+
export interface Opener {
|
|
14
|
+
command: string;
|
|
15
|
+
/** Arguments that precede the target path. */
|
|
16
|
+
args: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The platform's "open this with whatever handles it" command.
|
|
21
|
+
*
|
|
22
|
+
* magus ships darwin-arm64, darwin-x64 and linux-x64, so macOS and Linux
|
|
23
|
+
* both have to work — the existing `open "${url}"` call in the Skills screen is
|
|
24
|
+
* macOS-only and silently does nothing on Linux.
|
|
25
|
+
*/
|
|
26
|
+
export function openerFor(platform: NodeJS.Platform): Opener {
|
|
27
|
+
if (platform === "darwin") return { command: "open", args: [] };
|
|
28
|
+
// `start` is a cmd.exe builtin, and its first quoted argument is the window
|
|
29
|
+
// title — hence the empty string, or a quoted path would be consumed as one.
|
|
30
|
+
if (platform === "win32")
|
|
31
|
+
return { command: "cmd", args: ["/c", "start", ""] };
|
|
32
|
+
return { command: "xdg-open", args: [] };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Open `target` in the default application, without blocking the TUI.
|
|
37
|
+
*
|
|
38
|
+
* The child is spawned WITHOUT a shell and the target is passed as its own
|
|
39
|
+
* argv entry, so a filename containing a space, quote or `$(…)` is opened
|
|
40
|
+
* rather than interpreted. Resolves once the process has been spawned, not
|
|
41
|
+
* when it exits — the viewer outlives us by design.
|
|
42
|
+
*/
|
|
43
|
+
export function openInDefaultApp(
|
|
44
|
+
target: string,
|
|
45
|
+
platform: NodeJS.Platform = process.platform,
|
|
46
|
+
/**
|
|
47
|
+
* Test seam. The default is the platform's real opener; a test passes a
|
|
48
|
+
* command that cannot exist, so the missing-opener path is exercised
|
|
49
|
+
* deterministically on every OS — CI runners ship `xdg-open`, so "assume
|
|
50
|
+
* the opener is absent" fails exactly there, and "spawn the real one"
|
|
51
|
+
* launches applications on a build machine.
|
|
52
|
+
*/
|
|
53
|
+
opener: Opener = openerFor(platform),
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
const { command, args } = opener;
|
|
56
|
+
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
let settled = false;
|
|
59
|
+
const child = spawn(command, [...args, target], {
|
|
60
|
+
detached: true,
|
|
61
|
+
stdio: "ignore",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
child.once("error", (error: NodeJS.ErrnoException) => {
|
|
65
|
+
if (settled) return;
|
|
66
|
+
settled = true;
|
|
67
|
+
reject(
|
|
68
|
+
error.code === "ENOENT"
|
|
69
|
+
? new Error(
|
|
70
|
+
`No way to open files here — "${command}" is not on PATH.`,
|
|
71
|
+
)
|
|
72
|
+
: error,
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
child.once("spawn", () => {
|
|
77
|
+
if (settled) return;
|
|
78
|
+
settled = true;
|
|
79
|
+
// Detach so magus exiting does not take the editor with it.
|
|
80
|
+
child.unref();
|
|
81
|
+
resolve();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
package/src/utils/run.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Running an installer, with the shell split out from the argv.
|
|
3
|
+
*
|
|
4
|
+
* Lives in `utils/` rather than `cli/prompt.ts` because the update engine needs
|
|
5
|
+
* it and a service must not import from the CLI layer — `cli/` and `ui/` are
|
|
6
|
+
* sibling front ends over `services/`, and an arrow from a service back up into
|
|
7
|
+
* one of them is how the two front ends ended up with different update logic in
|
|
8
|
+
* the first place.
|
|
9
|
+
*
|
|
10
|
+
* Both functions live in ONE file on purpose: {@link runShellScript}'s doc only
|
|
11
|
+
* makes sense next to {@link runCommand}, because most of what it has to say is
|
|
12
|
+
* "you almost certainly want the other one".
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
import type { Command } from "./command-utils.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Run a built command as argv, streaming its output; true on exit 0.
|
|
20
|
+
*
|
|
21
|
+
* **This is the one every installer uses.** No `shell` option, so:
|
|
22
|
+
*
|
|
23
|
+
* - `cmd` is resolved on PATH as an executable. There is no profile to source
|
|
24
|
+
* and therefore no way for a shell alias to appear to work — the reason the
|
|
25
|
+
* shipped `pip install` bug could exist at all was that `/bin/sh` was asked
|
|
26
|
+
* to resolve a word that only interactive zsh knew.
|
|
27
|
+
* - Every argument stays exactly one argument. Package names reach here from
|
|
28
|
+
* `.claude/profiles.json` and plugin `requires.bin` — hand-authored data
|
|
29
|
+
* committed by teammates and fetched from marketplaces — and under a shell a
|
|
30
|
+
* space or a `$(…)` in one of them would have been executed. Nothing
|
|
31
|
+
* validates those names, and after this nothing has to.
|
|
32
|
+
*
|
|
33
|
+
* stdio is inherited: the installer owns the terminal while it runs, which is
|
|
34
|
+
* what makes brew's and bun's own progress output visible. A caller painting an
|
|
35
|
+
* animated region must pause it around this (see `ApplyReporter.detach`).
|
|
36
|
+
*/
|
|
37
|
+
export async function runCommand(c: Command): Promise<boolean> {
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
const child = spawn(c.cmd, [...c.args], { stdio: "inherit" });
|
|
40
|
+
child.on("exit", (code) => resolve(code === 0));
|
|
41
|
+
child.on("error", () => resolve(false));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Run a shell SCRIPT through `/bin/sh`, streaming its output; true on exit 0.
|
|
47
|
+
*
|
|
48
|
+
* **Not for installer commands — use {@link runCommand}.** This exists for
|
|
49
|
+
* exactly one input: the values of `TOOLCHAIN_BOOTSTRAP`, which are the
|
|
50
|
+
* upstream-published one-liners for bun and Homebrew. Those genuinely are shell
|
|
51
|
+
* text — one contains a pipe, the other a command substitution — so they cannot
|
|
52
|
+
* be expressed as argv without wrapping them in a fake `/bin/sh -c` at the one
|
|
53
|
+
* place where a shell is actually correct.
|
|
54
|
+
*
|
|
55
|
+
* The distinction is who wrote the string. A bootstrap line is a constant in
|
|
56
|
+
* this repo, copied from bun.sh and brew.sh. An installer command is assembled
|
|
57
|
+
* from names that arrive with a profile. Passing the second kind here is the
|
|
58
|
+
* defect that motivated splitting the two, so
|
|
59
|
+
* `src/__tests__/shell-script-callers.test.ts` pins the caller set to one file.
|
|
60
|
+
*/
|
|
61
|
+
export async function runShellScript(script: string): Promise<boolean> {
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
const child = spawn(script, { stdio: "inherit", shell: true });
|
|
64
|
+
child.on("exit", (code) => resolve(code === 0));
|
|
65
|
+
child.on("error", () => resolve(false));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared string utilities for magus
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Format marketplace name for display
|
|
7
|
+
* e.g., "magus" -> "Magus"
|
|
8
|
+
*/
|
|
9
|
+
export function formatMarketplaceName(name: string): string {
|
|
10
|
+
return name
|
|
11
|
+
.split(/[-_]/)
|
|
12
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
13
|
+
.join(" ");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Validate GitHub repository format (org/repo)
|
|
18
|
+
* Prevents SSRF via malicious repo strings
|
|
19
|
+
*/
|
|
20
|
+
export function isValidGitHubRepo(repo: string): boolean {
|
|
21
|
+
if (!repo || typeof repo !== "string") {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Must match GitHub username/repo format
|
|
26
|
+
// Username: alphanumeric, hyphens (no consecutive, not start/end)
|
|
27
|
+
// Repo: alphanumeric, hyphens, underscores, dots
|
|
28
|
+
const GITHUB_REPO_PATTERN =
|
|
29
|
+
/^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]\/[a-zA-Z0-9_.-]+$/;
|
|
30
|
+
|
|
31
|
+
// Allow single-char usernames too
|
|
32
|
+
const GITHUB_REPO_PATTERN_SHORT = /^[a-zA-Z0-9]\/[a-zA-Z0-9_.-]+$/;
|
|
33
|
+
|
|
34
|
+
if (
|
|
35
|
+
!GITHUB_REPO_PATTERN.test(repo) &&
|
|
36
|
+
!GITHUB_REPO_PATTERN_SHORT.test(repo)
|
|
37
|
+
) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Blocklist suspicious patterns
|
|
42
|
+
const BLOCKLIST = [
|
|
43
|
+
/\.\./, // Path traversal
|
|
44
|
+
/@/, // URL authority injection
|
|
45
|
+
/:/, // Port specification
|
|
46
|
+
/\s/, // Whitespace
|
|
47
|
+
/%/, // URL encoding
|
|
48
|
+
/\\/, // Backslash
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
return !BLOCKLIST.some((pattern) => pattern.test(repo));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse plugin ID into components
|
|
56
|
+
* Format: pluginName@marketplaceName
|
|
57
|
+
*/
|
|
58
|
+
export interface ParsedPluginId {
|
|
59
|
+
pluginName: string;
|
|
60
|
+
marketplace: string;
|
|
61
|
+
raw: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function parsePluginId(pluginId: string): ParsedPluginId | null {
|
|
65
|
+
if (!pluginId || typeof pluginId !== "string") {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const trimmed = pluginId.trim();
|
|
70
|
+
const atIndex = trimmed.lastIndexOf("@");
|
|
71
|
+
|
|
72
|
+
if (atIndex <= 0) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const pluginName = trimmed.slice(0, atIndex).trim();
|
|
77
|
+
const marketplace = trimmed.slice(atIndex + 1).trim();
|
|
78
|
+
|
|
79
|
+
if (!pluginName || !marketplace) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
pluginName,
|
|
85
|
+
marketplace,
|
|
86
|
+
raw: trimmed,
|
|
87
|
+
};
|
|
88
|
+
}
|