claudeup 6.2.0 → 6.3.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 (153) hide show
  1. package/README.md +22 -0
  2. package/bin/claudeup.js +6 -1
  3. package/package.json +6 -6
  4. package/src/__tests__/alias-adopt.test.ts +331 -329
  5. package/src/__tests__/alias-parser.test.ts +384 -376
  6. package/src/__tests__/alias-shell-writer.test.ts +638 -625
  7. package/src/__tests__/alias-store.test.ts +143 -143
  8. package/src/__tests__/catalog-cache-store.test.ts +265 -265
  9. package/src/__tests__/catalog-notice.test.ts +123 -123
  10. package/src/__tests__/cli-apply-seams.test.ts +12 -8
  11. package/src/__tests__/cli-router.test.ts +132 -3
  12. package/src/__tests__/cli-update-view.test.ts +27 -8
  13. package/src/__tests__/community-staleness.test.ts +0 -2
  14. package/src/__tests__/component-badge.test.ts +4 -4
  15. package/src/__tests__/content-drift.test.ts +5 -1
  16. package/src/__tests__/conventions-integration.test.ts +774 -689
  17. package/src/__tests__/conventions-manager.test.ts +1216 -1152
  18. package/src/__tests__/doctor-bins.test.ts +14 -6
  19. package/src/__tests__/doctor.test.ts +438 -425
  20. package/src/__tests__/dual-write-prevention.test.ts +277 -282
  21. package/src/__tests__/enabled-not-installed.test.ts +71 -63
  22. package/src/__tests__/file-locking.test.ts +196 -196
  23. package/src/__tests__/gap-fill-versions.test.ts +318 -311
  24. package/src/__tests__/github-budget.test.ts +170 -164
  25. package/src/__tests__/gitignore-detector.test.ts +160 -152
  26. package/src/__tests__/gitignore-fixer.test.ts +285 -247
  27. package/src/__tests__/gitignore-prerun.test.ts +57 -57
  28. package/src/__tests__/gitignore-resolver.test.ts +173 -146
  29. package/src/__tests__/gitignore-service.test.ts +121 -119
  30. package/src/__tests__/go-module.test.ts +43 -0
  31. package/src/__tests__/install-command.test.ts +3 -3
  32. package/src/__tests__/install-plan.test.ts +13 -7
  33. package/src/__tests__/manifest.test.ts +16 -5
  34. package/src/__tests__/markdown-renderer.test.ts +0 -1
  35. package/src/__tests__/marketplace-badge.test.ts +111 -90
  36. package/src/__tests__/marketplace-version.test.ts +64 -61
  37. package/src/__tests__/plugin-manager-fallback.test.ts +249 -239
  38. package/src/__tests__/plugin-requires.test.ts +55 -1
  39. package/src/__tests__/plugin-setup.test.ts +111 -9
  40. package/src/__tests__/profile-command.test.ts +1 -7
  41. package/src/__tests__/profile-materializer.test.ts +31 -8
  42. package/src/__tests__/profile-sync.test.ts +23 -5
  43. package/src/__tests__/registry-version-resolution.test.ts +61 -47
  44. package/src/__tests__/resolve-executable.test.ts +1 -1
  45. package/src/__tests__/resolver.test.ts +38 -13
  46. package/src/__tests__/scope-action.test.ts +52 -50
  47. package/src/__tests__/scope-squares.test.tsx +4 -5
  48. package/src/__tests__/skill-install-files.test.ts +111 -103
  49. package/src/__tests__/symlink-manager.test.ts +27 -10
  50. package/src/__tests__/theme-env.test.ts +281 -0
  51. package/src/__tests__/theme-resolve.test.ts +380 -0
  52. package/src/__tests__/update-apply.test.ts +1 -3
  53. package/src/__tests__/useGitignoreModal.test.ts +69 -61
  54. package/src/__tests__/version-snapshot.test.ts +183 -165
  55. package/src/cli/ansi.ts +23 -15
  56. package/src/cli/doctor.ts +10 -9
  57. package/src/cli/install.ts +3 -3
  58. package/src/cli/profile.ts +5 -4
  59. package/src/cli/prompt.ts +0 -1
  60. package/src/cli/router.ts +69 -2
  61. package/src/cli/update-view.ts +11 -6
  62. package/src/cli/update.ts +9 -4
  63. package/src/cli/upgrade.ts +6 -4
  64. package/src/data/alias-flags.ts +260 -260
  65. package/src/data/gitignore-defaults.ts +20 -20
  66. package/src/data/gitignore-reasons.ts +86 -92
  67. package/src/data/gitignore-templates.ts +14 -14
  68. package/src/data/predefined-profiles.ts +1 -2
  69. package/src/data/skill-repos.ts +43 -35
  70. package/src/main.tsx +29 -6
  71. package/src/opentui.d.ts +1 -3
  72. package/src/prerunner/index.ts +7 -8
  73. package/src/services/alias-settings.ts +20 -23
  74. package/src/services/alias-shell-writer.ts +800 -803
  75. package/src/services/alias-store.ts +77 -77
  76. package/src/services/claude-settings.ts +27 -18
  77. package/src/services/content-drift.ts +14 -4
  78. package/src/services/conventions-manager.ts +692 -617
  79. package/src/services/doctor.ts +464 -403
  80. package/src/services/gitignore-detector.ts +113 -103
  81. package/src/services/gitignore-fixer.ts +204 -189
  82. package/src/services/gitignore-prerun.ts +26 -26
  83. package/src/services/gitignore-resolver.ts +162 -144
  84. package/src/services/gitignore-service.ts +120 -117
  85. package/src/services/install-plan.ts +4 -4
  86. package/src/services/local-marketplace.ts +7 -5
  87. package/src/services/manifest.ts +1 -1
  88. package/src/services/marketplace-catalog-git.ts +5 -2
  89. package/src/services/marketplace-fetcher.ts +241 -235
  90. package/src/services/plugin-manager.ts +17 -20
  91. package/src/services/plugin-mcp-config.ts +2 -2
  92. package/src/services/plugin-requires.ts +16 -8
  93. package/src/services/plugin-setup.ts +32 -15
  94. package/src/services/plugin-version-check.ts +4 -4
  95. package/src/services/profile-materializer.ts +9 -5
  96. package/src/services/profile-sync.ts +12 -4
  97. package/src/services/profiles.ts +3 -3
  98. package/src/services/resolver.ts +10 -6
  99. package/src/services/settings-manager.ts +82 -46
  100. package/src/services/skills-manager.ts +11 -9
  101. package/src/services/symlink-manager.ts +2 -5
  102. package/src/services/toolchain.ts +3 -3
  103. package/src/services/update-cache.ts +1 -1
  104. package/src/services/update-engine.ts +4 -4
  105. package/src/services/update-plan.ts +9 -11
  106. package/src/services/version-snapshot.ts +88 -88
  107. package/src/types/gitignore.ts +28 -28
  108. package/src/ui/App.tsx +36 -36
  109. package/src/ui/adapters/settingsAdapter.ts +2 -2
  110. package/src/ui/components/CategoryHeader.tsx +1 -1
  111. package/src/ui/components/FlagDetailEditor.tsx +0 -0
  112. package/src/ui/components/ScopeIndicator.tsx +3 -1
  113. package/src/ui/components/ScrollableList.tsx +2 -1
  114. package/src/ui/components/StyledText.tsx +1 -1
  115. package/src/ui/components/layout/FooterHints.tsx +5 -3
  116. package/src/ui/components/layout/Panel.tsx +2 -2
  117. package/src/ui/components/layout/ScreenLayout.tsx +5 -4
  118. package/src/ui/components/modals/InputModal.tsx +7 -1
  119. package/src/ui/components/modals/ModalContainer.tsx +3 -3
  120. package/src/ui/components/modals/SelectModal.tsx +1 -1
  121. package/src/ui/components/primitives/DetailSection.tsx +1 -1
  122. package/src/ui/components/primitives/KeyValueLine.tsx +1 -1
  123. package/src/ui/components/primitives/ScopeSquares.tsx +2 -2
  124. package/src/ui/hooks/index.ts +0 -1
  125. package/src/ui/hooks/useGitignoreModal.ts +1 -5
  126. package/src/ui/hooks/useMismatchModal.ts +4 -10
  127. package/src/ui/renderers/cliToolRenderers.tsx +2 -2
  128. package/src/ui/renderers/gitignoreRenderers.tsx +2 -2
  129. package/src/ui/renderers/mcpRenderers.tsx +1 -1
  130. package/src/ui/renderers/pluginRenderers.tsx +3 -9
  131. package/src/ui/renderers/profileRenderers.tsx +9 -8
  132. package/src/ui/renderers/settingsRenderers.tsx +4 -4
  133. package/src/ui/renderers/skillRenderers.tsx +14 -14
  134. package/src/ui/screens/AliasScreen.tsx +31 -34
  135. package/src/ui/screens/CliToolsScreen.tsx +14 -13
  136. package/src/ui/screens/EnvVarsScreen.tsx +8 -8
  137. package/src/ui/screens/GitignoreScreen.tsx +15 -14
  138. package/src/ui/screens/McpRegistryScreen.tsx +10 -7
  139. package/src/ui/screens/McpScreen.tsx +10 -10
  140. package/src/ui/screens/PluginsScreen.tsx +17 -21
  141. package/src/ui/screens/ProfilesScreen.tsx +25 -24
  142. package/src/ui/screens/SkillsScreen.tsx +16 -14
  143. package/src/ui/state/AppContext.tsx +4 -8
  144. package/src/ui/state/DimensionsContext.tsx +3 -2
  145. package/src/ui/theme-mode.ts +20 -38
  146. package/src/ui/theme-resolve.ts +148 -0
  147. package/src/utils/clipboard.ts +5 -5
  148. package/src/utils/command-utils.ts +2 -2
  149. package/src/utils/config-dir.ts +1 -3
  150. package/src/utils/file-locking.ts +88 -86
  151. package/src/utils/go-module.ts +36 -0
  152. package/src/ui/hooks/useAsyncData.ts +0 -127
  153. package/src/ui/state/AnimationContext.tsx +0 -76
