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.
Files changed (331) hide show
  1. package/README.md +309 -0
  2. package/bin/magus.js +170 -0
  3. package/package.json +67 -3
  4. package/scripts/build-binaries.ts +113 -0
  5. package/scripts/capture-builtin.ts +412 -0
  6. package/scripts/check-optional-deps.ts +49 -0
  7. package/scripts/test-isolated.ts +49 -0
  8. package/scripts/verify-binary.ts +95 -0
  9. package/scripts/verify-community-registry.ts +272 -0
  10. package/src/__tests__/active-closure.test.ts +184 -0
  11. package/src/__tests__/alias-adopt.test.ts +365 -0
  12. package/src/__tests__/alias-parser.test.ts +417 -0
  13. package/src/__tests__/alias-shell-writer.test.ts +681 -0
  14. package/src/__tests__/alias-store.test.ts +163 -0
  15. package/src/__tests__/binary-signing.test.ts +120 -0
  16. package/src/__tests__/blackbox-active-closure.test.ts +119 -0
  17. package/src/__tests__/blackbox-convergence.test.ts +334 -0
  18. package/src/__tests__/blackbox-parity.test.ts +684 -0
  19. package/src/__tests__/blackbox-plugin-state.test.ts +523 -0
  20. package/src/__tests__/blackbox-tool-state.test.ts +669 -0
  21. package/src/__tests__/cache-provenance.test.ts +84 -0
  22. package/src/__tests__/capture-builtin.test.ts +92 -0
  23. package/src/__tests__/catalog-cache-store.test.ts +309 -0
  24. package/src/__tests__/catalog-notice.test.ts +155 -0
  25. package/src/__tests__/cli-ansi.test.ts +297 -0
  26. package/src/__tests__/cli-apply-seams.test.ts +288 -0
  27. package/src/__tests__/cli-live.test.ts +391 -0
  28. package/src/__tests__/cli-router.test.ts +240 -0
  29. package/src/__tests__/cli-tool-commands.test.ts +239 -0
  30. package/src/__tests__/cli-tools-adapter.test.ts +294 -0
  31. package/src/__tests__/cli-update-view.test.ts +445 -0
  32. package/src/__tests__/community-fetch.test.ts +545 -0
  33. package/src/__tests__/community-registry.test.ts +269 -0
  34. package/src/__tests__/community-staleness.test.ts +720 -0
  35. package/src/__tests__/component-badge.test.ts +98 -0
  36. package/src/__tests__/component-icons.test.ts +94 -0
  37. package/src/__tests__/content-drift.test.ts +353 -0
  38. package/src/__tests__/conventions-integration.test.ts +830 -0
  39. package/src/__tests__/conventions-manager.test.ts +1236 -0
  40. package/src/__tests__/doctor-bins.test.ts +43 -0
  41. package/src/__tests__/doctor.test.ts +463 -0
  42. package/src/__tests__/dotenv.test.ts +195 -0
  43. package/src/__tests__/enabled-not-installed.test.ts +94 -0
  44. package/src/__tests__/file-locking.test.ts +208 -0
  45. package/src/__tests__/fixtures/tool-probe/brew-list-versions-tmux.txt +2 -0
  46. package/src/__tests__/fixtures/tool-probe/brew-outdated-behind-homebrew-spec.txt +29 -0
  47. package/src/__tests__/fixtures/tool-probe/brew-outdated-tmux-current.txt +7 -0
  48. package/src/__tests__/fixtures/tool-probe/brew-outdated-unknown-formula.txt +6 -0
  49. package/src/__tests__/fixtures/tool-probe/go-version-m-tmux-mcp.txt +27 -0
  50. package/src/__tests__/fixtures/tool-probe/npm-dist-tags-missing-scoped.txt +4 -0
  51. package/src/__tests__/fixtures/tool-probe/npm-dist-tags-openai-codex.txt +4 -0
  52. package/src/__tests__/fixtures/tool-probe/pypi-cowsay.txt +3 -0
  53. package/src/__tests__/fixtures/tool-probe/uv-receipt-browser-use.toml +10 -0
  54. package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-git.toml +8 -0
  55. package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-pinned.toml +8 -0
  56. package/src/__tests__/fixtures/tool-probe/uv-receipt-graphifyy.toml +7 -0
  57. package/src/__tests__/fixtures/tool-probe/uv-receipt-mcp.toml +6 -0
  58. package/src/__tests__/fixtures/tool-probe/uv-receipt-serena-agent.toml +9 -0
  59. package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated-pinned.txt +5 -0
  60. package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated.txt +11 -0
  61. package/src/__tests__/fixtures/tool-probe/uv-tool-list.txt +22 -0
  62. package/src/__tests__/footer-hints.test.ts +40 -0
  63. package/src/__tests__/format-command.test.ts +78 -0
  64. package/src/__tests__/git-worktree.test.ts +108 -0
  65. package/src/__tests__/github-budget.test.ts +206 -0
  66. package/src/__tests__/gitignore-check.test.ts +71 -0
  67. package/src/__tests__/gitignore-detector.test.ts +176 -0
  68. package/src/__tests__/gitignore-fixer.test.ts +317 -0
  69. package/src/__tests__/gitignore-resolver.test.ts +193 -0
  70. package/src/__tests__/gitignore-service.test.ts +137 -0
  71. package/src/__tests__/go-module.test.ts +43 -0
  72. package/src/__tests__/hook-import-policy.test.ts +90 -0
  73. package/src/__tests__/hook-process.test.ts +256 -0
  74. package/src/__tests__/hook-registration.test.ts +224 -0
  75. package/src/__tests__/install-command.test.ts +81 -0
  76. package/src/__tests__/install-plan.test.ts +104 -0
  77. package/src/__tests__/launcher-signal.test.ts +308 -0
  78. package/src/__tests__/local-marketplace-scan.test.ts +174 -0
  79. package/src/__tests__/manifest.test.ts +281 -0
  80. package/src/__tests__/markdown-renderer.test.ts +142 -0
  81. package/src/__tests__/marketplace-badge.test.ts +140 -0
  82. package/src/__tests__/marketplace-refresh.test.ts +335 -0
  83. package/src/__tests__/marketplace-version.test.ts +84 -0
  84. package/src/__tests__/marketplaces.test.ts +128 -0
  85. package/src/__tests__/mate-availability.test.ts +156 -0
  86. package/src/__tests__/mate-catalog.test.ts +295 -0
  87. package/src/__tests__/mcp-registry.test.ts +158 -0
  88. package/src/__tests__/model-visuals.test.tsx +2466 -0
  89. package/src/__tests__/models-adapter.test.ts +332 -0
  90. package/src/__tests__/models-cli.test.ts +273 -0
  91. package/src/__tests__/models-core.test.ts +802 -0
  92. package/src/__tests__/models-manager.test.ts +500 -0
  93. package/src/__tests__/models-presets-marketplace.test.ts +168 -0
  94. package/src/__tests__/models-screen-state.test.ts +315 -0
  95. package/src/__tests__/moved-marketplace.test.ts +86 -0
  96. package/src/__tests__/open-file.test.ts +59 -0
  97. package/src/__tests__/owned-marketplaces.test.ts +93 -0
  98. package/src/__tests__/plugin-checkout.test.ts +66 -0
  99. package/src/__tests__/plugin-cli-argv.test.ts +109 -0
  100. package/src/__tests__/plugin-contents.test.ts +371 -0
  101. package/src/__tests__/plugin-manager-fallback.test.ts +320 -0
  102. package/src/__tests__/plugin-releases.test.ts +98 -0
  103. package/src/__tests__/plugin-requires.test.ts +187 -0
  104. package/src/__tests__/plugin-row-text.test.ts +336 -0
  105. package/src/__tests__/plugin-setup.test.ts +1016 -0
  106. package/src/__tests__/profile-adopt.test.ts +177 -0
  107. package/src/__tests__/profile-command.test.ts +110 -0
  108. package/src/__tests__/profile-materializer.test.ts +238 -0
  109. package/src/__tests__/profile-sync.test.ts +154 -0
  110. package/src/__tests__/registry-version-resolution.test.ts +88 -0
  111. package/src/__tests__/resolve-executable.test.ts +42 -0
  112. package/src/__tests__/resolver.test.ts +351 -0
  113. package/src/__tests__/scope-action.test.ts +134 -0
  114. package/src/__tests__/scope-squares.test.tsx +164 -0
  115. package/src/__tests__/selection-scope.test.ts +100 -0
  116. package/src/__tests__/settings-file.test.ts +179 -0
  117. package/src/__tests__/shell-script-callers.test.ts +92 -0
  118. package/src/__tests__/skill-install-files.test.ts +136 -0
  119. package/src/__tests__/skills-catalog-client.test.ts +48 -0
  120. package/src/__tests__/style-wrap.test.ts +220 -0
  121. package/src/__tests__/styles-manager.test.ts +1098 -0
  122. package/src/__tests__/styles-origins.test.ts +414 -0
  123. package/src/__tests__/styles-screen-state.test.ts +457 -0
  124. package/src/__tests__/styles-status-line.test.ts +72 -0
  125. package/src/__tests__/styles-sync.test.ts +452 -0
  126. package/src/__tests__/symlink-manager.test.ts +180 -0
  127. package/src/__tests__/tabbar-layout.test.ts +100 -0
  128. package/src/__tests__/terminology-filler.test.ts +214 -0
  129. package/src/__tests__/theme-adaptive-colors.test.ts +354 -0
  130. package/src/__tests__/theme-env.test.ts +281 -0
  131. package/src/__tests__/theme-resolve.test.ts +380 -0
  132. package/src/__tests__/tool-probe.test.ts +1400 -0
  133. package/src/__tests__/toolchain.test.ts +232 -0
  134. package/src/__tests__/ui-version-writers.test.ts +88 -0
  135. package/src/__tests__/update-apply.test.ts +637 -0
  136. package/src/__tests__/update-plan.test.ts +1373 -0
  137. package/src/__tests__/uppercase-keybindings.test.ts +101 -0
  138. package/src/__tests__/useGitignoreModal.test.ts +79 -0
  139. package/src/__tests__/version-check-refresh.test.ts +45 -0
  140. package/src/__tests__/version-snapshot.test.ts +212 -0
  141. package/src/__tests__/version-unknown-state.test.ts +388 -0
  142. package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
  143. package/src/cli/ansi.ts +520 -0
  144. package/src/cli/bootstrap.ts +134 -0
  145. package/src/cli/doctor.ts +218 -0
  146. package/src/cli/hook.ts +129 -0
  147. package/src/cli/install.ts +382 -0
  148. package/src/cli/live.ts +374 -0
  149. package/src/cli/models.ts +297 -0
  150. package/src/cli/profile.ts +197 -0
  151. package/src/cli/prompt.ts +32 -0
  152. package/src/cli/router.ts +207 -0
  153. package/src/cli/update-view.ts +526 -0
  154. package/src/cli/update.ts +613 -0
  155. package/src/cli/upgrade.ts +97 -0
  156. package/src/data/alias-flags.ts +301 -0
  157. package/src/data/cli-tools.ts +146 -0
  158. package/src/data/community-styles.ts +531 -0
  159. package/src/data/gitignore-defaults.ts +33 -0
  160. package/src/data/gitignore-reasons.ts +93 -0
  161. package/src/data/gitignore-templates.ts +25 -0
  162. package/src/data/marketplaces.ts +278 -0
  163. package/src/data/mcp-servers.ts +51 -0
  164. package/src/data/models-presets.ts +329 -0
  165. package/src/data/predefined-profiles.ts +252 -0
  166. package/src/data/settings-catalog.ts +714 -0
  167. package/src/data/skill-repos.ts +91 -0
  168. package/src/data/styles/asd-ste100.md +38 -0
  169. package/src/data/styles/calibrated.md +24 -0
  170. package/src/data/styles/direct.md +23 -0
  171. package/src/data/styles/evidence-first.md +22 -0
  172. package/src/data/styles/explanatory.md +23 -0
  173. package/src/data/styles/index.ts +57 -0
  174. package/src/data/styles/no-slop.md +32 -0
  175. package/src/data/styles/plain-language.md +23 -0
  176. package/src/data/styles/structured.md +30 -0
  177. package/src/data/styles/terminology.md +28 -0
  178. package/src/data/styles/terse.md +20 -0
  179. package/src/main.tsx +110 -0
  180. package/src/markdown.d.ts +13 -0
  181. package/src/opentui.d.ts +203 -0
  182. package/src/services/active-closure.ts +117 -0
  183. package/src/services/alias-settings.ts +60 -0
  184. package/src/services/alias-shell-writer.ts +1244 -0
  185. package/src/services/alias-store.ts +172 -0
  186. package/src/services/binary-signing.ts +78 -0
  187. package/src/services/catalog-cache-store.ts +316 -0
  188. package/src/services/claude-cli.ts +304 -0
  189. package/src/services/claude-runner.ts +31 -0
  190. package/src/services/claude-settings.ts +1589 -0
  191. package/src/services/cli-tool-commands.ts +133 -0
  192. package/src/services/community-fetcher.ts +90 -0
  193. package/src/services/community-styles.ts +1194 -0
  194. package/src/services/content-drift.ts +208 -0
  195. package/src/services/conventions-manager.ts +945 -0
  196. package/src/services/doctor-bins.ts +63 -0
  197. package/src/services/doctor.ts +570 -0
  198. package/src/services/dotenv.ts +181 -0
  199. package/src/services/git-worktree.ts +129 -0
  200. package/src/services/github-budget.ts +274 -0
  201. package/src/services/gitignore-check.ts +63 -0
  202. package/src/services/gitignore-detector.ts +167 -0
  203. package/src/services/gitignore-fixer.ts +280 -0
  204. package/src/services/gitignore-resolver.ts +208 -0
  205. package/src/services/gitignore-service.ts +167 -0
  206. package/src/services/hook-registration.ts +218 -0
  207. package/src/services/install-plan.ts +107 -0
  208. package/src/services/local-marketplace.ts +587 -0
  209. package/src/services/manifest.ts +250 -0
  210. package/src/services/marketplace-catalog-git.ts +173 -0
  211. package/src/services/marketplace-catalog.ts +129 -0
  212. package/src/services/marketplace-fetcher.ts +400 -0
  213. package/src/services/marketplace-refresh.ts +253 -0
  214. package/src/services/marketplace-sync.ts +145 -0
  215. package/src/services/mate-availability.ts +133 -0
  216. package/src/services/mate-catalog.ts +265 -0
  217. package/src/services/mcp-registry.ts +171 -0
  218. package/src/services/models-core.ts +969 -0
  219. package/src/services/models-manager.ts +606 -0
  220. package/src/services/plugin-checkout.ts +186 -0
  221. package/src/services/plugin-manager.ts +878 -0
  222. package/src/services/plugin-mcp-config.ts +242 -0
  223. package/src/services/plugin-releases.ts +47 -0
  224. package/src/services/plugin-requires.ts +227 -0
  225. package/src/services/plugin-setup.ts +965 -0
  226. package/src/services/profile-adopt.ts +206 -0
  227. package/src/services/profile-materializer.ts +133 -0
  228. package/src/services/profile-sync.ts +158 -0
  229. package/src/services/profiles.ts +225 -0
  230. package/src/services/resolver.ts +344 -0
  231. package/src/services/settings-file.ts +69 -0
  232. package/src/services/settings-manager.ts +307 -0
  233. package/src/services/skills-manager.ts +1111 -0
  234. package/src/services/skillsmp-client.ts +193 -0
  235. package/src/services/styles-manager.ts +1294 -0
  236. package/src/services/symlink-manager.ts +189 -0
  237. package/src/services/terminology-filler.ts +266 -0
  238. package/src/services/tool-probe.ts +1637 -0
  239. package/src/services/toolchain.ts +431 -0
  240. package/src/services/update-engine.ts +691 -0
  241. package/src/services/update-plan.ts +863 -0
  242. package/src/services/version-check.ts +127 -0
  243. package/src/services/version-snapshot.ts +189 -0
  244. package/src/tui.tsx +112 -0
  245. package/src/types/bun.d.ts +27 -0
  246. package/src/types/gitignore.ts +68 -0
  247. package/src/types/index.ts +447 -0
  248. package/src/ui/App.tsx +538 -0
  249. package/src/ui/adapters/catalogNotice.ts +122 -0
  250. package/src/ui/adapters/cliToolsAdapter.ts +176 -0
  251. package/src/ui/adapters/modelsAdapter.ts +213 -0
  252. package/src/ui/adapters/pluginsAdapter.ts +413 -0
  253. package/src/ui/adapters/settingsAdapter.ts +168 -0
  254. package/src/ui/adapters/skillsAdapter.ts +229 -0
  255. package/src/ui/adapters/stylesAdapter.ts +403 -0
  256. package/src/ui/components/CategoryHeader.tsx +64 -0
  257. package/src/ui/components/EmptyFilterState.tsx +32 -0
  258. package/src/ui/components/FlagDetailEditor.tsx +0 -0
  259. package/src/ui/components/ScopeIndicator.tsx +63 -0
  260. package/src/ui/components/ScrollableList.tsx +95 -0
  261. package/src/ui/components/SearchInput.tsx +57 -0
  262. package/src/ui/components/StyledText.tsx +75 -0
  263. package/src/ui/components/TabBar.tsx +101 -0
  264. package/src/ui/components/layout/FooterHints.tsx +78 -0
  265. package/src/ui/components/layout/Panel.tsx +63 -0
  266. package/src/ui/components/layout/ProgressBar.tsx +44 -0
  267. package/src/ui/components/layout/ScopeTabs.tsx +54 -0
  268. package/src/ui/components/layout/ScreenLayout.tsx +321 -0
  269. package/src/ui/components/layout/index.ts +9 -0
  270. package/src/ui/components/modals/ConfirmModal.tsx +62 -0
  271. package/src/ui/components/modals/InputModal.tsx +75 -0
  272. package/src/ui/components/modals/LoadingModal.tsx +51 -0
  273. package/src/ui/components/modals/MessageModal.tsx +66 -0
  274. package/src/ui/components/modals/ModalContainer.tsx +159 -0
  275. package/src/ui/components/modals/SelectModal.tsx +90 -0
  276. package/src/ui/components/modals/index.ts +6 -0
  277. package/src/ui/components/primitives/ActionHints.tsx +44 -0
  278. package/src/ui/components/primitives/DetailSection.tsx +23 -0
  279. package/src/ui/components/primitives/KeyValueLine.tsx +26 -0
  280. package/src/ui/components/primitives/ListCategoryRow.tsx +45 -0
  281. package/src/ui/components/primitives/MetaText.tsx +40 -0
  282. package/src/ui/components/primitives/ScopeDetail.tsx +67 -0
  283. package/src/ui/components/primitives/ScopeSquares.tsx +58 -0
  284. package/src/ui/components/primitives/SelectableRow.tsx +30 -0
  285. package/src/ui/components/primitives/index.ts +9 -0
  286. package/src/ui/hooks/index.ts +2 -0
  287. package/src/ui/hooks/useGitignoreModal.ts +95 -0
  288. package/src/ui/hooks/useKeyboard.ts +24 -0
  289. package/src/ui/hooks/useKeyboardHandler.ts +63 -0
  290. package/src/ui/registry.ts +33 -0
  291. package/src/ui/renderers/cliToolRenderers.tsx +326 -0
  292. package/src/ui/renderers/gitignoreRenderers.tsx +80 -0
  293. package/src/ui/renderers/markdownRenderer.tsx +302 -0
  294. package/src/ui/renderers/mcpRenderers.tsx +149 -0
  295. package/src/ui/renderers/modelRenderers.tsx +1309 -0
  296. package/src/ui/renderers/modelVisuals.tsx +1402 -0
  297. package/src/ui/renderers/pluginRenderers.tsx +1089 -0
  298. package/src/ui/renderers/pluginRowText.ts +70 -0
  299. package/src/ui/renderers/pluginStatusText.ts +56 -0
  300. package/src/ui/renderers/profileRenderers.tsx +406 -0
  301. package/src/ui/renderers/settingsRenderers.tsx +218 -0
  302. package/src/ui/renderers/skillRenderers.tsx +479 -0
  303. package/src/ui/renderers/styleRenderers.tsx +813 -0
  304. package/src/ui/screens/AliasScreen.tsx +1558 -0
  305. package/src/ui/screens/CliToolsScreen.tsx +367 -0
  306. package/src/ui/screens/EnvVarsScreen.tsx +221 -0
  307. package/src/ui/screens/GitignoreScreen.tsx +562 -0
  308. package/src/ui/screens/McpRegistryScreen.tsx +366 -0
  309. package/src/ui/screens/McpScreen.tsx +256 -0
  310. package/src/ui/screens/ModelsScreen.tsx +542 -0
  311. package/src/ui/screens/PluginsScreen.tsx +1464 -0
  312. package/src/ui/screens/ProfilesScreen.tsx +438 -0
  313. package/src/ui/screens/SkillsScreen.tsx +853 -0
  314. package/src/ui/screens/StylesScreen.tsx +1064 -0
  315. package/src/ui/screens/index.ts +11 -0
  316. package/src/ui/state/AppContext.tsx +233 -0
  317. package/src/ui/state/DimensionsContext.tsx +154 -0
  318. package/src/ui/state/reducer.ts +810 -0
  319. package/src/ui/state/types.ts +503 -0
  320. package/src/ui/theme-mode.ts +352 -0
  321. package/src/ui/theme-resolve.ts +147 -0
  322. package/src/ui/theme.ts +175 -0
  323. package/src/utils/clipboard.ts +58 -0
  324. package/src/utils/command-utils.ts +144 -0
  325. package/src/utils/config-dir.ts +45 -0
  326. package/src/utils/file-locking.ts +146 -0
  327. package/src/utils/fuzzy-search.ts +138 -0
  328. package/src/utils/go-module.ts +36 -0
  329. package/src/utils/open-file.ts +84 -0
  330. package/src/utils/run.ts +67 -0
  331. package/src/utils/string-utils.ts +88 -0
