@f5xc-salesdemos/xcsh 19.5.0 → 19.6.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,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5xc-salesdemos/xcsh",
4
- "version": "19.5.0",
4
+ "version": "19.6.0",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/f5xc-salesdemos/xcsh",
7
7
  "author": "Can Boluk",
@@ -50,12 +50,12 @@
50
50
  "dependencies": {
51
51
  "@agentclientprotocol/sdk": "0.16.1",
52
52
  "@mozilla/readability": "^0.6",
53
- "@f5xc-salesdemos/xcsh-stats": "19.5.0",
54
- "@f5xc-salesdemos/pi-agent-core": "19.5.0",
55
- "@f5xc-salesdemos/pi-ai": "19.5.0",
56
- "@f5xc-salesdemos/pi-natives": "19.5.0",
57
- "@f5xc-salesdemos/pi-tui": "19.5.0",
58
- "@f5xc-salesdemos/pi-utils": "19.5.0",
53
+ "@f5xc-salesdemos/xcsh-stats": "19.6.0",
54
+ "@f5xc-salesdemos/pi-agent-core": "19.6.0",
55
+ "@f5xc-salesdemos/pi-ai": "19.6.0",
56
+ "@f5xc-salesdemos/pi-natives": "19.6.0",
57
+ "@f5xc-salesdemos/pi-tui": "19.6.0",
58
+ "@f5xc-salesdemos/pi-utils": "19.6.0",
59
59
  "@sinclair/typebox": "^0.34",
60
60
  "@xterm/headless": "^6.0",
61
61
  "ajv": "^8.20",
@@ -19,6 +19,7 @@ import {
19
19
  listXcshPluginRoots,
20
20
  loadFilesFromDir,
21
21
  scanSkillsFromDir,
22
+ scopeToLevel,
22
23
  type XcshPluginRoot,
23
24
  } from "./helpers";
24
25
 
@@ -45,7 +46,7 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
45
46
  const result = await scanSkillsFromDir(ctx, {
46
47
  dir: skillsDir,
47
48
  providerId: PROVIDER_ID,
48
- level: root.scope,
49
+ level: scopeToLevel(root.scope),
49
50
  });
50
51
  return { root, result };
51
52
  }),
