Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions src/cli/commands/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
} from '../../core/user-workspace.js';
import { updatePlugin, type InstalledPluginUpdateResult } from '../../core/plugin.js';
import { getAllSkillsFromPlugins } from '../../core/skills.js';
import { getWorkspaceStatus } from '../../core/status.js';
import { parseMarketplaceManifest } from '../../utils/marketplace-manifest-parser.js';
import { isJsonMode, jsonOutput } from '../json-output.js';
import { buildDescription, conciseSubcommands } from '../help.js';
Expand Down Expand Up @@ -772,6 +773,23 @@ const pluginListCmd = command({
const projectPlugins = await getInstalledProjectPlugins(process.cwd());
const allInstalled = [...userPlugins, ...projectPlugins];

// Build a source→kind map so each listed entry can be labelled
// 'skill' (root SKILL.md, no skills/ subdir) or 'plugin' (everything
// else, including not-yet-resolved sources).
const kindBySource = new Map<string, 'skill' | 'plugin'>();
try {
const status = await getWorkspaceStatus(process.cwd());
for (const p of [
...status.plugins,
...(status.userPlugins ?? []),
]) {
kindBySource.set(p.source, p.kind);
}
} catch {
// Best-effort: if status lookup fails, fall back to labelling
// everything as 'plugin' below.
}

// Load native plugins from sync state
const userSyncState = await loadSyncState(getAllagentsDir());
const projectSyncState = cwdIsHome ? null : await loadSyncState(process.cwd());
Expand All @@ -782,6 +800,7 @@ const pluginListCmd = command({
name: string;
marketplace: string;
scope: 'user' | 'project';
kind: 'skill' | 'plugin';
fileClients: string[];
nativeClients: string[];
}
Expand All @@ -794,6 +813,7 @@ const pluginListCmd = command({
name: p.name,
marketplace: p.marketplace,
scope: p.scope,
kind: kindBySource.get(p.spec) ?? 'plugin',
fileClients: pluginClients.get(`${p.spec}:${p.scope}`) ?? [],
nativeClients: [],
});
Expand Down Expand Up @@ -821,6 +841,7 @@ const pluginListCmd = command({
name: parsed?.plugin ?? spec,
marketplace: parsed?.marketplaceName ?? '',
scope,
kind: kindBySource.get(spec) ?? 'plugin',
fileClients: [],
nativeClients: [client],
});
Expand All @@ -840,6 +861,7 @@ const pluginListCmd = command({
name: p.name,
marketplace: p.marketplace,
scope: p.scope,
kind: p.kind,
...(p.fileClients.length > 0 && { clients: p.fileClients }),
...(p.nativeClients.length > 0 && { nativeClients: p.nativeClients }),
})),
Expand All @@ -858,9 +880,13 @@ const pluginListCmd = command({
return;
}

console.log('Installed plugins:\n');
const skillCount = plugins.filter((p) => p.kind === 'skill').length;
const pluginCount = plugins.length - skillCount;

console.log('Installed:\n');
for (const p of plugins) {
console.log(` ❯ ${p.spec}`);
console.log(` Type: ${p.kind}`);
console.log(` Scope: ${p.scope}`);

const hasClients = p.fileClients.length > 0 || p.nativeClients.length > 0;
Expand All @@ -874,7 +900,10 @@ const pluginListCmd = command({
console.log('');
}

console.log(`Total: ${plugins.length} installed`);
const summaryParts: string[] = [];
if (pluginCount > 0) summaryParts.push(`${pluginCount} plugin${pluginCount === 1 ? '' : 's'}`);
if (skillCount > 0) summaryParts.push(`${skillCount} skill${skillCount === 1 ? '' : 's'}`);
console.log(`Total: ${summaryParts.join(', ')}`);
} catch (error) {
if (error instanceof Error) {
if (isJsonMode()) {
Expand Down
14 changes: 7 additions & 7 deletions src/cli/commands/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,19 +467,19 @@ function formatTimingMs(ms: number): string {
function formatPluginStatusLine(plugin: {
source: string;
type: 'local' | 'github' | 'marketplace';
kind: 'skill' | 'plugin';
available: boolean;
}): string {
const status = plugin.available ? '✓' : '✗';
let typeLabel: string | undefined;
const labels: string[] = [plugin.kind];
if (plugin.type === 'marketplace') {
typeLabel = plugin.available ? undefined : 'not synced';
if (!plugin.available) labels.push('not synced');
} else if (plugin.type === 'github') {
typeLabel = plugin.available ? 'cached' : 'not cached';
labels.push(plugin.available ? 'cached' : 'not cached');
} else {
typeLabel = 'local';
labels.push('local');
}
const suffix = typeLabel ? ` (${typeLabel})` : '';
return `${status} ${formatPluginSource(plugin.source)}${suffix}`;
return `${status} ${formatPluginSource(plugin.source)} (${labels.join(', ')})`;
}

const statusCmd = command({
Expand Down Expand Up @@ -838,7 +838,7 @@ const repoCmd = conciseSubcommands({
// workspace subcommands group
// =============================================================================

export { syncCmd, initCmd };
export { syncCmd, initCmd, statusCmd };

export const workspaceCmd = conciseSubcommands({
name: 'workspace',
Expand Down
3 changes: 2 additions & 1 deletion src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { run } from 'cmd-ts';
import { conciseSubcommands } from './help.js';
import { workspaceCmd, syncCmd, initCmd } from './commands/workspace.js';
import { workspaceCmd, syncCmd, initCmd, statusCmd } from './commands/workspace.js';
import { pluginCmd } from './commands/plugin.js';
import { mcpCmd } from './commands/mcp.js';
import { selfCmd } from './commands/self.js';
Expand Down Expand Up @@ -31,6 +31,7 @@ const app = conciseSubcommands({
cmds: {
init: initCmd,
update: syncCmd,
status: statusCmd,
workspace: workspaceCmd,
plugin: pluginCmd,
mcp: mcpCmd,
Expand Down
8 changes: 4 additions & 4 deletions src/cli/metadata/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,15 @@ export const marketplaceBrowseMeta: AgentCommandMeta = {

export const pluginListMeta: AgentCommandMeta = {
command: 'plugin list',
description: 'List installed plugins',
whenToUse: 'To see which plugins are currently installed in your workspace',
description: 'List installed plugins and standalone skills',
whenToUse: 'To see which plugins and skills are currently installed in your workspace',
examples: [
'allagents plugin list',
],
expectedOutput:
'Lists installed plugins with their marketplace and scope. If none installed, suggests using marketplace browse.',
'Lists installed items with type (plugin or skill), marketplace, and scope. If none installed, suggests using marketplace browse.',
outputSchema: {
plugins: [{ name: 'string', marketplace: 'string', scope: 'string' }],
plugins: [{ name: 'string', marketplace: 'string', scope: 'string', kind: 'string' }],
total: 'number',
},
};
Expand Down
9 changes: 5 additions & 4 deletions src/cli/metadata/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,17 @@ export const pruneMeta: AgentCommandMeta = {
};

export const statusMeta: AgentCommandMeta = {
command: 'workspace status',
command: 'status',
description: 'Show sync status of plugins',
whenToUse: 'To check which plugins are configured and whether they are available locally',
whenToUse: 'To check which plugins and skills are configured and whether they are available locally',
examples: [
'allagents status',
'allagents workspace status',
],
expectedOutput:
'Lists all configured plugins with availability status and configured clients. Exit 0 on success, exit 1 if workspace is not initialized.',
'Lists all configured plugins/skills with availability status and configured clients. Exit 0 on success, exit 1 if workspace is not initialized.',
outputSchema: {
plugins: [{ source: 'string', type: 'string', available: 'boolean' }],
plugins: [{ source: 'string', type: 'string', kind: 'string', available: 'boolean' }],
clients: ['string'],
},
};
4 changes: 2 additions & 2 deletions src/cli/tui/actions/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,15 +303,15 @@ export async function runPlugins(context: TuiContext, cache?: TuiCache): Promise
options.push({
label: plugin.source,
value: key,
hint: `${plugin.type} · project`,
hint: `${plugin.kind} · ${plugin.type} · project`,
});
}
for (const plugin of status.userPlugins ?? []) {
const key = `user:${plugin.source}`;
options.push({
label: plugin.source,
value: key,
hint: `${plugin.type} · user`,
hint: `${plugin.kind} · ${plugin.type} · user`,
});
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/cli/tui/actions/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export async function runStatus(context: TuiContext, cache?: TuiCache): Promise<
lines.push('Project plugins:');
for (const plugin of status.plugins) {
const icon = plugin.available ? '\u2713' : '\u2717';
lines.push(` ${icon} ${plugin.source} (${plugin.type})`);
lines.push(` ${icon} ${plugin.source} (${plugin.kind}, ${plugin.type})`);
}
}

Expand All @@ -56,7 +56,7 @@ export async function runStatus(context: TuiContext, cache?: TuiCache): Promise<
lines.push('User plugins:');
for (const plugin of userPlugins) {
const icon = plugin.available ? '\u2713' : '\u2717';
lines.push(` ${icon} ${plugin.source} (${plugin.type})`);
lines.push(` ${icon} ${plugin.source} (${plugin.kind}, ${plugin.type})`);
}
}

Expand Down
42 changes: 42 additions & 0 deletions src/core/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { parseWorkspaceConfig } from '../utils/workspace-parser.js';
import { getPluginSource, getClientTypes } from '../models/workspace-config.js';
import {
parsePluginSource,
parseGitHubUrl,
getPluginCachePath,
type ParsedPluginSource,
} from '../utils/plugin-path.js';
Expand All @@ -20,12 +21,39 @@ import { getUserWorkspaceConfig, isUserConfigPath } from './user-workspace.js';
export interface PluginStatus {
source: string;
type: 'local' | 'github' | 'marketplace';
/**
* 'skill' when the resolved path looks like a single-skill source (root
* SKILL.md, no skills/ subdir — matches the auto-wrap layout from #232/#249).
* 'plugin' otherwise, including when the path can't be inspected (not cached,
* not synced, missing locally).
*/
kind: 'skill' | 'plugin';
available: boolean;
path: string;
owner?: string;
repo?: string;
}

/**
* Classify a resolved cache/local path as a standalone skill or a plugin
* bundle. A "skill" has a SKILL.md at its root and no skills/ subdir; anything
* else (including paths that don't exist) is treated as a plugin.
*/
function classifyKind(path: string): 'skill' | 'plugin' {
if (!path) return 'plugin';
try {
if (
existsSync(join(path, 'SKILL.md')) &&
!existsSync(join(path, 'skills'))
) {
return 'skill';
}
} catch {
// ignore — default to plugin
}
return 'plugin';
}

/**
* Result of workspace status check
*/
Expand Down Expand Up @@ -106,9 +134,21 @@ function getPluginStatus(parsed: ParsedPluginSource): PluginStatus {
: '';
const available = cachePath ? existsSync(cachePath) : false;

// For GitHub plugins with a subpath, classify the resolved subdir rather
// than the repo root — that's what users actually consume. ParsedPluginSource
// drops the subpath, so re-parse the original URL to recover it.
const subpath = parseGitHubUrl(parsed.original)?.subpath;
const classifyPath =
available && cachePath
? subpath
? join(cachePath, subpath)
: cachePath
: '';

return {
source: parsed.original,
type: 'github',
kind: classifyKind(classifyPath),
available,
path: cachePath,
...(parsed.owner && { owner: parsed.owner }),
Expand All @@ -122,6 +162,7 @@ function getPluginStatus(parsed: ParsedPluginSource): PluginStatus {
return {
source: parsed.original,
type: 'local',
kind: classifyKind(available ? parsed.normalized : ''),
available,
path: parsed.normalized,
};
Expand Down Expand Up @@ -156,6 +197,7 @@ async function getMarketplacePluginStatus(spec: string): Promise<PluginStatus> {
return {
source: spec,
type: 'marketplace',
kind: classifyKind(resolved.success ? (resolved.path ?? '') : ''),
available: resolved.success,
path: resolved.path ?? '',
};
Expand Down
6 changes: 3 additions & 3 deletions tests/unit/cli/agent-help.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ describe('agent command metadata', () => {
'skill list',
'skill remove',
'skill search',
'status',
'update',
'workspace status',
]);
});

Expand Down Expand Up @@ -131,8 +131,8 @@ describe('agent command metadata', () => {
expect(installCmd.positionals![0].required).toBe(true);
});

test('workspace status has no positionals or options', () => {
const statusCmd = allCommands.find((c) => c.command === 'workspace status')!;
test('status has no positionals or options', () => {
const statusCmd = allCommands.find((c) => c.command === 'status')!;
expect(statusCmd.positionals).toBeUndefined();
expect(statusCmd.options).toBeUndefined();
});
Expand Down