@@ -19,18 +19,18 @@ import { ALIAS_FLAGS, type AliasFlag } from "../data/alias-flags.js";
19
19
  * Per-flag value shapes. Discriminator is `kind`, mirroring `AliasFlag.kind`.
20
20
  */
21
21
  export type FlagValue =
22
- | { kind: "boolean"; enabled: boolean }
23
- | { kind: "tri-state"; state: "unset" | "on" | "off" }
24
- | { kind: "select"; enabled: boolean; value: string }
25
- | { kind: "text"; enabled: boolean; value: string }
26
- | { kind: "optional-text"; enabled: boolean; value: string }
27
- | { kind: "text-list"; enabled: boolean; values: string[] }
28
- | {
29
- kind: "multi-with-custom";
30
- enabled: boolean;
31
- picked: string[];
32
- custom: string[];
33
- };
22
+ | { kind: "boolean"; enabled: boolean }
23
+ | { kind: "tri-state"; state: "unset" | "on" | "off" }
24
+ | { kind: "select"; enabled: boolean; value: string }
25
+ | { kind: "text"; enabled: boolean; value: string }
26
+ | { kind: "optional-text"; enabled: boolean; value: string }
27
+ | { kind: "text-list"; enabled: boolean; values: string[] }
28
+ | {
29
+ kind: "multi-with-custom";
30
+ enabled: boolean;
31
+ picked: string[];
32
+ custom: string[];
33
+ };
34
34
 
