magus-cli 0.0.0-placeholder → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +309 -0
- package/bin/magus.js +170 -0
- package/package.json +67 -3
- package/scripts/build-binaries.ts +113 -0
- package/scripts/capture-builtin.ts +412 -0
- package/scripts/check-optional-deps.ts +49 -0
- package/scripts/test-isolated.ts +49 -0
- package/scripts/verify-binary.ts +95 -0
- package/scripts/verify-community-registry.ts +272 -0
- package/src/__tests__/active-closure.test.ts +184 -0
- package/src/__tests__/alias-adopt.test.ts +365 -0
- package/src/__tests__/alias-parser.test.ts +417 -0
- package/src/__tests__/alias-shell-writer.test.ts +681 -0
- package/src/__tests__/alias-store.test.ts +163 -0
- package/src/__tests__/binary-signing.test.ts +120 -0
- package/src/__tests__/blackbox-active-closure.test.ts +119 -0
- package/src/__tests__/blackbox-convergence.test.ts +334 -0
- package/src/__tests__/blackbox-parity.test.ts +684 -0
- package/src/__tests__/blackbox-plugin-state.test.ts +523 -0
- package/src/__tests__/blackbox-tool-state.test.ts +669 -0
- package/src/__tests__/cache-provenance.test.ts +84 -0
- package/src/__tests__/capture-builtin.test.ts +92 -0
- package/src/__tests__/catalog-cache-store.test.ts +309 -0
- package/src/__tests__/catalog-notice.test.ts +155 -0
- package/src/__tests__/cli-ansi.test.ts +297 -0
- package/src/__tests__/cli-apply-seams.test.ts +288 -0
- package/src/__tests__/cli-live.test.ts +391 -0
- package/src/__tests__/cli-router.test.ts +240 -0
- package/src/__tests__/cli-tool-commands.test.ts +239 -0
- package/src/__tests__/cli-tools-adapter.test.ts +294 -0
- package/src/__tests__/cli-update-view.test.ts +445 -0
- package/src/__tests__/community-fetch.test.ts +545 -0
- package/src/__tests__/community-registry.test.ts +269 -0
- package/src/__tests__/community-staleness.test.ts +720 -0
- package/src/__tests__/component-badge.test.ts +98 -0
- package/src/__tests__/component-icons.test.ts +94 -0
- package/src/__tests__/content-drift.test.ts +353 -0
- package/src/__tests__/conventions-integration.test.ts +830 -0
- package/src/__tests__/conventions-manager.test.ts +1236 -0
- package/src/__tests__/doctor-bins.test.ts +43 -0
- package/src/__tests__/doctor.test.ts +463 -0
- package/src/__tests__/dotenv.test.ts +195 -0
- package/src/__tests__/enabled-not-installed.test.ts +94 -0
- package/src/__tests__/file-locking.test.ts +208 -0
- package/src/__tests__/fixtures/tool-probe/brew-list-versions-tmux.txt +2 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-behind-homebrew-spec.txt +29 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-tmux-current.txt +7 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-unknown-formula.txt +6 -0
- package/src/__tests__/fixtures/tool-probe/go-version-m-tmux-mcp.txt +27 -0
- package/src/__tests__/fixtures/tool-probe/npm-dist-tags-missing-scoped.txt +4 -0
- package/src/__tests__/fixtures/tool-probe/npm-dist-tags-openai-codex.txt +4 -0
- package/src/__tests__/fixtures/tool-probe/pypi-cowsay.txt +3 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-browser-use.toml +10 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-git.toml +8 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-pinned.toml +8 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-graphifyy.toml +7 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-mcp.toml +6 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-serena-agent.toml +9 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated-pinned.txt +5 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated.txt +11 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list.txt +22 -0
- package/src/__tests__/footer-hints.test.ts +40 -0
- package/src/__tests__/format-command.test.ts +78 -0
- package/src/__tests__/git-worktree.test.ts +108 -0
- package/src/__tests__/github-budget.test.ts +206 -0
- package/src/__tests__/gitignore-check.test.ts +71 -0
- package/src/__tests__/gitignore-detector.test.ts +176 -0
- package/src/__tests__/gitignore-fixer.test.ts +317 -0
- package/src/__tests__/gitignore-resolver.test.ts +193 -0
- package/src/__tests__/gitignore-service.test.ts +137 -0
- package/src/__tests__/go-module.test.ts +43 -0
- package/src/__tests__/hook-import-policy.test.ts +90 -0
- package/src/__tests__/hook-process.test.ts +256 -0
- package/src/__tests__/hook-registration.test.ts +224 -0
- package/src/__tests__/install-command.test.ts +81 -0
- package/src/__tests__/install-plan.test.ts +104 -0
- package/src/__tests__/launcher-signal.test.ts +308 -0
- package/src/__tests__/local-marketplace-scan.test.ts +174 -0
- package/src/__tests__/manifest.test.ts +281 -0
- package/src/__tests__/markdown-renderer.test.ts +142 -0
- package/src/__tests__/marketplace-badge.test.ts +140 -0
- package/src/__tests__/marketplace-refresh.test.ts +335 -0
- package/src/__tests__/marketplace-version.test.ts +84 -0
- package/src/__tests__/marketplaces.test.ts +128 -0
- package/src/__tests__/mate-availability.test.ts +156 -0
- package/src/__tests__/mate-catalog.test.ts +295 -0
- package/src/__tests__/mcp-registry.test.ts +158 -0
- package/src/__tests__/model-visuals.test.tsx +2466 -0
- package/src/__tests__/models-adapter.test.ts +332 -0
- package/src/__tests__/models-cli.test.ts +273 -0
- package/src/__tests__/models-core.test.ts +802 -0
- package/src/__tests__/models-manager.test.ts +500 -0
- package/src/__tests__/models-presets-marketplace.test.ts +168 -0
- package/src/__tests__/models-screen-state.test.ts +315 -0
- package/src/__tests__/moved-marketplace.test.ts +86 -0
- package/src/__tests__/open-file.test.ts +59 -0
- package/src/__tests__/owned-marketplaces.test.ts +93 -0
- package/src/__tests__/plugin-checkout.test.ts +66 -0
- package/src/__tests__/plugin-cli-argv.test.ts +109 -0
- package/src/__tests__/plugin-contents.test.ts +371 -0
- package/src/__tests__/plugin-manager-fallback.test.ts +320 -0
- package/src/__tests__/plugin-releases.test.ts +98 -0
- package/src/__tests__/plugin-requires.test.ts +187 -0
- package/src/__tests__/plugin-row-text.test.ts +336 -0
- package/src/__tests__/plugin-setup.test.ts +1016 -0
- package/src/__tests__/profile-adopt.test.ts +177 -0
- package/src/__tests__/profile-command.test.ts +110 -0
- package/src/__tests__/profile-materializer.test.ts +238 -0
- package/src/__tests__/profile-sync.test.ts +154 -0
- package/src/__tests__/registry-version-resolution.test.ts +88 -0
- package/src/__tests__/resolve-executable.test.ts +42 -0
- package/src/__tests__/resolver.test.ts +351 -0
- package/src/__tests__/scope-action.test.ts +134 -0
- package/src/__tests__/scope-squares.test.tsx +164 -0
- package/src/__tests__/selection-scope.test.ts +100 -0
- package/src/__tests__/settings-file.test.ts +179 -0
- package/src/__tests__/shell-script-callers.test.ts +92 -0
- package/src/__tests__/skill-install-files.test.ts +136 -0
- package/src/__tests__/skills-catalog-client.test.ts +48 -0
- package/src/__tests__/style-wrap.test.ts +220 -0
- package/src/__tests__/styles-manager.test.ts +1098 -0
- package/src/__tests__/styles-origins.test.ts +414 -0
- package/src/__tests__/styles-screen-state.test.ts +457 -0
- package/src/__tests__/styles-status-line.test.ts +72 -0
- package/src/__tests__/styles-sync.test.ts +452 -0
- package/src/__tests__/symlink-manager.test.ts +180 -0
- package/src/__tests__/tabbar-layout.test.ts +100 -0
- package/src/__tests__/terminology-filler.test.ts +214 -0
- package/src/__tests__/theme-adaptive-colors.test.ts +354 -0
- package/src/__tests__/theme-env.test.ts +281 -0
- package/src/__tests__/theme-resolve.test.ts +380 -0
- package/src/__tests__/tool-probe.test.ts +1400 -0
- package/src/__tests__/toolchain.test.ts +232 -0
- package/src/__tests__/ui-version-writers.test.ts +88 -0
- package/src/__tests__/update-apply.test.ts +637 -0
- package/src/__tests__/update-plan.test.ts +1373 -0
- package/src/__tests__/uppercase-keybindings.test.ts +101 -0
- package/src/__tests__/useGitignoreModal.test.ts +79 -0
- package/src/__tests__/version-check-refresh.test.ts +45 -0
- package/src/__tests__/version-snapshot.test.ts +212 -0
- package/src/__tests__/version-unknown-state.test.ts +388 -0
- package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
- package/src/cli/ansi.ts +520 -0
- package/src/cli/bootstrap.ts +134 -0
- package/src/cli/doctor.ts +218 -0
- package/src/cli/hook.ts +129 -0
- package/src/cli/install.ts +382 -0
- package/src/cli/live.ts +374 -0
- package/src/cli/models.ts +297 -0
- package/src/cli/profile.ts +197 -0
- package/src/cli/prompt.ts +32 -0
- package/src/cli/router.ts +207 -0
- package/src/cli/update-view.ts +526 -0
- package/src/cli/update.ts +613 -0
- package/src/cli/upgrade.ts +97 -0
- package/src/data/alias-flags.ts +301 -0
- package/src/data/cli-tools.ts +146 -0
- package/src/data/community-styles.ts +531 -0
- package/src/data/gitignore-defaults.ts +33 -0
- package/src/data/gitignore-reasons.ts +93 -0
- package/src/data/gitignore-templates.ts +25 -0
- package/src/data/marketplaces.ts +278 -0
- package/src/data/mcp-servers.ts +51 -0
- package/src/data/models-presets.ts +329 -0
- package/src/data/predefined-profiles.ts +252 -0
- package/src/data/settings-catalog.ts +714 -0
- package/src/data/skill-repos.ts +91 -0
- package/src/data/styles/asd-ste100.md +38 -0
- package/src/data/styles/calibrated.md +24 -0
- package/src/data/styles/direct.md +23 -0
- package/src/data/styles/evidence-first.md +22 -0
- package/src/data/styles/explanatory.md +23 -0
- package/src/data/styles/index.ts +57 -0
- package/src/data/styles/no-slop.md +32 -0
- package/src/data/styles/plain-language.md +23 -0
- package/src/data/styles/structured.md +30 -0
- package/src/data/styles/terminology.md +28 -0
- package/src/data/styles/terse.md +20 -0
- package/src/main.tsx +110 -0
- package/src/markdown.d.ts +13 -0
- package/src/opentui.d.ts +203 -0
- package/src/services/active-closure.ts +117 -0
- package/src/services/alias-settings.ts +60 -0
- package/src/services/alias-shell-writer.ts +1244 -0
- package/src/services/alias-store.ts +172 -0
- package/src/services/binary-signing.ts +78 -0
- package/src/services/catalog-cache-store.ts +316 -0
- package/src/services/claude-cli.ts +304 -0
- package/src/services/claude-runner.ts +31 -0
- package/src/services/claude-settings.ts +1589 -0
- package/src/services/cli-tool-commands.ts +133 -0
- package/src/services/community-fetcher.ts +90 -0
- package/src/services/community-styles.ts +1194 -0
- package/src/services/content-drift.ts +208 -0
- package/src/services/conventions-manager.ts +945 -0
- package/src/services/doctor-bins.ts +63 -0
- package/src/services/doctor.ts +570 -0
- package/src/services/dotenv.ts +181 -0
- package/src/services/git-worktree.ts +129 -0
- package/src/services/github-budget.ts +274 -0
- package/src/services/gitignore-check.ts +63 -0
- package/src/services/gitignore-detector.ts +167 -0
- package/src/services/gitignore-fixer.ts +280 -0
- package/src/services/gitignore-resolver.ts +208 -0
- package/src/services/gitignore-service.ts +167 -0
- package/src/services/hook-registration.ts +218 -0
- package/src/services/install-plan.ts +107 -0
- package/src/services/local-marketplace.ts +587 -0
- package/src/services/manifest.ts +250 -0
- package/src/services/marketplace-catalog-git.ts +173 -0
- package/src/services/marketplace-catalog.ts +129 -0
- package/src/services/marketplace-fetcher.ts +400 -0
- package/src/services/marketplace-refresh.ts +253 -0
- package/src/services/marketplace-sync.ts +145 -0
- package/src/services/mate-availability.ts +133 -0
- package/src/services/mate-catalog.ts +265 -0
- package/src/services/mcp-registry.ts +171 -0
- package/src/services/models-core.ts +969 -0
- package/src/services/models-manager.ts +606 -0
- package/src/services/plugin-checkout.ts +186 -0
- package/src/services/plugin-manager.ts +878 -0
- package/src/services/plugin-mcp-config.ts +242 -0
- package/src/services/plugin-releases.ts +47 -0
- package/src/services/plugin-requires.ts +227 -0
- package/src/services/plugin-setup.ts +965 -0
- package/src/services/profile-adopt.ts +206 -0
- package/src/services/profile-materializer.ts +133 -0
- package/src/services/profile-sync.ts +158 -0
- package/src/services/profiles.ts +225 -0
- package/src/services/resolver.ts +344 -0
- package/src/services/settings-file.ts +69 -0
- package/src/services/settings-manager.ts +307 -0
- package/src/services/skills-manager.ts +1111 -0
- package/src/services/skillsmp-client.ts +193 -0
- package/src/services/styles-manager.ts +1294 -0
- package/src/services/symlink-manager.ts +189 -0
- package/src/services/terminology-filler.ts +266 -0
- package/src/services/tool-probe.ts +1637 -0
- package/src/services/toolchain.ts +431 -0
- package/src/services/update-engine.ts +691 -0
- package/src/services/update-plan.ts +863 -0
- package/src/services/version-check.ts +127 -0
- package/src/services/version-snapshot.ts +189 -0
- package/src/tui.tsx +112 -0
- package/src/types/bun.d.ts +27 -0
- package/src/types/gitignore.ts +68 -0
- package/src/types/index.ts +447 -0
- package/src/ui/App.tsx +538 -0
- package/src/ui/adapters/catalogNotice.ts +122 -0
- package/src/ui/adapters/cliToolsAdapter.ts +176 -0
- package/src/ui/adapters/modelsAdapter.ts +213 -0
- package/src/ui/adapters/pluginsAdapter.ts +413 -0
- package/src/ui/adapters/settingsAdapter.ts +168 -0
- package/src/ui/adapters/skillsAdapter.ts +229 -0
- package/src/ui/adapters/stylesAdapter.ts +403 -0
- package/src/ui/components/CategoryHeader.tsx +64 -0
- package/src/ui/components/EmptyFilterState.tsx +32 -0
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScopeIndicator.tsx +63 -0
- package/src/ui/components/ScrollableList.tsx +95 -0
- package/src/ui/components/SearchInput.tsx +57 -0
- package/src/ui/components/StyledText.tsx +75 -0
- package/src/ui/components/TabBar.tsx +101 -0
- package/src/ui/components/layout/FooterHints.tsx +78 -0
- package/src/ui/components/layout/Panel.tsx +63 -0
- package/src/ui/components/layout/ProgressBar.tsx +44 -0
- package/src/ui/components/layout/ScopeTabs.tsx +54 -0
- package/src/ui/components/layout/ScreenLayout.tsx +321 -0
- package/src/ui/components/layout/index.ts +9 -0
- package/src/ui/components/modals/ConfirmModal.tsx +62 -0
- package/src/ui/components/modals/InputModal.tsx +75 -0
- package/src/ui/components/modals/LoadingModal.tsx +51 -0
- package/src/ui/components/modals/MessageModal.tsx +66 -0
- package/src/ui/components/modals/ModalContainer.tsx +159 -0
- package/src/ui/components/modals/SelectModal.tsx +90 -0
- package/src/ui/components/modals/index.ts +6 -0
- package/src/ui/components/primitives/ActionHints.tsx +44 -0
- package/src/ui/components/primitives/DetailSection.tsx +23 -0
- package/src/ui/components/primitives/KeyValueLine.tsx +26 -0
- package/src/ui/components/primitives/ListCategoryRow.tsx +45 -0
- package/src/ui/components/primitives/MetaText.tsx +40 -0
- package/src/ui/components/primitives/ScopeDetail.tsx +67 -0
- package/src/ui/components/primitives/ScopeSquares.tsx +58 -0
- package/src/ui/components/primitives/SelectableRow.tsx +30 -0
- package/src/ui/components/primitives/index.ts +9 -0
- package/src/ui/hooks/index.ts +2 -0
- package/src/ui/hooks/useGitignoreModal.ts +95 -0
- package/src/ui/hooks/useKeyboard.ts +24 -0
- package/src/ui/hooks/useKeyboardHandler.ts +63 -0
- package/src/ui/registry.ts +33 -0
- package/src/ui/renderers/cliToolRenderers.tsx +326 -0
- package/src/ui/renderers/gitignoreRenderers.tsx +80 -0
- package/src/ui/renderers/markdownRenderer.tsx +302 -0
- package/src/ui/renderers/mcpRenderers.tsx +149 -0
- package/src/ui/renderers/modelRenderers.tsx +1309 -0
- package/src/ui/renderers/modelVisuals.tsx +1402 -0
- package/src/ui/renderers/pluginRenderers.tsx +1089 -0
- package/src/ui/renderers/pluginRowText.ts +70 -0
- package/src/ui/renderers/pluginStatusText.ts +56 -0
- package/src/ui/renderers/profileRenderers.tsx +406 -0
- package/src/ui/renderers/settingsRenderers.tsx +218 -0
- package/src/ui/renderers/skillRenderers.tsx +479 -0
- package/src/ui/renderers/styleRenderers.tsx +813 -0
- package/src/ui/screens/AliasScreen.tsx +1558 -0
- package/src/ui/screens/CliToolsScreen.tsx +367 -0
- package/src/ui/screens/EnvVarsScreen.tsx +221 -0
- package/src/ui/screens/GitignoreScreen.tsx +562 -0
- package/src/ui/screens/McpRegistryScreen.tsx +366 -0
- package/src/ui/screens/McpScreen.tsx +256 -0
- package/src/ui/screens/ModelsScreen.tsx +542 -0
- package/src/ui/screens/PluginsScreen.tsx +1464 -0
- package/src/ui/screens/ProfilesScreen.tsx +438 -0
- package/src/ui/screens/SkillsScreen.tsx +853 -0
- package/src/ui/screens/StylesScreen.tsx +1064 -0
- package/src/ui/screens/index.ts +11 -0
- package/src/ui/state/AppContext.tsx +233 -0
- package/src/ui/state/DimensionsContext.tsx +154 -0
- package/src/ui/state/reducer.ts +810 -0
- package/src/ui/state/types.ts +503 -0
- package/src/ui/theme-mode.ts +352 -0
- package/src/ui/theme-resolve.ts +147 -0
- package/src/ui/theme.ts +175 -0
- package/src/utils/clipboard.ts +58 -0
- package/src/utils/command-utils.ts +144 -0
- package/src/utils/config-dir.ts +45 -0
- package/src/utils/file-locking.ts +146 -0
- package/src/utils/fuzzy-search.ts +138 -0
- package/src/utils/go-module.ts +36 -0
- package/src/utils/open-file.ts +84 -0
- package/src/utils/run.ts +67 -0
- package/src/utils/string-utils.ts +88 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adopt a project into profile management.
|
|
3
|
+
*
|
|
4
|
+
* `install` and `update` both speak the language of `.claude/profiles.json`,
|
|
5
|
+
* and most repos do not have one. The answer is to write one from what the
|
|
6
|
+
* project already has — enabled plugins, MCP servers, settings — rather than
|
|
7
|
+
* to run a second, manifest-free code path beside it. One command shape, one
|
|
8
|
+
* source of truth, and the generated file is a reviewable diff the user can
|
|
9
|
+
* commit and hand to the rest of the team.
|
|
10
|
+
*
|
|
11
|
+
* Plugins are adopted as "latest", never as the version that happens to be
|
|
12
|
+
* installed today. Pinning what a machine drifted to would turn one developer's
|
|
13
|
+
* accident into the team's contract.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
McpServerConfig,
|
|
18
|
+
ProfileManifest,
|
|
19
|
+
ProfileManifestEntry,
|
|
20
|
+
} from "../types/index.js";
|
|
21
|
+
import {
|
|
22
|
+
getEnabledPlugins,
|
|
23
|
+
getGlobalEnabledPlugins,
|
|
24
|
+
readMcpConfig,
|
|
25
|
+
readSettings,
|
|
26
|
+
} from "./claude-settings.js";
|
|
27
|
+
import { emptyManifest } from "./manifest.js";
|
|
28
|
+
import { withDerivedMarketplaces } from "./resolver.js";
|
|
29
|
+
|
|
30
|
+
/** The profile id used when adopting a project that has no manifest. */
|
|
31
|
+
export const ADOPTED_PROFILE_ID = "default";
|
|
32
|
+
|
|
33
|
+
export interface AdoptInput {
|
|
34
|
+
/** Union of enabled plugins across every scope: id -> enabled. */
|
|
35
|
+
enabledPlugins: Record<string, boolean>;
|
|
36
|
+
/** Project `.claude/settings.json`, verbatim. */
|
|
37
|
+
settings: Record<string, unknown>;
|
|
38
|
+
/** Project `.mcp.json` servers. */
|
|
39
|
+
mcpServers: Record<string, McpServerConfig>;
|
|
40
|
+
/** Display name for the generated profile. */
|
|
41
|
+
name?: string;
|
|
42
|
+
/** Injectable clock so the output is deterministic in tests. */
|
|
43
|
+
now?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Build a manifest entry from captured project state. Pure.
|
|
48
|
+
*
|
|
49
|
+
* `enabledPlugins` is dropped from `settings` deliberately: it is expressed as
|
|
50
|
+
* the entry's `plugins` map, and materialization regenerates it. Carrying both
|
|
51
|
+
* would let them disagree.
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* Settings keys that describe THIS MACHINE, not the project, and must never be
|
|
55
|
+
* copied into a manifest meant to be committed.
|
|
56
|
+
*
|
|
57
|
+
* - `enabledPlugins` is expressed as the entry's `plugins` map and regenerated
|
|
58
|
+
* by materialization; carrying both lets them disagree.
|
|
59
|
+
* - `installedPluginVersions` is magus's private record of what this machine
|
|
60
|
+
* has on disk. Committing it publishes one developer's install state as if it
|
|
61
|
+
* were the team's contract, and it goes stale the moment anyone updates.
|
|
62
|
+
*/
|
|
63
|
+
const MACHINE_ONLY_SETTINGS_KEYS = [
|
|
64
|
+
"enabledPlugins",
|
|
65
|
+
"installedPluginVersions",
|
|
66
|
+
] as const;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Settings values that mention an absolute path under the user's home
|
|
70
|
+
* directory. Not stripped — a `permissions.additionalDirectories` list is often
|
|
71
|
+
* genuinely wanted — but surfaced so the user reviews them before committing.
|
|
72
|
+
*/
|
|
73
|
+
export function findMachineSpecificPaths(
|
|
74
|
+
settings: Record<string, unknown>,
|
|
75
|
+
home: string = process.env.HOME ?? "",
|
|
76
|
+
): string[] {
|
|
77
|
+
if (!home) return [];
|
|
78
|
+
const hits: string[] = [];
|
|
79
|
+
const walk = (value: unknown, path: string): void => {
|
|
80
|
+
if (typeof value === "string") {
|
|
81
|
+
if (value.includes(home)) hits.push(`${path} = ${value}`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
value.forEach((v, i) => walk(v, `${path}[${i}]`));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (value && typeof value === "object") {
|
|
89
|
+
for (const [k, v] of Object.entries(value)) walk(v, `${path}.${k}`);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
for (const [k, v] of Object.entries(settings)) walk(v, k);
|
|
93
|
+
return hits;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function buildAdoptedProfile(input: AdoptInput): ProfileManifestEntry {
|
|
97
|
+
const plugins: Record<string, string> = {};
|
|
98
|
+
for (const [id, enabled] of Object.entries(input.enabledPlugins)) {
|
|
99
|
+
// A plugin id must name its marketplace; anything else cannot be resolved
|
|
100
|
+
// or reinstalled on another machine, so it has no place in a team manifest.
|
|
101
|
+
if (enabled && id.includes("@")) plugins[id] = "latest";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const settings = { ...input.settings };
|
|
105
|
+
for (const key of MACHINE_ONLY_SETTINGS_KEYS) delete settings[key];
|
|
106
|
+
|
|
107
|
+
const entry: ProfileManifestEntry = {
|
|
108
|
+
name: input.name ?? "Default",
|
|
109
|
+
marketplaces: withDerivedMarketplaces(undefined, plugins),
|
|
110
|
+
plugins,
|
|
111
|
+
createdAt: input.now ?? new Date().toISOString(),
|
|
112
|
+
};
|
|
113
|
+
if (Object.keys(input.mcpServers).length > 0) {
|
|
114
|
+
entry.mcpServers = input.mcpServers;
|
|
115
|
+
}
|
|
116
|
+
if (Object.keys(settings).length > 0) entry.settings = settings;
|
|
117
|
+
return entry;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface AdoptResult {
|
|
121
|
+
manifest: ProfileManifest;
|
|
122
|
+
profileId: string;
|
|
123
|
+
entry: ProfileManifestEntry;
|
|
124
|
+
counts: { plugins: number; mcpServers: number; settingsKeys: number };
|
|
125
|
+
/** Which settings file the plugin list came from. */
|
|
126
|
+
source: AdoptSource;
|
|
127
|
+
/** Enabled plugin ids skipped because they name no marketplace. */
|
|
128
|
+
skipped: string[];
|
|
129
|
+
/** Carried-over settings values naming an absolute path under $HOME. */
|
|
130
|
+
machinePaths: string[];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Which settings file the adopted plugin list came from. */
|
|
134
|
+
export type AdoptSource = "project" | "user";
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Find the plugins to adopt, preferring PROJECT scope.
|
|
138
|
+
*
|
|
139
|
+
* The generated manifest is meant to be committed, so its plugin list must
|
|
140
|
+
* describe the repo, not the machine. `.claude/settings.json` is exactly that:
|
|
141
|
+
* the repo's own committed choice. User scope (`~/.claude/settings.json`) is
|
|
142
|
+
* one developer's global set and local scope is gitignored personal state —
|
|
143
|
+
* adopting either silently would publish a personal setup as the team's
|
|
144
|
+
* contract.
|
|
145
|
+
*
|
|
146
|
+
* User scope is still offered as a fallback, because a developer who enables
|
|
147
|
+
* everything globally would otherwise have nothing to adopt. The caller is told
|
|
148
|
+
* which source was used and says so, so the choice is never invisible.
|
|
149
|
+
*/
|
|
150
|
+
async function collectEnabledPlugins(
|
|
151
|
+
projectPath: string,
|
|
152
|
+
): Promise<{ plugins: Record<string, boolean>; source: AdoptSource }> {
|
|
153
|
+
const project = await getEnabledPlugins(projectPath).catch(() => ({}));
|
|
154
|
+
if (Object.values(project).some(Boolean)) {
|
|
155
|
+
return { plugins: project, source: "project" };
|
|
156
|
+
}
|
|
157
|
+
const user = await getGlobalEnabledPlugins().catch(() => ({}));
|
|
158
|
+
return { plugins: user, source: "user" };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Read this project's current state and produce the manifest that describes it.
|
|
163
|
+
* Does not write — the caller confirms first.
|
|
164
|
+
*/
|
|
165
|
+
export async function adoptCurrentProject(
|
|
166
|
+
projectPath: string,
|
|
167
|
+
name?: string,
|
|
168
|
+
): Promise<AdoptResult> {
|
|
169
|
+
const { plugins: enabledPlugins, source } =
|
|
170
|
+
await collectEnabledPlugins(projectPath);
|
|
171
|
+
const settings = (await readSettings(projectPath)) as unknown as Record<
|
|
172
|
+
string,
|
|
173
|
+
unknown
|
|
174
|
+
>;
|
|
175
|
+
const mcp = await readMcpConfig(projectPath);
|
|
176
|
+
|
|
177
|
+
const entry = buildAdoptedProfile({
|
|
178
|
+
enabledPlugins,
|
|
179
|
+
settings,
|
|
180
|
+
mcpServers: mcp.mcpServers ?? {},
|
|
181
|
+
name,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const manifest = emptyManifest();
|
|
185
|
+
manifest.profiles[ADOPTED_PROFILE_ID] = entry;
|
|
186
|
+
|
|
187
|
+
const skipped = Object.entries(enabledPlugins)
|
|
188
|
+
.filter(([id, on]) => on && !id.includes("@"))
|
|
189
|
+
.map(([id]) => id);
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
manifest,
|
|
193
|
+
profileId: ADOPTED_PROFILE_ID,
|
|
194
|
+
entry,
|
|
195
|
+
source,
|
|
196
|
+
counts: {
|
|
197
|
+
plugins: Object.keys(entry.plugins ?? {}).length,
|
|
198
|
+
mcpServers: Object.keys(entry.mcpServers ?? {}).length,
|
|
199
|
+
settingsKeys: Object.keys(entry.settings ?? {}).length,
|
|
200
|
+
},
|
|
201
|
+
skipped,
|
|
202
|
+
machinePaths: findMachineSpecificPaths(
|
|
203
|
+
(entry.settings ?? {}) as Record<string, unknown>,
|
|
204
|
+
),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Materialize a resolved profile into `.claude/_profiles/<name>/`.
|
|
3
|
+
*
|
|
4
|
+
* Writes the shared config the symlink manager later points at:
|
|
5
|
+
* _profiles/<name>/settings.json — enabledPlugins + the profile's settings
|
|
6
|
+
* _profiles/<name>/mcp.json — the profile's MCP servers
|
|
7
|
+
* _profiles/<name>/models.json — per-subagent model routing, when the
|
|
8
|
+
* profile declares any; REMOVED when it
|
|
9
|
+
* does not
|
|
10
|
+
* _profiles/<name>/skills/ — created empty; skills install into it
|
|
11
|
+
* (through the activated symlink)
|
|
12
|
+
*
|
|
13
|
+
* This is pure output derived from a ResolvedClosure — it performs no installs
|
|
14
|
+
* and touches no global state, so it is fully testable with a temp dir.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import fs from "fs-extra";
|
|
19
|
+
import type { ResolvedClosure } from "../types/index.js";
|
|
20
|
+
import { profileDir } from "./symlink-manager.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build the settings.json contents for a profile: every resolved plugin
|
|
24
|
+
* enabled, merged with the profile's declared settings. The profile's settings
|
|
25
|
+
* win over the derived enabledPlugins only if it explicitly sets that key
|
|
26
|
+
* (it normally won't).
|
|
27
|
+
*
|
|
28
|
+
* `allPluginIds` is the manifest-wide union of plugins across every profile.
|
|
29
|
+
* Plugins in the union that this profile does NOT want are written as an
|
|
30
|
+
* explicit `false`, which is what makes `profile switch` exclusive rather than
|
|
31
|
+
* additive. Claude Code resolves enabledPlugins **per plugin id**, walking from
|
|
32
|
+
* the highest-precedence scope down and taking the first scope that mentions
|
|
33
|
+
* the id — an id a scope omits falls through to the next one. Since
|
|
34
|
+
* `magus install` installs the union at user scope, every non-member would
|
|
35
|
+
* otherwise inherit that user-scope `true` and stay enabled while a profile
|
|
36
|
+
* that excludes it is active. Omitting the union (the default) preserves the
|
|
37
|
+
* old additive behavior for callers that have no cross-profile view.
|
|
38
|
+
*/
|
|
39
|
+
export function buildProfileSettings(
|
|
40
|
+
closure: ResolvedClosure,
|
|
41
|
+
allPluginIds: readonly string[] = [],
|
|
42
|
+
): Record<string, unknown> {
|
|
43
|
+
const enabledPlugins: Record<string, boolean> = {};
|
|
44
|
+
for (const pluginId of allPluginIds) {
|
|
45
|
+
enabledPlugins[pluginId] = false;
|
|
46
|
+
}
|
|
47
|
+
for (const pluginId of Object.keys(closure.plugins)) {
|
|
48
|
+
enabledPlugins[pluginId] = true;
|
|
49
|
+
}
|
|
50
|
+
return { enabledPlugins, ...closure.settings };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Build the .mcp.json contents for a profile. */
|
|
54
|
+
export function buildProfileMcp(closure: ResolvedClosure): {
|
|
55
|
+
mcpServers: Record<string, unknown>;
|
|
56
|
+
} {
|
|
57
|
+
return { mcpServers: { ...closure.mcpServers } };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Seed a profile's skills/ dir from the project's pre-existing `.claude/skills/`.
|
|
62
|
+
*
|
|
63
|
+
* Activating a profile replaces `.claude/skills` with a symlink into the profile
|
|
64
|
+
* dir, and replacing means `fs.remove` first. A project that committed its own
|
|
65
|
+
* skills before adopting profiles would lose them at that moment. So the first
|
|
66
|
+
* time a profile's skills/ dir is created, any real (non-symlink) project skills
|
|
67
|
+
* are copied in — every profile inherits what the project already had, and the
|
|
68
|
+
* link swap destroys nothing.
|
|
69
|
+
*
|
|
70
|
+
* Only runs when the profile's skills/ dir does not exist yet, so it never
|
|
71
|
+
* fights a later `skills-manager` install or re-adds a skill the user removed.
|
|
72
|
+
*/
|
|
73
|
+
async function seedSkillsFromProject(
|
|
74
|
+
skillsDir: string,
|
|
75
|
+
projectPath?: string,
|
|
76
|
+
): Promise<void> {
|
|
77
|
+
if (await fs.pathExists(skillsDir)) return;
|
|
78
|
+
|
|
79
|
+
const projectSkills = path.join(
|
|
80
|
+
projectPath ?? process.cwd(),
|
|
81
|
+
".claude",
|
|
82
|
+
"skills",
|
|
83
|
+
);
|
|
84
|
+
try {
|
|
85
|
+
// lstat, not pathExists: an existing profile symlink is not a source.
|
|
86
|
+
const stat = await fs.lstat(projectSkills);
|
|
87
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) return;
|
|
88
|
+
if ((await fs.readdir(projectSkills)).length === 0) return;
|
|
89
|
+
await fs.copy(projectSkills, skillsDir, { dereference: true });
|
|
90
|
+
} catch {
|
|
91
|
+
// No project skills dir (or unreadable) — nothing to carry over.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Write `_profiles/<name>/` from a resolved closure. Idempotent: overwrites
|
|
97
|
+
* settings.json / mcp.json and ensures skills/ exists. Returns the dir path.
|
|
98
|
+
*/
|
|
99
|
+
export async function materializeProfile(
|
|
100
|
+
name: string,
|
|
101
|
+
closure: ResolvedClosure,
|
|
102
|
+
projectPath?: string,
|
|
103
|
+
allPluginIds: readonly string[] = [],
|
|
104
|
+
): Promise<string> {
|
|
105
|
+
const dir = profileDir(name, projectPath);
|
|
106
|
+
await fs.ensureDir(dir);
|
|
107
|
+
await fs.writeJson(
|
|
108
|
+
path.join(dir, "settings.json"),
|
|
109
|
+
buildProfileSettings(closure, allPluginIds),
|
|
110
|
+
{ spaces: 2 },
|
|
111
|
+
);
|
|
112
|
+
await fs.writeJson(path.join(dir, "mcp.json"), buildProfileMcp(closure), {
|
|
113
|
+
spaces: 2,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// models.json is the one artifact that is REMOVED when the closure lacks it.
|
|
117
|
+
// Every other file here is unconditional, so "overwrite" is enough; routing
|
|
118
|
+
// is optional, and a profile that dropped it would otherwise keep routing
|
|
119
|
+
// from a file nothing writes any more — `magus models off` could not turn
|
|
120
|
+
// routing off, because the hook walks up to the nearest models.json and this
|
|
121
|
+
// one would still be there (and still linked).
|
|
122
|
+
const modelsPath = path.join(dir, "models.json");
|
|
123
|
+
if (closure.models) {
|
|
124
|
+
await fs.writeJson(modelsPath, closure.models, { spaces: 2 });
|
|
125
|
+
} else {
|
|
126
|
+
await fs.remove(modelsPath);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const skillsDir = path.join(dir, "skills");
|
|
130
|
+
await seedSkillsFromProject(skillsDir, projectPath);
|
|
131
|
+
await fs.ensureDir(skillsDir);
|
|
132
|
+
return dir;
|
|
133
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `profile sync` — promote edits made to the active profile dir back into the
|
|
3
|
+
* committed manifest.
|
|
4
|
+
*
|
|
5
|
+
* `.claude/_profiles/<name>/` is generated build output, so an edit to the
|
|
6
|
+
* symlinked settings.json / mcp.json would be lost on the next `install`. Sync
|
|
7
|
+
* reverse-materializes those files into `.claude/profiles.json` so the change
|
|
8
|
+
* becomes a reviewable manifest diff instead of silent, throwaway drift.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import fs from "fs-extra";
|
|
13
|
+
import type {
|
|
14
|
+
McpServerConfig,
|
|
15
|
+
ProfileManifest,
|
|
16
|
+
ProfileManifestEntry,
|
|
17
|
+
} from "../types/index.js";
|
|
18
|
+
import { readManifest, writeManifest } from "./manifest.js";
|
|
19
|
+
import { profileDir } from "./symlink-manager.js";
|
|
20
|
+
|
|
21
|
+
export interface SyncChange {
|
|
22
|
+
kind: "plugin-added" | "plugin-removed" | "settings-changed" | "mcp-changed";
|
|
23
|
+
detail: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Merge a materialized profile's settings.json + mcp.json back into a manifest
|
|
28
|
+
* entry. Pure: returns the updated entry and the list of changes.
|
|
29
|
+
*
|
|
30
|
+
* - enabledPlugins present but not in the entry -> added as "latest".
|
|
31
|
+
* - entry plugins that are no longer enabled -> removed.
|
|
32
|
+
* - an existing version PIN is preserved (enabled != downgrade-to-latest).
|
|
33
|
+
* - non-enabledPlugins settings keys replace the entry's settings.
|
|
34
|
+
* - mcpServers replace the entry's mcpServers when they differ.
|
|
35
|
+
*/
|
|
36
|
+
export function computeSync(
|
|
37
|
+
entry: ProfileManifestEntry,
|
|
38
|
+
materializedSettings: Record<string, unknown>,
|
|
39
|
+
materializedMcp: { mcpServers?: Record<string, McpServerConfig> },
|
|
40
|
+
now: string = new Date().toISOString(),
|
|
41
|
+
): { entry: ProfileManifestEntry; changes: SyncChange[] } {
|
|
42
|
+
const changes: SyncChange[] = [];
|
|
43
|
+
const next: ProfileManifestEntry = { ...entry };
|
|
44
|
+
|
|
45
|
+
const enabled = (materializedSettings.enabledPlugins ?? {}) as Record<
|
|
46
|
+
string,
|
|
47
|
+
boolean
|
|
48
|
+
>;
|
|
49
|
+
const currentPlugins = { ...(entry.plugins ?? {}) };
|
|
50
|
+
|
|
51
|
+
// Additions: enabled plugins the manifest doesn't list.
|
|
52
|
+
for (const [id, on] of Object.entries(enabled)) {
|
|
53
|
+
if (on && !(id in currentPlugins)) {
|
|
54
|
+
currentPlugins[id] = "latest";
|
|
55
|
+
changes.push({ kind: "plugin-added", detail: id });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Removals: manifest plugins no longer enabled.
|
|
59
|
+
for (const id of Object.keys(entry.plugins ?? {})) {
|
|
60
|
+
if (!enabled[id]) {
|
|
61
|
+
delete currentPlugins[id];
|
|
62
|
+
changes.push({ kind: "plugin-removed", detail: id });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
next.plugins = currentPlugins;
|
|
66
|
+
|
|
67
|
+
// Settings = everything in the materialized file except enabledPlugins.
|
|
68
|
+
const { enabledPlugins: _drop, ...settings } = materializedSettings;
|
|
69
|
+
if (JSON.stringify(settings) !== JSON.stringify(entry.settings ?? {})) {
|
|
70
|
+
if (Object.keys(settings).length > 0) next.settings = settings;
|
|
71
|
+
else {
|
|
72
|
+
// biome-ignore lint/performance/noDelete: the manifest entry is serialised to profiles.json and diffed with JSON.stringify/toStrictEqual; the key must be absent, not undefined
|
|
73
|
+
delete next.settings;
|
|
74
|
+
}
|
|
75
|
+
changes.push({ kind: "settings-changed", detail: "settings updated" });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// MCP servers.
|
|
79
|
+
const mcp = materializedMcp.mcpServers ?? {};
|
|
80
|
+
if (JSON.stringify(mcp) !== JSON.stringify(entry.mcpServers ?? {})) {
|
|
81
|
+
if (Object.keys(mcp).length > 0) next.mcpServers = mcp;
|
|
82
|
+
else {
|
|
83
|
+
// biome-ignore lint/performance/noDelete: the manifest entry is serialised to profiles.json and diffed with JSON.stringify/toStrictEqual; the key must be absent, not undefined
|
|
84
|
+
delete next.mcpServers;
|
|
85
|
+
}
|
|
86
|
+
changes.push({ kind: "mcp-changed", detail: "mcp servers updated" });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Refresh the timestamp when anything changed. `now` is injectable so
|
|
90
|
+
// callers/tests control it (and computeSync stays pure).
|
|
91
|
+
if (changes.length > 0) next.updatedAt = now;
|
|
92
|
+
|
|
93
|
+
return { entry: next, changes };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Detect drift between a materialized profile and the manifest WITHOUT writing.
|
|
98
|
+
* Returns the changes `sync` would promote (empty = in sync). Returns [] if the
|
|
99
|
+
* profile isn't materialized (nothing to lose).
|
|
100
|
+
*/
|
|
101
|
+
export async function detectProfileDrift(
|
|
102
|
+
name: string,
|
|
103
|
+
projectPath?: string,
|
|
104
|
+
): Promise<SyncChange[]> {
|
|
105
|
+
const manifest = await readManifest(projectPath);
|
|
106
|
+
const entry = manifest.profiles[name];
|
|
107
|
+
if (!entry) return [];
|
|
108
|
+
|
|
109
|
+
const dir = profileDir(name, projectPath);
|
|
110
|
+
const settingsPath = path.join(dir, "settings.json");
|
|
111
|
+
if (!(await fs.pathExists(settingsPath))) return [];
|
|
112
|
+
|
|
113
|
+
const mcpPath = path.join(dir, "mcp.json");
|
|
114
|
+
const materializedSettings = await fs.readJson(settingsPath);
|
|
115
|
+
const materializedMcp = (await fs.pathExists(mcpPath))
|
|
116
|
+
? await fs.readJson(mcpPath)
|
|
117
|
+
: { mcpServers: {} };
|
|
118
|
+
|
|
119
|
+
return computeSync(entry, materializedSettings, materializedMcp).changes;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Read the materialized active profile, merge it into the manifest, and write
|
|
124
|
+
* the manifest back if anything changed. Returns the applied changes.
|
|
125
|
+
*/
|
|
126
|
+
export async function syncProfile(
|
|
127
|
+
name: string,
|
|
128
|
+
projectPath?: string,
|
|
129
|
+
): Promise<{ changes: SyncChange[]; wrote: boolean }> {
|
|
130
|
+
const manifest: ProfileManifest = await readManifest(projectPath);
|
|
131
|
+
const entry = manifest.profiles[name];
|
|
132
|
+
if (!entry) throw new Error(`Profile "${name}" not found in manifest`);
|
|
133
|
+
|
|
134
|
+
const dir = profileDir(name, projectPath);
|
|
135
|
+
const settingsPath = path.join(dir, "settings.json");
|
|
136
|
+
const mcpPath = path.join(dir, "mcp.json");
|
|
137
|
+
if (!(await fs.pathExists(settingsPath))) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`Profile "${name}" isn't materialized (${settingsPath} missing).`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const materializedSettings = await fs.readJson(settingsPath);
|
|
144
|
+
const materializedMcp = (await fs.pathExists(mcpPath))
|
|
145
|
+
? await fs.readJson(mcpPath)
|
|
146
|
+
: { mcpServers: {} };
|
|
147
|
+
|
|
148
|
+
const { entry: nextEntry, changes } = computeSync(
|
|
149
|
+
entry,
|
|
150
|
+
materializedSettings,
|
|
151
|
+
materializedMcp,
|
|
152
|
+
);
|
|
153
|
+
if (changes.length === 0) return { changes: [], wrote: false };
|
|
154
|
+
|
|
155
|
+
manifest.profiles[name] = nextEntry;
|
|
156
|
+
await writeManifest(manifest, projectPath);
|
|
157
|
+
return { changes, wrote: true };
|
|
158
|
+
}
|