claudeup 4.3.0 → 4.5.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/package.json +1 -1
- package/src/__tests__/dual-write-prevention.test.ts +229 -0
- package/src/data/settings-catalog.js +9 -0
- package/src/data/settings-catalog.ts +13 -1
- package/src/prerunner/index.js +6 -4
- package/src/prerunner/index.ts +5 -4
- package/src/services/settings-manager.js +39 -0
- package/src/services/settings-manager.ts +36 -0
- package/src/ui/renderers/settingsRenderers.js +5 -3
- package/src/ui/renderers/settingsRenderers.tsx +5 -3
- package/src/ui/screens/PluginsScreen.js +1 -27
- package/src/ui/screens/PluginsScreen.tsx +0 -30
package/package.json
CHANGED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dual-write prevention tests
|
|
3
|
+
*
|
|
4
|
+
* Verifies that claudeup does NOT write plugin state (via saveGlobalInstalledPluginVersion
|
|
5
|
+
* or saveVersionAfterInstall) when the Claude CLI has already handled the write.
|
|
6
|
+
*
|
|
7
|
+
* RED before fix: PluginsScreen has 6 saveVersionAfterInstall calls after CLI ops;
|
|
8
|
+
* prerunner calls saveGlobalInstalledPluginVersion unconditionally.
|
|
9
|
+
* GREEN after fix: those calls are removed.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, mock, beforeEach, afterEach } from "bun:test";
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import * as url from "node:url";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// PART 1: PluginsScreen source-level policy tests
|
|
19
|
+
//
|
|
20
|
+
// The dual-write calls in PluginsScreen are inside React event handlers.
|
|
21
|
+
// Rendering the full TUI component in a unit test would require the opentui
|
|
22
|
+
// terminal runtime. Instead we assert the source does NOT contain the
|
|
23
|
+
// forbidden call pattern — this is a code-policy test that:
|
|
24
|
+
// - FAILS before the fix (6 saveVersionAfterInstall calls follow CLI calls)
|
|
25
|
+
// - PASSES after the fix (those calls are removed)
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
29
|
+
const PLUGINS_SCREEN_PATH = path.resolve(
|
|
30
|
+
__dirname,
|
|
31
|
+
"../ui/screens/PluginsScreen.tsx",
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
describe("PluginsScreen — no dual-write after CLI operations", () => {
|
|
35
|
+
let source: string;
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
source = fs.readFileSync(PLUGINS_SCREEN_PATH, "utf8");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("does not call saveVersionAfterInstall immediately after cliUpdatePlugin", () => {
|
|
42
|
+
// Find every occurrence of cliUpdatePlugin in the source. Each one should
|
|
43
|
+
// NOT be immediately followed (within 3 lines) by saveVersionAfterInstall.
|
|
44
|
+
const lines = source.split("\n");
|
|
45
|
+
const violations: number[] = [];
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < lines.length; i++) {
|
|
48
|
+
if (lines[i].includes("cliUpdatePlugin(")) {
|
|
49
|
+
// Check the next 3 lines for a dual-write call
|
|
50
|
+
for (let j = i + 1; j <= Math.min(i + 3, lines.length - 1); j++) {
|
|
51
|
+
if (lines[j].includes("saveVersionAfterInstall(")) {
|
|
52
|
+
violations.push(i + 1); // 1-indexed line number
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
expect(violations).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("does not call saveVersionAfterInstall immediately after cliInstallPlugin", () => {
|
|
63
|
+
const lines = source.split("\n");
|
|
64
|
+
const violations: number[] = [];
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < lines.length; i++) {
|
|
67
|
+
if (lines[i].includes("cliInstallPlugin(")) {
|
|
68
|
+
for (let j = i + 1; j <= Math.min(i + 3, lines.length - 1); j++) {
|
|
69
|
+
if (lines[j].includes("saveVersionAfterInstall(")) {
|
|
70
|
+
violations.push(i + 1);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
expect(violations).toEqual([]);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// PART 2: Prerunner — saveGlobalInstalledPluginVersion guarded by CLI success
|
|
83
|
+
//
|
|
84
|
+
// We mock the prerunner's module dependencies so we can call prerunClaude()
|
|
85
|
+
// directly and inspect call counts.
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
// Mutable state shared between mock factories and test assertions.
|
|
89
|
+
let mockSaveGlobal: ReturnType<typeof mock>;
|
|
90
|
+
let mockUpdatePlugin: ReturnType<typeof mock>;
|
|
91
|
+
let mockIsClaudeAvailable: ReturnType<typeof mock>;
|
|
92
|
+
let mockGetAvailablePlugins: ReturnType<typeof mock>;
|
|
93
|
+
|
|
94
|
+
// We must register mock.module before importing the module under test.
|
|
95
|
+
// Bun hoists mock.module() calls, so the mocks are active when the
|
|
96
|
+
// module is first loaded.
|
|
97
|
+
|
|
98
|
+
mock.module("../services/claude-settings.js", () => ({
|
|
99
|
+
recoverMarketplaceSettings: mock(() =>
|
|
100
|
+
Promise.resolve({ enabledAutoUpdate: [], removed: [] }),
|
|
101
|
+
),
|
|
102
|
+
migrateMarketplaceRename: mock(() =>
|
|
103
|
+
Promise.resolve({
|
|
104
|
+
projectMigrated: 0,
|
|
105
|
+
globalMigrated: 0,
|
|
106
|
+
localMigrated: 0,
|
|
107
|
+
registryMigrated: 0,
|
|
108
|
+
knownMarketplacesMigrated: false,
|
|
109
|
+
}),
|
|
110
|
+
),
|
|
111
|
+
getGlobalEnabledPlugins: mock(() => Promise.resolve({})),
|
|
112
|
+
getEnabledPlugins: mock(() => Promise.resolve({})),
|
|
113
|
+
getLocalEnabledPlugins: mock(() => Promise.resolve({})),
|
|
114
|
+
saveGlobalInstalledPluginVersion: (...args: unknown[]) =>
|
|
115
|
+
mockSaveGlobal(...args),
|
|
116
|
+
readGlobalSettings: mock(() => Promise.resolve({ hooks: {} })),
|
|
117
|
+
writeGlobalSettings: mock(() => Promise.resolve()),
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
mock.module("../services/claude-cli.js", () => ({
|
|
121
|
+
updatePlugin: (...args: unknown[]) => mockUpdatePlugin(...args),
|
|
122
|
+
isClaudeAvailable: (...args: unknown[]) => mockIsClaudeAvailable(...args),
|
|
123
|
+
addMarketplace: mock(() => Promise.resolve()),
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
mock.module("../services/plugin-manager.js", () => ({
|
|
127
|
+
getAvailablePlugins: (...args: unknown[]) =>
|
|
128
|
+
mockGetAvailablePlugins(...args),
|
|
129
|
+
clearMarketplaceCache: mock(() => undefined),
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
mock.module("../services/update-cache.js", () => ({
|
|
133
|
+
UpdateCache: class {
|
|
134
|
+
shouldCheckForUpdates = mock(() => Promise.resolve(true));
|
|
135
|
+
saveCheck = mock(() => Promise.resolve());
|
|
136
|
+
},
|
|
137
|
+
}));
|
|
138
|
+
|
|
139
|
+
mock.module("../services/claude-runner.js", () => ({
|
|
140
|
+
runClaude: mock(() => Promise.resolve(0)),
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
mock.module("../data/marketplaces.js", () => ({
|
|
144
|
+
defaultMarketplaces: [],
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
mock.module("../utils/string-utils.js", () => ({
|
|
148
|
+
parsePluginId: mock((_id: string) => null),
|
|
149
|
+
}));
|
|
150
|
+
|
|
151
|
+
mock.module("fs-extra", () => ({
|
|
152
|
+
default: {
|
|
153
|
+
pathExists: mock(() => Promise.resolve(false)),
|
|
154
|
+
readJson: mock(() => Promise.resolve({})),
|
|
155
|
+
writeJson: mock(() => Promise.resolve()),
|
|
156
|
+
ensureDir: mock(() => Promise.resolve()),
|
|
157
|
+
},
|
|
158
|
+
}));
|
|
159
|
+
|
|
160
|
+
// Now import the module under test (after mock.module registrations).
|
|
161
|
+
const { prerunClaude } = await import("../prerunner/index.js");
|
|
162
|
+
|
|
163
|
+
// Helper: build a minimal PluginInfo-like object
|
|
164
|
+
function makePlugin(overrides: Partial<{
|
|
165
|
+
id: string;
|
|
166
|
+
enabled: boolean;
|
|
167
|
+
hasUpdate: boolean;
|
|
168
|
+
installedVersion: string;
|
|
169
|
+
version: string;
|
|
170
|
+
}> = {}): {
|
|
171
|
+
id: string;
|
|
172
|
+
enabled: boolean;
|
|
173
|
+
hasUpdate: boolean;
|
|
174
|
+
installedVersion: string;
|
|
175
|
+
version: string;
|
|
176
|
+
} {
|
|
177
|
+
return {
|
|
178
|
+
id: "test-plugin@magus",
|
|
179
|
+
enabled: true,
|
|
180
|
+
hasUpdate: true,
|
|
181
|
+
installedVersion: "1.0.0",
|
|
182
|
+
version: "1.1.0",
|
|
183
|
+
...overrides,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
describe("prerunner — saveGlobalInstalledPluginVersion call count", () => {
|
|
188
|
+
beforeEach(() => {
|
|
189
|
+
mockSaveGlobal = mock(() => Promise.resolve());
|
|
190
|
+
mockUpdatePlugin = mock(() => Promise.resolve());
|
|
191
|
+
mockIsClaudeAvailable = mock(() => Promise.resolve(true));
|
|
192
|
+
mockGetAvailablePlugins = mock(() =>
|
|
193
|
+
Promise.resolve([makePlugin()]),
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("does NOT call saveGlobalInstalledPluginVersion when CLI is available and updatePlugin succeeds", async () => {
|
|
198
|
+
// CLI is available, updatePlugin succeeds.
|
|
199
|
+
// The FIXED code removes the saveGlobalInstalledPluginVersion call entirely —
|
|
200
|
+
// the CLI already wrote the state.
|
|
201
|
+
mockIsClaudeAvailable = mock(() => Promise.resolve(true));
|
|
202
|
+
mockUpdatePlugin = mock(() => Promise.resolve());
|
|
203
|
+
|
|
204
|
+
await prerunClaude(["--help"], { force: true });
|
|
205
|
+
|
|
206
|
+
expect(mockSaveGlobal).toHaveBeenCalledTimes(0);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("does NOT call saveGlobalInstalledPluginVersion when CLI is unavailable", async () => {
|
|
210
|
+
// CLI unavailable — we must NOT write phantom state.
|
|
211
|
+
mockIsClaudeAvailable = mock(() => Promise.resolve(false));
|
|
212
|
+
|
|
213
|
+
await prerunClaude(["--help"], { force: true });
|
|
214
|
+
|
|
215
|
+
expect(mockSaveGlobal).toHaveBeenCalledTimes(0);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("does NOT call saveGlobalInstalledPluginVersion when CLI updatePlugin throws", async () => {
|
|
219
|
+
// CLI available but updatePlugin fails — must NOT write phantom state.
|
|
220
|
+
mockIsClaudeAvailable = mock(() => Promise.resolve(true));
|
|
221
|
+
mockUpdatePlugin = mock(() =>
|
|
222
|
+
Promise.reject(new Error("CLI update failed")),
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
await prerunClaude(["--help"], { force: true });
|
|
226
|
+
|
|
227
|
+
expect(mockSaveGlobal).toHaveBeenCalledTimes(0);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
@@ -171,6 +171,15 @@ export const SETTINGS_CATALOG = [
|
|
|
171
171
|
storage: { type: "attribution" },
|
|
172
172
|
defaultValue: "true",
|
|
173
173
|
},
|
|
174
|
+
{
|
|
175
|
+
id: "attribution-text",
|
|
176
|
+
name: "Custom Attribution Text",
|
|
177
|
+
description: "Custom text for commit and PR attribution. Applied when AI Attribution is enabled. Leave empty to use Claude's default",
|
|
178
|
+
category: "workflow",
|
|
179
|
+
type: "string",
|
|
180
|
+
storage: { type: "attribution-text" },
|
|
181
|
+
defaultValue: "Crafted with agentic harness Magus (https://github.com/MadAppGang/magus)",
|
|
182
|
+
},
|
|
174
183
|
{
|
|
175
184
|
id: "output-style",
|
|
176
185
|
name: "Output Style",
|
|
@@ -10,7 +10,8 @@ export type SettingType = "boolean" | "string" | "select";
|
|
|
10
10
|
export type SettingStorage =
|
|
11
11
|
| { type: "env"; key: string }
|
|
12
12
|
| { type: "setting"; key: string }
|
|
13
|
-
| { type: "attribution" }
|
|
13
|
+
| { type: "attribution" }
|
|
14
|
+
| { type: "attribution-text" };
|
|
14
15
|
|
|
15
16
|
export interface SettingDefinition {
|
|
16
17
|
id: string;
|
|
@@ -215,6 +216,17 @@ export const SETTINGS_CATALOG: SettingDefinition[] = [
|
|
|
215
216
|
storage: { type: "attribution" },
|
|
216
217
|
defaultValue: "true",
|
|
217
218
|
},
|
|
219
|
+
{
|
|
220
|
+
id: "attribution-text",
|
|
221
|
+
name: "Custom Attribution Text",
|
|
222
|
+
description:
|
|
223
|
+
"Custom text for commit and PR attribution. Applied when AI Attribution is enabled. Leave empty to use Claude's default",
|
|
224
|
+
category: "workflow",
|
|
225
|
+
type: "string",
|
|
226
|
+
storage: { type: "attribution-text" },
|
|
227
|
+
defaultValue:
|
|
228
|
+
"Crafted with agentic harness Magus (https://github.com/MadAppGang/magus)",
|
|
229
|
+
},
|
|
218
230
|
{
|
|
219
231
|
id: "output-style",
|
|
220
232
|
name: "Output Style",
|
package/src/prerunner/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import os from "node:os";
|
|
|
4
4
|
import { UpdateCache } from "../services/update-cache.js";
|
|
5
5
|
import { getAvailablePlugins, clearMarketplaceCache, } from "../services/plugin-manager.js";
|
|
6
6
|
import { runClaude } from "../services/claude-runner.js";
|
|
7
|
-
import { recoverMarketplaceSettings, migrateMarketplaceRename, getGlobalEnabledPlugins, getEnabledPlugins, getLocalEnabledPlugins,
|
|
7
|
+
import { recoverMarketplaceSettings, migrateMarketplaceRename, getGlobalEnabledPlugins, getEnabledPlugins, getLocalEnabledPlugins, readGlobalSettings, writeGlobalSettings, } from "../services/claude-settings.js";
|
|
8
8
|
import { parsePluginId } from "../utils/string-utils.js";
|
|
9
9
|
import { defaultMarketplaces } from "../data/marketplaces.js";
|
|
10
10
|
import { updatePlugin, addMarketplace, isClaudeAvailable, } from "../services/claude-cli.js";
|
|
@@ -194,10 +194,12 @@ export async function prerunClaude(claudeArgs, options = {}) {
|
|
|
194
194
|
plugin.installedVersion &&
|
|
195
195
|
plugin.version) {
|
|
196
196
|
try {
|
|
197
|
-
if (cliAvailable) {
|
|
198
|
-
|
|
197
|
+
if (!cliAvailable) {
|
|
198
|
+
// CLI unavailable — skip update entirely, don't write phantom state
|
|
199
|
+
continue;
|
|
199
200
|
}
|
|
200
|
-
await
|
|
201
|
+
await updatePlugin(plugin.id, "user");
|
|
202
|
+
// CLI wrote all plugin state; no claudeup write needed
|
|
201
203
|
autoUpdatedPlugins.push({
|
|
202
204
|
pluginId: plugin.id,
|
|
203
205
|
oldVersion: plugin.installedVersion,
|
package/src/prerunner/index.ts
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
getGlobalEnabledPlugins,
|
|
14
14
|
getEnabledPlugins,
|
|
15
15
|
getLocalEnabledPlugins,
|
|
16
|
-
saveGlobalInstalledPluginVersion,
|
|
17
16
|
readGlobalSettings,
|
|
18
17
|
writeGlobalSettings,
|
|
19
18
|
} from "../services/claude-settings.js";
|
|
@@ -278,10 +277,12 @@ export async function prerunClaude(
|
|
|
278
277
|
plugin.version
|
|
279
278
|
) {
|
|
280
279
|
try {
|
|
281
|
-
if (cliAvailable) {
|
|
282
|
-
|
|
280
|
+
if (!cliAvailable) {
|
|
281
|
+
// CLI unavailable — skip update entirely, don't write phantom state
|
|
282
|
+
continue;
|
|
283
283
|
}
|
|
284
|
-
await
|
|
284
|
+
await updatePlugin(plugin.id, "user");
|
|
285
|
+
// CLI wrote all plugin state; no claudeup write needed
|
|
285
286
|
|
|
286
287
|
autoUpdatedPlugins.push({
|
|
287
288
|
pluginId: plugin.id,
|
|
@@ -12,6 +12,27 @@ export async function readSettingValue(setting, scope, projectPath) {
|
|
|
12
12
|
}
|
|
13
13
|
return undefined; // default (enabled)
|
|
14
14
|
}
|
|
15
|
+
else if (setting.storage.type === "attribution-text") {
|
|
16
|
+
// Custom attribution text: read from attribution.commit, strip the Co-Authored-By trailer prefix
|
|
17
|
+
const attr = settings.attribution;
|
|
18
|
+
if (!attr || (attr.commit === "" && attr.pr === "")) {
|
|
19
|
+
// Attribution is disabled or not set — no custom text stored
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const commit = attr.commit;
|
|
23
|
+
if (!commit) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
// The commit value is "Co-Authored-By: Magus <magus@madappgang.com>\n\n{customText}"
|
|
27
|
+
// Strip the trailer prefix if present
|
|
28
|
+
const trailerPrefix = "Co-Authored-By: Magus <magus@madappgang.com>\n\n";
|
|
29
|
+
if (commit.startsWith(trailerPrefix)) {
|
|
30
|
+
const text = commit.slice(trailerPrefix.length);
|
|
31
|
+
return text.length > 0 ? text : undefined;
|
|
32
|
+
}
|
|
33
|
+
// If no trailer prefix, the value is the raw text (or Claude default — return undefined)
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
15
36
|
else if (setting.storage.type === "env") {
|
|
16
37
|
const env = settings.env;
|
|
17
38
|
return env?.[setting.storage.key];
|
|
@@ -40,6 +61,24 @@ export async function writeSettingValue(setting, value, scope, projectPath) {
|
|
|
40
61
|
delete settings.attribution;
|
|
41
62
|
}
|
|
42
63
|
}
|
|
64
|
+
else if (setting.storage.type === "attribution-text") {
|
|
65
|
+
const attr = settings.attribution;
|
|
66
|
+
// If attribution is explicitly disabled ({ commit: "", pr: "" }), do not overwrite it
|
|
67
|
+
if (attr && attr.commit === "" && attr.pr === "") {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (value && value.trim().length > 0) {
|
|
71
|
+
// Write custom text: commit gets a Co-Authored-By trailer + the text; pr gets the text
|
|
72
|
+
settings.attribution = {
|
|
73
|
+
commit: `Co-Authored-By: Magus <magus@madappgang.com>\n\n${value}`,
|
|
74
|
+
pr: value,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// Empty value: remove attribution key entirely (revert to Claude defaults)
|
|
79
|
+
delete settings.attribution;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
43
82
|
else if (setting.storage.type === "env") {
|
|
44
83
|
// Write to the "env" block in settings.json
|
|
45
84
|
settings.env = settings.env || {};
|
|
@@ -26,6 +26,26 @@ export async function readSettingValue(
|
|
|
26
26
|
return "false";
|
|
27
27
|
}
|
|
28
28
|
return undefined; // default (enabled)
|
|
29
|
+
} else if (setting.storage.type === "attribution-text") {
|
|
30
|
+
// Custom attribution text: read from attribution.commit, strip the Co-Authored-By trailer prefix
|
|
31
|
+
const attr = (settings as any).attribution;
|
|
32
|
+
if (!attr || (attr.commit === "" && attr.pr === "")) {
|
|
33
|
+
// Attribution is disabled or not set — no custom text stored
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
const commit: string | undefined = attr.commit;
|
|
37
|
+
if (!commit) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
// The commit value is "Co-Authored-By: Magus <magus@madappgang.com>\n\n{customText}"
|
|
41
|
+
// Strip the trailer prefix if present
|
|
42
|
+
const trailerPrefix = "Co-Authored-By: Magus <magus@madappgang.com>\n\n";
|
|
43
|
+
if (commit.startsWith(trailerPrefix)) {
|
|
44
|
+
const text = commit.slice(trailerPrefix.length);
|
|
45
|
+
return text.length > 0 ? text : undefined;
|
|
46
|
+
}
|
|
47
|
+
// If no trailer prefix, the value is the raw text (or Claude default — return undefined)
|
|
48
|
+
return undefined;
|
|
29
49
|
} else if (setting.storage.type === "env") {
|
|
30
50
|
const env = (settings as any).env as Record<string, string> | undefined;
|
|
31
51
|
return env?.[setting.storage.key];
|
|
@@ -59,6 +79,22 @@ export async function writeSettingValue(
|
|
|
59
79
|
} else {
|
|
60
80
|
delete (settings as any).attribution;
|
|
61
81
|
}
|
|
82
|
+
} else if (setting.storage.type === "attribution-text") {
|
|
83
|
+
const attr = (settings as any).attribution;
|
|
84
|
+
// If attribution is explicitly disabled ({ commit: "", pr: "" }), do not overwrite it
|
|
85
|
+
if (attr && attr.commit === "" && attr.pr === "") {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (value && value.trim().length > 0) {
|
|
89
|
+
// Write custom text: commit gets a Co-Authored-By trailer + the text; pr gets the text
|
|
90
|
+
(settings as any).attribution = {
|
|
91
|
+
commit: `Co-Authored-By: Magus <magus@madappgang.com>\n\n${value}`,
|
|
92
|
+
pr: value,
|
|
93
|
+
};
|
|
94
|
+
} else {
|
|
95
|
+
// Empty value: remove attribution key entirely (revert to Claude defaults)
|
|
96
|
+
delete (settings as any).attribution;
|
|
97
|
+
}
|
|
62
98
|
} else if (setting.storage.type === "env") {
|
|
63
99
|
// Write to the "env" block in settings.json
|
|
64
100
|
(settings as any).env = (settings as any).env || {};
|
|
@@ -33,9 +33,11 @@ const settingRenderer = {
|
|
|
33
33
|
const scoped = item.scopedValues;
|
|
34
34
|
const storageDesc = setting.storage.type === "attribution"
|
|
35
35
|
? "settings.json: attribution"
|
|
36
|
-
: setting.storage.type === "
|
|
37
|
-
?
|
|
38
|
-
:
|
|
36
|
+
: setting.storage.type === "attribution-text"
|
|
37
|
+
? "settings.json: attribution"
|
|
38
|
+
: setting.storage.type === "env"
|
|
39
|
+
? `env: ${setting.storage.key}`
|
|
40
|
+
: `settings.json: ${setting.storage.key}`;
|
|
39
41
|
const userValue = formatValue(setting, scoped.user);
|
|
40
42
|
const projectValue = formatValue(setting, scoped.project);
|
|
41
43
|
const userIsSet = scoped.user !== undefined && scoped.user !== "";
|
|
@@ -81,9 +81,11 @@ const settingRenderer: ItemRenderer<SettingsSettingItem> = {
|
|
|
81
81
|
const storageDesc =
|
|
82
82
|
setting.storage.type === "attribution"
|
|
83
83
|
? "settings.json: attribution"
|
|
84
|
-
: setting.storage.type === "
|
|
85
|
-
?
|
|
86
|
-
:
|
|
84
|
+
: setting.storage.type === "attribution-text"
|
|
85
|
+
? "settings.json: attribution"
|
|
86
|
+
: setting.storage.type === "env"
|
|
87
|
+
? `env: ${setting.storage.key}`
|
|
88
|
+
: `settings.json: ${setting.storage.key}`;
|
|
87
89
|
|
|
88
90
|
const userValue = formatValue(setting, scoped.user);
|
|
89
91
|
const projectValue = formatValue(setting, scoped.project);
|
|
@@ -9,9 +9,8 @@ import { EmptyFilterState } from "../components/EmptyFilterState.js";
|
|
|
9
9
|
import { fuzzyFilter } from "../../utils/fuzzy-search.js";
|
|
10
10
|
import { getAllMarketplaces } from "../../data/marketplaces.js";
|
|
11
11
|
import { getAvailablePlugins, refreshAllMarketplaces, clearMarketplaceCache, getLocalMarketplacesInfo, } from "../../services/plugin-manager.js";
|
|
12
|
-
import { setMcpEnvVar, getMcpEnvVars, readSettings,
|
|
12
|
+
import { setMcpEnvVar, getMcpEnvVars, readSettings, } from "../../services/claude-settings.js";
|
|
13
13
|
import { saveProfile } from "../../services/profiles.js";
|
|
14
|
-
import { saveInstalledPluginVersion } from "../../services/plugin-manager.js";
|
|
15
14
|
import { installPlugin as cliInstallPlugin, uninstallPlugin as cliUninstallPlugin, updatePlugin as cliUpdatePlugin, } from "../../services/claude-cli.js";
|
|
16
15
|
import { getPluginEnvRequirements, getPluginSourcePath, } from "../../services/plugin-mcp-config.js";
|
|
17
16
|
import { getPluginSetupFromSource, checkMissingDeps, installPluginDeps, } from "../../services/plugin-setup.js";
|
|
@@ -355,25 +354,6 @@ export function PluginsScreen() {
|
|
|
355
354
|
console.error("Error installing plugin deps:", error);
|
|
356
355
|
}
|
|
357
356
|
};
|
|
358
|
-
/**
|
|
359
|
-
* Save installed plugin version to settings after CLI install/update.
|
|
360
|
-
*/
|
|
361
|
-
const saveVersionAfterInstall = async (pluginId, version, scope) => {
|
|
362
|
-
try {
|
|
363
|
-
if (scope === "user") {
|
|
364
|
-
await saveGlobalInstalledPluginVersion(pluginId, version);
|
|
365
|
-
}
|
|
366
|
-
else if (scope === "local") {
|
|
367
|
-
await saveLocalInstalledPluginVersion(pluginId, version, state.projectPath);
|
|
368
|
-
}
|
|
369
|
-
else {
|
|
370
|
-
await saveInstalledPluginVersion(pluginId, version, state.projectPath);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
catch {
|
|
374
|
-
// Non-fatal: version display may be stale but plugin still works
|
|
375
|
-
}
|
|
376
|
-
};
|
|
377
357
|
const handleSelect = async () => {
|
|
378
358
|
const item = selectableItems[pluginsState.selectedIndex];
|
|
379
359
|
if (!item)
|
|
@@ -459,11 +439,9 @@ export function PluginsScreen() {
|
|
|
459
439
|
}
|
|
460
440
|
else if (action === "update") {
|
|
461
441
|
await cliUpdatePlugin(plugin.id, scope);
|
|
462
|
-
await saveVersionAfterInstall(plugin.id, latestVersion, scope);
|
|
463
442
|
}
|
|
464
443
|
else {
|
|
465
444
|
await cliInstallPlugin(plugin.id, scope);
|
|
466
|
-
await saveVersionAfterInstall(plugin.id, latestVersion, scope);
|
|
467
445
|
modal.hideModal();
|
|
468
446
|
await collectPluginEnvVars(plugin.name, plugin.marketplace);
|
|
469
447
|
await installPluginSystemDeps(plugin.name, plugin.marketplace);
|
|
@@ -488,7 +466,6 @@ export function PluginsScreen() {
|
|
|
488
466
|
modal.loading(`Updating ${plugin.name}...`);
|
|
489
467
|
try {
|
|
490
468
|
await cliUpdatePlugin(plugin.id, scope);
|
|
491
|
-
await saveVersionAfterInstall(plugin.id, plugin.version || "0.0.0", scope);
|
|
492
469
|
modal.hideModal();
|
|
493
470
|
fetchData();
|
|
494
471
|
}
|
|
@@ -508,7 +485,6 @@ export function PluginsScreen() {
|
|
|
508
485
|
try {
|
|
509
486
|
for (const plugin of updatable) {
|
|
510
487
|
await cliUpdatePlugin(plugin.id, scope);
|
|
511
|
-
await saveVersionAfterInstall(plugin.id, plugin.version || "0.0.0", scope);
|
|
512
488
|
}
|
|
513
489
|
modal.hideModal();
|
|
514
490
|
fetchData();
|
|
@@ -558,11 +534,9 @@ export function PluginsScreen() {
|
|
|
558
534
|
}
|
|
559
535
|
else if (action === "update") {
|
|
560
536
|
await cliUpdatePlugin(plugin.id, scope);
|
|
561
|
-
await saveVersionAfterInstall(plugin.id, latestVersion, scope);
|
|
562
537
|
}
|
|
563
538
|
else {
|
|
564
539
|
await cliInstallPlugin(plugin.id, scope);
|
|
565
|
-
await saveVersionAfterInstall(plugin.id, latestVersion, scope);
|
|
566
540
|
modal.hideModal();
|
|
567
541
|
await collectPluginEnvVars(plugin.name, plugin.marketplace);
|
|
568
542
|
await installPluginSystemDeps(plugin.name, plugin.marketplace);
|
|
@@ -18,11 +18,8 @@ import {
|
|
|
18
18
|
setMcpEnvVar,
|
|
19
19
|
getMcpEnvVars,
|
|
20
20
|
readSettings,
|
|
21
|
-
saveGlobalInstalledPluginVersion,
|
|
22
|
-
saveLocalInstalledPluginVersion,
|
|
23
21
|
} from "../../services/claude-settings.js";
|
|
24
22
|
import { saveProfile } from "../../services/profiles.js";
|
|
25
|
-
import { saveInstalledPluginVersion } from "../../services/plugin-manager.js";
|
|
26
23
|
import {
|
|
27
24
|
installPlugin as cliInstallPlugin,
|
|
28
25
|
uninstallPlugin as cliUninstallPlugin,
|
|
@@ -452,27 +449,6 @@ export function PluginsScreen() {
|
|
|
452
449
|
}
|
|
453
450
|
};
|
|
454
451
|
|
|
455
|
-
/**
|
|
456
|
-
* Save installed plugin version to settings after CLI install/update.
|
|
457
|
-
*/
|
|
458
|
-
const saveVersionAfterInstall = async (
|
|
459
|
-
pluginId: string,
|
|
460
|
-
version: string,
|
|
461
|
-
scope: PluginScope,
|
|
462
|
-
): Promise<void> => {
|
|
463
|
-
try {
|
|
464
|
-
if (scope === "user") {
|
|
465
|
-
await saveGlobalInstalledPluginVersion(pluginId, version);
|
|
466
|
-
} else if (scope === "local") {
|
|
467
|
-
await saveLocalInstalledPluginVersion(pluginId, version, state.projectPath);
|
|
468
|
-
} else {
|
|
469
|
-
await saveInstalledPluginVersion(pluginId, version, state.projectPath);
|
|
470
|
-
}
|
|
471
|
-
} catch {
|
|
472
|
-
// Non-fatal: version display may be stale but plugin still works
|
|
473
|
-
}
|
|
474
|
-
};
|
|
475
|
-
|
|
476
452
|
const handleSelect = async () => {
|
|
477
453
|
const item = selectableItems[pluginsState.selectedIndex];
|
|
478
454
|
if (!item) return;
|
|
@@ -573,10 +549,8 @@ export function PluginsScreen() {
|
|
|
573
549
|
await cliUninstallPlugin(plugin.id, scope, state.projectPath);
|
|
574
550
|
} else if (action === "update") {
|
|
575
551
|
await cliUpdatePlugin(plugin.id, scope);
|
|
576
|
-
await saveVersionAfterInstall(plugin.id, latestVersion, scope);
|
|
577
552
|
} else {
|
|
578
553
|
await cliInstallPlugin(plugin.id, scope);
|
|
579
|
-
await saveVersionAfterInstall(plugin.id, latestVersion, scope);
|
|
580
554
|
modal.hideModal();
|
|
581
555
|
await collectPluginEnvVars(plugin.name, plugin.marketplace);
|
|
582
556
|
await installPluginSystemDeps(plugin.name, plugin.marketplace);
|
|
@@ -602,7 +576,6 @@ export function PluginsScreen() {
|
|
|
602
576
|
modal.loading(`Updating ${plugin.name}...`);
|
|
603
577
|
try {
|
|
604
578
|
await cliUpdatePlugin(plugin.id, scope);
|
|
605
|
-
await saveVersionAfterInstall(plugin.id, plugin.version || "0.0.0", scope);
|
|
606
579
|
modal.hideModal();
|
|
607
580
|
fetchData();
|
|
608
581
|
} catch (error) {
|
|
@@ -623,7 +596,6 @@ export function PluginsScreen() {
|
|
|
623
596
|
try {
|
|
624
597
|
for (const plugin of updatable) {
|
|
625
598
|
await cliUpdatePlugin(plugin.id, scope);
|
|
626
|
-
await saveVersionAfterInstall(plugin.id, plugin.version || "0.0.0", scope);
|
|
627
599
|
}
|
|
628
600
|
modal.hideModal();
|
|
629
601
|
fetchData();
|
|
@@ -679,10 +651,8 @@ export function PluginsScreen() {
|
|
|
679
651
|
await cliUninstallPlugin(plugin.id, scope, state.projectPath);
|
|
680
652
|
} else if (action === "update") {
|
|
681
653
|
await cliUpdatePlugin(plugin.id, scope);
|
|
682
|
-
await saveVersionAfterInstall(plugin.id, latestVersion, scope);
|
|
683
654
|
} else {
|
|
684
655
|
await cliInstallPlugin(plugin.id, scope);
|
|
685
|
-
await saveVersionAfterInstall(plugin.id, latestVersion, scope);
|
|
686
656
|
modal.hideModal();
|
|
687
657
|
await collectPluginEnvVars(plugin.name, plugin.marketplace);
|
|
688
658
|
await installPluginSystemDeps(plugin.name, plugin.marketplace);
|