@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 +7 -7
- package/src/discovery/claude-plugins.ts +9 -8
- package/src/discovery/helpers.ts +23 -2
- package/src/extensibility/plugins/marketplace/fetcher.ts +3 -2
- package/src/extensibility/plugins/marketplace/manager.ts +45 -15
- package/src/extensibility/plugins/marketplace/types.ts +4 -2
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/modes/components/plugin-selector.ts +1 -1
- package/src/modes/components/plugins/plugin-dashboard.ts +2 -2
- package/src/modes/components/plugins/plugin-inspector-pane.ts +1 -1
- package/src/modes/components/plugins/plugin-list-pane.ts +2 -2
- package/src/modes/components/plugins/state-manager.ts +7 -4
- package/src/modes/components/plugins/types.ts +3 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "19.
|
|
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.
|
|
54
|
-
"@f5xc-salesdemos/pi-agent-core": "19.
|
|
55
|
-
"@f5xc-salesdemos/pi-ai": "19.
|
|
56
|
-
"@f5xc-salesdemos/pi-natives": "19.
|
|
57
|
-
"@f5xc-salesdemos/pi-tui": "19.
|
|
58
|
-
"@f5xc-salesdemos/pi-utils": "19.
|
|
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
|
}
|
package/src/discovery/helpers.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
281
|
-
|
|
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
|
-
|
|
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<
|
|
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"]> = [
|
|
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.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "19.6.0",
|
|
21
|
+
"commit": "8505c9827499b1e269615db372fec4132b61c506",
|
|
22
|
+
"shortCommit": "8505c98",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v19.
|
|
25
|
-
"commitDate": "2026-06-
|
|
26
|
-
"buildDate": "2026-06-
|
|
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/
|
|
32
|
-
"releaseUrl": "https://github.com/f5xc-salesdemos/xcsh/releases/tag/v19.
|
|
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 === "
|
|
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 "
|
|
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 === "
|
|
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
|
|
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 (
|
|
132
|
-
tabs.push({ id: "
|
|
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 "
|
|
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" | "
|
|
23
|
+
export type PluginTabId = "installed" | "discover" | "updates";
|
|
23
24
|
|
|
24
25
|
export interface PluginTab {
|
|
25
26
|
id: PluginTabId;
|