@@ -76,7 +77,7 @@ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashComm
76
77
  const results = await Promise.all(
77
78
  roots.map(async root => {
78
79
  const commandsDir = path.join(root.path, "commands");
79
- return loadFilesFromDir<SlashCommand>(ctx, commandsDir, PROVIDER_ID, root.scope, {
80
+ return loadFilesFromDir<SlashCommand>(ctx, commandsDir, PROVIDER_ID, scopeToLevel(root.scope), {
80
81
  extensions: ["md"],
81
82
  transform: (name, content, filePath, source) => {
82
83
  const cmdName = name.replace(/\.md$/, "");
@@ -84,7 +85,7 @@ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashComm
84
85
  name: root.plugin ? `${root.plugin}:${cmdName}` : cmdName,
85
86
  path: filePath,
86
87
  content,
87
- level: root.scope,
88
+ level: scopeToLevel(root.scope),
88
89
  _source: source,
89
90
  };
90
91
  },
@@ -123,7 +124,7 @@ async function loadHooks(ctx: LoadContext): Promise<LoadResult<Hook>> {
123
124
  const results = await Promise.all(
124
125
  loadTasks.map(async ({ root, hookType }) => {
125
126
  const hooksDir = path.join(root.path, "hooks", hookType);
126
- return loadFilesFromDir<Hook>(ctx, hooksDir, PROVIDER_ID, root.scope, {
127
+ return loadFilesFromDir<Hook>(ctx, hooksDir, PROVIDER_ID, scopeToLevel(root.scope), {
127
128
  transform: (name, _content, filePath, source) => {
128
129
  const toolName = name.replace(/\.(sh|bash|zsh|fish)$/, "");
129
130
  return {
@@ -131,7 +132,7 @@ async function loadHooks(ctx: LoadContext): Promise<LoadResult<Hook>> {
131
132
  path: filePath,
132
133
  type: hookType,
133
134
  tool: toolName,
134
- level: root.scope,
135
+ level: scopeToLevel(root.scope),
135
136
  _source: source,
136
137
  };
137
138
  },
@@ -161,14 +162,14 @@ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
161
162
  const results = await Promise.all(
162
163
  roots.map(async root => {
163
164
  const toolsDir = path.join(root.path, "tools");
164
- return loadFilesFromDir<CustomTool>(ctx, toolsDir, PROVIDER_ID, root.scope, {
165
+ return loadFilesFromDir<CustomTool>(ctx, toolsDir, PROVIDER_ID, scopeToLevel(root.scope), {
165
166
  transform: (name, _content, filePath, source) => {
166
167
  const toolName = name.replace(/\.(ts|js|sh|bash|py)$/, "");
167
168
  return {
168
169
  name: toolName,
169
170
  path: filePath,
170
171
  description: `${toolName} custom tool`,
171
- level: root.scope,
172
+ level: scopeToLevel(root.scope),
172
173
  _source: source,
173
174
  };
174
175
  },
@@ -242,7 +243,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
242
243
  ...(raw.auth !== undefined && { auth: raw.auth }),
243
244
  ...(raw.oauth !== undefined && { oauth: raw.oauth }),
244
245
  ...(raw.type !== undefined && { transport: raw.type as MCPServer["transport"] }),
245
- _source: createSourceMeta(PROVIDER_ID, mcpPath, root.scope),
246
+ _source: createSourceMeta(PROVIDER_ID, mcpPath, scopeToLevel(root.scope)),
246
247
  };
247
248
  items.push(server);
248
249
  }
@@ -365,6 +365,22 @@ function expandEnvVars(value: string, extraEnv?: Record<string, string>): string
365
365
  });
366
366
  }
367
367
 
368
+ /**
369
+ * Build plugin-specific environment variables for variable substitution.
370
+ * These are injected as extraEnv when loading plugin configs (hooks, MCP, LSP).
371
+ */
372
+ export function buildPluginEnvVars(pluginRoot: string, pluginId: string, projectDir?: string): Record<string, string> {
373
+ const dataDir = path.join(os.homedir(), getConfigDirName(), "plugins", "data", pluginId.replace("@", "__"));
374
+ const vars: Record<string, string> = {
375
+ XCSH_PLUGIN_ROOT: pluginRoot,
376
+ XCSH_PLUGIN_DATA: dataDir,
377
+ };
378
+ if (projectDir) {
379
+ vars.XCSH_PROJECT_DIR = projectDir;
380
+ }
381
+ return vars;
382
+ }
383
+
368
384
  /**
369
385
  * Recursively expand environment variables in an object.
370
386
  */
@@ -621,8 +637,13 @@ export interface XcshPluginRoot {
621
637
  version: string;
622
638
  /** Absolute path to plugin root */
623
639
  path: string;
624
- /** Whether this is a user or project scope plugin */
625
- scope: "user" | "project";
640
+ /** Whether this is a user, project, or local scope plugin */
641
+ scope: "user" | "project" | "local";
642
+ }
643
+
644
+ /** Map plugin scope to the loading level — "local" behaves as "project" for capability loading. */
645
+ export function scopeToLevel(scope: "user" | "project" | "local"): "user" | "project" {
646
+ return scope === "local" ? "project" : scope;
626
647
  }
627
648
 
628
649
  /**
@@ -277,8 +277,9 @@ async function cloneAndReadCatalog(url: string, cacheDir: string): Promise<Fetch
277
277
  const tmpDir = path.join(cacheDir, `.tmp-clone-${Date.now()}`);
278
278
  await fs.mkdir(cacheDir, { recursive: true });
279
279
 
280
- logger.debug(`[marketplace] cloning ${url} ${tmpDir}`);
281
- await git.clone(url, tmpDir);
280
+ const timeoutMs = Number(Bun.env.XCSH_PLUGIN_GIT_TIMEOUT_MS) || 120_000;
281
+ logger.debug(`[marketplace] cloning ${url} → ${tmpDir} (timeout: ${timeoutMs}ms)`);
282
+ await git.clone(url, tmpDir, { signal: AbortSignal.timeout(timeoutMs) });
282
283
 
283
284
  const catalogPath = path.join(tmpDir, CATALOG_RELATIVE_PATH);
284
285
  let content: string;
@@ -49,6 +49,11 @@ export interface MarketplaceManagerOptions {
49
49
  * Resolved by resolveActiveProjectRegistryPath(cwd) in callers.
50
50
  */
51
51
  projectInstalledRegistryPath?: string;
52
+ /**
53
+ * Path to the local-scoped installed_plugins.json (gitignored, project-specific).
54
+ * Same as project path but under a `.local` variant so it stays out of VCS.
55
+ */
56
+ localInstalledRegistryPath?: string;
52
57
  marketplacesCacheDir: string;
53
58
  pluginsCacheDir: string;
54
59
  /** Injected for testing; production callers pass clearXcshPluginRootsCache.
@@ -143,7 +148,17 @@ export class MarketplaceManager {
143
148
  throw new Error(`Marketplace "${name}" not found`);
144
149
  }
145
150
 
146
- const { catalog, clonePath } = await fetchMarketplace(existing.sourceUri, this.#opts.marketplacesCacheDir);
151
+ let fetchResult: { catalog: MarketplaceCatalog; clonePath?: string };
152
+ try {
153
+ fetchResult = await fetchMarketplace(existing.sourceUri, this.#opts.marketplacesCacheDir);
154
+ } catch (err) {
155
+ if (Bun.env.XCSH_PLUGIN_KEEP_MARKETPLACE_ON_FAILURE === "1") {
156
+ logger.debug("Marketplace fetch failed, preserving cached catalog", { name, error: String(err) });
157
+ return existing;
158
+ }
159
+ throw err;
160
+ }
161
+ const { catalog, clonePath } = fetchResult;
147
162
 
148
163
  // Guard against upstream catalog silently renaming itself — the registry
149
164
  // entry is keyed by name, so a drift would corrupt the entry on next read.
@@ -227,7 +242,7 @@ export class MarketplaceManager {
227
242
  async installPlugin(
228
243
  name: string,
229
244
  marketplace: string,
230
- options?: { force?: boolean; scope?: "user" | "project" },
245
+ options?: { force?: boolean; scope?: "user" | "project" | "local" },
231
246
  ): Promise<InstalledPluginEntry> {
232
247
  const force = options?.force ?? false;
233
248
  const scope = options?.scope ?? "user";
@@ -323,13 +338,15 @@ export class MarketplaceManager {
323
338
  const now = new Date().toISOString();
324
339
  // Carry over enabled flag from existing entry — a disabled plugin must stay disabled after upgrade
325
340
  const wasDisabled = existing?.some(e => e.enabled === false);
341
+ // Honor defaultEnabled from catalog — new installs with defaultEnabled: false start disabled
342
+ const defaultDisabled = !existing && pluginEntry.defaultEnabled === false;
326
343
  const installedEntry: InstalledPluginEntry = {
327
344
  scope,
328
345
  installPath: cachePath,
329
346
  version,
330
347
  installedAt: now,
331
348
  lastUpdated: now,
332
- ...(wasDisabled ? { enabled: false } : {}),
349
+ ...(wasDisabled || defaultDisabled ? { enabled: false } : {}),
333
350
  };
334
351
 
335
352
  const freshInstReg = await readInstalledPluginsRegistry(registryPath);
@@ -376,7 +393,7 @@ export class MarketplaceManager {
376
393
  return "0.0.0";
377
394
  }
378
395
 
379
- async uninstallPlugin(pluginId: string, scope?: "user" | "project"): Promise<void> {
396
+ async uninstallPlugin(pluginId: string, scope?: "user" | "project" | "local"): Promise<void> {
380
397
  const parsed = parsePluginId(pluginId);
381
398
  if (!parsed) {
382
399
  throw new Error(`Invalid plugin ID format: "${pluginId}". Expected "name@marketplace".`);
@@ -392,7 +409,7 @@ export class MarketplaceManager {
392
409
  }
393
410
 
394
411
  // Disambiguation: if installed in both scopes and no explicit scope, require one.
395
- let targetScope: "user" | "project";
412
+ let targetScope: "user" | "project" | "local";
396
413
  if (inUser && inProject) {
397
414
  if (!scope) {
398
415
  throw new Error(
@@ -476,7 +493,7 @@ export class MarketplaceManager {
476
493
  return results;
477
494
  }
478
495
 
479
- async setPluginEnabled(pluginId: string, enabled: boolean, scope?: "user" | "project"): Promise<void> {
496
+ async setPluginEnabled(pluginId: string, enabled: boolean, scope?: "user" | "project" | "local"): Promise<void> {
480
497
  const { userEntries, projectEntries, userReg, projectReg } = await this.#findInBothRegistries(pluginId);
481
498
 
482
499
  const inUser = userEntries && userEntries.length > 0;
@@ -487,7 +504,7 @@ export class MarketplaceManager {
487
504
  }
488
505
 
489
506
  // Disambiguation: if installed in both scopes and no explicit scope, require one.
490
- let targetScope: "user" | "project";
507
+ let targetScope: "user" | "project" | "local";
491
508
  if (inUser && inProject) {
492
509
  if (!scope) {
493
510
  throw new Error(
@@ -546,16 +563,23 @@ export class MarketplaceManager {
546
563
  // Compare installed plugin versions against their catalog entries.
547
564
  // Returns one entry per (pluginId, scope) pair where the catalog declares a newer version.
548
565
  // Catalog entries without a version field are skipped.
549
- async checkForUpdates(): Promise<Array<{ pluginId: string; scope: "user" | "project"; from: string; to: string }>> {
566
+ async checkForUpdates(): Promise<
567
+ Array<{ pluginId: string; scope: "user" | "project" | "local"; from: string; to: string }>
568
+ > {
550
569
  const mktReg = await readMarketplacesRegistry(this.#opts.marketplacesRegistryPath);
551
- const updates: Array<{ pluginId: string; scope: "user" | "project"; from: string; to: string }> = [];
570
+ const updates: Array<{ pluginId: string; scope: "user" | "project" | "local"; from: string; to: string }> = [];
552
571
 
553
572
  // Keyed by (path, scope) so each scope is checked independently.
554
573
  // A plugin current in user scope but stale in project scope must still appear.
555
- const registryEntries: Array<[string, "user" | "project"]> = [[this.#opts.installedRegistryPath, "user"]];
574
+ const registryEntries: Array<[string, "user" | "project" | "local"]> = [
575
+ [this.#opts.installedRegistryPath, "user"],
576
+ ];
556
577
  if (this.#opts.projectInstalledRegistryPath) {
557
578
  registryEntries.push([this.#opts.projectInstalledRegistryPath, "project"]);
558
579
  }
580
+ if (this.#opts.localInstalledRegistryPath) {
581
+ registryEntries.push([this.#opts.localInstalledRegistryPath, "local"]);
582
+ }
559
583
 
560
584
  for (const [regPath, scope] of registryEntries) {
561
585
  const instReg = await readInstalledPluginsRegistry(regPath);
@@ -596,7 +620,7 @@ export class MarketplaceManager {
596
620
  }
597
621
 
598
622
  // Re-install a specific plugin at the latest catalog version (force-overwrites).
599
- async upgradePlugin(pluginId: string, scope?: "user" | "project"): Promise<InstalledPluginEntry> {
623
+ async upgradePlugin(pluginId: string, scope?: "user" | "project" | "local"): Promise<InstalledPluginEntry> {
600
624
  const parsed = parsePluginId(pluginId);
601
625
  if (!parsed) {
602
626
  throw new Error(`Invalid plugin ID: "${pluginId}". Expected "name@marketplace".`);
@@ -611,7 +635,7 @@ export class MarketplaceManager {
611
635
  throw new Error(`Plugin "${pluginId}" is not installed`);
612
636
  }
613
637
 
614
- let resolvedScope: "user" | "project";
638
+ let resolvedScope: "user" | "project" | "local";
615
639
  if (inUser && inProject) {
616
640
  if (!scope) {
617
641
  throw new Error(
@@ -665,10 +689,10 @@ export class MarketplaceManager {
665
689
  // Only stale scopes are touched; a current user install is not re-installed when only
666
690
  // the project scope is stale. Per-entry failures are skipped — partial success is returned.
667
691
  async upgradeAllPlugins(): Promise<
668
- Array<{ pluginId: string; scope: "user" | "project"; from: string; to: string }>
692
+ Array<{ pluginId: string; scope: "user" | "project" | "local"; from: string; to: string }>
669
693
  > {
670
694
  const updates = await this.checkForUpdates();
671
- const results: Array<{ pluginId: string; scope: "user" | "project"; from: string; to: string }> = [];
695
+ const results: Array<{ pluginId: string; scope: "user" | "project" | "local"; from: string; to: string }> = [];
672
696
  for (const update of updates) {
673
697
  try {
674
698
  const entry = await this.upgradePlugin(update.pluginId, update.scope);
@@ -682,13 +706,19 @@ export class MarketplaceManager {
682
706
 
683
707
  // ── Private helpers ───────────────────────────────────────────────────────
684
708
 
685
- #registryPath(scope: "user" | "project"): string {
709
+ #registryPath(scope: "user" | "project" | "local"): string {
686
710
  if (scope === "project") {
687
711
  if (!this.#opts.projectInstalledRegistryPath) {
688
712
  throw new Error("project-scoped install requires running inside a project directory");
689
713
  }
690
714
  return this.#opts.projectInstalledRegistryPath;
691
715
  }
716
+ if (scope === "local") {
717
+ if (!this.#opts.localInstalledRegistryPath) {
718
+ throw new Error("local-scoped install requires running inside a project directory");
719
+ }
720
+ return this.#opts.localInstalledRegistryPath;
721
+ }
692
722
  return this.#opts.installedRegistryPath;
693
723
  }
694
724
 
@@ -76,6 +76,7 @@ export interface MarketplacePluginAuthor {
76
76
 
77
77
  export interface MarketplacePluginEntry {
78
78
  name: string;
79
+ displayName?: string;
79
80
  source: PluginSource;
80
81
  description?: string;
81
82
  version?: string;
@@ -87,6 +88,7 @@ export interface MarketplacePluginEntry {
87
88
  category?: string;
88
89
  tags?: string[];
89
90
  strict?: boolean;
91
+ defaultEnabled?: boolean;
90
92
  commands?: string | string[];
91
93
  agents?: string | string[];
92
94
  hooks?: string | Record<string, unknown>;
@@ -161,7 +163,7 @@ export interface InstalledPluginsRegistry {
161
163
  }
162
164
 
163
165
  export interface InstalledPluginEntry {
164
- scope: "user" | "project";
166
+ scope: "user" | "project" | "local";
165
167
  /** Absolute path to cached plugin directory. */
166
168
  installPath: string;
167
169
  version: string;
@@ -184,7 +186,7 @@ export interface InstalledPluginEntry {
184
186
  */
185
187
  export interface InstalledPluginSummary {
186
188
  id: string;
187
- scope: "user" | "project";
189
+ scope: "user" | "project" | "local";
188
190
  entries: InstalledPluginEntry[];
189
191
  /** Set when a user-scoped plugin is overridden by a project-scoped install. */
190
192
  shadowedBy?: "project";
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "19.5.0",
21
- "commit": "1845641b31e29a09b04c301d1bcfc297a719c5f9",
22
- "shortCommit": "1845641",
20
+ "version": "19.6.0",
21
+ "commit": "8505c9827499b1e269615db372fec4132b61c506",
22
+ "shortCommit": "8505c98",
23
23
  "branch": "main",
24
- "tag": "v19.5.0",
25
- "commitDate": "2026-06-04T16:42:25Z",
26
- "buildDate": "2026-06-04T17:10:47.523Z",
24
+ "tag": "v19.6.0",
25
+ "commitDate": "2026-06-04T18:26:24Z",
26
+ "buildDate": "2026-06-04T18:51:12.169Z",
27
27
  "dirty": true,
28
28
  "prNumber": "",
29
29
  "repoUrl": "https://github.com/f5xc-salesdemos/xcsh",
30
30
  "repoSlug": "f5xc-salesdemos/xcsh",
31
- "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/1845641b31e29a09b04c301d1bcfc297a719c5f9",
32
- "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.5.0"
31
+ "commitUrl": "https://github.com/f5xc-salesdemos/xcsh/commit/8505c9827499b1e269615db372fec4132b61c506",
32
+ "releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.6.0"
33
33
  };
@@ -17,7 +17,7 @@ export interface PluginItem {
17
17
  plugin: { name: string; version?: string; description?: string };
18
18
  marketplace: string;
19
19
  /** Scope of this entry. When set, appended to the label and forwarded to onSelect. */
20
- scope?: "user" | "project";
20
+ scope?: "user" | "project" | "local";
21
21
  }
22
22
 
23
23
  export class PluginSelectorComponent extends Container {
@@ -251,7 +251,7 @@ export class PluginDashboard extends Container {
251
251
  if (!plugin) return;
252
252
  const tabId = this.#activeTabId();
253
253
 
254
- if (tabId === "available" && !plugin.installed) {
254
+ if (tabId === "discover" && !plugin.installed) {
255
255
  await this.#installPlugin(plugin);
256
256
  } else if (tabId === "updates" && plugin.hasUpdate) {
257
257
  await this.#upgradePlugin(plugin);
@@ -320,7 +320,7 @@ export class PluginDashboard extends Container {
320
320
  #getHelpText(): string {
321
321
  const tabId = this.#activeTabId();
322
322
  switch (tabId) {
323
- case "available":
323
+ case "discover":
324
324
  return " ↑/↓: navigate Enter: install Tab: next tab Ctrl+R: reload Esc: close";
325
325
  case "updates":
326
326
  return " ↑/↓: navigate Enter: upgrade Tab: next tab Ctrl+R: reload Esc: close";
@@ -14,7 +14,7 @@ export class PluginInspectorPane implements Component {
14
14
  const lines: string[] = [];
15
15
  const p = this.plugin;
16
16
 
17
- lines.push(theme.bold(theme.fg("contentAccent", replaceTabs(p.name))));
17
+ lines.push(theme.bold(theme.fg("contentAccent", replaceTabs(p.displayName || p.name))));
18
18
  lines.push("");
19
19
 
20
20
  lines.push(`${theme.fg("muted", "Source:")} ${p.source}`);
@@ -22,7 +22,7 @@ export class PluginListPane implements Component {
22
22
 
23
23
  if (this.plugins.length === 0) {
24
24
  const msg =
25
- this.activeTab === "available"
25
+ this.activeTab === "discover"
26
26
  ? "No plugins available. Add a marketplace first."
27
27
  : this.activeTab === "updates"
28
28
  ? "All plugins are up to date."
@@ -71,7 +71,7 @@ export class PluginListPane implements Component {
71
71
  }
72
72
 
73
73
  parts.push(" ");
74
- parts.push(plugin.name);
74
+ parts.push(plugin.displayName || plugin.name);
75
75
 
76
76
  if (plugin.version) {
77
77
  parts.push(theme.fg("dim", ` v${plugin.version}`));
@@ -48,6 +48,7 @@ function catalogToDashboard(entry: MarketplacePluginEntry, marketplace: string):
48
48
  return {
49
49
  id: `${entry.name}@${marketplace}`,
50
50
  name: normalizePluginDisplayName(entry.name),
51
+ displayName: entry.displayName,
51
52
  marketplace,
52
53
  source: "marketplace",
53
54
  version: entry.version,
@@ -96,6 +97,7 @@ export async function loadAllPlugins(mgr: MarketplaceManager, npmMgr: PluginMana
96
97
  if (installedIds.has(pluginId)) {
97
98
  const existing = plugins.find(p => p.id === pluginId);
98
99
  if (existing) {
100
+ existing.displayName = existing.displayName || entry.displayName;
99
101
  existing.description = existing.description || entry.description;
100
102
  existing.category = existing.category || entry.category;
101
103
  existing.tags = existing.tags || entry.tags;
@@ -124,12 +126,12 @@ export async function loadAllPlugins(mgr: MarketplaceManager, npmMgr: PluginMana
124
126
  export function buildTabs(plugins: DashboardPlugin[]): PluginTab[] {
125
127
  const tabs: PluginTab[] = [];
126
128
  const installedCount = plugins.filter(p => p.installed).length;
127
- const availableCount = plugins.filter(p => !p.installed).length;
129
+ const discoverCount = plugins.filter(p => !p.installed).length;
128
130
  const updatesCount = plugins.filter(p => p.hasUpdate).length;
129
131
 
130
132
  tabs.push({ id: "installed", label: "Installed", count: installedCount });
131
- if (availableCount > 0) {
132
- tabs.push({ id: "available", label: "Available", count: availableCount });
133
+ if (discoverCount > 0) {
134
+ tabs.push({ id: "discover", label: "Discover", count: discoverCount });
133
135
  }
134
136
  if (updatesCount > 0) {
135
137
  tabs.push({ id: "updates", label: "Updates", count: updatesCount });
@@ -141,7 +143,7 @@ export function filterByTab(plugins: DashboardPlugin[], tabId: PluginTabId): Das
141
143
  switch (tabId) {
142
144
  case "installed":
143
145
  return plugins.filter(p => p.installed);
144
- case "available":
146
+ case "discover":
145
147
  return plugins.filter(p => !p.installed);
146
148
  case "updates":
147
149
  return plugins.filter(p => p.hasUpdate);
@@ -155,6 +157,7 @@ export function applySearch(plugins: DashboardPlugin[], query: string): Dashboar
155
157
  const q = query.toLowerCase();
156
158
  return plugins.filter(p => {
157
159
  if (p.name.toLowerCase().includes(q)) return true;
160
+ if (p.displayName?.toLowerCase().includes(q)) return true;
158
161
  if (p.description?.toLowerCase().includes(q)) return true;
159
162
  if (p.marketplace?.toLowerCase().includes(q)) return true;
160
163
  if (p.category?.toLowerCase().includes(q)) return true;
@@ -1,9 +1,10 @@
1
1
  export interface DashboardPlugin {
2
2
  id: string;
3
3
  name: string;
4
+ displayName?: string;
4
5
  marketplace?: string;
5
6
  source: "npm" | "marketplace";
6
- scope?: "user" | "project";
7
+ scope?: "user" | "project" | "local";
7
8
  version?: string;
8
9
  catalogVersion?: string;
9
10
  description?: string;
@@ -19,7 +20,7 @@ export interface DashboardPlugin {
19
20
  updateVersion?: string;
20
21
  }
21
22
 
22
- export type PluginTabId = "installed" | "available" | "updates";
23
+ export type PluginTabId = "installed" | "discover" | "updates";
23
24
 
24
25
  export interface PluginTab {
25
26
  id: PluginTabId;