claudeup 4.19.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__/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 +7 -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/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/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-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 +0 -4
- package/src/ui/screens/CliToolsScreen.tsx +11 -3
- package/src/ui/screens/index.ts +0 -1
- package/src/ui/state/reducer.ts +0 -101
- package/src/ui/state/types.ts +0 -35
- package/src/utils/command-utils.ts +23 -0
- package/src/utils/file-locking.ts +144 -0
- package/src/data/alias-flags.js +0 -205
- 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 -1018
- package/src/services/alias-store.js +0 -129
- 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 -499
- 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 -355
- 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/FlagDetailEditor.js +0 -0
- 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 -1111
- 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 -769
- 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 -547
- 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
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
1
|
import semver from "semver";
|
|
3
|
-
|
|
4
|
-
const require = createRequire(import.meta.url);
|
|
2
|
+
import pkg from "../../package.json";
|
|
5
3
|
|
|
6
4
|
// Cache the result for the session
|
|
7
5
|
let cachedResult: VersionCheckResult | null = null;
|
|
@@ -14,15 +12,12 @@ export interface VersionCheckResult {
|
|
|
14
12
|
}
|
|
15
13
|
|
|
16
14
|
/**
|
|
17
|
-
* Get the current version from package.json
|
|
15
|
+
* Get the current version from package.json.
|
|
16
|
+
* Static import so it resolves inside a `bun build --compile` binary — a
|
|
17
|
+
* dynamic require("../../package.json") returns no `.version` there.
|
|
18
18
|
*/
|
|
19
19
|
export function getCurrentVersion(): string {
|
|
20
|
-
|
|
21
|
-
const pkg = require("../../package.json");
|
|
22
|
-
return pkg.version;
|
|
23
|
-
} catch {
|
|
24
|
-
return "0.0.0";
|
|
25
|
-
}
|
|
20
|
+
return (pkg as { version?: string }).version ?? "0.0.0";
|
|
26
21
|
}
|
|
27
22
|
|
|
28
23
|
/**
|
|
@@ -97,7 +92,11 @@ export async function checkForUpdates(): Promise<VersionCheckResult> {
|
|
|
97
92
|
return cachedResult;
|
|
98
93
|
}
|
|
99
94
|
|
|
100
|
-
|
|
95
|
+
// Guard: never let a malformed version crash the update check.
|
|
96
|
+
const updateAvailable =
|
|
97
|
+
semver.valid(latestVersion) != null &&
|
|
98
|
+
semver.valid(currentVersion) != null &&
|
|
99
|
+
semver.gt(latestVersion, currentVersion);
|
|
101
100
|
|
|
102
101
|
cachedResult = {
|
|
103
102
|
currentVersion,
|
package/src/types/index.ts
CHANGED
|
@@ -60,12 +60,6 @@ export interface Plugin {
|
|
|
60
60
|
hasUpdate?: boolean;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
export interface StatusLineConfig {
|
|
64
|
-
name: string;
|
|
65
|
-
description: string;
|
|
66
|
-
template: string;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
63
|
export interface MarketplaceSource {
|
|
70
64
|
source: {
|
|
71
65
|
source: "github";
|
|
@@ -250,6 +244,35 @@ export interface InstalledPluginsRegistry {
|
|
|
250
244
|
plugins: Record<string, InstalledPluginEntry[]>;
|
|
251
245
|
}
|
|
252
246
|
|
|
247
|
+
// ─── Plugin Conventions (declarative project hygiene) ───────────────────────────
|
|
248
|
+
|
|
249
|
+
/** Convention declarations for a plugin */
|
|
250
|
+
export interface PluginConventions {
|
|
251
|
+
gitignore?: GitignoreConventions;
|
|
252
|
+
claudemd?: ClaudeMdConventions;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Gitignore entries a plugin needs */
|
|
256
|
+
export interface GitignoreConventions {
|
|
257
|
+
/** Entries for project .gitignore (team-visible, committed) */
|
|
258
|
+
project?: string[];
|
|
259
|
+
/** Advisory entries for global gitignore (personal, not committed).
|
|
260
|
+
* Resolved via `git config --global core.excludesfile`,
|
|
261
|
+
* falling back to ~/.config/git/ignore if unset. */
|
|
262
|
+
global?: string[];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** CLAUDE.md section injection config */
|
|
266
|
+
export interface ClaudeMdConventions {
|
|
267
|
+
/** Section identifier used in markers: <!-- BEGIN magus:{section} -->
|
|
268
|
+
* MUST equal the plugin's `name` field. */
|
|
269
|
+
section: string;
|
|
270
|
+
/** Path to markdown template, relative to plugin root */
|
|
271
|
+
template: string;
|
|
272
|
+
/** Semantic version of the template content */
|
|
273
|
+
version: string;
|
|
274
|
+
}
|
|
275
|
+
|
|
253
276
|
// ─── Plugin Profile Types ──────────────────────────────────────────────────────
|
|
254
277
|
|
|
255
278
|
export interface Profile {
|
|
@@ -274,35 +297,119 @@ export interface ProfileEntry {
|
|
|
274
297
|
scope: "user" | "project";
|
|
275
298
|
}
|
|
276
299
|
|
|
277
|
-
//
|
|
300
|
+
// NOTE: The built-in profile type lives with its data in
|
|
301
|
+
// data/predefined-profiles.ts (that file declares and exports its own
|
|
302
|
+
// PredefinedProfile). A second, unused definition previously lived here and
|
|
303
|
+
// has been removed to avoid two competing shapes.
|
|
304
|
+
|
|
305
|
+
// ─── Team Profile Manifest (v2, committed .claude/profiles.json) ────────────────
|
|
306
|
+
//
|
|
307
|
+
// The v1 `Profile`/`ProfilesFile` above are the TUI's saved presets (plugins
|
|
308
|
+
// only). The v2 manifest below is the committed, team-shared source of truth:
|
|
309
|
+
// a superset that also carries marketplaces, MCP servers, CLI tools, skills,
|
|
310
|
+
// settings, and env requirements. A v1 profile is a structural subset of a v2
|
|
311
|
+
// entry, so v1 files migrate forward by a version bump alone.
|
|
312
|
+
|
|
313
|
+
/** A marketplace a profile depends on (registered before its plugins install). */
|
|
314
|
+
export interface ProfileMarketplaceRef {
|
|
315
|
+
source: "github" | "directory";
|
|
316
|
+
/** owner/repo — for source: "github" */
|
|
317
|
+
repo?: string;
|
|
318
|
+
/** absolute or repo-relative path — for source: "directory" */
|
|
319
|
+
path?: string;
|
|
320
|
+
}
|
|
278
321
|
|
|
279
|
-
/** A skill
|
|
280
|
-
export interface
|
|
322
|
+
/** A skill a profile installs (resolved from a GitHub repo + path). */
|
|
323
|
+
export interface ProfileSkillRef {
|
|
281
324
|
name: string;
|
|
282
325
|
repo: string;
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/** Settings that can be configured in a predefined profile */
|
|
287
|
-
export interface PredefinedSettings {
|
|
288
|
-
effortLevel?: "low" | "medium" | "high";
|
|
289
|
-
alwaysThinkingEnabled?: boolean;
|
|
290
|
-
model?: "claude-sonnet-4-6" | "claude-opus-4-6";
|
|
291
|
-
outputStyle?: "concise" | "explanatory" | "formal";
|
|
292
|
-
CLAUDE_CODE_ENABLE_TASKS?: boolean;
|
|
293
|
-
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS?: boolean;
|
|
294
|
-
includeGitInstructions?: boolean;
|
|
295
|
-
respectGitignore?: boolean;
|
|
296
|
-
enableAllProjectMcpServers?: boolean;
|
|
326
|
+
path: string;
|
|
297
327
|
}
|
|
298
328
|
|
|
299
|
-
/**
|
|
300
|
-
export interface
|
|
301
|
-
|
|
329
|
+
/** Environment variables a profile declares (values live in settings.local.json). */
|
|
330
|
+
export interface ProfileEnvRequirements {
|
|
331
|
+
required?: string[];
|
|
332
|
+
optional?: string[];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* A single team profile in .claude/profiles.json (v2).
|
|
337
|
+
* `plugins` maps a plugin id to a version pin ("1.2.0"), "latest", or `true`
|
|
338
|
+
* (equivalent to "latest" — the shape v1 saved profiles use).
|
|
339
|
+
*/
|
|
340
|
+
export interface ProfileManifestEntry {
|
|
302
341
|
name: string;
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
342
|
+
/** Optional predefined-profile id to inherit plugins/skills/settings from. */
|
|
343
|
+
extends?: string;
|
|
344
|
+
marketplaces?: Record<string, ProfileMarketplaceRef>;
|
|
345
|
+
plugins?: Record<string, string | boolean>;
|
|
346
|
+
mcpServers?: Record<string, McpServerConfig>;
|
|
347
|
+
/** cliTool name -> version pin | "latest". */
|
|
348
|
+
cliTools?: Record<string, string>;
|
|
349
|
+
skills?: ProfileSkillRef[];
|
|
350
|
+
settings?: Record<string, unknown>;
|
|
351
|
+
env?: ProfileEnvRequirements;
|
|
352
|
+
/** Optional bookkeeping carried over from v1 saved profiles. */
|
|
353
|
+
createdAt?: string;
|
|
354
|
+
updatedAt?: string;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** The .claude/profiles.json manifest (v2). */
|
|
358
|
+
export interface ProfileManifest {
|
|
359
|
+
version: 2;
|
|
360
|
+
/** When true, `install --check` fails on any plugin version drift. */
|
|
361
|
+
strictVersions?: boolean;
|
|
362
|
+
profiles: Record<string, ProfileManifestEntry>;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ─── Plugin binary requirements (plugin.json `requires.bin`) ────────────────────
|
|
366
|
+
|
|
367
|
+
/** How a binary dependency is installed. */
|
|
368
|
+
export type BinInstaller = "bun" | "brew" | "npm" | "pip" | "go";
|
|
369
|
+
|
|
370
|
+
/** A single binary dependency a plugin (or profile) declares. */
|
|
371
|
+
export interface BinRequirement {
|
|
372
|
+
/** The executable name expected on PATH (also the dedup key). */
|
|
373
|
+
name: string;
|
|
374
|
+
via: BinInstaller;
|
|
375
|
+
/** Package id for bun/npm/pip (defaults to `name`). */
|
|
376
|
+
package?: string;
|
|
377
|
+
/** Formula for brew (defaults to `name`). */
|
|
378
|
+
formula?: string;
|
|
379
|
+
/** Module path for `go install` (defaults to `name`). */
|
|
380
|
+
module?: string;
|
|
381
|
+
/** Command that verifies presence, e.g. "tmux -V" (defaults to `<name> --version`). */
|
|
382
|
+
check?: string;
|
|
383
|
+
/** Optional version pin (real for bun/npm/pip; advisory for brew/go). */
|
|
384
|
+
version?: string;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** The `requires` block a plugin may declare in its plugin.json. */
|
|
388
|
+
export interface PluginRequires {
|
|
389
|
+
bin?: BinRequirement[];
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ─── Resolved install closure (resolver output) ─────────────────────────────────
|
|
393
|
+
|
|
394
|
+
/** A binary requirement after dedup + pin precedence, with provenance. */
|
|
395
|
+
export interface ResolvedBin extends BinRequirement {
|
|
396
|
+
/** Where this requirement came from, for diagnostics. */
|
|
397
|
+
sources: string[];
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* The fully-resolved set of things to install for a profile (or the union of
|
|
402
|
+
* all profiles). Produced by the resolver; consumed by `claudeup install`.
|
|
403
|
+
*/
|
|
404
|
+
export interface ResolvedClosure {
|
|
405
|
+
marketplaces: Record<string, ProfileMarketplaceRef>;
|
|
406
|
+
/** pluginId -> version pin | "latest". */
|
|
407
|
+
plugins: Record<string, string>;
|
|
408
|
+
mcpServers: Record<string, McpServerConfig>;
|
|
409
|
+
bins: ResolvedBin[];
|
|
410
|
+
skills: ProfileSkillRef[];
|
|
411
|
+
settings: Record<string, unknown>;
|
|
412
|
+
env: { required: string[]; optional: string[] };
|
|
413
|
+
/** Cross-profile version conflicts detected while unioning profiles. */
|
|
414
|
+
conflicts?: string[];
|
|
308
415
|
}
|
package/src/ui/App.tsx
CHANGED
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
McpRegistryScreen,
|
|
19
19
|
SettingsScreen,
|
|
20
20
|
CliToolsScreen,
|
|
21
|
-
ModelSelectorScreen,
|
|
22
21
|
ProfilesScreen,
|
|
23
22
|
SkillsScreen,
|
|
24
23
|
GitignoreScreen,
|
|
@@ -72,8 +71,6 @@ function Router() {
|
|
|
72
71
|
return <SettingsScreen />;
|
|
73
72
|
case "cli-tools":
|
|
74
73
|
return <CliToolsScreen />;
|
|
75
|
-
case "model-selector":
|
|
76
|
-
return <ModelSelectorScreen />;
|
|
77
74
|
case "profiles":
|
|
78
75
|
return <ProfilesScreen />;
|
|
79
76
|
case "skills":
|
|
@@ -139,7 +136,6 @@ function GlobalKeyHandler({
|
|
|
139
136
|
"mcp-registry",
|
|
140
137
|
"settings",
|
|
141
138
|
"cli-tools",
|
|
142
|
-
"model-selector",
|
|
143
139
|
"profiles",
|
|
144
140
|
"skills",
|
|
145
141
|
"gitignore",
|
|
@@ -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 {
|
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
|
@@ -38,13 +38,6 @@ export const initialState: AppState = {
|
|
|
38
38
|
servers: { status: "idle" },
|
|
39
39
|
},
|
|
40
40
|
|
|
41
|
-
statusline: {
|
|
42
|
-
scope: "project",
|
|
43
|
-
selectedIndex: 0,
|
|
44
|
-
presets: { status: "idle" },
|
|
45
|
-
currentPreset: null,
|
|
46
|
-
},
|
|
47
|
-
|
|
48
41
|
settings: {
|
|
49
42
|
selectedIndex: 0,
|
|
50
43
|
values: { status: "idle" },
|
|
@@ -55,12 +48,6 @@ export const initialState: AppState = {
|
|
|
55
48
|
tools: { status: "idle" },
|
|
56
49
|
},
|
|
57
50
|
|
|
58
|
-
modelSelector: {
|
|
59
|
-
selectedIndex: 0,
|
|
60
|
-
searchQuery: "",
|
|
61
|
-
taskSize: "large",
|
|
62
|
-
},
|
|
63
|
-
|
|
64
51
|
profiles: {
|
|
65
52
|
selectedIndex: 0,
|
|
66
53
|
profiles: { status: "idle" },
|
|
@@ -266,63 +253,6 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
266
253
|
},
|
|
267
254
|
};
|
|
268
255
|
|
|
269
|
-
// =========================================================================
|
|
270
|
-
// Status Line Screen
|
|
271
|
-
// =========================================================================
|
|
272
|
-
case "STATUSLINE_SET_SCOPE":
|
|
273
|
-
return {
|
|
274
|
-
...state,
|
|
275
|
-
statusline: {
|
|
276
|
-
...state.statusline,
|
|
277
|
-
scope: action.scope,
|
|
278
|
-
selectedIndex: 0,
|
|
279
|
-
presets: { status: "loading" },
|
|
280
|
-
},
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
case "STATUSLINE_TOGGLE_SCOPE":
|
|
284
|
-
return appReducer(state, {
|
|
285
|
-
type: "STATUSLINE_SET_SCOPE",
|
|
286
|
-
scope: state.statusline.scope === "project" ? "global" : "project",
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
case "STATUSLINE_SELECT":
|
|
290
|
-
return {
|
|
291
|
-
...state,
|
|
292
|
-
statusline: {
|
|
293
|
-
...state.statusline,
|
|
294
|
-
selectedIndex: action.index,
|
|
295
|
-
},
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
case "STATUSLINE_DATA_LOADING":
|
|
299
|
-
return {
|
|
300
|
-
...state,
|
|
301
|
-
statusline: {
|
|
302
|
-
...state.statusline,
|
|
303
|
-
presets: { status: "loading" },
|
|
304
|
-
},
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
case "STATUSLINE_DATA_SUCCESS":
|
|
308
|
-
return {
|
|
309
|
-
...state,
|
|
310
|
-
statusline: {
|
|
311
|
-
...state.statusline,
|
|
312
|
-
presets: { status: "success", data: action.presets },
|
|
313
|
-
currentPreset: action.currentPreset,
|
|
314
|
-
},
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
case "STATUSLINE_DATA_ERROR":
|
|
318
|
-
return {
|
|
319
|
-
...state,
|
|
320
|
-
statusline: {
|
|
321
|
-
...state.statusline,
|
|
322
|
-
presets: { status: "error", error: action.error },
|
|
323
|
-
},
|
|
324
|
-
};
|
|
325
|
-
|
|
326
256
|
// =========================================================================
|
|
327
257
|
// Settings Screen
|
|
328
258
|
// =========================================================================
|
|
@@ -417,37 +347,6 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
417
347
|
};
|
|
418
348
|
}
|
|
419
349
|
|
|
420
|
-
// =========================================================================
|
|
421
|
-
// Model Selector Screen
|
|
422
|
-
// =========================================================================
|
|
423
|
-
case "MODEL_SELECTOR_SELECT":
|
|
424
|
-
return {
|
|
425
|
-
...state,
|
|
426
|
-
modelSelector: {
|
|
427
|
-
...state.modelSelector,
|
|
428
|
-
selectedIndex: action.index,
|
|
429
|
-
},
|
|
430
|
-
};
|
|
431
|
-
|
|
432
|
-
case "MODEL_SELECTOR_SET_SEARCH":
|
|
433
|
-
return {
|
|
434
|
-
...state,
|
|
435
|
-
modelSelector: {
|
|
436
|
-
...state.modelSelector,
|
|
437
|
-
searchQuery: action.query,
|
|
438
|
-
selectedIndex: 0, // Reset selection when search changes
|
|
439
|
-
},
|
|
440
|
-
};
|
|
441
|
-
|
|
442
|
-
case "MODEL_SELECTOR_SET_TASK_SIZE":
|
|
443
|
-
return {
|
|
444
|
-
...state,
|
|
445
|
-
modelSelector: {
|
|
446
|
-
...state.modelSelector,
|
|
447
|
-
taskSize: action.size,
|
|
448
|
-
},
|
|
449
|
-
};
|
|
450
|
-
|
|
451
350
|
// =========================================================================
|
|
452
351
|
// Profiles Screen
|
|
453
352
|
// =========================================================================
|
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[]>;
|
|
@@ -221,10 +205,8 @@ export interface AppState {
|
|
|
221
205
|
plugins: PluginsScreenState;
|
|
222
206
|
mcp: McpScreenState;
|
|
223
207
|
mcpRegistry: McpRegistryScreenState;
|
|
224
|
-
statusline: StatusLineScreenState;
|
|
225
208
|
settings: SettingsScreenState;
|
|
226
209
|
cliTools: CliToolsScreenState;
|
|
227
|
-
modelSelector: ModelSelectorScreenState;
|
|
228
210
|
profiles: ProfilesScreenState;
|
|
229
211
|
skills: SkillsScreenState;
|
|
230
212
|
}
|
|
@@ -270,18 +252,6 @@ export type AppAction =
|
|
|
270
252
|
| { type: "MCP_REGISTRY_DATA_SUCCESS"; servers: McpServer[] }
|
|
271
253
|
| { type: "MCP_REGISTRY_DATA_ERROR"; error: Error }
|
|
272
254
|
|
|
273
|
-
// Status line screen
|
|
274
|
-
| { type: "STATUSLINE_SET_SCOPE"; scope: "project" | "global" }
|
|
275
|
-
| { type: "STATUSLINE_TOGGLE_SCOPE" }
|
|
276
|
-
| { type: "STATUSLINE_SELECT"; index: number }
|
|
277
|
-
| { type: "STATUSLINE_DATA_LOADING" }
|
|
278
|
-
| {
|
|
279
|
-
type: "STATUSLINE_DATA_SUCCESS";
|
|
280
|
-
presets: StatusLineConfig[];
|
|
281
|
-
currentPreset: string | null;
|
|
282
|
-
}
|
|
283
|
-
| { type: "STATUSLINE_DATA_ERROR"; error: Error }
|
|
284
|
-
|
|
285
255
|
// Settings screen
|
|
286
256
|
| { type: "SETTINGS_SELECT"; index: number }
|
|
287
257
|
| { type: "SETTINGS_DATA_LOADING" }
|
|
@@ -295,11 +265,6 @@ export type AppAction =
|
|
|
295
265
|
| { type: "CLITOOLS_DATA_ERROR"; error: Error }
|
|
296
266
|
| { type: "CLITOOLS_UPDATE_TOOL"; name: string; updates: Partial<CliTool> }
|
|
297
267
|
|
|
298
|
-
// Model Selector Screen
|
|
299
|
-
| { type: "MODEL_SELECTOR_SELECT"; index: number }
|
|
300
|
-
| { type: "MODEL_SELECTOR_SET_SEARCH"; query: string }
|
|
301
|
-
| { type: "MODEL_SELECTOR_SET_TASK_SIZE"; size: "large" | "small" }
|
|
302
|
-
|
|
303
268
|
// Modals
|
|
304
269
|
| { type: "SHOW_MODAL"; modal: ModalState }
|
|
305
270
|
| { type: "HIDE_MODAL" }
|
|
@@ -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
|
+
}
|