@@ -0,0 +1,307 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { SettingDefinition } from "../data/settings-catalog.js";
4
+ import type { ClaudeSettings } from "../types/index.js";
5
+ import {
6
+ readGlobalSettings,
7
+ readSettings,
8
+ writeGlobalSettings,
9
+ writeSettings,
10
+ } from "./claude-settings.js";
11
+
12
+ export type SettingScope = "user" | "project";
13
+
14
+ /**
15
+ * settings.json is an open JSON object: the catalog addresses keys that the
16
+ * typed `ClaudeSettings` shape does not enumerate (`attribution`, `env`, any
17
+ * dotted path). Read and write through this record view and narrow each value.
18
+ */
19
+ type SettingsRecord = ClaudeSettings & Record<string, unknown>;
20
+
21
+ function isRecord(value: unknown): value is Record<string, unknown> {
22
+ return typeof value === "object" && value !== null && !Array.isArray(value);
23
+ }
24
+
25
+ /** The `attribution` block as Claude Code stores it. */
26
+ interface Attribution {
27
+ commit?: unknown;
28
+ pr?: unknown;
29
+ }
30
+
31
+ function readAttribution(settings: SettingsRecord): Attribution | undefined {
32
+ const raw = settings.attribution;
33
+ return isRecord(raw) ? raw : undefined;
34
+ }
35
+
36
+ async function loadSettings(
37
+ scope: SettingScope,
38
+ projectPath?: string,
39
+ ): Promise<SettingsRecord> {
40
+ const settings =
41
+ scope === "user"
42
+ ? await readGlobalSettings()
43
+ : await readSettings(projectPath);
44
+ return settings as SettingsRecord;
45
+ }
46
+
47
+ /** Read the current value of a setting from the given scope */
48
+ export async function readSettingValue(
49
+ setting: SettingDefinition,
50
+ scope: SettingScope,
51
+ projectPath?: string,
52
+ ): Promise<string | undefined> {
53
+ const settings = await loadSettings(scope, projectPath);
54
+
55
+ if (setting.storage.type === "attribution") {
56
+ // Attribution is an object: { commit: "", pr: "" } means disabled
57
+ const attr = readAttribution(settings);
58
+ if (attr && attr.commit === "" && attr.pr === "") {
59
+ return "false";
60
+ }
61
+ return undefined; // default (enabled)
62
+ }
63
+ if (setting.storage.type === "attribution-text") {
64
+ // Custom attribution text: read from attribution.commit, strip the Co-Authored-By trailer prefix
65
+ const attr = readAttribution(settings);
66
+ if (!attr || (attr.commit === "" && attr.pr === "")) {
67
+ // Attribution is disabled or not set — no custom text stored
68
+ return undefined;
69
+ }
70
+ const commit = attr.commit;
71
+ if (typeof commit !== "string" || commit === "") {
72
+ return undefined;
73
+ }
74
+ // The commit value is "Co-Authored-By: Magus <magus@madappgang.com>\n\n{customText}"
75
+ // Strip the trailer prefix if present
76
+ const trailerPrefix = "Co-Authored-By: Magus <magus@madappgang.com>\n\n";
77
+ if (commit.startsWith(trailerPrefix)) {
78
+ const text = commit.slice(trailerPrefix.length);
79
+ return text.length > 0 ? text : undefined;
80
+ }
81
+ // If no trailer prefix, the value is the raw text (or Claude default — return undefined)
82
+ return undefined;
83
+ }
84
+ if (setting.storage.type === "env") {
85
+ const env = settings.env;
86
+ const raw = isRecord(env) ? env[setting.storage.key] : undefined;
87
+ return typeof raw === "string" ? raw : undefined;
88
+ }
89
+ // Direct settings key (supports dot notation like "permissions.defaultMode")
90
+ const keys = setting.storage.key.split(".");
91
+ let value: unknown = settings;
92
+ for (const k of keys) {
93
+ value = isRecord(value) ? value[k] : undefined;
94
+ }
95
+ return value !== undefined && value !== null ? String(value) : undefined;
96
+ }
97
+
98
+ /** Write a setting value to the given scope */
99
+ export async function writeSettingValue(
100
+ setting: SettingDefinition,
101
+ value: string | undefined,
102
+ scope: SettingScope,
103
+ projectPath?: string,
104
+ ): Promise<void> {
105
+ const settings = await loadSettings(scope, projectPath);
106
+
107
+ if (setting.storage.type === "attribution") {
108
+ // Boolean toggle: "false" -> write { commit: "", pr: "" }, "true"/undefined -> delete key
109
+ if (value === "false") {
110
+ settings.attribution = { commit: "", pr: "" };
111
+ } else {
112
+ // biome-ignore lint/performance/noDelete: the key must be absent from the serialised settings.json; `undefined` would still be listed by Object.keys and defeat the "revert to Claude defaults" contract
113
+ delete settings.attribution;
114
+ }
115
+ } else if (setting.storage.type === "attribution-text") {
116
+ if (value && value.trim().length > 0) {
117
+ // Write custom text: commit gets a Co-Authored-By trailer + the text; pr gets the text
118
+ settings.attribution = {
119
+ commit: `Co-Authored-By: Magus <magus@madappgang.com>\n\n${value}`,
120
+ pr: value,
121
+ };
122
+ } else {
123
+ // Empty value: remove attribution key entirely (revert to Claude defaults)
124
+ // biome-ignore lint/performance/noDelete: the key must be absent from the serialised settings.json, not present with an undefined value
125
+ delete settings.attribution;
126
+ }
127
+ } else if (setting.storage.type === "env") {
128
+ // Write to the "env" block in settings.json
129
+ const env = isRecord(settings.env) ? settings.env : {};
130
+ settings.env = env;
131
+ if (value === undefined || value === "" || value === setting.defaultValue) {
132
+ delete env[setting.storage.key];
133
+ // Clean up empty env block
134
+ if (Object.keys(env).length === 0) {
135
+ // biome-ignore lint/performance/noDelete: an empty env block is removed from the file entirely; assigning undefined would leave the key for Object.keys and JSON readers that check `in`
136
+ delete settings.env;
137
+ }
138
+ } else {
139
+ env[setting.storage.key] = value;
140
+ }
141
+ } else {
142
+ // Direct settings key (supports dot notation)
143
+ const keys = setting.storage.key.split(".");
144
+ if (keys.length === 1) {
145
+ if (value === undefined || value === "") {
146
+ delete settings[keys[0]];
147
+ } else {
148
+ // Try to preserve type: boolean, number, or string
149
+ settings[keys[0]] = parseSettingValue(value, setting.type);
150
+ }
151
+ } else {
152
+ // Nested key like "permissions.defaultMode"
153
+ let obj: Record<string, unknown> = settings;
154
+ for (let i = 0; i < keys.length - 1; i++) {
155
+ const existing = obj[keys[i]];
156
+ const next = isRecord(existing) ? existing : {};
157
+ obj[keys[i]] = next;
158
+ obj = next;
159
+ }
160
+ const lastKey = keys[keys.length - 1];
161
+ if (value === undefined || value === "") {
162
+ delete obj[lastKey];
163
+ } else {
164
+ obj[lastKey] = parseSettingValue(value, setting.type);
165
+ }
166
+ }
167
+ }
168
+
169
+ if (scope === "user") {
170
+ await writeGlobalSettings(settings);
171
+ } else {
172
+ await writeSettings(settings, projectPath);
173
+ }
174
+ }
175
+
176
+ function parseSettingValue(value: string, type: string): string | boolean {
177
+ if (type === "boolean") {
178
+ return value === "true" || value === "1";
179
+ }
180
+ // Try number
181
+ const num = Number(value);
182
+ if (!Number.isNaN(num) && value.trim() !== "") {
183
+ return value; // Keep as string for env vars
184
+ }
185
+ return value;
186
+ }
187
+
188
+ /** Read all setting values for the catalog */
189
+ export async function readAllSettings(
190
+ catalog: SettingDefinition[],
191
+ scope: SettingScope,
192
+ projectPath?: string,
193
+ ): Promise<Map<string, string | undefined>> {
194
+ const values = new Map<string, string | undefined>();
195
+ for (const setting of catalog) {
196
+ values.set(setting.id, await readSettingValue(setting, scope, projectPath));
197
+ }
198
+ return values;
199
+ }
200
+
201
+ export interface ScopedSettingValues {
202
+ user: string | undefined;
203
+ project: string | undefined;
204
+ }
205
+
206
+ /** Read all setting values from both scopes simultaneously */
207
+ export async function readAllSettingsBothScopes(
208
+ catalog: SettingDefinition[],
209
+ projectPath?: string,
210
+ ): Promise<Map<string, ScopedSettingValues>> {
211
+ const result = new Map<string, ScopedSettingValues>();
212
+ const [userValues, projectValues] = await Promise.all([
213
+ readAllSettings(catalog, "user", projectPath),
214
+ readAllSettings(catalog, "project", projectPath),
215
+ ]);
216
+ for (const setting of catalog) {
217
+ result.set(setting.id, {
218
+ user: userValues.get(setting.id),
219
+ project: projectValues.get(setting.id),
220
+ });
221
+ }
222
+ return result;
223
+ }
224
+
225
+ /**
226
+ * Discover available output styles from installed plugins.
227
+ * Scans enabled plugins for outputStyles entries and *-output-style plugin names.
228
+ * Returns options suitable for a select setting.
229
+ */
230
+ export async function discoverOutputStyles(
231
+ projectPath?: string,
232
+ ): Promise<Array<{ label: string; value: string }>> {
233
+ const styles: Array<{ label: string; value: string }> = [
234
+ { label: "Default", value: "" },
235
+ ];
236
+
237
+ const homeDir = process.env.HOME || process.env.USERPROFILE || "";
238
+ const cacheDir = path.join(homeDir, ".claude", "plugins", "cache");
239
+
240
+ // Collect enabled plugins from both scopes
241
+ const enabledPlugins = new Set<string>();
242
+ try {
243
+ const userSettings = await readGlobalSettings();
244
+ for (const [k, v] of Object.entries(userSettings.enabledPlugins ?? {})) {
245
+ if (v) enabledPlugins.add(k);
246
+ }
247
+ } catch {}
248
+ try {
249
+ const projSettings = await readSettings(projectPath);
250
+ for (const [k, v] of Object.entries(projSettings.enabledPlugins ?? {})) {
251
+ if (v) enabledPlugins.add(k);
252
+ }
253
+ } catch {}
254
+
255
+ const seen = new Set<string>();
256
+
257
+ for (const pluginId of enabledPlugins) {
258
+ const [pluginName, marketplace] = pluginId.split("@");
259
+
260
+ // Check if this is a dedicated output-style plugin (e.g. "explanatory-output-style")
261
+ if (pluginName.endsWith("-output-style")) {
262
+ const styleName = pluginName
263
+ .replace(/-output-style$/, "")
264
+ .split("-")
265
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
266
+ .join(" ");
267
+ if (!seen.has(styleName)) {
268
+ seen.add(styleName);
269
+ styles.push({ label: styleName, value: styleName });
270
+ }
271
+ continue;
272
+ }
273
+
274
+ // Check plugin.json for outputStyles entries
275
+ if (!marketplace) continue;
276
+ const pluginDir = path.join(cacheDir, marketplace, pluginName);
277
+ try {
278
+ const versions = fs.readdirSync(pluginDir).sort();
279
+ const latest = versions[versions.length - 1];
280
+ if (!latest) continue;
281
+ const manifestPath = path.join(pluginDir, latest, "plugin.json");
282
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
283
+ const outputStyles = manifest.outputStyles as string[] | undefined;
284
+ if (!outputStyles?.length) continue;
285
+
286
+ for (const stylePath of outputStyles) {
287
+ const fullPath = path.join(pluginDir, latest, stylePath);
288
+ try {
289
+ const content = fs.readFileSync(fullPath, "utf-8");
290
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
291
+ if (fmMatch) {
292
+ const nameMatch = fmMatch[1].match(/^name:\s*(.+)$/m);
293
+ if (nameMatch) {
294
+ const name = nameMatch[1].trim();
295
+ if (!seen.has(name)) {
296
+ seen.add(name);
297
+ styles.push({ label: name, value: name });
298
+ }
299
+ }
300
+ }
301
+ } catch {}
302
+ }
303
+ } catch {}
304
+ }
305
+
306
+ return styles;
307
+ }