35
35
  /**
36
36
  * In-memory alias configuration. No `version` field since this isn't
@@ -38,13 +38,13 @@ export type FlagValue =
38
38
  * and flag values come from the rc file.
39
39
  */
40
40
  export interface AliasConfig {
41
- /**
42
- * Name of the shell alias. Default `"c"` so users can keep typing the
43
- * vanilla `claude` command alongside the wrapped one.
44
- */
45
- aliasName: string;
46
- /** Per-flag state, keyed by `AliasFlag.id`. */
47
- flags: Record<string, FlagValue>;
41
+ /**
42
+ * Name of the shell alias. Default `"c"` so users can keep typing the
43
+ * vanilla `claude` command alongside the wrapped one.
44
+ */
45
+ aliasName: string;
46
+ /** Per-flag state, keyed by `AliasFlag.id`. */
47
+ flags: Record<string, FlagValue>;
48
48
  }
49
49
 
50
50
  /** Default alias name. POSIX/fish-safe, short, and not in conflict with `claude`. */
@@ -59,12 +59,12 @@ export const DEFAULT_ALIAS_NAME = "c";
59
59
  * set, and avoids the need to quote the name on either side of the `=`.
60
60
  */
61
61
  export function validateAliasName(name: string): string | null {
62
- if (name.length === 0) return "Alias name cannot be empty.";
63
- if (name.length > 32) return "Alias name is unreasonably long (>32 chars).";
64
- if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(name)) {
65
- return "Alias name must start with a letter or underscore and contain only letters, digits, _ or -.";
66
- }
67
- return null;
62
+ if (name.length === 0) return "Alias name cannot be empty.";
63
+ if (name.length > 32) return "Alias name is unreasonably long (>32 chars).";
64
+ if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(name)) {
65
+ return "Alias name must start with a letter or underscore and contain only letters, digits, _ or -.";
66
+ }
67
+ return null;
68
68
  }
