claudeup 4.2.0 → 4.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeup",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -162,6 +162,15 @@ export const SETTINGS_CATALOG = [
162
162
  storage: { type: "env", key: "CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS" },
163
163
  defaultValue: "false",
164
164
  },
165
+ {
166
+ id: "attribution",
167
+ name: "AI Attribution in Commits & PRs",
168
+ description: "Add 'Co-Authored-By: Claude' and '🤖 Generated with Claude Code' to git commits and PR descriptions",
169
+ category: "workflow",
170
+ type: "boolean",
171
+ storage: { type: "attribution" },
172
+ defaultValue: "true",
173
+ },
165
174
  {
166
175
  id: "output-style",
167
176
  name: "Output Style",
@@ -9,7 +9,8 @@ export type SettingCategory =
9
9
  export type SettingType = "boolean" | "string" | "select";
10
10
  export type SettingStorage =
11
11
  | { type: "env"; key: string }
12
- | { type: "setting"; key: string };
12
+ | { type: "setting"; key: string }
13
+ | { type: "attribution" };
13
14
 
14
15
  export interface SettingDefinition {
15
16
  id: string;
@@ -204,6 +205,16 @@ export const SETTINGS_CATALOG: SettingDefinition[] = [
204
205
  storage: { type: "env", key: "CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS" },
205
206
  defaultValue: "false",
206
207
  },
208
+ {
209
+ id: "attribution",
210
+ name: "AI Attribution in Commits & PRs",
211
+ description:
212
+ "Add 'Co-Authored-By: Claude' and '🤖 Generated with Claude Code' to git commits and PR descriptions",
213
+ category: "workflow",
214
+ type: "boolean",
215
+ storage: { type: "attribution" },
216
+ defaultValue: "true",
217
+ },
207
218
  {
208
219
  id: "output-style",
209
220
  name: "Output Style",
@@ -496,6 +496,35 @@ function migrateSettingsObject(settings) {
496
496
  * Runs across project settings, global settings, local settings,
497
497
  * known_marketplaces.json, and installed_plugins.json.
498
498
  */
499
+ /**
500
+ * Decode a ~/.claude/projects/ directory name back to a filesystem path.
501
+ * Claude Code encodes paths by replacing "/" with "-", which is lossy when
502
+ * directory names themselves contain dashes (e.g., "circl-infra" → "circl/infra").
503
+ * Strategy: split on "-", then greedily recombine segments by checking which
504
+ * combinations actually exist on disk.
505
+ * Returns null if no valid path can be resolved.
506
+ */
507
+ async function decodeProjectDirName(encoded) {
508
+ // Split into segments: "-Users-jack-dev-circl-infra" → ["Users","jack","dev","circl","infra"]
509
+ const segments = encoded.replace(/^-/, "").split("-");
510
+ if (segments.length === 0)
511
+ return null;
512
+ // Build path greedily: at each step, try joining the next segment with a dash first
513
+ // (preserving directory names like "circl-infra"), fall back to slash (new path segment)
514
+ let current = "/" + segments[0];
515
+ for (let i = 1; i < segments.length; i++) {
516
+ const withDash = current + "-" + segments[i];
517
+ const withSlash = current + "/" + segments[i];
518
+ // Prefer dash (keeps compound names intact) if that path prefix exists
519
+ if (await fs.pathExists(withDash)) {
520
+ current = withDash;
521
+ }
522
+ else {
523
+ current = withSlash;
524
+ }
525
+ }
526
+ return (await fs.pathExists(current)) ? current : null;
527
+ }
499
528
  export async function migrateMarketplaceRename(projectPath) {
500
529
  const result = {
501
530
  projectMigrated: 0,
@@ -664,9 +693,8 @@ export async function migrateMarketplaceRename(projectPath) {
664
693
  const currentProject = projectPath || process.cwd();
665
694
  seenPaths.add(currentProject);
666
695
  for (const entry of entries) {
667
- // Directory names encode paths: -Users-jack-dev-foo → /Users/jack/dev/foo
668
- const decoded = entry.replace(/^-/, "/").replace(/-/g, "/");
669
- if (seenPaths.has(decoded))
696
+ const decoded = await decodeProjectDirName(entry);
697
+ if (!decoded || seenPaths.has(decoded))
670
698
  continue;
671
699
  seenPaths.add(decoded);
672
700
  const settingsFile = path.join(decoded, ".claude", "settings.json");
@@ -731,6 +731,36 @@ export interface MigrationResult {
731
731
  * Runs across project settings, global settings, local settings,
732
732
  * known_marketplaces.json, and installed_plugins.json.
733
733
  */
734
+ /**
735
+ * Decode a ~/.claude/projects/ directory name back to a filesystem path.
736
+ * Claude Code encodes paths by replacing "/" with "-", which is lossy when
737
+ * directory names themselves contain dashes (e.g., "circl-infra" → "circl/infra").
738
+ * Strategy: split on "-", then greedily recombine segments by checking which
739
+ * combinations actually exist on disk.
740
+ * Returns null if no valid path can be resolved.
741
+ */
742
+ async function decodeProjectDirName(encoded: string): Promise<string | null> {
743
+ // Split into segments: "-Users-jack-dev-circl-infra" → ["Users","jack","dev","circl","infra"]
744
+ const segments = encoded.replace(/^-/, "").split("-");
745
+ if (segments.length === 0) return null;
746
+
747
+ // Build path greedily: at each step, try joining the next segment with a dash first
748
+ // (preserving directory names like "circl-infra"), fall back to slash (new path segment)
749
+ let current = "/" + segments[0];
750
+ for (let i = 1; i < segments.length; i++) {
751
+ const withDash = current + "-" + segments[i];
752
+ const withSlash = current + "/" + segments[i];
753
+ // Prefer dash (keeps compound names intact) if that path prefix exists
754
+ if (await fs.pathExists(withDash)) {
755
+ current = withDash;
756
+ } else {
757
+ current = withSlash;
758
+ }
759
+ }
760
+
761
+ return (await fs.pathExists(current)) ? current : null;
762
+ }
763
+
734
764
  export async function migrateMarketplaceRename(
735
765
  projectPath?: string,
736
766
  ): Promise<MigrationResult> {
@@ -914,9 +944,8 @@ export async function migrateMarketplaceRename(
914
944
  seenPaths.add(currentProject);
915
945
 
916
946
  for (const entry of entries) {
917
- // Directory names encode paths: -Users-jack-dev-foo → /Users/jack/dev/foo
918
- const decoded = entry.replace(/^-/, "/").replace(/-/g, "/");
919
- if (seenPaths.has(decoded)) continue;
947
+ const decoded = await decodeProjectDirName(entry);
948
+ if (!decoded || seenPaths.has(decoded)) continue;
920
949
  seenPaths.add(decoded);
921
950
 
922
951
  const settingsFile = path.join(decoded, ".claude", "settings.json");
@@ -4,7 +4,15 @@ export async function readSettingValue(setting, scope, projectPath) {
4
4
  const settings = scope === "user"
5
5
  ? await readGlobalSettings()
6
6
  : await readSettings(projectPath);
7
- if (setting.storage.type === "env") {
7
+ if (setting.storage.type === "attribution") {
8
+ // Attribution is an object: { commit: "", pr: "" } means disabled
9
+ const attr = settings.attribution;
10
+ if (attr && attr.commit === "" && attr.pr === "") {
11
+ return "false";
12
+ }
13
+ return undefined; // default (enabled)
14
+ }
15
+ else if (setting.storage.type === "env") {
8
16
  const env = settings.env;
9
17
  return env?.[setting.storage.key];
10
18
  }
@@ -23,7 +31,16 @@ export async function writeSettingValue(setting, value, scope, projectPath) {
23
31
  const settings = scope === "user"
24
32
  ? await readGlobalSettings()
25
33
  : await readSettings(projectPath);
26
- if (setting.storage.type === "env") {
34
+ if (setting.storage.type === "attribution") {
35
+ // Boolean toggle: "false" -> write { commit: "", pr: "" }, "true"/undefined -> delete key
36
+ if (value === "false") {
37
+ settings.attribution = { commit: "", pr: "" };
38
+ }
39
+ else {
40
+ delete settings.attribution;
41
+ }
42
+ }
43
+ else if (setting.storage.type === "env") {
27
44
  // Write to the "env" block in settings.json
28
45
  settings.env = settings.env || {};
29
46
  if (value === undefined || value === "" || value === setting.defaultValue) {
@@ -19,7 +19,14 @@ export async function readSettingValue(
19
19
  ? await readGlobalSettings()
20
20
  : await readSettings(projectPath);
21
21
 
22
- if (setting.storage.type === "env") {
22
+ if (setting.storage.type === "attribution") {
23
+ // Attribution is an object: { commit: "", pr: "" } means disabled
24
+ const attr = (settings as any).attribution;
25
+ if (attr && attr.commit === "" && attr.pr === "") {
26
+ return "false";
27
+ }
28
+ return undefined; // default (enabled)
29
+ } else if (setting.storage.type === "env") {
23
30
  const env = (settings as any).env as Record<string, string> | undefined;
24
31
  return env?.[setting.storage.key];
25
32
  } else {
@@ -45,7 +52,14 @@ export async function writeSettingValue(
45
52
  ? await readGlobalSettings()
46
53
  : await readSettings(projectPath);
47
54
 
48
- if (setting.storage.type === "env") {
55
+ if (setting.storage.type === "attribution") {
56
+ // Boolean toggle: "false" -> write { commit: "", pr: "" }, "true"/undefined -> delete key
57
+ if (value === "false") {
58
+ (settings as any).attribution = { commit: "", pr: "" };
59
+ } else {
60
+ delete (settings as any).attribution;
61
+ }
62
+ } else if (setting.storage.type === "env") {
49
63
  // Write to the "env" block in settings.json
50
64
  (settings as any).env = (settings as any).env || {};
51
65
  if (value === undefined || value === "" || value === setting.defaultValue) {
@@ -31,9 +31,11 @@ const settingRenderer = {
31
31
  renderDetail: ({ item }) => {
32
32
  const { setting } = item;
33
33
  const scoped = item.scopedValues;
34
- const storageDesc = setting.storage.type === "env"
35
- ? `env: ${setting.storage.key}`
36
- : `settings.json: ${setting.storage.key}`;
34
+ const storageDesc = setting.storage.type === "attribution"
35
+ ? "settings.json: attribution"
36
+ : setting.storage.type === "env"
37
+ ? `env: ${setting.storage.key}`
38
+ : `settings.json: ${setting.storage.key}`;
37
39
  const userValue = formatValue(setting, scoped.user);
38
40
  const projectValue = formatValue(setting, scoped.project);
39
41
  const userIsSet = scoped.user !== undefined && scoped.user !== "";
@@ -79,9 +79,11 @@ const settingRenderer: ItemRenderer<SettingsSettingItem> = {
79
79
  const { setting } = item;
80
80
  const scoped = item.scopedValues;
81
81
  const storageDesc =
82
- setting.storage.type === "env"
83
- ? `env: ${setting.storage.key}`
84
- : `settings.json: ${setting.storage.key}`;
82
+ setting.storage.type === "attribution"
83
+ ? "settings.json: attribution"
84
+ : setting.storage.type === "env"
85
+ ? `env: ${setting.storage.key}`
86
+ : `settings.json: ${setting.storage.key}`;
85
87
 
86
88
  const userValue = formatValue(setting, scoped.user);
87
89
  const projectValue = formatValue(setting, scoped.project);