claudeup 4.18.0 → 4.22.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/bin/claudeup.js +43 -19
- package/package.json +14 -19
- package/scripts/build-binaries.ts +77 -0
- package/scripts/check-optional-deps.ts +49 -0
- package/scripts/test-isolated.ts +43 -0
- package/src/__tests__/alias-adopt.test.ts +364 -0
- package/src/__tests__/alias-parser.test.ts +92 -0
- package/src/__tests__/alias-shell-writer.test.ts +7 -0
- package/src/__tests__/alias-store.test.ts +77 -0
- package/src/__tests__/cli-router.test.ts +65 -0
- package/src/__tests__/conventions-integration.test.ts +745 -0
- package/src/__tests__/conventions-manager.test.ts +1172 -0
- package/src/__tests__/doctor-bins.test.ts +35 -0
- package/src/__tests__/doctor.test.ts +450 -0
- package/src/__tests__/dual-write-prevention.test.ts +14 -4
- package/src/__tests__/file-locking.test.ts +208 -0
- package/src/__tests__/gitignore-prerun.test.ts +1 -0
- package/src/__tests__/install-command.test.ts +81 -0
- package/src/__tests__/install-plan.test.ts +98 -0
- package/src/__tests__/manifest.test.ts +136 -0
- package/src/__tests__/marketplace-refresh.test.ts +267 -0
- package/src/__tests__/plugin-requires.test.ts +133 -0
- package/src/__tests__/plugin-setup.test.ts +118 -0
- package/src/__tests__/profile-command.test.ts +116 -0
- package/src/__tests__/profile-materializer.test.ts +82 -0
- package/src/__tests__/profile-sync.test.ts +136 -0
- package/src/__tests__/registry-version-resolution.test.ts +74 -0
- package/src/__tests__/resolve-executable.test.ts +42 -0
- package/src/__tests__/resolver.test.ts +218 -0
- package/src/__tests__/symlink-manager.test.ts +99 -0
- package/src/__tests__/toolchain.test.ts +56 -0
- package/src/cli/claude.ts +21 -0
- package/src/cli/doctor.ts +132 -0
- package/src/cli/install.ts +360 -0
- package/src/cli/profile.ts +166 -0
- package/src/cli/router.ts +107 -0
- package/src/cli/update.ts +85 -0
- package/src/data/alias-flags.ts +11 -1
- package/src/data/cli-tools.ts +7 -7
- package/src/data/gitignore-defaults.ts +4 -0
- package/src/data/marketplaces.ts +12 -0
- package/src/data/predefined-profiles.ts +3 -4
- package/src/main.tsx +11 -154
- package/src/prerunner/index.ts +62 -27
- package/src/services/alias-shell-writer.ts +382 -8
- package/src/services/alias-store.ts +60 -0
- package/src/services/claude-cli.ts +19 -22
- package/src/services/claude-settings.ts +99 -26
- package/src/services/conventions-manager.ts +865 -0
- package/src/services/doctor-bins.ts +49 -0
- package/src/services/doctor.ts +509 -0
- package/src/services/install-plan.ts +107 -0
- package/src/services/manifest.ts +166 -0
- package/src/services/marketplace-refresh.ts +173 -0
- package/src/services/plugin-manager.ts +7 -1
- package/src/services/plugin-requires.ts +215 -0
- package/src/services/plugin-setup.ts +61 -4
- package/src/services/plugin-version-check.ts +9 -4
- package/src/services/profile-materializer.ts +61 -0
- package/src/services/profile-sync.ts +150 -0
- package/src/services/resolver.ts +293 -0
- package/src/services/symlink-manager.ts +146 -0
- package/src/services/toolchain.ts +97 -0
- package/src/services/version-check.ts +10 -11
- package/src/types/index.ts +138 -31
- package/src/ui/App.tsx +16 -11
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/modals/ConfirmModal.tsx +1 -1
- package/src/ui/screens/AliasScreen.tsx +491 -359
- package/src/ui/screens/CliToolsScreen.tsx +11 -3
- package/src/ui/screens/PluginsScreen.tsx +3 -1
- package/src/ui/screens/index.ts +0 -1
- package/src/ui/state/reducer.ts +6 -102
- package/src/ui/state/types.ts +8 -35
- package/src/utils/command-utils.ts +23 -0
- package/src/utils/file-locking.ts +144 -0
- package/src/data/alias-flags.js +0 -196
- package/src/data/cli-tools.js +0 -123
- package/src/data/gitignore-defaults.js +0 -24
- package/src/data/gitignore-reasons.js +0 -97
- package/src/data/gitignore-templates.js +0 -21
- package/src/data/marketplaces.js +0 -138
- package/src/data/mcp-servers.js +0 -509
- package/src/data/predefined-profiles.js +0 -248
- package/src/data/settings-catalog.js +0 -625
- package/src/data/skill-repos.js +0 -160
- package/src/data/statuslines.js +0 -159
- package/src/data/statuslines.ts +0 -188
- package/src/index.js +0 -4
- package/src/index.ts +0 -5
- package/src/main.js +0 -170
- package/src/prerunner/index.js +0 -252
- package/src/services/alias-settings.js +0 -51
- package/src/services/alias-shell-writer.js +0 -764
- package/src/services/alias-store.js +0 -77
- package/src/services/claude-cli.js +0 -189
- package/src/services/claude-runner.js +0 -28
- package/src/services/claude-settings.js +0 -1223
- package/src/services/gitignore-detector.js +0 -155
- package/src/services/gitignore-fixer.js +0 -231
- package/src/services/gitignore-prerun.js +0 -46
- package/src/services/gitignore-resolver.js +0 -143
- package/src/services/gitignore-service.js +0 -117
- package/src/services/local-marketplace.js +0 -339
- package/src/services/marketplace-fetcher.js +0 -96
- package/src/services/marketplace-sync.js +0 -94
- package/src/services/mcp-registry.js +0 -87
- package/src/services/plugin-manager.js +0 -473
- package/src/services/plugin-mcp-config.js +0 -177
- package/src/services/plugin-setup.js +0 -444
- package/src/services/plugin-version-check.js +0 -262
- package/src/services/profiles.js +0 -161
- package/src/services/settings-manager.js +0 -243
- package/src/services/skills-manager.js +0 -393
- package/src/services/skillsmp-client.js +0 -87
- package/src/services/update-cache.js +0 -52
- package/src/services/version-check.js +0 -99
- package/src/types/gitignore.js +0 -6
- package/src/types/index.js +0 -1
- package/src/ui/App.js +0 -346
- package/src/ui/adapters/pluginsAdapter.js +0 -139
- package/src/ui/adapters/settingsAdapter.js +0 -111
- package/src/ui/adapters/skillsAdapter.js +0 -154
- package/src/ui/components/CategoryHeader.js +0 -9
- package/src/ui/components/EmptyFilterState.js +0 -4
- package/src/ui/components/ScopeIndicator.js +0 -30
- package/src/ui/components/ScrollableList.js +0 -36
- package/src/ui/components/SearchInput.js +0 -19
- package/src/ui/components/StyledText.js +0 -39
- package/src/ui/components/TabBar.js +0 -19
- package/src/ui/components/layout/FooterHints.js +0 -29
- package/src/ui/components/layout/Panel.js +0 -6
- package/src/ui/components/layout/ProgressBar.js +0 -14
- package/src/ui/components/layout/ScopeTabs.js +0 -6
- package/src/ui/components/layout/ScreenLayout.js +0 -16
- package/src/ui/components/layout/index.js +0 -5
- package/src/ui/components/modals/ConfirmModal.js +0 -14
- package/src/ui/components/modals/InputModal.js +0 -5
- package/src/ui/components/modals/LoadingModal.js +0 -17
- package/src/ui/components/modals/MessageModal.js +0 -16
- package/src/ui/components/modals/ModalContainer.js +0 -137
- package/src/ui/components/modals/SelectModal.js +0 -18
- package/src/ui/components/modals/VersionMismatchModal.js +0 -36
- package/src/ui/components/modals/index.js +0 -6
- package/src/ui/components/primitives/ActionHints.js +0 -13
- package/src/ui/components/primitives/DetailSection.js +0 -7
- package/src/ui/components/primitives/KeyValueLine.js +0 -8
- package/src/ui/components/primitives/ListCategoryRow.js +0 -8
- package/src/ui/components/primitives/MetaText.js +0 -8
- package/src/ui/components/primitives/ScopeDetail.js +0 -32
- package/src/ui/components/primitives/ScopeSquares.js +0 -11
- package/src/ui/components/primitives/SelectableRow.js +0 -5
- package/src/ui/components/primitives/index.js +0 -8
- package/src/ui/hooks/index.js +0 -4
- package/src/ui/hooks/useAsyncData.js +0 -77
- package/src/ui/hooks/useGitignoreModal.js +0 -74
- package/src/ui/hooks/useKeyboard.js +0 -13
- package/src/ui/hooks/useKeyboardHandler.js +0 -39
- package/src/ui/hooks/useMismatchModal.js +0 -77
- package/src/ui/registry.js +0 -1
- package/src/ui/renderers/cliToolRenderers.js +0 -54
- package/src/ui/renderers/gitignoreRenderers.js +0 -46
- package/src/ui/renderers/mcpRenderers.js +0 -26
- package/src/ui/renderers/pluginRenderers.js +0 -124
- package/src/ui/renderers/profileRenderers.js +0 -177
- package/src/ui/renderers/settingsRenderers.js +0 -73
- package/src/ui/renderers/skillRenderers.js +0 -138
- package/src/ui/screens/AliasScreen.js +0 -1008
- package/src/ui/screens/CliToolsScreen.js +0 -338
- package/src/ui/screens/EnvVarsScreen.js +0 -152
- package/src/ui/screens/GitignoreScreen.js +0 -328
- package/src/ui/screens/McpRegistryScreen.js +0 -238
- package/src/ui/screens/McpScreen.js +0 -176
- package/src/ui/screens/ModelSelectorScreen.js +0 -296
- package/src/ui/screens/ModelSelectorScreen.tsx +0 -444
- package/src/ui/screens/PluginsScreen.js +0 -766
- package/src/ui/screens/ProfilesScreen.js +0 -298
- package/src/ui/screens/SkillsScreen.js +0 -549
- package/src/ui/screens/StatusLineScreen.js +0 -206
- package/src/ui/screens/StatusLineScreen.tsx +0 -416
- package/src/ui/screens/index.js +0 -10
- package/src/ui/state/AnimationContext.js +0 -34
- package/src/ui/state/AppContext.js +0 -162
- package/src/ui/state/DimensionsContext.js +0 -71
- package/src/ui/state/reducer.js +0 -543
- package/src/ui/state/types.js +0 -1
- package/src/ui/theme.js +0 -47
- package/src/utils/clipboard.js +0 -56
- package/src/utils/command-utils.js +0 -19
- package/src/utils/fuzzy-search.js +0 -101
- package/src/utils/string-utils.js +0 -62
|
@@ -171,9 +171,17 @@ async function getLatestPipVersion(packageName: string): Promise<string | undefi
|
|
|
171
171
|
async function getLatestVersion(
|
|
172
172
|
tool: import("../../data/cli-tools.js").CliTool,
|
|
173
173
|
): Promise<string | undefined> {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
:
|
|
174
|
+
switch (tool.packageManager) {
|
|
175
|
+
// bun packages are published to the npm registry, so `npm view` resolves them.
|
|
176
|
+
case "bun":
|
|
177
|
+
case "npm":
|
|
178
|
+
return getLatestNpmVersion(tool.packageName);
|
|
179
|
+
case "pip":
|
|
180
|
+
return getLatestPipVersion(tool.packageName);
|
|
181
|
+
// brew/go have no uniform remote version query here — skip the check.
|
|
182
|
+
default:
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
177
185
|
}
|
|
178
186
|
|
|
179
187
|
function compareVersions(v1: string, v2: string): number {
|
|
@@ -486,7 +486,8 @@ export function PluginsScreen() {
|
|
|
486
486
|
(missing.pip?.length || 0) +
|
|
487
487
|
(missing.brew?.length || 0) +
|
|
488
488
|
(missing.npm?.length || 0) +
|
|
489
|
-
(missing.cargo?.length || 0)
|
|
489
|
+
(missing.cargo?.length || 0) +
|
|
490
|
+
(missing.go?.length || 0) >
|
|
490
491
|
0;
|
|
491
492
|
|
|
492
493
|
if (!hasMissing) return;
|
|
@@ -497,6 +498,7 @@ export function PluginsScreen() {
|
|
|
497
498
|
if (missing.npm?.length) parts.push(`npm: ${missing.npm.join(", ")}`);
|
|
498
499
|
if (missing.cargo?.length)
|
|
499
500
|
parts.push(`cargo: ${missing.cargo.join(", ")}`);
|
|
501
|
+
if (missing.go?.length) parts.push(`go: ${missing.go.join(", ")}`);
|
|
500
502
|
|
|
501
503
|
const wantInstall = await modal.confirm(
|
|
502
504
|
"Install Dependencies?",
|
package/src/ui/screens/index.ts
CHANGED
|
@@ -3,7 +3,6 @@ export { McpScreen } from "./McpScreen.js";
|
|
|
3
3
|
export { McpRegistryScreen } from "./McpRegistryScreen.js";
|
|
4
4
|
export { SettingsScreen } from "./EnvVarsScreen.js";
|
|
5
5
|
export { CliToolsScreen } from "./CliToolsScreen.js";
|
|
6
|
-
export { ModelSelectorScreen } from "./ModelSelectorScreen.js";
|
|
7
6
|
export { ProfilesScreen } from "./ProfilesScreen.js";
|
|
8
7
|
export { SkillsScreen } from "./SkillsScreen.js";
|
|
9
8
|
export { GitignoreScreen } from "./GitignoreScreen.js";
|
package/src/ui/state/reducer.ts
CHANGED
|
@@ -11,6 +11,7 @@ export const initialState: AppState = {
|
|
|
11
11
|
modal: null,
|
|
12
12
|
progress: null,
|
|
13
13
|
isSearching: false,
|
|
14
|
+
detailActive: false,
|
|
14
15
|
|
|
15
16
|
// Refresh counter for triggering data refetch
|
|
16
17
|
dataRefreshVersion: 0,
|
|
@@ -37,13 +38,6 @@ export const initialState: AppState = {
|
|
|
37
38
|
servers: { status: "idle" },
|
|
38
39
|
},
|
|
39
40
|
|
|
40
|
-
statusline: {
|
|
41
|
-
scope: "project",
|
|
42
|
-
selectedIndex: 0,
|
|
43
|
-
presets: { status: "idle" },
|
|
44
|
-
currentPreset: null,
|
|
45
|
-
},
|
|
46
|
-
|
|
47
41
|
settings: {
|
|
48
42
|
selectedIndex: 0,
|
|
49
43
|
values: { status: "idle" },
|
|
@@ -54,12 +48,6 @@ export const initialState: AppState = {
|
|
|
54
48
|
tools: { status: "idle" },
|
|
55
49
|
},
|
|
56
50
|
|
|
57
|
-
modelSelector: {
|
|
58
|
-
selectedIndex: 0,
|
|
59
|
-
searchQuery: "",
|
|
60
|
-
taskSize: "large",
|
|
61
|
-
},
|
|
62
|
-
|
|
63
51
|
profiles: {
|
|
64
52
|
selectedIndex: 0,
|
|
65
53
|
profiles: { status: "idle" },
|
|
@@ -83,8 +71,9 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
83
71
|
return {
|
|
84
72
|
...state,
|
|
85
73
|
currentRoute: action.route,
|
|
86
|
-
// Clear
|
|
74
|
+
// Clear transient sub-view state when navigating away
|
|
87
75
|
isSearching: false,
|
|
76
|
+
detailActive: false,
|
|
88
77
|
};
|
|
89
78
|
|
|
90
79
|
// =========================================================================
|
|
@@ -264,63 +253,6 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
264
253
|
},
|
|
265
254
|
};
|
|
266
255
|
|
|
267
|
-
// =========================================================================
|
|
268
|
-
// Status Line Screen
|
|
269
|
-
// =========================================================================
|
|
270
|
-
case "STATUSLINE_SET_SCOPE":
|
|
271
|
-
return {
|
|
272
|
-
...state,
|
|
273
|
-
statusline: {
|
|
274
|
-
...state.statusline,
|
|
275
|
-
scope: action.scope,
|
|
276
|
-
selectedIndex: 0,
|
|
277
|
-
presets: { status: "loading" },
|
|
278
|
-
},
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
case "STATUSLINE_TOGGLE_SCOPE":
|
|
282
|
-
return appReducer(state, {
|
|
283
|
-
type: "STATUSLINE_SET_SCOPE",
|
|
284
|
-
scope: state.statusline.scope === "project" ? "global" : "project",
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
case "STATUSLINE_SELECT":
|
|
288
|
-
return {
|
|
289
|
-
...state,
|
|
290
|
-
statusline: {
|
|
291
|
-
...state.statusline,
|
|
292
|
-
selectedIndex: action.index,
|
|
293
|
-
},
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
case "STATUSLINE_DATA_LOADING":
|
|
297
|
-
return {
|
|
298
|
-
...state,
|
|
299
|
-
statusline: {
|
|
300
|
-
...state.statusline,
|
|
301
|
-
presets: { status: "loading" },
|
|
302
|
-
},
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
case "STATUSLINE_DATA_SUCCESS":
|
|
306
|
-
return {
|
|
307
|
-
...state,
|
|
308
|
-
statusline: {
|
|
309
|
-
...state.statusline,
|
|
310
|
-
presets: { status: "success", data: action.presets },
|
|
311
|
-
currentPreset: action.currentPreset,
|
|
312
|
-
},
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
case "STATUSLINE_DATA_ERROR":
|
|
316
|
-
return {
|
|
317
|
-
...state,
|
|
318
|
-
statusline: {
|
|
319
|
-
...state.statusline,
|
|
320
|
-
presets: { status: "error", error: action.error },
|
|
321
|
-
},
|
|
322
|
-
};
|
|
323
|
-
|
|
324
256
|
// =========================================================================
|
|
325
257
|
// Settings Screen
|
|
326
258
|
// =========================================================================
|
|
@@ -415,37 +347,6 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
415
347
|
};
|
|
416
348
|
}
|
|
417
349
|
|
|
418
|
-
// =========================================================================
|
|
419
|
-
// Model Selector Screen
|
|
420
|
-
// =========================================================================
|
|
421
|
-
case "MODEL_SELECTOR_SELECT":
|
|
422
|
-
return {
|
|
423
|
-
...state,
|
|
424
|
-
modelSelector: {
|
|
425
|
-
...state.modelSelector,
|
|
426
|
-
selectedIndex: action.index,
|
|
427
|
-
},
|
|
428
|
-
};
|
|
429
|
-
|
|
430
|
-
case "MODEL_SELECTOR_SET_SEARCH":
|
|
431
|
-
return {
|
|
432
|
-
...state,
|
|
433
|
-
modelSelector: {
|
|
434
|
-
...state.modelSelector,
|
|
435
|
-
searchQuery: action.query,
|
|
436
|
-
selectedIndex: 0, // Reset selection when search changes
|
|
437
|
-
},
|
|
438
|
-
};
|
|
439
|
-
|
|
440
|
-
case "MODEL_SELECTOR_SET_TASK_SIZE":
|
|
441
|
-
return {
|
|
442
|
-
...state,
|
|
443
|
-
modelSelector: {
|
|
444
|
-
...state.modelSelector,
|
|
445
|
-
taskSize: action.size,
|
|
446
|
-
},
|
|
447
|
-
};
|
|
448
|
-
|
|
449
350
|
// =========================================================================
|
|
450
351
|
// Profiles Screen
|
|
451
352
|
// =========================================================================
|
|
@@ -603,6 +504,9 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
603
504
|
case "SET_SEARCHING":
|
|
604
505
|
return { ...state, isSearching: action.isSearching };
|
|
605
506
|
|
|
507
|
+
case "SET_DETAIL_ACTIVE":
|
|
508
|
+
return { ...state, detailActive: action.detailActive };
|
|
509
|
+
|
|
606
510
|
// =========================================================================
|
|
607
511
|
// Data Refresh
|
|
608
512
|
// =========================================================================
|
package/src/ui/state/types.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
Marketplace,
|
|
3
3
|
McpServer,
|
|
4
|
-
StatusLineConfig,
|
|
5
4
|
ProfileEntry,
|
|
6
5
|
SkillInfo,
|
|
7
6
|
} from "../../types/index.js";
|
|
@@ -18,7 +17,6 @@ export type Screen =
|
|
|
18
17
|
| "mcp-registry"
|
|
19
18
|
| "settings"
|
|
20
19
|
| "cli-tools"
|
|
21
|
-
| "model-selector"
|
|
22
20
|
| "profiles"
|
|
23
21
|
| "skills"
|
|
24
22
|
| "gitignore"
|
|
@@ -30,7 +28,6 @@ export type Route =
|
|
|
30
28
|
| { screen: "mcp-registry"; query?: string }
|
|
31
29
|
| { screen: "settings" }
|
|
32
30
|
| { screen: "cli-tools" }
|
|
33
|
-
| { screen: "model-selector" }
|
|
34
31
|
| { screen: "profiles" }
|
|
35
32
|
| { screen: "skills" }
|
|
36
33
|
| { screen: "gitignore" }
|
|
@@ -134,13 +131,6 @@ export interface McpRegistryScreenState {
|
|
|
134
131
|
servers: AsyncData<McpServer[]>;
|
|
135
132
|
}
|
|
136
133
|
|
|
137
|
-
export interface StatusLineScreenState {
|
|
138
|
-
scope: "project" | "global";
|
|
139
|
-
selectedIndex: number;
|
|
140
|
-
presets: AsyncData<StatusLineConfig[]>;
|
|
141
|
-
currentPreset: string | null;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
134
|
export interface ScopedSettingValues {
|
|
145
135
|
user: string | undefined;
|
|
146
136
|
project: string | undefined;
|
|
@@ -171,12 +161,6 @@ export interface CliToolsScreenState {
|
|
|
171
161
|
tools: AsyncData<CliTool[]>;
|
|
172
162
|
}
|
|
173
163
|
|
|
174
|
-
export interface ModelSelectorScreenState {
|
|
175
|
-
selectedIndex: number;
|
|
176
|
-
searchQuery: string;
|
|
177
|
-
taskSize: "large" | "small";
|
|
178
|
-
}
|
|
179
|
-
|
|
180
164
|
export interface ProfilesScreenState {
|
|
181
165
|
selectedIndex: number;
|
|
182
166
|
profiles: AsyncData<ProfileEntry[]>;
|
|
@@ -207,6 +191,11 @@ export interface AppState {
|
|
|
207
191
|
modal: ModalState | null;
|
|
208
192
|
progress: ProgressState | null;
|
|
209
193
|
isSearching: boolean;
|
|
194
|
+
// True while a screen has a focused sub-view open that owns Escape (e.g.
|
|
195
|
+
// the Alias tab's flag-detail editor). The global key handler skips its
|
|
196
|
+
// Escape→navigate branch so Escape backs out of the sub-view instead of
|
|
197
|
+
// jumping to the home tab. Mirrors how `modal`/`isSearching` gate keys.
|
|
198
|
+
detailActive: boolean;
|
|
210
199
|
|
|
211
200
|
// Refresh counter - incremented when marketplace sync completes
|
|
212
201
|
// Screens can watch this to know when to refetch data
|
|
@@ -216,10 +205,8 @@ export interface AppState {
|
|
|
216
205
|
plugins: PluginsScreenState;
|
|
217
206
|
mcp: McpScreenState;
|
|
218
207
|
mcpRegistry: McpRegistryScreenState;
|
|
219
|
-
statusline: StatusLineScreenState;
|
|
220
208
|
settings: SettingsScreenState;
|
|
221
209
|
cliTools: CliToolsScreenState;
|
|
222
|
-
modelSelector: ModelSelectorScreenState;
|
|
223
210
|
profiles: ProfilesScreenState;
|
|
224
211
|
skills: SkillsScreenState;
|
|
225
212
|
}
|
|
@@ -265,18 +252,6 @@ export type AppAction =
|
|
|
265
252
|
| { type: "MCP_REGISTRY_DATA_SUCCESS"; servers: McpServer[] }
|
|
266
253
|
| { type: "MCP_REGISTRY_DATA_ERROR"; error: Error }
|
|
267
254
|
|
|
268
|
-
// Status line screen
|
|
269
|
-
| { type: "STATUSLINE_SET_SCOPE"; scope: "project" | "global" }
|
|
270
|
-
| { type: "STATUSLINE_TOGGLE_SCOPE" }
|
|
271
|
-
| { type: "STATUSLINE_SELECT"; index: number }
|
|
272
|
-
| { type: "STATUSLINE_DATA_LOADING" }
|
|
273
|
-
| {
|
|
274
|
-
type: "STATUSLINE_DATA_SUCCESS";
|
|
275
|
-
presets: StatusLineConfig[];
|
|
276
|
-
currentPreset: string | null;
|
|
277
|
-
}
|
|
278
|
-
| { type: "STATUSLINE_DATA_ERROR"; error: Error }
|
|
279
|
-
|
|
280
255
|
// Settings screen
|
|
281
256
|
| { type: "SETTINGS_SELECT"; index: number }
|
|
282
257
|
| { type: "SETTINGS_DATA_LOADING" }
|
|
@@ -290,11 +265,6 @@ export type AppAction =
|
|
|
290
265
|
| { type: "CLITOOLS_DATA_ERROR"; error: Error }
|
|
291
266
|
| { type: "CLITOOLS_UPDATE_TOOL"; name: string; updates: Partial<CliTool> }
|
|
292
267
|
|
|
293
|
-
// Model Selector Screen
|
|
294
|
-
| { type: "MODEL_SELECTOR_SELECT"; index: number }
|
|
295
|
-
| { type: "MODEL_SELECTOR_SET_SEARCH"; query: string }
|
|
296
|
-
| { type: "MODEL_SELECTOR_SET_TASK_SIZE"; size: "large" | "small" }
|
|
297
|
-
|
|
298
268
|
// Modals
|
|
299
269
|
| { type: "SHOW_MODAL"; modal: ModalState }
|
|
300
270
|
| { type: "HIDE_MODAL" }
|
|
@@ -307,6 +277,9 @@ export type AppAction =
|
|
|
307
277
|
// Search state (blocks global key handlers)
|
|
308
278
|
| { type: "SET_SEARCHING"; isSearching: boolean }
|
|
309
279
|
|
|
280
|
+
// Detail sub-view state (gates global Escape→navigate)
|
|
281
|
+
| { type: "SET_DETAIL_ACTIVE"; detailActive: boolean }
|
|
282
|
+
|
|
310
283
|
// Profiles screen
|
|
311
284
|
| { type: "PROFILES_SELECT"; index: number }
|
|
312
285
|
| { type: "PROFILES_DATA_LOADING" }
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
+
import { access, realpath } from "node:fs/promises";
|
|
4
|
+
import { constants } from "node:fs";
|
|
3
5
|
|
|
4
6
|
const execFileAsync = promisify(execFile);
|
|
5
7
|
|
|
@@ -18,3 +20,24 @@ export async function which(command: string): Promise<string | null> {
|
|
|
18
20
|
return null;
|
|
19
21
|
}
|
|
20
22
|
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Like {@link which}, but verifies the command actually RESOLVES and is
|
|
26
|
+
* executable — a dangling symlink (e.g. a stale npx-cache link) returns null.
|
|
27
|
+
* `which` alone reports the symlink's own path even when its target is gone,
|
|
28
|
+
* which is exactly how a plugin's binary can look "present" yet fail at runtime.
|
|
29
|
+
* @returns The real (symlink-resolved) executable path, or null.
|
|
30
|
+
*/
|
|
31
|
+
export async function resolveExecutable(
|
|
32
|
+
command: string,
|
|
33
|
+
): Promise<string | null> {
|
|
34
|
+
const p = await which(command);
|
|
35
|
+
if (!p) return null;
|
|
36
|
+
try {
|
|
37
|
+
const real = await realpath(p); // follows symlinks; throws if dangling
|
|
38
|
+
await access(real, constants.X_OK); // must be executable
|
|
39
|
+
return real;
|
|
40
|
+
} catch {
|
|
41
|
+
return null; // dangling link or not executable
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File locking utilities for concurrent write protection
|
|
3
|
+
*
|
|
4
|
+
* Prevents race conditions when multiple processes modify settings files
|
|
5
|
+
* Uses lock files for cross-platform compatibility
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { open, mkdir } from 'node:fs/promises';
|
|
9
|
+
import { dirname } from 'node:path';
|
|
10
|
+
import { existsSync } from 'node:fs';
|
|
11
|
+
import type { FileHandle } from 'node:fs/promises';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Maximum time to wait for a lock (milliseconds)
|
|
15
|
+
*/
|
|
16
|
+
const LOCK_TIMEOUT_MS = 10000;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Lock retry interval (milliseconds)
|
|
20
|
+
*/
|
|
21
|
+
const LOCK_RETRY_INTERVAL_MS = 100;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Acquire exclusive lock on file and execute operation
|
|
25
|
+
* Uses lock files for cross-platform compatibility
|
|
26
|
+
*
|
|
27
|
+
* @param filePath - Path to file to lock
|
|
28
|
+
* @param operation - Async operation to execute with lock held
|
|
29
|
+
* @returns Result of the operation
|
|
30
|
+
* @throws Error if lock cannot be acquired within timeout
|
|
31
|
+
*/
|
|
32
|
+
export async function withFileLock<T>(
|
|
33
|
+
filePath: string,
|
|
34
|
+
operation: () => Promise<T>,
|
|
35
|
+
): Promise<T> {
|
|
36
|
+
const lockPath = `${filePath}.lock`;
|
|
37
|
+
const lockDir = dirname(lockPath);
|
|
38
|
+
|
|
39
|
+
// Ensure lock directory exists
|
|
40
|
+
if (!existsSync(lockDir)) {
|
|
41
|
+
await mkdir(lockDir, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const startTime = Date.now();
|
|
45
|
+
let fileHandle: FileHandle | null = null;
|
|
46
|
+
|
|
47
|
+
// Try to acquire lock with retries
|
|
48
|
+
while (Date.now() - startTime < LOCK_TIMEOUT_MS) {
|
|
49
|
+
try {
|
|
50
|
+
// Try to create lock file exclusively (fails if exists)
|
|
51
|
+
fileHandle = await open(lockPath, 'wx');
|
|
52
|
+
|
|
53
|
+
// Write PID to lock file for debugging
|
|
54
|
+
await fileHandle.write(`${process.pid}\n${new Date().toISOString()}\n`);
|
|
55
|
+
|
|
56
|
+
break;
|
|
57
|
+
} catch (error: unknown) {
|
|
58
|
+
// Lock file exists, retry after interval
|
|
59
|
+
if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
|
|
60
|
+
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_INTERVAL_MS));
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (fileHandle === null) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Failed to acquire lock on ${filePath} after ${LOCK_TIMEOUT_MS}ms. ` +
|
|
70
|
+
`Another process may be holding the lock. Lock file: ${lockPath}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
// Execute operation with lock held
|
|
76
|
+
return await operation();
|
|
77
|
+
} finally {
|
|
78
|
+
// Release lock
|
|
79
|
+
try {
|
|
80
|
+
await fileHandle.close();
|
|
81
|
+
// Delete lock file
|
|
82
|
+
const fs = await import('node:fs/promises');
|
|
83
|
+
await fs.unlink(lockPath);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
// Log error but don't throw - operation succeeded
|
|
86
|
+
console.error(`Warning: Failed to release lock ${lockPath}:`, error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check if file is currently locked
|
|
93
|
+
*
|
|
94
|
+
* @param filePath - Path to file to check
|
|
95
|
+
* @returns True if lock file exists
|
|
96
|
+
*/
|
|
97
|
+
export function isFileLocked(filePath: string): boolean {
|
|
98
|
+
const lockPath = `${filePath}.lock`;
|
|
99
|
+
return existsSync(lockPath);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Force remove lock file (use with caution)
|
|
104
|
+
* Only use if you're certain the lock is stale
|
|
105
|
+
*
|
|
106
|
+
* @param filePath - Path to file whose lock to remove
|
|
107
|
+
*/
|
|
108
|
+
export async function forceRemoveLock(filePath: string): Promise<void> {
|
|
109
|
+
const lockPath = `${filePath}.lock`;
|
|
110
|
+
try {
|
|
111
|
+
const fs = await import('node:fs/promises');
|
|
112
|
+
await fs.unlink(lockPath);
|
|
113
|
+
} catch (error: unknown) {
|
|
114
|
+
// Ignore ENOENT (lock doesn't exist)
|
|
115
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get information about current lock
|
|
123
|
+
*
|
|
124
|
+
* @param filePath - Path to file to check
|
|
125
|
+
* @returns Lock info or null if not locked
|
|
126
|
+
*/
|
|
127
|
+
export async function getLockInfo(
|
|
128
|
+
filePath: string,
|
|
129
|
+
): Promise<{ pid: string; timestamp: string } | null> {
|
|
130
|
+
const lockPath = `${filePath}.lock`;
|
|
131
|
+
|
|
132
|
+
if (!existsSync(lockPath)) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const fs = await import('node:fs/promises');
|
|
138
|
+
const content = await fs.readFile(lockPath, 'utf-8');
|
|
139
|
+
const [pid, timestamp] = content.trim().split('\n');
|
|
140
|
+
return { pid, timestamp };
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
package/src/data/alias-flags.js
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Catalog of `claude` CLI flags that the Alias tab can compose into a managed
|
|
3
|
-
* shell alias. Verified against `claude --help` output and binary string scan
|
|
4
|
-
* of the shipped Claude Code 2.1.x binary.
|
|
5
|
-
*
|
|
6
|
-
* The catalog drives both the UI editor for each flag and the renderer that
|
|
7
|
-
* emits the final `alias claude='claude …'` string per shell.
|
|
8
|
-
*/
|
|
9
|
-
/**
|
|
10
|
-
* Tokens supported in `templated: true` flag values. Description text is
|
|
11
|
-
* shown to the user; the actual shell-substitution code lives in the writer
|
|
12
|
-
* because it needs per-shell variants.
|
|
13
|
-
*
|
|
14
|
-
* Intentionally excludes `{hash}` for v1 — its only portable form is
|
|
15
|
-
* `od -An -N4 -tx1 /dev/urandom | tr -d ' \n'`, which looks frightening in a
|
|
16
|
-
* shell rc file. Users who want randomness can pair this prefix with bare
|
|
17
|
-
* `--remote-control` (which already auto-generates a unique suffix).
|
|
18
|
-
*/
|
|
19
|
-
export const TEMPLATE_TOKENS = [
|
|
20
|
-
{ token: "{folder}", description: "current directory's basename" },
|
|
21
|
-
{ token: "{day}", description: "day of month, zero-padded (01-31)" },
|
|
22
|
-
{ token: "{month}", description: "month, zero-padded (01-12)" },
|
|
23
|
-
{ token: "{year}", description: "4-digit year" },
|
|
24
|
-
];
|
|
25
|
-
export const FLAG_GROUPS = [
|
|
26
|
-
{ id: "danger", label: "Permissions" },
|
|
27
|
-
{ id: "session", label: "Session" },
|
|
28
|
-
{ id: "context", label: "Context" },
|
|
29
|
-
{ id: "channels", label: "Development channels" },
|
|
30
|
-
{ id: "debug", label: "Debug" },
|
|
31
|
-
{ id: "integration", label: "Integrations" },
|
|
32
|
-
{ id: "worktree", label: "Worktree" },
|
|
33
|
-
];
|
|
34
|
-
export const ALIAS_FLAGS = [
|
|
35
|
-
{
|
|
36
|
-
id: "dangerously-skip-permissions",
|
|
37
|
-
label: "Skip permission checks",
|
|
38
|
-
flag: "--dangerously-skip-permissions",
|
|
39
|
-
kind: "boolean",
|
|
40
|
-
group: "danger",
|
|
41
|
-
description: "Bypass all permission checks. Recommended only for sandboxes with no internet access.",
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
id: "allow-dangerously-skip-permissions",
|
|
45
|
-
label: "Allow skip-permissions opt-in",
|
|
46
|
-
flag: "--allow-dangerously-skip-permissions",
|
|
47
|
-
kind: "boolean",
|
|
48
|
-
group: "danger",
|
|
49
|
-
description: "Enable the skip-permissions option without making it the default.",
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
id: "remote-control",
|
|
53
|
-
label: "Remote control",
|
|
54
|
-
flag: "--remote-control",
|
|
55
|
-
kind: "boolean",
|
|
56
|
-
group: "session",
|
|
57
|
-
description: "Start an interactive session with Remote Control enabled. Claude Code auto-generates a unique session name; use --remote-control-session-name-prefix to customize it.",
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
id: "remote-control-session-name-prefix",
|
|
61
|
-
label: "Remote control name prefix",
|
|
62
|
-
flag: "--remote-control-session-name-prefix",
|
|
63
|
-
kind: "text",
|
|
64
|
-
templated: true,
|
|
65
|
-
group: "session",
|
|
66
|
-
description: "Prefix for auto-generated Remote Control session names. Default: hostname. Supports template tokens that expand on each shell invocation: {folder} (basename of $PWD), {day}, {month}, {year}. Example: dev-{folder}-{day} → dev-myproject-15.",
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
id: "ide",
|
|
70
|
-
label: "Auto-connect to IDE",
|
|
71
|
-
flag: "--ide",
|
|
72
|
-
kind: "boolean",
|
|
73
|
-
group: "integration",
|
|
74
|
-
description: "Automatically connect to IDE on startup if exactly one valid IDE is available.",
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
id: "chrome",
|
|
78
|
-
label: "Chrome integration",
|
|
79
|
-
flag: "--chrome",
|
|
80
|
-
triStateOff: "--no-chrome",
|
|
81
|
-
kind: "tri-state",
|
|
82
|
-
group: "integration",
|
|
83
|
-
description: "Enable or disable Claude in Chrome integration. Unset leaves the project default in place.",
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
id: "effort",
|
|
87
|
-
label: "Effort level",
|
|
88
|
-
flag: "--effort",
|
|
89
|
-
kind: "select",
|
|
90
|
-
group: "session",
|
|
91
|
-
description: "Effort level for the current session.",
|
|
92
|
-
options: [
|
|
93
|
-
{ label: "low", value: "low" },
|
|
94
|
-
{ label: "medium", value: "medium" },
|
|
95
|
-
{ label: "high", value: "high" },
|
|
96
|
-
{ label: "xhigh", value: "xhigh" },
|
|
97
|
-
{ label: "max", value: "max" },
|
|
98
|
-
],
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
id: "system-prompt",
|
|
102
|
-
label: "System prompt",
|
|
103
|
-
flag: "--system-prompt",
|
|
104
|
-
kind: "text",
|
|
105
|
-
group: "context",
|
|
106
|
-
xorGroup: "system-prompt",
|
|
107
|
-
description: "System prompt text to use for the session. Mutually exclusive with --system-prompt-file.",
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
id: "system-prompt-file",
|
|
111
|
-
label: "System prompt file",
|
|
112
|
-
flag: "--system-prompt-file",
|
|
113
|
-
kind: "text",
|
|
114
|
-
group: "context",
|
|
115
|
-
xorGroup: "system-prompt",
|
|
116
|
-
description: "Path to a file whose contents replace the default system prompt. Mutually exclusive with --system-prompt.",
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
id: "append-system-prompt",
|
|
120
|
-
label: "Append system prompt",
|
|
121
|
-
flag: "--append-system-prompt",
|
|
122
|
-
kind: "text",
|
|
123
|
-
group: "context",
|
|
124
|
-
description: "Text appended to the default system prompt. Composes with --system-prompt.",
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
id: "append-system-prompt-file",
|
|
128
|
-
label: "Append system prompt file",
|
|
129
|
-
flag: "--append-system-prompt-file",
|
|
130
|
-
kind: "text",
|
|
131
|
-
group: "context",
|
|
132
|
-
description: "Path to a file whose contents are appended to the default system prompt.",
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
id: "dangerously-load-development-channels",
|
|
136
|
-
label: "Load development channels",
|
|
137
|
-
flag: "--dangerously-load-development-channels",
|
|
138
|
-
kind: "text-list",
|
|
139
|
-
group: "channels",
|
|
140
|
-
description: "Sideload development channels from magus plugins. Default seed: claudish (the only magus plugin currently shipping channels).",
|
|
141
|
-
defaultValues: ["plugin:claudish@magus"],
|
|
142
|
-
},
|
|
143
|
-
{
|
|
144
|
-
id: "debug",
|
|
145
|
-
label: "Debug categories",
|
|
146
|
-
flag: "--debug",
|
|
147
|
-
shortFlag: "-d",
|
|
148
|
-
kind: "multi-with-custom",
|
|
149
|
-
group: "debug",
|
|
150
|
-
description: "Enable debug mode with optional category filter. Pick from common categories or add custom tokens (including negations like !file).",
|
|
151
|
-
picklist: [
|
|
152
|
-
"api",
|
|
153
|
-
"hooks",
|
|
154
|
-
"mcp",
|
|
155
|
-
"tools",
|
|
156
|
-
"plugins",
|
|
157
|
-
"skills",
|
|
158
|
-
"settings",
|
|
159
|
-
"permissions",
|
|
160
|
-
"auth",
|
|
161
|
-
"cache",
|
|
162
|
-
"statsig",
|
|
163
|
-
"file",
|
|
164
|
-
"1p",
|
|
165
|
-
],
|
|
166
|
-
defaultValues: [],
|
|
167
|
-
},
|
|
168
|
-
{
|
|
169
|
-
id: "worktree",
|
|
170
|
-
label: "Use worktree",
|
|
171
|
-
flag: "--worktree",
|
|
172
|
-
kind: "boolean",
|
|
173
|
-
group: "worktree",
|
|
174
|
-
description: "Create a git worktree for the session. Prerequisite for --tmux.",
|
|
175
|
-
},
|
|
176
|
-
{
|
|
177
|
-
id: "tmux",
|
|
178
|
-
label: "tmux session",
|
|
179
|
-
flag: "--tmux",
|
|
180
|
-
kind: "select",
|
|
181
|
-
group: "worktree",
|
|
182
|
-
requires: "worktree",
|
|
183
|
-
description: "Create a tmux session for the worktree. Requires --worktree. iTerm2 native panes when available; classic for traditional tmux.",
|
|
184
|
-
options: [
|
|
185
|
-
{ label: "auto (iTerm2 native)", value: "" },
|
|
186
|
-
{ label: "classic", value: "classic" },
|
|
187
|
-
],
|
|
188
|
-
},
|
|
189
|
-
];
|
|
190
|
-
/** Look up a flag by id. Throws if missing — keeps callers honest. */
|
|
191
|
-
export function getFlagById(id) {
|
|
192
|
-
const f = ALIAS_FLAGS.find((flag) => flag.id === id);
|
|
193
|
-
if (!f)
|
|
194
|
-
throw new Error(`Unknown alias flag id: ${id}`);
|
|
195
|
-
return f;
|
|
196
|
-
}
|