69
69
 
70
70
  /**
@@ -72,11 +72,11 @@ export function validateAliasName(name: string): string | null {
72
72
  * the rc file (every flag disabled, default alias name).
73
73
  */
74
74
  export function defaultAliasConfig(): AliasConfig {
75
- const flags: Record<string, FlagValue> = {};
76
- for (const flag of ALIAS_FLAGS) {
77
- flags[flag.id] = defaultValueFor(flag);
78
- }
79
- return { aliasName: DEFAULT_ALIAS_NAME, flags };
75
+ const flags: Record<string, FlagValue> = {};
76
+ for (const flag of ALIAS_FLAGS) {
77
+ flags[flag.id] = defaultValueFor(flag);
78
+ }
79
+ return { aliasName: DEFAULT_ALIAS_NAME, flags };
80
80
  }
81
81
 
82
82
  // ─── Channels ⊇ dev-load (derive-on-display) ───────────────────────────
@@ -99,7 +99,7 @@ export const DEVLOAD_FLAG_ID = "dangerously-load-development-channels";
99
99
 
100
100
  /** Read the value list off a flag value, or `[]` for non-list kinds. */
101
101
  export function listValues(v: FlagValue | undefined): string[] {
102
- return v && v.kind === "text-list" ? v.values : [];
102
+ return v && v.kind === "text-list" ? v.values : [];
103
103
  }
104
104
 
105
105
  /**
@@ -108,11 +108,11 @@ export function listValues(v: FlagValue | undefined): string[] {
108
108
  * flag is enabled (a disabled dev-load contributes nothing to the alias).
109
109
  */
110
110
  export function derivedChannelValues(
111
- flags: Record<string, FlagValue>,
111
+ flags: Record<string, FlagValue>,
112
112
  ): string[] {
113
- const devload = flags[DEVLOAD_FLAG_ID];
114
- if (!devload || devload.kind !== "text-list" || !devload.enabled) return [];
115
- return devload.values;
113
+ const devload = flags[DEVLOAD_FLAG_ID];
114
+ if (!devload || devload.kind !== "text-list" || !devload.enabled) return [];
115
+ return devload.values;
116
116
  }
117
117
 
118
118
  /**
@@ -123,50 +123,50 @@ export function derivedChannelValues(
123
123
  * it stops emitting. Pure — leaves `channels.values` untouched.
124
124
  */
