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,272 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* verify-community-registry.ts — the gate between a proposed community style
|
|
4
|
+
* entry and a committed one.
|
|
5
|
+
*
|
|
6
|
+
* This is NOT a test. It hits the real network, it is run by a human who is
|
|
7
|
+
* curating the registry, and it must never run in CI: it would burn the shared
|
|
8
|
+
* 60/hr `api.github.com` budget on every push and then fail on GitHub's rate
|
|
9
|
+
* limit rather than on our bug, which is the worst possible signal.
|
|
10
|
+
*
|
|
11
|
+
* Two modes:
|
|
12
|
+
*
|
|
13
|
+
* --discover list the real `.md` blobs and their upstream
|
|
14
|
+
* --discover <repo> <dir> `name`, for every registry source or for one
|
|
15
|
+
* candidate repo. Prints a paste-ready registry
|
|
16
|
+
* block. NEVER hand-write a path from a README:
|
|
17
|
+
* a wrong path is a 404 the user sees, and the
|
|
18
|
+
* error text blames magus for it, correctly.
|
|
19
|
+
*
|
|
20
|
+
* --check re-fetch every committed entry and report 404s,
|
|
21
|
+
* size outliers and unparseable files. Run this
|
|
22
|
+
* before committing a registry change.
|
|
23
|
+
*
|
|
24
|
+
* Cost: `--discover` spends ONE api.github.com call per repo (a recursive git
|
|
25
|
+
* tree), then reads names over raw.githubusercontent.com, which costs nothing.
|
|
26
|
+
* `--check` spends zero API budget.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { parseArgs } from "node:util";
|
|
30
|
+
import {
|
|
31
|
+
COMMUNITY_SOURCES,
|
|
32
|
+
COMMUNITY_STYLES,
|
|
33
|
+
type CommunityStyleSource,
|
|
34
|
+
coordinateProblem,
|
|
35
|
+
rawUrlFor,
|
|
36
|
+
} from "../src/data/community-styles.js";
|
|
37
|
+
import { githubFetcher } from "../src/services/community-fetcher.js";
|
|
38
|
+
import { MAX_STYLE_BYTES } from "../src/services/community-styles.js";
|
|
39
|
+
import { splitFrontmatter } from "../src/services/styles-manager.js";
|
|
40
|
+
|
|
41
|
+
interface DiscoveredFile {
|
|
42
|
+
path: string;
|
|
43
|
+
/** Upstream's own `name`, or the basename when the file declares none. */
|
|
44
|
+
displayName: string;
|
|
45
|
+
description: string;
|
|
46
|
+
bytes: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** `attention-span/output-styles/zen-master.md` -> `zen-master`. */
|
|
50
|
+
function slugFromPath(filePath: string): string {
|
|
51
|
+
return (filePath.split("/").pop() ?? filePath).replace(/\.md$/, "");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function discover(
|
|
55
|
+
repo: string,
|
|
56
|
+
dir: string,
|
|
57
|
+
ref: string,
|
|
58
|
+
): Promise<DiscoveredFile[]> {
|
|
59
|
+
// ONE API call for the whole repo. Asking per-directory would cost one call
|
|
60
|
+
// per level and tell us less.
|
|
61
|
+
const treeUrl = `https://api.github.com/repos/${repo}/git/trees/${ref}?recursive=1`;
|
|
62
|
+
const tree = await githubFetcher(treeUrl, {
|
|
63
|
+
accept: "application/vnd.github+json",
|
|
64
|
+
});
|
|
65
|
+
if (tree.status !== 200) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`${repo}: HTTP ${tree.status} listing the git tree${
|
|
68
|
+
tree.status === 403 || tree.status === 429
|
|
69
|
+
? " (rate limited — wait, or export GITHUB_TOKEN)"
|
|
70
|
+
: ""
|
|
71
|
+
}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const parsed = JSON.parse(tree.body) as {
|
|
75
|
+
truncated?: boolean;
|
|
76
|
+
tree?: Array<{ path?: string; type?: string }>;
|
|
77
|
+
};
|
|
78
|
+
if (parsed.truncated) {
|
|
79
|
+
console.warn(
|
|
80
|
+
` ! ${repo}: git tree was truncated by GitHub — list is partial`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const prefix = `${dir}/`;
|
|
85
|
+
const paths = (parsed.tree ?? [])
|
|
86
|
+
.filter(
|
|
87
|
+
(entry) =>
|
|
88
|
+
entry.type === "blob" &&
|
|
89
|
+
typeof entry.path === "string" &&
|
|
90
|
+
entry.path.startsWith(prefix) &&
|
|
91
|
+
entry.path.endsWith(".md") &&
|
|
92
|
+
// One directory deep only: the cache is flat and a nested style would
|
|
93
|
+
// need a slug that is not a plain basename.
|
|
94
|
+
!entry.path.slice(prefix.length).includes("/"),
|
|
95
|
+
)
|
|
96
|
+
.map((entry) => entry.path as string)
|
|
97
|
+
.sort();
|
|
98
|
+
|
|
99
|
+
const files: DiscoveredFile[] = [];
|
|
100
|
+
for (const filePath of paths) {
|
|
101
|
+
// raw.githubusercontent.com costs no API budget, so reading every file to
|
|
102
|
+
// get its real `name` is free. This is the only way to get a displayName
|
|
103
|
+
// that is upstream's own rather than one we invented from a filename.
|
|
104
|
+
const raw = await githubFetcher(
|
|
105
|
+
`https://raw.githubusercontent.com/${repo}/${ref}/${filePath}`,
|
|
106
|
+
);
|
|
107
|
+
if (raw.status !== 200) {
|
|
108
|
+
console.warn(` ! ${filePath}: HTTP ${raw.status}`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const { frontmatter } = splitFrontmatter(raw.body);
|
|
112
|
+
files.push({
|
|
113
|
+
path: filePath,
|
|
114
|
+
displayName: frontmatter.name || slugFromPath(filePath),
|
|
115
|
+
description: frontmatter.description || "",
|
|
116
|
+
bytes: Buffer.byteLength(raw.body, "utf8"),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return files;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function printDiscovered(
|
|
123
|
+
sourceId: string,
|
|
124
|
+
repo: string,
|
|
125
|
+
files: DiscoveredFile[],
|
|
126
|
+
): void {
|
|
127
|
+
console.log(`\n${repo} — ${files.length} style file(s)`);
|
|
128
|
+
console.log("─".repeat(72));
|
|
129
|
+
for (const file of files) {
|
|
130
|
+
const flag = file.bytes > MAX_STYLE_BYTES ? " ** OVER 64 KiB **" : "";
|
|
131
|
+
console.log(
|
|
132
|
+
` {\n\t\tid: "${sourceId}--${slugFromPath(file.path)}",\n\t\tsourceId: "${sourceId}",\n\t\tpath: "${file.path}",\n\t\tdisplayName: ${JSON.stringify(file.displayName)},\n\t\tsummary: ${JSON.stringify(file.description.slice(0, 100))},\n\t},${flag}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function runDiscover(repo?: string, dir?: string): Promise<number> {
|
|
138
|
+
const targets: Array<{ id: string; repo: string; dir: string; ref: string }> =
|
|
139
|
+
repo && dir
|
|
140
|
+
? [{ id: repo.split("/")[1] ?? repo, repo, dir, ref: "HEAD" }]
|
|
141
|
+
: COMMUNITY_SOURCES.map((source: CommunityStyleSource) => ({
|
|
142
|
+
id: source.id,
|
|
143
|
+
repo: source.repo,
|
|
144
|
+
dir: source.dir,
|
|
145
|
+
ref: source.ref,
|
|
146
|
+
}));
|
|
147
|
+
|
|
148
|
+
let failures = 0;
|
|
149
|
+
for (const target of targets) {
|
|
150
|
+
try {
|
|
151
|
+
const files = await discover(target.repo, target.dir, target.ref);
|
|
152
|
+
printDiscovered(target.id, target.repo, files);
|
|
153
|
+
if (files.length === 0) {
|
|
154
|
+
// The rule that keeps `caveman` and `humanizer` out: zero style files
|
|
155
|
+
// means there is nothing to import, whatever the star count says.
|
|
156
|
+
console.log(
|
|
157
|
+
" (no style files — this repo must NOT get a registry entry)",
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
failures++;
|
|
162
|
+
console.error(
|
|
163
|
+
` ! ${target.repo}: ${error instanceof Error ? error.message : String(error)}`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return failures;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function runCheck(): Promise<number> {
|
|
171
|
+
let problems = 0;
|
|
172
|
+
console.log(
|
|
173
|
+
`Checking ${COMMUNITY_STYLES.length} entries across ${COMMUNITY_SOURCES.length} sources — zero API budget.\n`,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
for (const style of COMMUNITY_STYLES) {
|
|
177
|
+
const source = COMMUNITY_SOURCES.find((s) => s.id === style.sourceId);
|
|
178
|
+
if (!source) {
|
|
179
|
+
console.error(
|
|
180
|
+
`✗ ${style.id}: sourceId "${style.sourceId}" resolves to nothing`,
|
|
181
|
+
);
|
|
182
|
+
problems++;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const coordinate = coordinateProblem(style, source);
|
|
186
|
+
if (coordinate) {
|
|
187
|
+
console.error(`✗ ${style.id}: ${coordinate}`);
|
|
188
|
+
problems++;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const response = await githubFetcher(rawUrlFor(style, source));
|
|
193
|
+
if (response.status !== 200) {
|
|
194
|
+
console.error(
|
|
195
|
+
`✗ ${style.id}: HTTP ${response.status} — ${rawUrlFor(style, source)}`,
|
|
196
|
+
);
|
|
197
|
+
problems++;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const bytes = Buffer.byteLength(response.body, "utf8");
|
|
201
|
+
if (bytes > MAX_STYLE_BYTES) {
|
|
202
|
+
console.error(`✗ ${style.id}: ${bytes} bytes exceeds the 64 KiB cap`);
|
|
203
|
+
problems++;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const { frontmatter, body } = splitFrontmatter(response.body);
|
|
207
|
+
if (Object.keys(frontmatter).length === 0 || body.trim().length === 0) {
|
|
208
|
+
console.error(
|
|
209
|
+
`✗ ${style.id}: not a valid output style (no frontmatter or no body)`,
|
|
210
|
+
);
|
|
211
|
+
problems++;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
// Advisory, not a failure: upstream is free to rename a style, and the
|
|
215
|
+
// coordinate id is deliberately independent of it. But a drifted
|
|
216
|
+
// displayName is what the user reads, so it is worth seeing.
|
|
217
|
+
const upstreamName = frontmatter.name;
|
|
218
|
+
const drift =
|
|
219
|
+
upstreamName && upstreamName !== style.displayName
|
|
220
|
+
? ` (upstream name is now "${upstreamName}", registry says "${style.displayName}")`
|
|
221
|
+
: "";
|
|
222
|
+
console.log(
|
|
223
|
+
`✓ ${style.id.padEnd(38)} ${String(bytes).padStart(6)} B${drift}`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log(
|
|
228
|
+
problems === 0
|
|
229
|
+
? "\nRegistry is clean — safe to commit."
|
|
230
|
+
: `\n${problems} problem(s). Do NOT commit until they are fixed.`,
|
|
231
|
+
);
|
|
232
|
+
return problems;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function main(): Promise<number> {
|
|
236
|
+
const { values, positionals } = parseArgs({
|
|
237
|
+
args: process.argv.slice(2),
|
|
238
|
+
options: {
|
|
239
|
+
discover: { type: "boolean", default: false },
|
|
240
|
+
check: { type: "boolean", default: false },
|
|
241
|
+
},
|
|
242
|
+
allowPositionals: true,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if (!values.discover && !values.check) {
|
|
246
|
+
console.error(
|
|
247
|
+
"usage: bun scripts/verify-community-registry.ts --discover [<repo> <dir>]\n" +
|
|
248
|
+
" bun scripts/verify-community-registry.ts --check",
|
|
249
|
+
);
|
|
250
|
+
return 1;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
let problems = 0;
|
|
254
|
+
if (values.discover) {
|
|
255
|
+
problems += await runDiscover(positionals[0], positionals[1]);
|
|
256
|
+
}
|
|
257
|
+
if (values.check) {
|
|
258
|
+
problems += await runCheck();
|
|
259
|
+
}
|
|
260
|
+
return problems === 0 ? 0 : 1;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (import.meta.main) {
|
|
264
|
+
main()
|
|
265
|
+
.then((code) => process.exit(code))
|
|
266
|
+
.catch((error) => {
|
|
267
|
+
console.error(
|
|
268
|
+
`error: ${error instanceof Error ? error.message : String(error)}`,
|
|
269
|
+
);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
type ActiveClosureDeps,
|
|
4
|
+
loadActiveClosure,
|
|
5
|
+
selectProfileId,
|
|
6
|
+
} from "../services/active-closure.js";
|
|
7
|
+
import { resolveProfile } from "../services/resolver.js";
|
|
8
|
+
import type {
|
|
9
|
+
BinRequirement,
|
|
10
|
+
ProfileManifest,
|
|
11
|
+
ResolvedClosure,
|
|
12
|
+
} from "../types/index.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The CLI and the TUI choose a profile through ONE function. They used to
|
|
16
|
+
* differ — the CLI inline, the TUI not at all — so an exact pin could never
|
|
17
|
+
* produce the same state in both.
|
|
18
|
+
*
|
|
19
|
+
* Every IO edge is injected; nothing here reads the real manifest, the real
|
|
20
|
+
* active-profile symlink, or any plugin.json.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const PROJECT = "/tmp/no-such-project";
|
|
24
|
+
|
|
25
|
+
function manifest(
|
|
26
|
+
profiles: ProfileManifest["profiles"],
|
|
27
|
+
over: Partial<ProfileManifest> = {},
|
|
28
|
+
): ProfileManifest {
|
|
29
|
+
return { version: 2, profiles, ...over };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const ONE = manifest({
|
|
33
|
+
default: {
|
|
34
|
+
name: "default",
|
|
35
|
+
plugins: { "dev@magus": "latest", "terminal@magus": "5.0.1" },
|
|
36
|
+
cliTools: { claudish: "latest" },
|
|
37
|
+
},
|
|
38
|
+
} as ProfileManifest["profiles"]);
|
|
39
|
+
|
|
40
|
+
const TWO = manifest({
|
|
41
|
+
frontend: { name: "frontend", plugins: { "dev@magus": "latest" } },
|
|
42
|
+
backend: { name: "backend", plugins: { "terminal@magus": "latest" } },
|
|
43
|
+
} as ProfileManifest["profiles"]);
|
|
44
|
+
|
|
45
|
+
/** A plugin.json resolver that fails the test if the screen reads one. */
|
|
46
|
+
const MUST_NOT_READ = async (pluginId: string): Promise<BinRequirement[]> => {
|
|
47
|
+
throw new Error(`read plugin.json for ${pluginId}`);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
describe("selectProfileId", () => {
|
|
51
|
+
const ids = ["frontend", "backend"];
|
|
52
|
+
|
|
53
|
+
test.each([
|
|
54
|
+
// [explicit, active, expected]
|
|
55
|
+
["frontend", "backend", "frontend"], // explicit wins
|
|
56
|
+
[undefined, "backend", "backend"], // then the active one
|
|
57
|
+
[undefined, null, null], // two profiles, no choice: ambiguous
|
|
58
|
+
["missing", "backend", "backend"], // unknown explicit is ignored
|
|
59
|
+
[undefined, "stale", null], // stale active, two profiles: ambiguous
|
|
60
|
+
] as const)("explicit=%p active=%p → %p", (explicit, active, expected) => {
|
|
61
|
+
expect(selectProfileId(ids, explicit, active)).toBe(expected);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("a single profile needs no activation", () => {
|
|
65
|
+
expect(selectProfileId(["default"], undefined, null)).toBe("default");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("a stale active symlink with a single profile picks the single one", () => {
|
|
69
|
+
// The profile the symlink names was removed from the manifest. The CLI
|
|
70
|
+
// used to pick the stale name and then fail to resolve it.
|
|
71
|
+
expect(selectProfileId(["default"], undefined, "removed")).toBe("default");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("no profiles at all selects nothing", () => {
|
|
75
|
+
expect(selectProfileId([], undefined, null)).toBeNull();
|
|
76
|
+
expect(selectProfileId([], "x", "y")).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/** Deps over a fixed manifest and active name, recording every resolve call. */
|
|
81
|
+
function fakeDeps(m: ProfileManifest, active: string | null) {
|
|
82
|
+
const calls: Array<{
|
|
83
|
+
id: string;
|
|
84
|
+
binResolver?: (id: string) => Promise<BinRequirement[]>;
|
|
85
|
+
}> = [];
|
|
86
|
+
const deps: Partial<ActiveClosureDeps> = {
|
|
87
|
+
readManifest: async () => m,
|
|
88
|
+
activeProfile: async () => active,
|
|
89
|
+
resolveProfile: async (manifest, id, opts) => {
|
|
90
|
+
calls.push({ id, binResolver: opts?.binResolver });
|
|
91
|
+
// The real resolver, but never the real plugin.json reader: when the
|
|
92
|
+
// caller passed none, a reader that throws stands in for the default.
|
|
93
|
+
return resolveProfile(manifest, id, {
|
|
94
|
+
binResolver: opts?.binResolver ?? MUST_NOT_READ,
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
return { deps, calls };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
describe("loadActiveClosure", () => {
|
|
102
|
+
test("resolves the active profile's closure", async () => {
|
|
103
|
+
const { deps } = fakeDeps(TWO, "backend");
|
|
104
|
+
const got = await loadActiveClosure(PROJECT, { resolveBins: false, deps });
|
|
105
|
+
expect(got?.profileId).toBe("backend");
|
|
106
|
+
expect(got?.closure.plugins).toEqual({ "terminal@magus": "latest" });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("passes the manifest's pins through exactly", async () => {
|
|
110
|
+
const { deps } = fakeDeps(ONE, null);
|
|
111
|
+
const got = await loadActiveClosure(PROJECT, { resolveBins: false, deps });
|
|
112
|
+
expect(got?.closure.plugins).toEqual({
|
|
113
|
+
"dev@magus": "latest",
|
|
114
|
+
"terminal@magus": "5.0.1",
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("an invalid manifest is null, not a throw", async () => {
|
|
119
|
+
const { deps, calls } = fakeDeps(
|
|
120
|
+
{ version: 1, profiles: {} } as unknown as ProfileManifest,
|
|
121
|
+
null,
|
|
122
|
+
);
|
|
123
|
+
expect(await loadActiveClosure(PROJECT, { deps })).toBeNull();
|
|
124
|
+
expect(calls).toEqual([]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("no manifest (readManifest's empty v2) is null", async () => {
|
|
128
|
+
const { deps } = fakeDeps(manifest({}), null);
|
|
129
|
+
expect(await loadActiveClosure(PROJECT, { deps })).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("two profiles and none active is null — there is no choice to make", async () => {
|
|
133
|
+
const { deps, calls } = fakeDeps(TWO, null);
|
|
134
|
+
expect(await loadActiveClosure(PROJECT, { deps })).toBeNull();
|
|
135
|
+
expect(calls).toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("a stale active symlink with a single profile resolves the single one", async () => {
|
|
139
|
+
const { deps } = fakeDeps(ONE, "removed-profile");
|
|
140
|
+
const got = await loadActiveClosure(PROJECT, { resolveBins: false, deps });
|
|
141
|
+
expect(got?.profileId).toBe("default");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("resolveBins:false hands resolveProfile a resolver that returns []", async () => {
|
|
145
|
+
// The Plugins screen needs only pins. The default resolver reads every
|
|
146
|
+
// declared plugin's plugin.json on each fetch.
|
|
147
|
+
const { deps, calls } = fakeDeps(ONE, null);
|
|
148
|
+
const got = await loadActiveClosure(PROJECT, { resolveBins: false, deps });
|
|
149
|
+
expect(calls).toHaveLength(1);
|
|
150
|
+
const resolver = calls[0]?.binResolver;
|
|
151
|
+
expect(resolver).toBeDefined();
|
|
152
|
+
expect(await resolver?.("dev@magus")).toEqual([]);
|
|
153
|
+
// No plugin-declared bins; the profile's own cliTools still resolve.
|
|
154
|
+
expect(got?.closure.bins.map((b) => b.name)).toEqual(["claudish"]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("resolveBins defaults to true: the real plugin.json resolver is used", async () => {
|
|
158
|
+
const { deps, calls } = fakeDeps(ONE, null);
|
|
159
|
+
// MUST_NOT_READ stands in for the default reader, so the default path
|
|
160
|
+
// shows up as a rejected resolve — which loadActiveClosure reports as null.
|
|
161
|
+
expect(await loadActiveClosure(PROJECT, { deps })).toBeNull();
|
|
162
|
+
expect(calls).toHaveLength(1);
|
|
163
|
+
expect(calls[0]?.binResolver).toBeUndefined();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("any failing edge is null, never a throw", async () => {
|
|
167
|
+
const boom = async (): Promise<never> => {
|
|
168
|
+
throw new Error("boom");
|
|
169
|
+
};
|
|
170
|
+
for (const deps of [
|
|
171
|
+
{ readManifest: boom },
|
|
172
|
+
{ readManifest: async () => ONE, activeProfile: boom },
|
|
173
|
+
{
|
|
174
|
+
readManifest: async () => ONE,
|
|
175
|
+
activeProfile: async () => null,
|
|
176
|
+
resolveProfile: boom as unknown as (
|
|
177
|
+
...args: unknown[]
|
|
178
|
+
) => Promise<ResolvedClosure>,
|
|
179
|
+
},
|
|
180
|
+
]) {
|
|
181
|
+
expect(await loadActiveClosure(PROJECT, { deps })).toBeNull();
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
});
|