Skip to content

Commit c45a042

Browse files
authored
fix: remove warning when Claude/Copilot CLI is missing (#174)
* fix(sync): treat missing native CLI as informational * feat(cli): gate info sync messages behind --verbose * fix(native): include provider on native failures and clarify copilot path errors
1 parent fcbb571 commit c45a042

9 files changed

Lines changed: 117 additions & 13 deletions

File tree

src/cli/commands/workspace.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,10 @@ const syncCmd = command({
9999
offline: flag({ long: 'offline', description: 'Use cached plugins without fetching latest from remote' }),
100100
dryRun: flag({ long: 'dry-run', short: 'n', description: 'Simulate sync without making changes' }),
101101
force: flag({ long: 'force', short: 'f', description: 'Overwrite existing MCP server entries that differ from plugin config' }),
102+
verbose: flag({ long: 'verbose', short: 'v', description: 'Show informational sync messages' }),
102103
client: option({ type: optional(string), long: 'client', short: 'c', description: 'Sync only the specified client (e.g., opencode, claude)' }),
103104
},
104-
handler: async ({ offline, dryRun, force, client }) => {
105+
handler: async ({ offline, dryRun, force, verbose, client }) => {
105106
try {
106107
if (!isJsonMode() && dryRun) {
107108
console.log('Dry run mode - no changes will be made\n');
@@ -210,6 +211,14 @@ const syncCmd = command({
210211
}
211212
}
212213

214+
// Show informational messages
215+
if (verbose && result.messages && result.messages.length > 0) {
216+
console.log('');
217+
for (const message of result.messages) {
218+
console.log(` ${message}`);
219+
}
220+
}
221+
213222
// Print MCP server sync results
214223
if (result.mcpResult) {
215224
const mcpLines = formatMcpResult(result.mcpResult);

src/cli/format-sync.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,9 @@ export function formatNativeResult(nativeResult: NativeSyncResult): string[] {
5050
lines.push(` + ${plugin} (installed via native CLI)`);
5151
}
5252

53-
for (const { plugin, error } of nativeResult.pluginsFailed) {
54-
lines.push(` \u2717 ${plugin}: ${error}`);
53+
for (const { client, plugin, error } of nativeResult.pluginsFailed) {
54+
const provider = client ? `[${client}] ` : '';
55+
lines.push(` \u2717 ${provider}${plugin}: ${error}`);
5556
}
5657

5758
for (const plugin of nativeResult.skipped) {
@@ -70,6 +71,7 @@ export function buildSyncData(result: SyncResult) {
7071
generated: result.totalGenerated,
7172
failed: result.totalFailed,
7273
skipped: result.totalSkipped,
74+
...(result.messages && result.messages.length > 0 && { messages: result.messages }),
7375
plugins: result.pluginResults.map((pr) => ({
7476
plugin: pr.plugin,
7577
success: pr.success,

src/cli/metadata/workspace.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,14 @@ export const syncMeta: AgentCommandMeta = {
4040
'allagents workspace sync --dry-run',
4141
'allagents workspace sync --client claude',
4242
'allagents workspace sync --offline',
43+
'allagents workspace sync --verbose',
4344
],
4445
expectedOutput:
4546
'Lists synced files with status per plugin. Exit 0 on success, exit 1 if any files failed.',
4647
options: [
4748
{ flag: '--offline', type: 'boolean', description: 'Use cached plugins without fetching latest from remote' },
4849
{ flag: '--dry-run', short: '-n', type: 'boolean', description: 'Simulate sync without making changes' },
50+
{ flag: '--verbose', short: '-v', type: 'boolean', description: 'Show informational sync messages' },
4951
{ flag: '--client', short: '-c', type: 'string', description: 'Sync only the specified client (e.g., opencode, claude)' },
5052
],
5153
outputSchema: {

src/core/native/copilot.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,13 @@ export class CopilotNativeClient implements NativeClient {
100100
if (installResult.success) {
101101
result.pluginsInstalled.push(spec);
102102
} else {
103+
const rawError = installResult.error ?? 'Unknown error';
104+
const error = rawError.includes('Plugin path escapes marketplace directory')
105+
? `${rawError} (Copilot rejected a plugin path from this marketplace manifest. Use file install for copilot to avoid native install for this plugin.)`
106+
: rawError;
103107
result.pluginsFailed.push({
104108
plugin: spec,
105-
error: installResult.error ?? 'Unknown error',
109+
error,
106110
});
107111
}
108112
}

src/core/native/types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,16 @@ export interface NativeCommandResult {
66
error?: string;
77
}
88

9+
export interface NativePluginFailure {
10+
plugin: string;
11+
error: string;
12+
client?: string;
13+
}
14+
915
export interface NativeSyncResult {
1016
marketplacesAdded: string[];
1117
pluginsInstalled: string[];
12-
pluginsFailed: { plugin: string; error: string }[];
18+
pluginsFailed: NativePluginFailure[];
1319
skipped: string[];
1420
}
1521

src/core/sync.ts

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ export interface SyncResult {
130130
error?: string;
131131
/** Warnings for plugins that were skipped during sync */
132132
warnings?: string[];
133+
/** Informational messages (non-warning) */
134+
messages?: string[];
133135
/** Result of syncing MCP server configs to VS Code */
134136
mcpResult?: McpMergeResult;
135137
/** Result of native CLI plugin installations */
@@ -141,6 +143,7 @@ export interface SyncResult {
141143
*/
142144
export function mergeSyncResults(a: SyncResult, b: SyncResult): SyncResult {
143145
const warnings = [...(a.warnings || []), ...(b.warnings || [])];
146+
const messages = [...(a.messages || []), ...(b.messages || [])];
144147
const purgedPaths = [...(a.purgedPaths || []), ...(b.purgedPaths || [])];
145148
// Use whichever mcpResult is present (only user-scope sync produces one)
146149
const mcpResult = a.mcpResult ?? b.mcpResult;
@@ -161,6 +164,7 @@ export function mergeSyncResults(a: SyncResult, b: SyncResult): SyncResult {
161164
totalSkipped: a.totalSkipped + b.totalSkipped,
162165
totalGenerated: a.totalGenerated + b.totalGenerated,
163166
...(warnings.length > 0 && { warnings }),
167+
...(messages.length > 0 && { messages }),
164168
...(purgedPaths.length > 0 && { purgedPaths }),
165169
...(mcpResult && { mcpResult }),
166170
...(nativeResult && { nativeResult }),
@@ -275,6 +279,13 @@ function collectNativePluginSources(validPlugins: ValidatedPlugin[]): {
275279
return { pluginsByClient, marketplaceSourcesByClient };
276280
}
277281

282+
function attachNativeClientContext(result: NativeSyncResult, clientType: ClientType): NativeSyncResult {
283+
return {
284+
...result,
285+
pluginsFailed: result.pluginsFailed.map((failure) => ({ ...failure, client: clientType })),
286+
};
287+
}
288+
278289
function collectSyncClients(
279290
clientEntries: ClientEntry[],
280291
plans: PluginSyncPlan[],
@@ -1316,6 +1327,7 @@ export async function syncWorkspace(
13161327
...planWarnings,
13171328
...failedValidations.map((v) => `${v.plugin}: ${v.error} (skipped)`),
13181329
];
1330+
const messages: string[] = [];
13191331

13201332
// If ALL plugins failed, abort
13211333
if (validPlugins.length === 0 && filteredPlans.length > 0) {
@@ -1412,7 +1424,7 @@ export async function syncWorkspace(
14121424
if (!cliAvailable) {
14131425
const sources = nativePluginsByClient.get(clientType);
14141426
if (sources && sources.length > 0) {
1415-
warnings.push(`Native install: ${clientType} CLI not found, skipping native plugin installation`);
1427+
messages.push(`Native install: ${clientType} CLI not found, skipping native plugin installation`);
14161428
}
14171429
continue;
14181430
}
@@ -1443,7 +1455,12 @@ export async function syncWorkspace(
14431455

14441456
// Install
14451457
if (currentSources.length > 0) {
1446-
perClientResults.push(await nativeClient.syncPlugins(currentSources, 'project', { cwd: workspacePath }));
1458+
perClientResults.push(
1459+
attachNativeClientContext(
1460+
await nativeClient.syncPlugins(currentSources, 'project', { cwd: workspacePath }),
1461+
clientType,
1462+
),
1463+
);
14471464
}
14481465
}
14491466

@@ -1455,7 +1472,12 @@ export async function syncWorkspace(
14551472
for (const [clientType, sources] of nativePluginsByClient) {
14561473
const nativeClient = getNativeClient(clientType);
14571474
if (nativeClient && sources.length > 0) {
1458-
perClientResults.push(await nativeClient.syncPlugins(sources, 'project', { cwd: workspacePath, dryRun: true }));
1475+
perClientResults.push(
1476+
attachNativeClientContext(
1477+
await nativeClient.syncPlugins(sources, 'project', { cwd: workspacePath, dryRun: true }),
1478+
clientType,
1479+
),
1480+
);
14591481
}
14601482
}
14611483
if (perClientResults.length > 0) {
@@ -1654,6 +1676,7 @@ export async function syncWorkspace(
16541676
totalGenerated,
16551677
purgedPaths,
16561678
...(warnings.length > 0 && { warnings }),
1679+
...(messages.length > 0 && { messages }),
16571680
...(nativeResult && { nativeResult }),
16581681
};
16591682
}
@@ -1702,6 +1725,7 @@ export async function syncUserWorkspace(
17021725
...planWarnings,
17031726
...failedValidations.map((v) => `${v.plugin}: ${v.error} (skipped)`),
17041727
];
1728+
const messages: string[] = [];
17051729

17061730
// If ALL plugins failed, abort
17071731
if (validPlugins.length === 0 && pluginPlans.length > 0) {
@@ -1808,7 +1832,7 @@ export async function syncUserWorkspace(
18081832
if (!cliAvailable) {
18091833
const sources = nativePluginsByClient.get(clientType);
18101834
if (sources && sources.length > 0) {
1811-
warnings.push(`Native install: ${clientType} CLI not found, skipping native plugin installation`);
1835+
messages.push(`Native install: ${clientType} CLI not found, skipping native plugin installation`);
18121836
}
18131837
continue;
18141838
}
@@ -1838,7 +1862,12 @@ export async function syncUserWorkspace(
18381862

18391863
// Install
18401864
if (currentSources.length > 0) {
1841-
perClientResults.push(await nativeClient.syncPlugins(currentSources, 'user'));
1865+
perClientResults.push(
1866+
attachNativeClientContext(
1867+
await nativeClient.syncPlugins(currentSources, 'user'),
1868+
clientType,
1869+
),
1870+
);
18421871
}
18431872
}
18441873

@@ -1850,7 +1879,12 @@ export async function syncUserWorkspace(
18501879
for (const [clientType, sources] of nativePluginsByClient) {
18511880
const nativeClient = getNativeClient(clientType);
18521881
if (nativeClient && sources.length > 0) {
1853-
perClientResults.push(await nativeClient.syncPlugins(sources, 'user', { dryRun: true }));
1882+
perClientResults.push(
1883+
attachNativeClientContext(
1884+
await nativeClient.syncPlugins(sources, 'user', { dryRun: true }),
1885+
clientType,
1886+
),
1887+
);
18541888
}
18551889
}
18561890
if (perClientResults.length > 0) {
@@ -1891,6 +1925,7 @@ export async function syncUserWorkspace(
18911925
totalSkipped,
18921926
totalGenerated,
18931927
...(warnings.length > 0 && { warnings }),
1928+
...(messages.length > 0 && { messages }),
18941929
...(mcpResult && { mcpResult }),
18951930
...(nativeResult && { nativeResult }),
18961931
};

tests/unit/cli/agent-help.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ describe('agent command metadata', () => {
9393
test('workspace sync has expected options', () => {
9494
const syncCmd = allCommands.find((c) => c.command === 'workspace sync')!;
9595
expect(syncCmd.options).toBeInstanceOf(Array);
96-
expect(syncCmd.options!.length).toBe(3);
96+
expect(syncCmd.options!.length).toBe(4);
9797

9898
const dryRun = syncCmd.options!.find((o) => o.flag === '--dry-run');
9999
expect(dryRun).toBeDefined();
@@ -104,6 +104,11 @@ describe('agent command metadata', () => {
104104
expect(client).toBeDefined();
105105
expect(client!.type).toBe('string');
106106
expect(client!.short).toBe('-c');
107+
108+
const verbose = syncCmd.options!.find((o) => o.flag === '--verbose');
109+
expect(verbose).toBeDefined();
110+
expect(verbose!.type).toBe('boolean');
111+
expect(verbose!.short).toBe('-v');
107112
});
108113

109114
test('plugin install has required positional', () => {

tests/unit/cli/format-sync.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, expect, test } from 'bun:test';
2-
import { formatMcpResult } from '../../../src/cli/format-sync.js';
2+
import { formatMcpResult, formatNativeResult } from '../../../src/cli/format-sync.js';
33
import type { McpMergeResult } from '../../../src/core/vscode-mcp.js';
4+
import type { NativeSyncResult } from '../../../src/core/native/types.js';
45

56
function makeResult(overrides: Partial<McpMergeResult> = {}): McpMergeResult {
67
return {
@@ -67,3 +68,20 @@ describe('formatMcpResult', () => {
6768
expect(lines.some((l) => l.includes('File modified'))).toBe(false);
6869
});
6970
});
71+
72+
describe('formatNativeResult', () => {
73+
test('includes provider name when present on failed native installs', () => {
74+
const result: NativeSyncResult = {
75+
marketplacesAdded: [],
76+
pluginsInstalled: [],
77+
pluginsFailed: [
78+
{ client: 'copilot', plugin: 'glow@wtg-ai-prompts', error: 'boom' },
79+
],
80+
skipped: [],
81+
};
82+
83+
expect(formatNativeResult(result)).toEqual([
84+
' ✗ [copilot] glow@wtg-ai-prompts: boom',
85+
]);
86+
});
87+
});

tests/unit/core/sync-merge.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,29 @@ describe('mergeSyncResults', () => {
7676
expect(merged.warnings).toEqual(['warn1', 'warn2']);
7777
});
7878

79+
test('merges messages from both results', () => {
80+
const a: SyncResult = {
81+
success: true,
82+
pluginResults: [],
83+
totalCopied: 0,
84+
totalFailed: 0,
85+
totalSkipped: 0,
86+
totalGenerated: 0,
87+
messages: ['msg1'],
88+
};
89+
const b: SyncResult = {
90+
success: true,
91+
pluginResults: [],
92+
totalCopied: 0,
93+
totalFailed: 0,
94+
totalSkipped: 0,
95+
totalGenerated: 0,
96+
messages: ['msg2'],
97+
};
98+
const merged = mergeSyncResults(a, b);
99+
expect(merged.messages).toEqual(['msg1', 'msg2']);
100+
});
101+
79102
test('merges nativeResult from both results', () => {
80103
const a: SyncResult = {
81104
success: true,

0 commit comments

Comments
 (0)