125
125
  export function withoutDerivedChannel(
126
- flags: Record<string, FlagValue>,
127
- value: string,
126
+ flags: Record<string, FlagValue>,
127
+ value: string,
128
128
  ): Record<string, FlagValue> {
129
- const cur = flags[DEVLOAD_FLAG_ID];
130
- if (!cur || cur.kind !== "text-list") return flags;
131
- const values = cur.values.filter((v) => v !== value);
132
- return {
133
- ...flags,
134
- [DEVLOAD_FLAG_ID]: {
135
- kind: "text-list",
136
- enabled: values.length > 0 && cur.enabled,
137
- values,
138
- },
139
- };
129
+ const cur = flags[DEVLOAD_FLAG_ID];
130
+ if (!cur || cur.kind !== "text-list") return flags;
131
+ const values = cur.values.filter((v) => v !== value);
132
+ return {
133
+ ...flags,
134
+ [DEVLOAD_FLAG_ID]: {
135
+ kind: "text-list",
136
+ enabled: values.length > 0 && cur.enabled,
137
+ values,
138
+ },
139
+ };
140
140
  }
141
141
 
142
142
  export function defaultValueFor(flag: AliasFlag): FlagValue {
143
- switch (flag.kind) {
144
- case "boolean":
145
- return { kind: "boolean", enabled: false };
146
- case "tri-state":
147
- return { kind: "tri-state", state: "unset" };
148
- case "select":
149
- return {
150
- kind: "select",
151
- enabled: false,
152
- value: flag.options?.[0]?.value ?? "",
153
- };
154
- case "text":
155
- return { kind: "text", enabled: false, value: "" };
156
- case "optional-text":
157
- return { kind: "optional-text", enabled: false, value: "" };
158
- case "text-list":
159
- return {
160
- kind: "text-list",
161
- enabled: false,
162
- values: [...(flag.defaultValues ?? [])],
163
- };
164
- case "multi-with-custom":
165
- return {
166
- kind: "multi-with-custom",
167
- enabled: false,
168
- picked: [],
169
- custom: [...(flag.defaultValues ?? [])],
170
- };
171
- }
143
+ switch (flag.kind) {
144
+ case "boolean":
145
+ return { kind: "boolean", enabled: false };
146
+ case "tri-state":
147
+ return { kind: "tri-state", state: "unset" };
148
+ case "select":
149
+ return {
150
+ kind: "select",
151
+ enabled: false,
152
+ value: flag.options?.[0]?.value ?? "",
153
+ };
154
+ case "text":
155
+ return { kind: "text", enabled: false, value: "" };
156
+ case "optional-text":
157
+ return { kind: "optional-text", enabled: false, value: "" };
158
+ case "text-list":
159
+ return {
160
+ kind: "text-list",
161
+ enabled: false,
162
+ values: [...(flag.defaultValues ?? [])],
163
+ };
164
+ case "multi-with-custom":
165
+ return {
166
+ kind: "multi-with-custom",
167
+ enabled: false,
168
+ picked: [],
169
+ custom: [...(flag.defaultValues ?? [])],
170
+ };
171
+ }
172
172
  }
@@ -1,19 +1,19 @@
1
- import fs from "fs-extra";
2
- import path from "node:path";
3
1
  import os from "node:os";
2
+ import path from "node:path";
3
+ import fs from "fs-extra";
4
4
  import semver from "semver";
5
5
  import type {
6
- ClaudeSettings,
7
6
  ClaudeLocalSettings,
8
- McpServerConfig,
9
- MarketplaceSource,
7
+ ClaudeSettings,
10
8
  DiscoveredMarketplace,
11
- InstalledPluginsRegistry,
12
9
  InstalledPluginEntry,
10
+ InstalledPluginsRegistry,
11
+ MarketplaceSource,
12
+ McpServerConfig,
13
13
  } from "../types/index.js";
14
14
  import { parsePluginId } from "../utils/string-utils.js";
15
- import { inheritablePaths } from "./git-worktree.js";
16
15
  import { repoHeadSha } from "./content-drift.js";
16
+ import { inheritablePaths } from "./git-worktree.js";
17
17
 
18
18
  const CLAUDE_DIR = ".claude";
19
19
  const SETTINGS_FILE = "settings.json";
