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,600 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `magus update [profile] [--check] [--yes]`
|
|
3
|
+
*
|
|
4
|
+
* Dependency management, not package management: no resolver, no lockfile, no
|
|
5
|
+
* version negotiation. `magus install` puts the profile's declared set on
|
|
6
|
+
* this machine; `magus update` keeps it current — anything the profile
|
|
7
|
+
* declares but the machine lacks is installed, and anything behind the version
|
|
8
|
+
* its marketplace publishes is advanced to it.
|
|
9
|
+
*
|
|
10
|
+
* Scope is the ACTIVE profile, not the union of every profile: "update my
|
|
11
|
+
* dependencies" means the set actually in use. Pass a name to update another.
|
|
12
|
+
*
|
|
13
|
+
* Self-update moved to `magus upgrade`.
|
|
14
|
+
*
|
|
15
|
+
* ## Why this file drives a live region
|
|
16
|
+
*
|
|
17
|
+
* Most of an update's wall clock is spent inside four calls that print nothing:
|
|
18
|
+
* a git fetch per marketplace, a catalog read, a serial PATH probe per binary,
|
|
19
|
+
* then an install per plugin. The command used to print its first line after
|
|
20
|
+
* all of the first three had finished, and one line per plugin only once that
|
|
21
|
+
* plugin was already installed. On a slow network that is thirty seconds of a
|
|
22
|
+
* dead terminal, which reads as a hang, not as work.
|
|
23
|
+
*
|
|
24
|
+
* So every phase reports while it runs. `cli/live.ts` owns the mechanism. A
|
|
25
|
+
* line printed while a region is painting must go through the region, or it
|
|
26
|
+
* lands inside the animated block and the next repaint's cursor-up arithmetic
|
|
27
|
+
* erases it along with part of the frame. Prefer `live.note` here for anything
|
|
28
|
+
* this file emits deliberately — but the region also CAPTURES `console` while
|
|
29
|
+
* it paints, so a write from deeper in the call graph is routed rather than
|
|
30
|
+
* corrupting the display. That capture is not belt-and-braces: it is the only
|
|
31
|
+
* thing that covers `getAvailablePlugins`, which reaches a stale-marketplace
|
|
32
|
+
* advisory two layers down and used to erase itself mid-frame.
|
|
33
|
+
*
|
|
34
|
+
* `cli/update-view.ts` owns every string. Nothing in this file formats.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { selectProfileId } from "../services/active-closure.js";
|
|
38
|
+
import { isClaudeAvailable } from "../services/claude-cli.js";
|
|
39
|
+
import { clearContentDriftCache } from "../services/content-drift.js";
|
|
40
|
+
import { validateManifest } from "../services/manifest.js";
|
|
41
|
+
import {
|
|
42
|
+
refreshRegisteredMarketplaces,
|
|
43
|
+
reportRegisteredMarketplaces,
|
|
44
|
+
} from "../services/marketplace-refresh.js";
|
|
45
|
+
import { registerClosureMarketplaces } from "../services/marketplace-sync.js";
|
|
46
|
+
import {
|
|
47
|
+
type PluginInfo,
|
|
48
|
+
clearMarketplaceCache,
|
|
49
|
+
getAvailablePlugins,
|
|
50
|
+
} from "../services/plugin-manager.js";
|
|
51
|
+
import { resolveProfile } from "../services/resolver.js";
|
|
52
|
+
import { getInstalledSkillNames } from "../services/skills-manager.js";
|
|
53
|
+
import { activeProfile } from "../services/symlink-manager.js";
|
|
54
|
+
import { REAL_TOOL_PROBE_IO, probeTools } from "../services/tool-probe.js";
|
|
55
|
+
// The apply half lives in services/ so the TUI runs the SAME code — see the
|
|
56
|
+
// header of update-engine.ts for what drifted while it did not.
|
|
57
|
+
import {
|
|
58
|
+
type ApplyReporter,
|
|
59
|
+
applyPlan,
|
|
60
|
+
planWorkItems,
|
|
61
|
+
} from "../services/update-engine.js";
|
|
62
|
+
import {
|
|
63
|
+
type UpdatePlan,
|
|
64
|
+
planBinUpdates,
|
|
65
|
+
planHasWork,
|
|
66
|
+
planIsBehind,
|
|
67
|
+
planPluginUpdates,
|
|
68
|
+
planSkillUpdates,
|
|
69
|
+
summarizePlan,
|
|
70
|
+
} from "../services/update-plan.js";
|
|
71
|
+
import { brand } from "../ui/theme.js";
|
|
72
|
+
import { bold, dim, fg, width } from "./ansi.js";
|
|
73
|
+
import { ensureManifest } from "./bootstrap.js";
|
|
74
|
+
import { LiveRegion, withPaused } from "./live.js";
|
|
75
|
+
import {
|
|
76
|
+
type ApplyState,
|
|
77
|
+
type Step,
|
|
78
|
+
applyFrame,
|
|
79
|
+
applyRow,
|
|
80
|
+
fail as failLine,
|
|
81
|
+
header,
|
|
82
|
+
info,
|
|
83
|
+
note as noteLine,
|
|
84
|
+
nothingToApplyNotice,
|
|
85
|
+
ok as okLine,
|
|
86
|
+
planReport,
|
|
87
|
+
stepFrame,
|
|
88
|
+
stepRecord,
|
|
89
|
+
summaryLine,
|
|
90
|
+
uncheckedGateNotice,
|
|
91
|
+
unknownToolsNotice,
|
|
92
|
+
warn as warnLine,
|
|
93
|
+
} from "./update-view.js";
|
|
94
|
+
|
|
95
|
+
interface UpdateFlags {
|
|
96
|
+
/** Report drift and exit 1 if anything is behind. The CI gate. */
|
|
97
|
+
check: boolean;
|
|
98
|
+
/** Show the plan and stop. Always exits 0 — it is a preview, not a gate. */
|
|
99
|
+
dryRun: boolean;
|
|
100
|
+
profile?: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* `--yes` / `-y` are gone, and are silently ignored rather than rejected.
|
|
105
|
+
*
|
|
106
|
+
* They existed to skip a confirmation that no longer exists: `update` applies
|
|
107
|
+
* its plan. Both start with `-`, so an old script or a habit still runs and
|
|
108
|
+
* still does the right thing; there is no flag kept alive to mean nothing.
|
|
109
|
+
*/
|
|
110
|
+
export function parseArgs(args: string[]): UpdateFlags {
|
|
111
|
+
return {
|
|
112
|
+
check: args.includes("--check"),
|
|
113
|
+
dryRun: args.includes("--dry-run"),
|
|
114
|
+
profile: args.find((a) => !a.startsWith("-")),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// -- phase runner ------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Run one checklist step, keeping its row and the animation truthful.
|
|
122
|
+
*
|
|
123
|
+
* `work` gets a `say` callback so a long step can narrate what it is currently
|
|
124
|
+
* blocked on — the marketplace step names the clone it is pulling, the binary
|
|
125
|
+
* step names the binary it is probing. That detail is the difference between
|
|
126
|
+
* "something is happening" and "the network call to THIS marketplace is slow".
|
|
127
|
+
*
|
|
128
|
+
* A step that throws is marked `fail` and RE-THROWN. Marking it matters: the
|
|
129
|
+
* record printed on the way out then names the step that died, instead of a
|
|
130
|
+
* stack trace landing under a checklist whose last row is still spinning.
|
|
131
|
+
*/
|
|
132
|
+
function makeStepRunner(live: LiveRegion, all: Step[]) {
|
|
133
|
+
return async function runStep<T>(
|
|
134
|
+
step: Step,
|
|
135
|
+
work: (say: (detail: string) => void) => Promise<T>,
|
|
136
|
+
): Promise<T> {
|
|
137
|
+
step.state = "running";
|
|
138
|
+
step.startedAt = Date.now();
|
|
139
|
+
step.detail = step.detail || "…";
|
|
140
|
+
live.refresh();
|
|
141
|
+
const say = (detail: string) => {
|
|
142
|
+
step.detail = detail;
|
|
143
|
+
live.refresh();
|
|
144
|
+
};
|
|
145
|
+
try {
|
|
146
|
+
return await work(say);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
step.state = "fail";
|
|
149
|
+
step.detail = error instanceof Error ? error.message : String(error);
|
|
150
|
+
step.endedAt = Date.now();
|
|
151
|
+
step.progress = undefined;
|
|
152
|
+
// Collapse to the record on the way out. Without this the exception
|
|
153
|
+
// surfaces under a checklist frozen mid-spin, with the cursor still
|
|
154
|
+
// hidden — the process-exit hook restores the cursor but cannot know
|
|
155
|
+
// what the last frame should have said.
|
|
156
|
+
live.stop(stepRecord(all, Date.now()));
|
|
157
|
+
throw error;
|
|
158
|
+
} finally {
|
|
159
|
+
step.endedAt = Date.now();
|
|
160
|
+
step.progress = undefined;
|
|
161
|
+
if (step.state === "running") step.state = "done";
|
|
162
|
+
live.refresh();
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// -- entry point -------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
export async function runUpdateCommand(
|
|
170
|
+
args: string[],
|
|
171
|
+
projectPath: string = process.cwd(),
|
|
172
|
+
): Promise<number> {
|
|
173
|
+
const flags = parseArgs(args);
|
|
174
|
+
const startedAt = Date.now();
|
|
175
|
+
|
|
176
|
+
// The ONE prompt that survives, and only in a repo that has no manifest yet.
|
|
177
|
+
// Writing `.claude/profiles.json` is not part of updating: it authors the
|
|
178
|
+
// file a team commits and shares, possibly derived from this machine's
|
|
179
|
+
// personal user-scope plugin set. That is worth a human, once per repo,
|
|
180
|
+
// forever. Everything after it applies without asking.
|
|
181
|
+
const manifest = await ensureManifest(projectPath, {
|
|
182
|
+
check: flags.check || flags.dryRun,
|
|
183
|
+
});
|
|
184
|
+
if (!manifest) return 1;
|
|
185
|
+
|
|
186
|
+
const errors = validateManifest(manifest);
|
|
187
|
+
if (errors.length > 0) {
|
|
188
|
+
console.error(failLine("Invalid .claude/profiles.json:"));
|
|
189
|
+
for (const e of errors) console.error(noteLine(`${e.path}: ${e.message}`));
|
|
190
|
+
return 1;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const profileIds = Object.keys(manifest.profiles);
|
|
194
|
+
if (flags.profile && !manifest.profiles[flags.profile]) {
|
|
195
|
+
console.error(
|
|
196
|
+
failLine(
|
|
197
|
+
`Profile "${flags.profile}" not found. Available: ${profileIds.join(", ")}`,
|
|
198
|
+
),
|
|
199
|
+
);
|
|
200
|
+
return 1;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Active profile, not the union — "update my dependencies" means the set in
|
|
204
|
+
// use. A single-profile manifest needs no activation to be unambiguous.
|
|
205
|
+
// Chosen through the same function the TUI uses, so both judge a plugin
|
|
206
|
+
// against the same pin.
|
|
207
|
+
const targetId = selectProfileId(
|
|
208
|
+
profileIds,
|
|
209
|
+
flags.profile,
|
|
210
|
+
await activeProfile(projectPath),
|
|
211
|
+
);
|
|
212
|
+
if (!targetId) {
|
|
213
|
+
console.error(
|
|
214
|
+
failLine(
|
|
215
|
+
`No active profile. Pass one: magus update <${profileIds.join("|")}>`,
|
|
216
|
+
),
|
|
217
|
+
);
|
|
218
|
+
return 1;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for (const line of header(targetId)) console.log(line);
|
|
222
|
+
|
|
223
|
+
// ---- resolve phase, under one animated checklist -------------------------
|
|
224
|
+
|
|
225
|
+
const steps: Record<
|
|
226
|
+
"profile" | "marketplaces" | "catalog" | "binaries" | "skills",
|
|
227
|
+
Step
|
|
228
|
+
> = {
|
|
229
|
+
profile: { label: "profile", state: "pending", detail: "" },
|
|
230
|
+
marketplaces: { label: "marketplaces", state: "pending", detail: "" },
|
|
231
|
+
catalog: { label: "catalog", state: "pending", detail: "" },
|
|
232
|
+
binaries: { label: "binaries", state: "pending", detail: "" },
|
|
233
|
+
skills: { label: "skills", state: "pending", detail: "" },
|
|
234
|
+
};
|
|
235
|
+
const stepList = Object.values(steps);
|
|
236
|
+
|
|
237
|
+
const live = new LiveRegion();
|
|
238
|
+
live.start((tick) => stepFrame(stepList, tick, Date.now()));
|
|
239
|
+
const runStep = makeStepRunner(live, stepList);
|
|
240
|
+
|
|
241
|
+
/** Notices worth keeping, flushed after the checklist collapses. */
|
|
242
|
+
const notices: string[] = [];
|
|
243
|
+
|
|
244
|
+
const closure = await runStep(steps.profile, async (say) => {
|
|
245
|
+
say(`resolving "${targetId}"`);
|
|
246
|
+
const resolved = await resolveProfile(manifest, targetId);
|
|
247
|
+
steps.profile.detail = [
|
|
248
|
+
`${Object.keys(resolved.plugins).length} plugins`,
|
|
249
|
+
`${resolved.bins.length} tools`,
|
|
250
|
+
`${resolved.skills.length} skills`,
|
|
251
|
+
].join(" · ");
|
|
252
|
+
return resolved;
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// "latest" is only as fresh as the catalog. Fast-forward the marketplace
|
|
256
|
+
// clones first, then drop every cached answer derived from the old HEAD —
|
|
257
|
+
// skipping this is how a clone sits days behind while every plugin reads as
|
|
258
|
+
// up to date.
|
|
259
|
+
// ...but NOT under --dry-run or --check. Both promise to write nothing, and
|
|
260
|
+
// `git pull --ff-only` on a clone under ~/.claude/plugins/marketplaces/ is a
|
|
261
|
+
// write — it advances the user's marketplace to a new commit. The help text
|
|
262
|
+
// said "Writes nothing, exits 0" while the run printed "1 refreshed" in its
|
|
263
|
+
// own output, which is where this was caught. A read-only run plans against
|
|
264
|
+
// the clone as it stands; the notice below says so, so an unrefreshed
|
|
265
|
+
// catalog is never mistaken for a current one.
|
|
266
|
+
const refresh = await runStep(steps.marketplaces, async (say) => {
|
|
267
|
+
if (flags.dryRun || flags.check) {
|
|
268
|
+
say("read-only — selecting, not pulling");
|
|
269
|
+
// Report-only, NOT a skip: the autoUpdate-disabled verdict is a pure
|
|
270
|
+
// read and is the one diagnostic a read-only run must not lose.
|
|
271
|
+
const report = await reportRegisteredMarketplaces();
|
|
272
|
+
steps.marketplaces.detail = `not refreshed (read-only) · ${report.skipped.length} eligible`;
|
|
273
|
+
return report;
|
|
274
|
+
}
|
|
275
|
+
let settled = 0;
|
|
276
|
+
return refreshRegisteredMarketplaces([], {
|
|
277
|
+
onStart: (names) => {
|
|
278
|
+
// Only now is there a denominator; until `onStart` fires the step
|
|
279
|
+
// shows a sweep rather than a meter that would have to guess one.
|
|
280
|
+
steps.marketplaces.progress = { done: 0, total: names.length };
|
|
281
|
+
say(
|
|
282
|
+
names.length === 0 ? "none eligible" : `pulling ${names.join(", ")}`,
|
|
283
|
+
);
|
|
284
|
+
},
|
|
285
|
+
onSettled: (name, status) => {
|
|
286
|
+
settled++;
|
|
287
|
+
if (steps.marketplaces.progress)
|
|
288
|
+
steps.marketplaces.progress.done = settled;
|
|
289
|
+
say(`${name} ${status}`);
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
steps.marketplaces.detail = [
|
|
294
|
+
`${refresh.refreshed.length} refreshed`,
|
|
295
|
+
refresh.failed.length > 0 ? `${refresh.failed.length} failed` : "",
|
|
296
|
+
refresh.skipped.length > 0 ? `${refresh.skipped.length} skipped` : "",
|
|
297
|
+
]
|
|
298
|
+
.filter(Boolean)
|
|
299
|
+
.join(" · ");
|
|
300
|
+
if (refresh.failed.length > 0) steps.marketplaces.state = "fail";
|
|
301
|
+
|
|
302
|
+
for (const name of refresh.failed) {
|
|
303
|
+
notices.push(
|
|
304
|
+
failLine(
|
|
305
|
+
`${bold(name)}: refresh failed — clone left intact but stale, so its versions may be behind.`,
|
|
306
|
+
),
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
clearMarketplaceCache();
|
|
311
|
+
clearContentDriftCache();
|
|
312
|
+
|
|
313
|
+
const catalog = await runStep(steps.catalog, async (say) => {
|
|
314
|
+
say("reading installed and available plugins");
|
|
315
|
+
const map = new Map<string, PluginInfo>(
|
|
316
|
+
(await getAvailablePlugins(projectPath)).map((p) => [p.id, p]),
|
|
317
|
+
);
|
|
318
|
+
steps.catalog.detail = `${map.size} plugins known`;
|
|
319
|
+
return map;
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// The tool probe: installed version, owner, and the owning installer's own
|
|
323
|
+
// "behind?" answer — the same probe the TUI's CLI Tools screen uses. It used
|
|
324
|
+
// to be a presence check, which is why every present unpinned tool planned
|
|
325
|
+
// as an upgrade on every run.
|
|
326
|
+
const toolFacts = await runStep(steps.binaries, async (say) => {
|
|
327
|
+
if (closure.bins.length === 0) {
|
|
328
|
+
steps.binaries.detail = "none declared";
|
|
329
|
+
return [];
|
|
330
|
+
}
|
|
331
|
+
steps.binaries.progress = { done: 0, total: closure.bins.length };
|
|
332
|
+
const facts = await probeTools(closure.bins, REAL_TOOL_PROBE_IO, {
|
|
333
|
+
// `settled`, not `index`: results arrive in completion order, and a
|
|
334
|
+
// meter driven by index would jump backwards.
|
|
335
|
+
onResult: (f, { settled, total }) => {
|
|
336
|
+
steps.binaries.progress = { done: settled, total };
|
|
337
|
+
say(f.name);
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
const items = planBinUpdates(closure.bins, facts);
|
|
341
|
+
const n = (a: string) => items.filter((i) => i.action === a).length;
|
|
342
|
+
const behind = n("upgrade");
|
|
343
|
+
const unknown = n("unknown");
|
|
344
|
+
const missing = n("install");
|
|
345
|
+
steps.binaries.detail = [
|
|
346
|
+
`${items.length} checked`,
|
|
347
|
+
behind > 0 ? `${behind} behind` : "",
|
|
348
|
+
unknown > 0 ? `${unknown} unknown` : "",
|
|
349
|
+
missing > 0 ? `${missing} missing` : "",
|
|
350
|
+
]
|
|
351
|
+
.filter(Boolean)
|
|
352
|
+
.join(" · ");
|
|
353
|
+
if (missing + unknown > 0) steps.binaries.state = "warn";
|
|
354
|
+
return facts;
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const installedSkills = await runStep(steps.skills, async (say) => {
|
|
358
|
+
say("reading .claude/skills");
|
|
359
|
+
const names = await getInstalledSkillNames("project", projectPath);
|
|
360
|
+
steps.skills.detail =
|
|
361
|
+
closure.skills.length === 0
|
|
362
|
+
? "none declared"
|
|
363
|
+
: `${names.size} installed of ${closure.skills.length} declared`;
|
|
364
|
+
return names;
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// Collapse the animation into its permanent record, then flush the notices
|
|
368
|
+
// underneath it — a warning printed mid-checklist would have scrolled past.
|
|
369
|
+
live.stop(stepRecord(stepList, Date.now()));
|
|
370
|
+
for (const line of notices) console.log(line);
|
|
371
|
+
|
|
372
|
+
// ---- plan ----------------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
const plan: UpdatePlan = {
|
|
375
|
+
profileId: targetId,
|
|
376
|
+
plugins: planPluginUpdates(closure.plugins, catalog),
|
|
377
|
+
bins: planBinUpdates(closure.bins, toolFacts),
|
|
378
|
+
skills: planSkillUpdates(closure.skills, installedSkills),
|
|
379
|
+
binSpecs: new Map(closure.bins.map((b) => [b.name, b])),
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
for (const line of planReport(plan)) console.log(line);
|
|
383
|
+
|
|
384
|
+
const counts = summarizePlan(plan);
|
|
385
|
+
const unknownToolNames = plan.bins
|
|
386
|
+
.filter((b) => b.action === "unknown")
|
|
387
|
+
.map((b) => b.name);
|
|
388
|
+
const toolUnknownLine = () =>
|
|
389
|
+
unknownToolsNotice(counts.unknownTools, unknownToolNames);
|
|
390
|
+
// No plugin equivalent of `toolUnknownLine`. Every unverified plugin is
|
|
391
|
+
// already a row in the table above, under a heading that names its state,
|
|
392
|
+
// so a trailing "N plugin(s) could not be checked" restated the table in
|
|
393
|
+
// prose and — since the grouping landed — miscounted it: the count is raw
|
|
394
|
+
// `unknown`, which now includes rows filed under Current. A tool has no
|
|
395
|
+
// such row, which is why that notice stays and names them.
|
|
396
|
+
if (counts.unknownTools > 0) {
|
|
397
|
+
console.log("");
|
|
398
|
+
console.log(toolUnknownLine());
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// `--check` gates on planIsBehind, not planHasWork. An already-installed
|
|
402
|
+
// skill is re-fetched on every apply because its manifest ref carries no
|
|
403
|
+
// content hash, which is no evidence that anything is out of date — counting
|
|
404
|
+
// it made the gate exit 1 forever for any profile declaring a single skill.
|
|
405
|
+
//
|
|
406
|
+
// An `unknown` does not fail the gate, for plugins and tools alike (see
|
|
407
|
+
// planIsBehind). The same argument applies to both: a plugin whose
|
|
408
|
+
// marketplace publishes no version, or a tool another npm owns, is unknown
|
|
409
|
+
// permanently, so failing on it is the skill case again — this profile's 5
|
|
410
|
+
// unversioned official plugins made `--check` exit 1 on every run. A tool
|
|
411
|
+
// or plugin something says is BEHIND still does fail it. Because a failed
|
|
412
|
+
// check now passes silently by exit code, the unchecked count is printed
|
|
413
|
+
// beside the verdict; the per-kind notices and each row's reason are above.
|
|
414
|
+
if (flags.check) {
|
|
415
|
+
console.log("");
|
|
416
|
+
if (!planIsBehind(plan)) {
|
|
417
|
+
if (counts.unknown > 0) console.log(uncheckedGateNotice(counts.unknown));
|
|
418
|
+
console.log(okLine("Nothing missing and nothing behind."));
|
|
419
|
+
return 0;
|
|
420
|
+
}
|
|
421
|
+
console.error(
|
|
422
|
+
failLine(`Updates available. Run ${bold("magus update")} to apply.`),
|
|
423
|
+
);
|
|
424
|
+
return 1;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Exactly the items the engine will touch. Derived by the engine from the
|
|
428
|
+
// same predicates its loops use, so the meter's denominator cannot drift
|
|
429
|
+
// from the work — a bar stuck short of full reads as a hang.
|
|
430
|
+
const workItems = planWorkItems(plan);
|
|
431
|
+
|
|
432
|
+
if (!planHasWork(plan) || workItems.length === 0) {
|
|
433
|
+
for (const line of summaryLine(
|
|
434
|
+
0,
|
|
435
|
+
counts.current,
|
|
436
|
+
counts.unknown,
|
|
437
|
+
0,
|
|
438
|
+
Date.now() - startedAt,
|
|
439
|
+
))
|
|
440
|
+
console.log(line);
|
|
441
|
+
// `planHasWork` counts an `unknown` plugin as work, because from the
|
|
442
|
+
// planner's side "we could not check this" is genuinely not "this is
|
|
443
|
+
// fine". But nothing is APPLIED for an unknown — the apply loop skips it —
|
|
444
|
+
// so reporting on `planHasWork` alone claimed a run had happened when it
|
|
445
|
+
// had not. Say what is actually true instead.
|
|
446
|
+
if (counts.unknownPlugins + counts.unknownTools > 0) {
|
|
447
|
+
if (counts.unknownPlugins > 0) {
|
|
448
|
+
console.log(nothingToApplyNotice(counts.unknownPlugins));
|
|
449
|
+
}
|
|
450
|
+
if (counts.unknownTools > 0) console.log(toolUnknownLine());
|
|
451
|
+
return 0;
|
|
452
|
+
}
|
|
453
|
+
console.log(okLine("Everything is up to date."));
|
|
454
|
+
return 0;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// `--dry-run` stops here: the plan is printed above, nothing is written, and
|
|
458
|
+
// it exits 0. That is the difference from `--check`, which is a CI gate and
|
|
459
|
+
// exits 1 precisely when there IS something to do.
|
|
460
|
+
if (flags.dryRun) {
|
|
461
|
+
console.log("");
|
|
462
|
+
console.log(
|
|
463
|
+
info(
|
|
464
|
+
`Dry run — ${workItems.length} item(s) would be applied. Nothing written.`,
|
|
465
|
+
),
|
|
466
|
+
);
|
|
467
|
+
return 0;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// No confirmation. `update` moves the machine toward what the profile
|
|
471
|
+
// declares and can only go FORWARD: `claude plugin install` takes no version
|
|
472
|
+
// and installs the newest published one, so there is no downgrade and no
|
|
473
|
+
// delete for a prompt to protect against. The plan is printed above, so the
|
|
474
|
+
// run is still legible after the fact — and `--dry-run` shows it beforehand.
|
|
475
|
+
//
|
|
476
|
+
// Note this also runs a profile's CLI-tool installers unattended (`brew
|
|
477
|
+
// upgrade`, `bun install -g`). That is the deliberate cost of the choice.
|
|
478
|
+
|
|
479
|
+
if (!(await isClaudeAvailable())) {
|
|
480
|
+
console.error(
|
|
481
|
+
failLine(
|
|
482
|
+
"claude CLI not found on PATH — cannot install or update plugins.",
|
|
483
|
+
),
|
|
484
|
+
);
|
|
485
|
+
return 1;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Register any marketplace the profile names but this machine does not know.
|
|
489
|
+
// Without this an install fails with "not found in marketplace" for anything
|
|
490
|
+
// outside magus's built-in list, which is every private or third-party one.
|
|
491
|
+
await registerClosureMarketplaces(closure.marketplaces);
|
|
492
|
+
|
|
493
|
+
// ---- apply phase, under an animated meter --------------------------------
|
|
494
|
+
|
|
495
|
+
const nameWidth = Math.max(12, ...workItems.map((n) => width(n)));
|
|
496
|
+
|
|
497
|
+
const state: ApplyState = {
|
|
498
|
+
done: 0,
|
|
499
|
+
total: workItems.length,
|
|
500
|
+
current: "starting",
|
|
501
|
+
startedAt: Date.now(),
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
const applyLive = new LiveRegion();
|
|
505
|
+
/** The last detail each item finished with — the reason, for an unchanged one. */
|
|
506
|
+
const finishedDetail = new Map<string, string>();
|
|
507
|
+
const reporter: ApplyReporter = {
|
|
508
|
+
begin: (name) => {
|
|
509
|
+
state.current = name;
|
|
510
|
+
applyLive.refresh();
|
|
511
|
+
},
|
|
512
|
+
finish: (succeeded, name, detail, ms) => {
|
|
513
|
+
state.done++;
|
|
514
|
+
finishedDetail.set(name, detail);
|
|
515
|
+
applyLive.note(
|
|
516
|
+
applyRow(
|
|
517
|
+
succeeded,
|
|
518
|
+
name,
|
|
519
|
+
nameWidth,
|
|
520
|
+
!succeeded
|
|
521
|
+
? fg(brand.danger, detail)
|
|
522
|
+
: detail.startsWith("no change — ")
|
|
523
|
+
? fg(brand.warning, detail)
|
|
524
|
+
: dim(detail),
|
|
525
|
+
ms,
|
|
526
|
+
),
|
|
527
|
+
);
|
|
528
|
+
},
|
|
529
|
+
detach: (work) => withPaused(applyLive, work),
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
console.log("");
|
|
533
|
+
applyLive.start((tick) => applyFrame(state, tick, Date.now()));
|
|
534
|
+
|
|
535
|
+
const outcome = await applyPlan(plan, projectPath, { report: reporter });
|
|
536
|
+
|
|
537
|
+
applyLive.stop();
|
|
538
|
+
|
|
539
|
+
const { failed } = outcome;
|
|
540
|
+
|
|
541
|
+
for (const line of summaryLine(
|
|
542
|
+
outcome.changed,
|
|
543
|
+
counts.current,
|
|
544
|
+
counts.unknown,
|
|
545
|
+
failed.length,
|
|
546
|
+
Date.now() - startedAt,
|
|
547
|
+
outcome.unchanged.length,
|
|
548
|
+
))
|
|
549
|
+
console.log(line);
|
|
550
|
+
|
|
551
|
+
// Exited 0, read back unmoved. Reported, never fatal — the same precedent
|
|
552
|
+
// as the pin mismatches below: this is a standing property of the machine
|
|
553
|
+
// (a stale tap, a shadowing PATH entry), not something this run did wrong,
|
|
554
|
+
// and it will be replanned. What it must never be is counted as "changed".
|
|
555
|
+
if (outcome.unchanged.length > 0) {
|
|
556
|
+
console.log(
|
|
557
|
+
warnLine(
|
|
558
|
+
`${bold(String(outcome.unchanged.length))} item(s) ran but did not change:`,
|
|
559
|
+
),
|
|
560
|
+
);
|
|
561
|
+
for (const name of outcome.unchanged) {
|
|
562
|
+
const detail = (finishedDetail.get(name) ?? "").replace(
|
|
563
|
+
/^no change — /,
|
|
564
|
+
"",
|
|
565
|
+
);
|
|
566
|
+
console.log(noteLine(`${name}: ${detail}`));
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// An exact pin the installer could not deliver. Reported, never fatal: only
|
|
571
|
+
// the newest published version is installable, so this is a standing property
|
|
572
|
+
// of the manifest rather than something this run did wrong, and failing here
|
|
573
|
+
// would make `magus update` exit 1 forever for any profile with a pin.
|
|
574
|
+
// `install --check` with `strictVersions` is where pin drift is GATED.
|
|
575
|
+
if (outcome.plugins.mismatches.length > 0) {
|
|
576
|
+
console.log(
|
|
577
|
+
warnLine(
|
|
578
|
+
`${bold(String(outcome.plugins.mismatches.length))} pinned version(s) are not what is installed. Only the newest published version can be installed:`,
|
|
579
|
+
),
|
|
580
|
+
);
|
|
581
|
+
for (const m of outcome.plugins.mismatches) {
|
|
582
|
+
console.log(
|
|
583
|
+
noteLine(
|
|
584
|
+
`${m.pluginId} (${m.scope}): manifest pins ${m.pinned}, installed ${m.actual ?? "unknown"}`,
|
|
585
|
+
),
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
console.log(
|
|
589
|
+
noteLine(
|
|
590
|
+
"Recorded what is actually installed. Update the pin to match, or leave it as a record of intent.",
|
|
591
|
+
),
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (failed.length > 0) {
|
|
596
|
+
console.error(failLine(`Failed: ${failed.join(", ")}`));
|
|
597
|
+
return 1;
|
|
598
|
+
}
|
|
599
|
+
return 0;
|
|
600
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `magus upgrade` — self-update.
|
|
3
|
+
*
|
|
4
|
+
* This is what `magus update` used to do. `update` now means what it means
|
|
5
|
+
* in every package manager — bring THIS PROJECT's dependencies up to date —
|
|
6
|
+
* and advancing the tool itself moved here.
|
|
7
|
+
*
|
|
8
|
+
* Detects magus installs across bun + npm global, warns on duplicates
|
|
9
|
+
* (active vs shadowed), then re-installs `magus@latest` with whichever
|
|
10
|
+
* package manager owns the active binary. Resolves with the child's exit code.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { spawn } from "node:child_process";
|
|
14
|
+
import { constants } from "node:os";
|
|
15
|
+
|
|
16
|
+
export async function runUpgradeCommand(): Promise<number> {
|
|
17
|
+
const { execSync } = await import("node:child_process");
|
|
18
|
+
const { existsSync } = await import("node:fs");
|
|
19
|
+
|
|
20
|
+
// Detect all installations of magus across package managers
|
|
21
|
+
const installations: Array<{ manager: "bun" | "npm"; path: string }> = [];
|
|
22
|
+
|
|
23
|
+
// Check bun global
|
|
24
|
+
try {
|
|
25
|
+
const bunGlobalBin = execSync("bun pm -g bin", {
|
|
26
|
+
encoding: "utf-8",
|
|
27
|
+
timeout: 5000,
|
|
28
|
+
}).trim();
|
|
29
|
+
const bunPath = `${bunGlobalBin}/magus`;
|
|
30
|
+
if (existsSync(bunPath))
|
|
31
|
+
installations.push({ manager: "bun", path: bunPath });
|
|
32
|
+
} catch {
|
|
33
|
+
// bun not installed or no global magus
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check npm global
|
|
37
|
+
try {
|
|
38
|
+
const npmPrefix = execSync("npm prefix -g", {
|
|
39
|
+
encoding: "utf-8",
|
|
40
|
+
timeout: 5000,
|
|
41
|
+
}).trim();
|
|
42
|
+
const npmPath = `${npmPrefix}/bin/magus`;
|
|
43
|
+
if (existsSync(npmPath))
|
|
44
|
+
installations.push({ manager: "npm", path: npmPath });
|
|
45
|
+
} catch {
|
|
46
|
+
// npm not installed or no global magus
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Warn about duplicate installations
|
|
50
|
+
if (installations.length > 1) {
|
|
51
|
+
const activePath = execSync("which magus", {
|
|
52
|
+
encoding: "utf-8",
|
|
53
|
+
timeout: 5000,
|
|
54
|
+
}).trim();
|
|
55
|
+
|
|
56
|
+
console.log("⚠ magus is installed via multiple package managers:\n");
|
|
57
|
+
for (const inst of installations) {
|
|
58
|
+
const tag = inst.path === activePath ? " (active)" : " (shadowed)";
|
|
59
|
+
console.log(` ${inst.manager}: ${inst.path}${tag}`);
|
|
60
|
+
}
|
|
61
|
+
console.log("\nTo fix, keep one and remove the other:");
|
|
62
|
+
for (const inst of installations) {
|
|
63
|
+
console.log(` ${inst.manager} uninstall -g magus`);
|
|
64
|
+
}
|
|
65
|
+
console.log();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Determine which package manager to use for the update
|
|
69
|
+
let usesBun = false;
|
|
70
|
+
try {
|
|
71
|
+
const magusPath = execSync("which magus", {
|
|
72
|
+
encoding: "utf-8",
|
|
73
|
+
}).trim();
|
|
74
|
+
usesBun = magusPath.includes(".bun") || magusPath.includes("bun/bin");
|
|
75
|
+
} catch {
|
|
76
|
+
// If which fails, default to npm
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const pkgManager = usesBun ? "bun" : "npm";
|
|
80
|
+
console.log(`Updating magus using ${pkgManager}...`);
|
|
81
|
+
|
|
82
|
+
const installArgs = ["install", "-g", "magus@latest"];
|
|
83
|
+
|
|
84
|
+
return new Promise<number>((resolve) => {
|
|
85
|
+
const proc = spawn(pkgManager, installArgs, {
|
|
86
|
+
stdio: "inherit",
|
|
87
|
+
shell: false, // Avoid shell for security (fixes DEP0190 warning)
|
|
88
|
+
});
|
|
89
|
+
// `code ?? 0` is the same defect 6.8.1 removed from bin/magus.js: a
|
|
90
|
+
// package manager killed by a signal delivers code null, and reporting
|
|
91
|
+
// that upgrade as a success is the worst answer available.
|
|
92
|
+
proc.on("exit", (code, signal) =>
|
|
93
|
+
resolve(code ?? (signal ? 128 + (constants.signals[signal] ?? 0) : 1)),
|
|
94
|
+
);
|
|
95
|
+
proc.on("error", () => resolve(1));
|
|
96
|
+
});
|
|
97
|
+
}
|