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,412 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* capture-builtin.ts — turn a built-in Claude Code output style into an
|
|
4
|
+
* importable file on THIS machine.
|
|
5
|
+
*
|
|
6
|
+
* Built-in styles (Explanatory, Learning, Proactive) ship inside the Claude
|
|
7
|
+
* Code binary, so there is no file for the composer to read. This captures the
|
|
8
|
+
* real text instead of guessing it: a transparent proxy sits in front of the
|
|
9
|
+
* Anthropic API, one `claude -p` round trip runs with the style active, and
|
|
10
|
+
* the system prompt it actually sent is recorded.
|
|
11
|
+
*
|
|
12
|
+
* Why capture rather than grep the binary: the binary's symbol names are
|
|
13
|
+
* minified and change every release, so a grep breaks silently. A capture
|
|
14
|
+
* reads what the harness genuinely sent, for the exact version installed.
|
|
15
|
+
*
|
|
16
|
+
* Why on the user's machine rather than committed here: a snapshot in this
|
|
17
|
+
* repo would be Anthropic's prompt text redistributed, and would go stale on
|
|
18
|
+
* their release schedule rather than ours.
|
|
19
|
+
*
|
|
20
|
+
* Section boundaries are found by DIFF, not by heading. The style's own body
|
|
21
|
+
* contains H1 headings of its own (Explanatory ships "# Explanatory Style
|
|
22
|
+
* Active"), so splitting on the next "# " truncates it. Capturing once with
|
|
23
|
+
* the style and once without isolates exactly the inserted block.
|
|
24
|
+
*
|
|
25
|
+
* Usage:
|
|
26
|
+
* bun capture-builtin.ts --discover # names Anthropic ships today
|
|
27
|
+
* bun capture-builtin.ts --check # which captures fell behind
|
|
28
|
+
* bun capture-builtin.ts --all # capture every built-in
|
|
29
|
+
* bun capture-builtin.ts --style Explanatory # capture one
|
|
30
|
+
* [--out DIR] [--port 8899] [--timeout 180] [--dry-run]
|
|
31
|
+
*
|
|
32
|
+
* RE-RUN AFTER EVERY CLAUDE CODE UPGRADE. A capture records the version it
|
|
33
|
+
* came from; `--check` compares that against the installed binary and reports
|
|
34
|
+
* both stale files and built-ins that exist but were never captured, which is
|
|
35
|
+
* how a newly introduced style gets noticed.
|
|
36
|
+
*
|
|
37
|
+
* Costs one real API round trip per style, plus one shared baseline.
|
|
38
|
+
* Exit code: 1 on any failure, or from --check when anything is stale.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import {
|
|
42
|
+
existsSync,
|
|
43
|
+
mkdirSync,
|
|
44
|
+
mkdtempSync,
|
|
45
|
+
readFileSync,
|
|
46
|
+
readdirSync,
|
|
47
|
+
rmSync,
|
|
48
|
+
writeFileSync,
|
|
49
|
+
} from "node:fs";
|
|
50
|
+
import { homedir, tmpdir } from "node:os";
|
|
51
|
+
import { join } from "node:path";
|
|
52
|
+
import { parseArgs } from "node:util";
|
|
53
|
+
|
|
54
|
+
import { splitFrontmatter } from "../src/services/styles-manager.js";
|
|
55
|
+
|
|
56
|
+
const UPSTREAM = "https://api.anthropic.com";
|
|
57
|
+
|
|
58
|
+
interface CaptureResult {
|
|
59
|
+
system: string;
|
|
60
|
+
blocks: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Run one `claude -p` through a recording proxy and return the system prompt
|
|
65
|
+
* it sent. `styleName` of null captures the baseline with no style active.
|
|
66
|
+
*/
|
|
67
|
+
async function captureSystem(
|
|
68
|
+
styleName: string | null,
|
|
69
|
+
port: number,
|
|
70
|
+
timeoutMs: number,
|
|
71
|
+
): Promise<CaptureResult> {
|
|
72
|
+
const project = mkdtempSync(join(tmpdir(), "style-capture-"));
|
|
73
|
+
mkdirSync(join(project, ".claude"), { recursive: true });
|
|
74
|
+
writeFileSync(
|
|
75
|
+
join(project, ".claude", "settings.json"),
|
|
76
|
+
`${JSON.stringify(styleName ? { outputStyle: styleName } : {}, null, 2)}\n`,
|
|
77
|
+
"utf8",
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
let captured: unknown = null;
|
|
81
|
+
|
|
82
|
+
const server = Bun.serve({
|
|
83
|
+
port,
|
|
84
|
+
idleTimeout: 240,
|
|
85
|
+
async fetch(req) {
|
|
86
|
+
const url = new URL(req.url);
|
|
87
|
+
const body =
|
|
88
|
+
req.method === "GET" || req.method === "HEAD" ? undefined : await req.arrayBuffer();
|
|
89
|
+
|
|
90
|
+
if (body && captured === null) {
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(new TextDecoder().decode(body)) as { system?: unknown };
|
|
93
|
+
if (parsed.system) captured = parsed.system;
|
|
94
|
+
} catch {
|
|
95
|
+
// Not JSON. Forward it regardless — recording is best effort.
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const headers = new Headers(req.headers);
|
|
100
|
+
headers.set("host", new URL(UPSTREAM).host);
|
|
101
|
+
headers.delete("content-length");
|
|
102
|
+
// fetch() transparently decompresses, so forwarding the upstream
|
|
103
|
+
// content-encoding would tell the client to decompress plaintext.
|
|
104
|
+
// Ask for identity and drop the header on the way back.
|
|
105
|
+
headers.set("accept-encoding", "identity");
|
|
106
|
+
|
|
107
|
+
const upstream = await fetch(`${UPSTREAM}${url.pathname}${url.search}`, {
|
|
108
|
+
method: req.method,
|
|
109
|
+
headers,
|
|
110
|
+
body,
|
|
111
|
+
});
|
|
112
|
+
const out = new Headers(upstream.headers);
|
|
113
|
+
out.delete("content-encoding");
|
|
114
|
+
out.delete("content-length");
|
|
115
|
+
return new Response(upstream.body, { status: upstream.status, headers: out });
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const proc = Bun.spawn(["claude", "-p", "reply with the single word ok"], {
|
|
121
|
+
cwd: project,
|
|
122
|
+
env: { ...process.env, ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}` },
|
|
123
|
+
stdout: "pipe",
|
|
124
|
+
stderr: "pipe",
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const timer = setTimeout(() => proc.kill(), timeoutMs);
|
|
128
|
+
const exitCode = await proc.exited;
|
|
129
|
+
clearTimeout(timer);
|
|
130
|
+
|
|
131
|
+
if (captured === null) {
|
|
132
|
+
const stderr = await new Response(proc.stderr).text();
|
|
133
|
+
throw new Error(
|
|
134
|
+
`no system prompt captured (claude exited ${exitCode}). ${stderr.trim().slice(0, 300)}`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
} finally {
|
|
138
|
+
server.stop(true);
|
|
139
|
+
rmSync(project, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const blocks = Array.isArray(captured) ? captured : [captured];
|
|
143
|
+
const system = blocks
|
|
144
|
+
.map((block) =>
|
|
145
|
+
typeof block === "string" ? block : String((block as { text?: string })?.text ?? ""),
|
|
146
|
+
)
|
|
147
|
+
.join("\n");
|
|
148
|
+
|
|
149
|
+
return { system, blocks: blocks.length };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The inserted block is what the styled capture has and the baseline does not.
|
|
154
|
+
*
|
|
155
|
+
* Given baseline = A + B and styled = A + X + B, the end of X is fixed by
|
|
156
|
+
* ARITHMETIC, not by walking back from the end. A common-suffix walk keeps
|
|
157
|
+
* going whenever X's last characters happen to match B's last characters, and
|
|
158
|
+
* it does happen: the first version of this truncated Explanatory mid-sentence
|
|
159
|
+
* at "general programming concepts", eating the closing two sentences.
|
|
160
|
+
*/
|
|
161
|
+
/**
|
|
162
|
+
* Extract the section beginning at `marker` from `styled`.
|
|
163
|
+
*
|
|
164
|
+
* The end is found by heading, with `baseline` deciding which headings are
|
|
165
|
+
* STRUCTURAL. Splitting naively on the next `# ` truncates, because a style's
|
|
166
|
+
* own body contains H1s (Explanatory ships "# Explanatory Style Active"); but
|
|
167
|
+
* a heading that also appears in the baseline belongs to the harness, so it
|
|
168
|
+
* marks the real boundary.
|
|
169
|
+
*
|
|
170
|
+
* Character-level diffing was tried first and abandoned. If styled = A + X + B
|
|
171
|
+
* and baseline = A + B, the split is genuinely ambiguous whenever X starts or
|
|
172
|
+
* ends the way the surrounding text does — every rotation of X satisfies the
|
|
173
|
+
* length constraint. All three variants failed on real captures: a suffix walk
|
|
174
|
+
* cut Explanatory mid-sentence at "general programming concepts", a prefix
|
|
175
|
+
* walk slid past the marker entirely, and smallest-valid-split returned
|
|
176
|
+
* "D\nINSERTE" for "INSERTED\n".
|
|
177
|
+
*
|
|
178
|
+
* Residual limitation: a style whose body contains an H1 that also appears in
|
|
179
|
+
* the baseline would be cut there. None of the three built-ins does.
|
|
180
|
+
*/
|
|
181
|
+
export function extractSection(baseline: string, styled: string, marker: string): string {
|
|
182
|
+
const start = styled.indexOf(marker);
|
|
183
|
+
if (start === -1) return "";
|
|
184
|
+
|
|
185
|
+
const structural = new Set(
|
|
186
|
+
baseline
|
|
187
|
+
.split("\n")
|
|
188
|
+
.filter((line) => line.startsWith("# "))
|
|
189
|
+
.map((line) => line.trimEnd()),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const lines = styled.slice(start).split("\n");
|
|
193
|
+
const out: string[] = [lines[0]];
|
|
194
|
+
for (const line of lines.slice(1)) {
|
|
195
|
+
if (line.startsWith("# ") && structural.has(line.trimEnd())) break;
|
|
196
|
+
out.push(line);
|
|
197
|
+
}
|
|
198
|
+
return out.join("\n").trimEnd();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Strip the harness's own `# Output Style: <name>` header off the block. */
|
|
202
|
+
export function stripSectionHeader(block: string, styleName: string): string {
|
|
203
|
+
const marker = `# Output Style: ${styleName}`;
|
|
204
|
+
const at = block.indexOf(marker);
|
|
205
|
+
if (at === -1) return block.trim();
|
|
206
|
+
return block.slice(at + marker.length).trim();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function slugFor(styleName: string): string {
|
|
210
|
+
return `builtin-${styleName.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function toStyleFile(styleName: string, body: string, version: string): string {
|
|
214
|
+
return [
|
|
215
|
+
"---",
|
|
216
|
+
`name: ${slugFor(styleName)}`,
|
|
217
|
+
`description: "Captured built-in output style: ${styleName}"`,
|
|
218
|
+
// Our composed file sets this itself; setting it here too means the file
|
|
219
|
+
// is also safe to activate directly.
|
|
220
|
+
"keep-coding-instructions: true",
|
|
221
|
+
// Machine-readable so --check can tell when Claude Code moved on. Keep it
|
|
222
|
+
// a bare version: --check compares it, a human sentence would not parse.
|
|
223
|
+
`captured-from: ${version}`,
|
|
224
|
+
`captured-style: ${styleName}`,
|
|
225
|
+
'generated-by: "capture-builtin.ts. Re-run after a Claude Code upgrade."',
|
|
226
|
+
"---",
|
|
227
|
+
"",
|
|
228
|
+
body,
|
|
229
|
+
"",
|
|
230
|
+
].join("\n");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** "2.1.233 (Claude Code)" -> "2.1.233". --check compares these. */
|
|
234
|
+
export function normalizeVersion(raw: string): string {
|
|
235
|
+
return /(\d+\.\d+\.\d+)/.exec(raw)?.[1] ?? raw.trim() ?? "unknown";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function run(cmd: string[]): Promise<string> {
|
|
239
|
+
try {
|
|
240
|
+
const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "ignore" });
|
|
241
|
+
await proc.exited;
|
|
242
|
+
return (await new Response(proc.stdout).text()).trim();
|
|
243
|
+
} catch {
|
|
244
|
+
return "";
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function claudeVersion(): Promise<string> {
|
|
249
|
+
return normalizeVersion(await run(["claude", "--version"])) || "unknown";
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Names of the built-in styles, read out of the installed binary.
|
|
254
|
+
*
|
|
255
|
+
* This is the one place a minified-bundle grep is acceptable: it recovers
|
|
256
|
+
* NAMES, which are cheap to verify by eye against `/output-style` and
|
|
257
|
+
* harmless to get wrong. The prompt TEXT is never read this way — that is
|
|
258
|
+
* what the capture is for. If a future release changes the shape and this
|
|
259
|
+
* returns nothing, read the names off the `/output-style` picker and pass
|
|
260
|
+
* `--style` directly.
|
|
261
|
+
*/
|
|
262
|
+
export async function discoverBuiltins(): Promise<string[]> {
|
|
263
|
+
const binary = await run(["sh", "-c", "readlink -f \"$(command -v claude)\" 2>/dev/null || command -v claude"]);
|
|
264
|
+
if (!binary) return [];
|
|
265
|
+
const out = await run([
|
|
266
|
+
"grep",
|
|
267
|
+
"-o",
|
|
268
|
+
"-a",
|
|
269
|
+
"-E",
|
|
270
|
+
'name:"[A-Za-z0-9 -]+",source:"built-in"',
|
|
271
|
+
binary,
|
|
272
|
+
]);
|
|
273
|
+
const names = new Set<string>();
|
|
274
|
+
for (const line of out.split("\n")) {
|
|
275
|
+
const match = /name:"([^"]+)"/.exec(line);
|
|
276
|
+
if (match) names.add(match[1]);
|
|
277
|
+
}
|
|
278
|
+
return [...names].sort();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function main(): Promise<number> {
|
|
282
|
+
const { values } = parseArgs({
|
|
283
|
+
args: process.argv.slice(2),
|
|
284
|
+
options: {
|
|
285
|
+
style: { type: "string" },
|
|
286
|
+
all: { type: "boolean", default: false },
|
|
287
|
+
discover: { type: "boolean", default: false },
|
|
288
|
+
check: { type: "boolean", default: false },
|
|
289
|
+
out: { type: "string" },
|
|
290
|
+
port: { type: "string", default: "8899" },
|
|
291
|
+
timeout: { type: "string", default: "180" },
|
|
292
|
+
"dry-run": { type: "boolean", default: false },
|
|
293
|
+
},
|
|
294
|
+
allowPositionals: false,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const port = Number(values.port);
|
|
298
|
+
const timeoutMs = Number(values.timeout) * 1000;
|
|
299
|
+
const outDir = values.out ?? join(homedir(), ".claude", "output-styles");
|
|
300
|
+
const version = await claudeVersion();
|
|
301
|
+
|
|
302
|
+
if (values.discover) {
|
|
303
|
+
const names = await discoverBuiltins();
|
|
304
|
+
if (names.length === 0) {
|
|
305
|
+
console.error(
|
|
306
|
+
"Could not read the built-in names from the binary. Open /output-style and\n" +
|
|
307
|
+
"pass the names you see with --style.",
|
|
308
|
+
);
|
|
309
|
+
return 1;
|
|
310
|
+
}
|
|
311
|
+
console.log(`Built-in output styles in Claude Code ${version}:`);
|
|
312
|
+
for (const name of names) console.log(` ${name}`);
|
|
313
|
+
return 0;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (values.check) {
|
|
317
|
+
const captured = existsSync(outDir)
|
|
318
|
+
? readdirSync(outDir).filter((f) => f.startsWith("builtin-") && f.endsWith(".md"))
|
|
319
|
+
: [];
|
|
320
|
+
const known = await discoverBuiltins();
|
|
321
|
+
|
|
322
|
+
console.log(`Claude Code ${version}`);
|
|
323
|
+
console.log("════════════════════════════════════════");
|
|
324
|
+
let stale = 0;
|
|
325
|
+
for (const file of captured.sort()) {
|
|
326
|
+
const { frontmatter } = splitFrontmatter(readFileSync(join(outDir, file), "utf8"));
|
|
327
|
+
const from = frontmatter["captured-from"] ?? "unknown";
|
|
328
|
+
const ok = from === version;
|
|
329
|
+
if (!ok) stale++;
|
|
330
|
+
console.log(` ${ok ? "current" : "STALE "} ${file} (captured from ${from})`);
|
|
331
|
+
}
|
|
332
|
+
const missing = known.filter((name) => !captured.includes(`${slugFor(name)}.md`));
|
|
333
|
+
for (const name of missing) console.log(` MISSING ${name}`);
|
|
334
|
+
console.log("════════════════════════════════════════");
|
|
335
|
+
if (stale > 0 || missing.length > 0) {
|
|
336
|
+
console.log("Re-capture with: bun capture-builtin.ts --all");
|
|
337
|
+
return 1;
|
|
338
|
+
}
|
|
339
|
+
console.log("All built-ins captured at the current version.");
|
|
340
|
+
return 0;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let targets: string[];
|
|
344
|
+
if (values.all) {
|
|
345
|
+
targets = await discoverBuiltins();
|
|
346
|
+
if (targets.length === 0) {
|
|
347
|
+
console.error("error: --all found no built-in names. Use --style with an explicit name.");
|
|
348
|
+
return 1;
|
|
349
|
+
}
|
|
350
|
+
} else if (values.style) {
|
|
351
|
+
targets = [values.style];
|
|
352
|
+
} else {
|
|
353
|
+
console.error(
|
|
354
|
+
"error: pass --style <name>, or --all, or --discover to list what exists.",
|
|
355
|
+
);
|
|
356
|
+
return 1;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// One baseline serves every style — it is the same prompt minus the section.
|
|
360
|
+
console.error(`[capture] baseline (no output style) …`);
|
|
361
|
+
const baseline = await captureSystem(null, port, timeoutMs);
|
|
362
|
+
console.error(`[capture] baseline: ${baseline.system.length} chars, ${baseline.blocks} blocks`);
|
|
363
|
+
|
|
364
|
+
let failures = 0;
|
|
365
|
+
for (const [index, styleName] of targets.entries()) {
|
|
366
|
+
console.error(`[capture] with ${styleName} …`);
|
|
367
|
+
const styled = await captureSystem(styleName, port + 1 + index, timeoutMs);
|
|
368
|
+
|
|
369
|
+
const marker = `# Output Style: ${styleName}`;
|
|
370
|
+
const section = extractSection(baseline.system, styled.system, marker);
|
|
371
|
+
if (!section) {
|
|
372
|
+
console.error(
|
|
373
|
+
styled.system.includes(marker)
|
|
374
|
+
? `error: ${styleName}: found the marker but not where the section ends.`
|
|
375
|
+
: `error: ${styleName}: "${marker}" is not in the captured prompt. Real built-in?`,
|
|
376
|
+
);
|
|
377
|
+
failures++;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const body = stripSectionHeader(section, styleName);
|
|
382
|
+
const file = toStyleFile(styleName, body, version);
|
|
383
|
+
const path = join(outDir, `${slugFor(styleName)}.md`);
|
|
384
|
+
|
|
385
|
+
if (values["dry-run"]) {
|
|
386
|
+
console.log(`--- ${path}`);
|
|
387
|
+
console.log(file);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
mkdirSync(outDir, { recursive: true });
|
|
392
|
+
writeFileSync(path, file, "utf8");
|
|
393
|
+
console.log(
|
|
394
|
+
`captured ${styleName.padEnd(14)} ${body.length} chars -> --import user:${slugFor(styleName)}`,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (!values["dry-run"] && failures === 0) {
|
|
399
|
+
console.log(`\nAll captures are from Claude Code ${version}. Re-run after an upgrade;`);
|
|
400
|
+
console.log(`"bun capture-builtin.ts --check" reports when they fall behind.`);
|
|
401
|
+
}
|
|
402
|
+
return failures > 0 ? 1 : 0;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (import.meta.main) {
|
|
406
|
+
try {
|
|
407
|
+
process.exit(await main());
|
|
408
|
+
} catch (error) {
|
|
409
|
+
console.error(`error: ${error instanceof Error ? error.message : String(error)}`);
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* check-optional-deps.ts — assert the per-platform optionalDependencies are pinned
|
|
4
|
+
* to the main package's own version.
|
|
5
|
+
*
|
|
6
|
+
* build-binaries.ts stamps each generated per-platform package with
|
|
7
|
+
* `package.json`'s version, and the release workflow publishes those first so the
|
|
8
|
+
* main package's optionalDependencies already resolve. If the two drift, the main
|
|
9
|
+
* package ships pointing at versions that were never published.
|
|
10
|
+
*
|
|
11
|
+
* That failure is quiet: optionalDependencies are allowed to fail to resolve, so
|
|
12
|
+
* npm/bun install succeeds and the launcher silently falls back to running from
|
|
13
|
+
* source via Bun — meaning every user loses the prebuilt binary and anyone without
|
|
14
|
+
* Bun on PATH gets a broken `magus`. Nothing errors, so it can ship unnoticed.
|
|
15
|
+
*
|
|
16
|
+
* Bumping the package version without bumping these is easy to do by hand, which
|
|
17
|
+
* is exactly what happened going 4.20.0 -> 4.21.0.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import pkg from "../package.json";
|
|
21
|
+
|
|
22
|
+
const version = (pkg as { version: string }).version;
|
|
23
|
+
const optional = (pkg as { optionalDependencies?: Record<string, string> })
|
|
24
|
+
.optionalDependencies;
|
|
25
|
+
|
|
26
|
+
if (!optional || Object.keys(optional).length === 0) {
|
|
27
|
+
console.log("check:optional-deps — no optionalDependencies declared, nothing to verify");
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const mismatched = Object.entries(optional).filter(([, v]) => v !== version);
|
|
32
|
+
|
|
33
|
+
if (mismatched.length > 0) {
|
|
34
|
+
console.error(
|
|
35
|
+
`check:optional-deps FAILED — package version is ${version}, but:`,
|
|
36
|
+
);
|
|
37
|
+
for (const [name, v] of mismatched) {
|
|
38
|
+
console.error(` ${name}: ${v} (expected ${version})`);
|
|
39
|
+
}
|
|
40
|
+
console.error(
|
|
41
|
+
"\nThese are published from this same version by build-binaries.ts.\n" +
|
|
42
|
+
"Update optionalDependencies in package.json to match, then re-run.",
|
|
43
|
+
);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log(
|
|
48
|
+
`check:optional-deps — all ${Object.keys(optional).length} platform packages pinned to ${version}`,
|
|
49
|
+
);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Run each test file in its own `bun test` process.
|
|
4
|
+
*
|
|
5
|
+
* Some tests use bun's `mock.module`, which is process-global and NOT restored
|
|
6
|
+
* across files (bun 1.3.10). In a single shared process the outcome depends on
|
|
7
|
+
* file order — fine locally, flaky on CI (different order / workers). Per-file
|
|
8
|
+
* isolation removes the cross-file coupling entirely: every file gets a fresh
|
|
9
|
+
* module registry. Slower than `bun test`, but deterministic — which is what CI
|
|
10
|
+
* needs. For fast local iteration use `bun test`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readdirSync, statSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { spawnSync } from "node:child_process";
|
|
16
|
+
|
|
17
|
+
function findTests(dir: string): string[] {
|
|
18
|
+
const out: string[] = [];
|
|
19
|
+
for (const entry of readdirSync(dir)) {
|
|
20
|
+
const p = join(dir, entry);
|
|
21
|
+
if (statSync(p).isDirectory()) out.push(...findTests(p));
|
|
22
|
+
// `.tsx` as well as `.ts`. The suffix was written when no `.tsx` test
|
|
23
|
+
// existed, so it silently excluded the only file testing a UI primitive
|
|
24
|
+
// — the class of test most likely to be `.tsx` and least likely to be
|
|
25
|
+
// noticed missing, because a file that is never collected reports
|
|
26
|
+
// nothing at all. The printed `N/N` count below is the guard: it moves
|
|
27
|
+
// when collection changes.
|
|
28
|
+
else if (/\.test\.tsx?$/.test(p)) out.push(p);
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const root = join(import.meta.dir, "..");
|
|
34
|
+
const files = findTests(join(root, "src")).sort();
|
|
35
|
+
|
|
36
|
+
let failed = 0;
|
|
37
|
+
for (const file of files) {
|
|
38
|
+
const rel = file.slice(root.length + 1);
|
|
39
|
+
process.stdout.write(`\n── ${rel}\n`);
|
|
40
|
+
const result = spawnSync("bun", ["test", file], {
|
|
41
|
+
stdio: "inherit",
|
|
42
|
+
cwd: root,
|
|
43
|
+
});
|
|
44
|
+
if (result.status !== 0) failed++;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const passed = files.length - failed;
|
|
48
|
+
console.log(`\n${passed}/${files.length} test files passed (isolated).`);
|
|
49
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verify-binary.ts — prove a freshly built platform binary is shippable.
|
|
3
|
+
*
|
|
4
|
+
* Both `magus-ci.yml` (every PR) and `magus-release.yml` (every tag) run this.
|
|
5
|
+
* It exists as one script rather than two copies of the same shell block because
|
|
6
|
+
* the two workflows drifting is how a gate quietly stops gating: a fix applied to
|
|
7
|
+
* the release copy and missed on the CI copy leaves PRs green on the old rules.
|
|
8
|
+
*
|
|
9
|
+
* Three checks, each earned by a release that shipped broken:
|
|
10
|
+
*
|
|
11
|
+
* 1. STARTS. A binary that compiles but cannot start is worse than no binary,
|
|
12
|
+
* because the launcher prefers it over the working Bun-source fallback.
|
|
13
|
+
*
|
|
14
|
+
* 2. STARTS WHERE .env IS A SYMLINK TO A FIFO. v4.41.0 passed check 1 and still
|
|
15
|
+
* died in every git worktree whose .env is a FIFO (how 1Password serves
|
|
16
|
+
* secrets): the embedded Bun aborted while autoloading that file — exit 1,
|
|
17
|
+
* nothing on stdout or stderr, because the crash lands before any JS runs.
|
|
18
|
+
*
|
|
19
|
+
* 3. THE SIGNATURE COVERS THE WHOLE FILE (darwin only). `bun build --compile`
|
|
20
|
+
* signs the Mach-O and then appends the bundle after it, so the signature
|
|
21
|
+
* covers a shorter file than the one published — true of every release from
|
|
22
|
+
* 4.22.0 to 6.8.0. macOS 27 SIGKILLs such a binary at exec with no output.
|
|
23
|
+
* Check 1 does NOT catch it: GitHub's runner images are behind that
|
|
24
|
+
* enforcement, so the binary starts there while users get exit 137.
|
|
25
|
+
* `codesign --verify --strict` is enforcement-independent and fails on older
|
|
26
|
+
* macOS too, which is the only reason this is catchable in CI at all.
|
|
27
|
+
*
|
|
28
|
+
* Usage: bun scripts/verify-binary.ts <target> e.g. darwin-arm64
|
|
29
|
+
*/
|
|
30
|
+
import { spawnSync } from "node:child_process";
|
|
31
|
+
import { existsSync, mkdtempSync, rmSync, symlinkSync } from "node:fs";
|
|
32
|
+
import { tmpdir } from "node:os";
|
|
33
|
+
import path from "node:path";
|
|
34
|
+
|
|
35
|
+
const target = process.argv[2];
|
|
36
|
+
if (!target) {
|
|
37
|
+
console.error("usage: bun scripts/verify-binary.ts <target> e.g. darwin-arm64");
|
|
38
|
+
process.exit(2);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const root = path.join(import.meta.dir, "..");
|
|
42
|
+
const bin = path.join(root, "dist-binaries", `magus-cli-${target}`, "bin", "magus");
|
|
43
|
+
|
|
44
|
+
function fail(message: string): never {
|
|
45
|
+
console.error(`verify-binary(${target}): ${message}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// --- 1. present and executable -------------------------------------------
|
|
50
|
+
if (!existsSync(bin)) fail(`missing: ${bin}`);
|
|
51
|
+
const version = spawnSync(bin, ["--version"], { encoding: "utf8" });
|
|
52
|
+
if (version.error) fail(`cannot execute ${bin}: ${version.error.message}`);
|
|
53
|
+
if (version.status !== 0) {
|
|
54
|
+
// A signal leaves status null. Report it as itself — an unsigned darwin binary
|
|
55
|
+
// on an enforcing kernel lands here as SIGKILL, and "exit null" would hide that.
|
|
56
|
+
const how = version.signal ? `killed by ${version.signal}` : `exit ${version.status}`;
|
|
57
|
+
fail(`--version ${how}`);
|
|
58
|
+
}
|
|
59
|
+
console.log(` starts ${version.stdout.trim()}`);
|
|
60
|
+
|
|
61
|
+
// --- 2. starts where .env is a symlink to a FIFO --------------------------
|
|
62
|
+
const probe = mkdtempSync(path.join(tmpdir(), "magus-fifo-probe-"));
|
|
63
|
+
try {
|
|
64
|
+
const pipe = path.join(probe, "secrets-pipe");
|
|
65
|
+
const mkfifo = spawnSync("mkfifo", [pipe]);
|
|
66
|
+
if (mkfifo.status !== 0) fail("could not create the FIFO probe");
|
|
67
|
+
symlinkSync(pipe, path.join(probe, ".env"));
|
|
68
|
+
|
|
69
|
+
const inProbe = spawnSync(bin, ["--version"], { cwd: probe, encoding: "utf8" });
|
|
70
|
+
if (inProbe.error || inProbe.status !== 0) {
|
|
71
|
+
fail("cannot start where .env is a symlink to a FIFO");
|
|
72
|
+
}
|
|
73
|
+
console.log(" fifo .env ok");
|
|
74
|
+
} finally {
|
|
75
|
+
rmSync(probe, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// --- 3. the signature covers the shipped bytes (darwin only) --------------
|
|
79
|
+
if (target.startsWith("darwin")) {
|
|
80
|
+
const verified = spawnSync("codesign", ["--verify", "--strict", bin], {
|
|
81
|
+
encoding: "utf8",
|
|
82
|
+
});
|
|
83
|
+
if (verified.status !== 0) {
|
|
84
|
+
fail(
|
|
85
|
+
`code signature does not verify.\n${verified.stderr.trim()}\n\n` +
|
|
86
|
+
"This is the 4.22.0–6.8.0 failure: --compile signs the Mach-O and then " +
|
|
87
|
+
"appends the bundle, so the signature covers a shorter file. macOS 27 " +
|
|
88
|
+
"SIGKILLs such a binary at exec. Check that build-binaries.ts still calls " +
|
|
89
|
+
"signAndVerify().",
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
console.log(" code signature verified");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log(`verify-binary(${target}): OK`);
|