@@ -143,6 +143,7 @@ export async function addMcpServer(
143
143
  // Extract env vars from config - they go to settings.local.json, not .mcp.json
144
144
  const envVars = config.env || {};
145
145
  const configWithoutEnv: McpServerConfig = { ...config };
146
+ // biome-ignore lint/performance/noDelete: this object is serialised into .mcp.json; the env key must be absent, not present as undefined
146
147
  delete configWithoutEnv.env;
147
148
 
148
149
  // Add to .mcp.json (without env vars)
@@ -437,13 +438,18 @@ export async function removeLocalInstalledPluginVersion(
437
438
  // or { type: "command", command: "..." }. Writing a bare string causes a validation error
438
439
  // that skips the entire settings file.
439
440
 
440
- function wrapStatusLine(template: string): { type: string; template: string } | undefined {
441
+ function wrapStatusLine(
442
+ template: string,
443
+ ): { type: string; template: string } | undefined {
441
444
  if (!template) return undefined;
442
445
  return { type: "template", template };
443
446
  }
444
447
 
445
448
  function unwrapStatusLine(
446
- value: string | { type: string; template?: string; command?: string } | undefined,
449
+ value:
450
+ | string
451
+ | { type: string; template?: string; command?: string }
452
+ | undefined,
447
453
  ): string | undefined {
448
454
  if (!value) return undefined;
449
455
  if (typeof value === "string") return value; // legacy format
@@ -459,6 +465,7 @@ export async function setStatusLine(
459
465
  if (wrapped) {
460
466
  settings.statusLine = wrapped;
461
467
  } else {
468
+ // biome-ignore lint/performance/noDelete: settings is written back to settings.json; the key must be removed from the file, and readers check `Object.keys`/`in`
462
469
  delete settings.statusLine;
463
470
  }
464
471
  await writeSettings(settings, projectPath);
@@ -478,6 +485,7 @@ export async function setGlobalStatusLine(template: string): Promise<void> {
478
485
  if (wrapped) {
479
486
  settings.statusLine = wrapped;
480
487
  } else {
488
+ // biome-ignore lint/performance/noDelete: settings is written back to settings.json; the key must be removed from the file, and readers check `Object.keys`/`in`
481
489
  delete settings.statusLine;
482
490
  }
483
491
  await writeGlobalSettings(settings);
@@ -678,7 +686,9 @@ export async function saveGlobalInstalledPluginVersion(
678
686
  */
679
687
  export async function gapFillInstalledPluginVersions(
680
688
  projectPath?: string,
681
- ): Promise<Array<{ pluginId: string; oldVersion: string; newVersion: string }>> {
689
+ ): Promise<
690
+ Array<{ pluginId: string; oldVersion: string; newVersion: string }>
691
+ > {
682
692
  const globalVersions = await getGlobalInstalledPluginVersions();
683
693
 
684
694
  // Collect versions from project and local scopes
@@ -749,8 +759,7 @@ export async function gapFillInstalledPluginVersions(
749
759
  // Apply bumps in a single write
750
760
  if (bumped.length > 0) {
751
761
  const settings = await readGlobalSettings();
752
- settings.installedPluginVersions =
753
- settings.installedPluginVersions || {};
762
+ settings.installedPluginVersions = settings.installedPluginVersions || {};
754
763
  for (const { pluginId, newVersion } of bumped) {
755
764
  settings.installedPluginVersions[pluginId] = newVersion;
756
765
  }
@@ -1048,10 +1057,10 @@ async function decodeProjectDirName(encoded: string): Promise<string | null> {
1048
1057
 
1049
1058
  // Build path greedily: at each step, try joining the next segment with a dash first
1050
1059
  // (preserving directory names like "circl-infra"), fall back to slash (new path segment)
1051
- let current = "/" + segments[0];
1060
+ let current = `/${segments[0]}`;
1052
1061
  for (let i = 1; i < segments.length; i++) {
1053
- const withDash = current + "-" + segments[i];
1054
- const withSlash = current + "/" + segments[i];
1062
+ const withDash = `${current}-${segments[i]}`;
1063
+ const withSlash = `${current}/${segments[i]}`;
1055
1064
  // Prefer dash (keeps compound names intact) if that path prefix exists
1056
1065
  if (await fs.pathExists(withDash)) {
1057
1066
  current = withDash;
@@ -1388,6 +1397,7 @@ export async function cleanupExtraKnownMarketplaces(
1388
1397
  changed &&
1389
1398
  Object.keys(globalSettings.extraKnownMarketplaces).length === 0
1390
1399
  ) {
1400
+ // biome-ignore lint/performance/noDelete: the empty block is removed from settings.json entirely; an undefined value would survive Object.keys and `in` checks
1391
1401
  delete globalSettings.extraKnownMarketplaces;
1392
1402
  }
1393
1403
  if (changed) {
@@ -1417,6 +1427,7 @@ export async function cleanupExtraKnownMarketplaces(
1417
1427
  changed &&
1418
1428
  Object.keys(projectSettings.extraKnownMarketplaces).length === 0
1419
1429
  ) {
1430
+ // biome-ignore lint/performance/noDelete: the empty block is removed from settings.json entirely; an undefined value would survive Object.keys and `in` checks
1420
1431
  delete projectSettings.extraKnownMarketplaces;
1421
1432
  }
1422
1433
  if (changed) {
@@ -1452,9 +1463,7 @@ async function readKnownMarketplaces(): Promise<KnownMarketplaces> {
1452
1463
  * enough: an entry with a missing directory means the clone was lost; a
1453
1464
  * directory without an entry means Claude Code doesn't know about it.
1454
1465
  */
1455
- export async function isMarketplaceRegistered(
1456
- name: string,
1457
- ): Promise<boolean> {
1466
+ export async function isMarketplaceRegistered(name: string): Promise<boolean> {
1458
1467
  const known = await readKnownMarketplaces();
1459
1468
  if (!known[name]) return false;
1460
1469
  const marketplacesDir = path.join(
@@ -7,7 +7,7 @@
7
7
  * invisible. No update is offered, and the only cure is a reinstall — which is
8
8
  * exactly the "plugin says installed but its skills are missing" report.
9
9
  *
10
- * This is not a hypothetical. `publish-dist.sh` force-pushes a rebuilt tree, so
10
+ * This is not a hypothetical. `publish-dist.sh` pushes a rebuilt tree, so
11
11
  * same-version-different-content is a normal outcome of the release process.
12
12
  * Measured on 2026-08-06: `cache/magus/dev/3.0.1/skills` held
13
13
  * {audit, tui-progress} while `marketplaces/magus/plugins/dev/skills` held
@@ -50,9 +50,15 @@ export function clearContentDriftCache(): void {
50
50
  cache.clear();
51
51
  }
52
52
 
53
- function git(cwd: string, args: string[]): Promise<{ code: number; out: string }> {
53
+ function git(
54
+ cwd: string,
55
+ args: string[],
56
+ ): Promise<{ code: number; out: string }> {
54
57
  return new Promise((resolve) => {
55
- const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "ignore"] });
58
+ const child = spawn("git", args, {
59
+ cwd,
60
+ stdio: ["ignore", "pipe", "ignore"],
61
+ });
56
62
  let out = "";
57
63
  child.stdout.on("data", (d) => {
58
64
  out += String(d);
@@ -154,7 +160,11 @@ export async function hasContentDrift(q: DriftQuery): Promise<boolean> {
154
160
  const rel = (q.sourcePath ?? `plugins/${q.pluginName}`).replace(/^\.\//, "");
155
161
 
156
162
  // The recorded commit must exist locally, or the diff is meaningless.
157
- const known = await git(repo, ["cat-file", "-e", `${q.installedSha}^{commit}`]);
163
+ const known = await git(repo, [
164
+ "cat-file",
165
+ "-e",
166
+ `${q.installedSha}^{commit}`,
167
+ ]);
158
168
  if (known.code !== 0) {
159
169
  cache.set(key, false);
160
170
  return false;