Skip to content
Merged
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
58 changes: 57 additions & 1 deletion apps/cli/src/list-cmd.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,63 @@ describe('runPluginsCommand', () => {
const out = sink();
const code = await runPluginsCommand(['frob'], { cwd, home, output: out.stream });
expect(code).toBe(2);
expect(out.text()).toMatch(/Usage: deepcode plugins list/);
expect(out.text()).toMatch(/Usage: deepcode plugins/);
});

it('installs a local plugin and then lists it as trusted', async () => {
// A local plugin source dir.
const src = join(cwd, 'my-plugin');
await fs.mkdir(src, { recursive: true });
await fs.writeFile(
join(src, 'plugin.json'),
JSON.stringify({ name: 'localdemo', version: '0.1.0', description: 'local one' }),
);
const out = sink();
const code = await runPluginsCommand(['install', src], { cwd, home, output: out.stream });
expect(code).toBe(0);
expect(out.text()).toMatch(/Installed localdemo@0\.1\.0/);

// Now it's installed + trusted → appears in the loaded list (not "Not loaded").
const list = sink();
await runPluginsCommand(['list'], { cwd, home, output: list.stream, json: true });
const parsed = JSON.parse(list.text()) as { plugins: Array<{ name: string }> };
expect(parsed.plugins.some((p) => p.name === 'localdemo')).toBe(true);
});

it('uninstalls an installed plugin', async () => {
const src = join(cwd, 'p2');
await fs.mkdir(src, { recursive: true });
await fs.writeFile(join(src, 'plugin.json'), JSON.stringify({ name: 'p2', version: '1.0.0' }));
await runPluginsCommand(['install', src], { cwd, home, output: sink().stream });

const out = sink();
const code = await runPluginsCommand(['uninstall', 'p2'], { cwd, home, output: out.stream });
expect(code).toBe(0);
expect(out.text()).toMatch(/Uninstalled p2/);

const missing = sink();
const code2 = await runPluginsCommand(['uninstall', 'p2'], {
cwd,
home,
output: missing.stream,
});
expect(code2).toBe(1);
expect(missing.text()).toMatch(/No plugin named/);
});

it('install with no spec → usage exit 2; bad gh spec → error exit 1', async () => {
const noSpec = sink();
expect(await runPluginsCommand(['install'], { cwd, home, errOutput: noSpec.stream })).toBe(2);
expect(noSpec.text()).toMatch(/Usage: deepcode plugins install/);

const badGh = sink();
const code = await runPluginsCommand(['install', 'gh:not-a-valid-spec!!'], {
cwd,
home,
errOutput: badGh.stream,
});
expect(code).toBe(1);
expect(badGh.text()).toMatch(/Install failed.*Invalid GitHub spec/);
});
});

Expand Down
65 changes: 62 additions & 3 deletions apps/cli/src/list-cmd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@
// scripting and the desktop app.
// Spec: docs/DEVELOPMENT_PLAN.md §3.13 (skills) / §3.14 (plugins)

import { discoverPlugins, loadSettings, loadSkills } from '@deepcode/core';
import {
discoverPlugins,
installFromGithub,
installFromNpm,
installLocal,
loadSettings,
loadSkills,
uninstallPlugin,
} from '@deepcode/core';
import { resolve } from 'node:path';
import type { Writable } from 'node:stream';
import { resolveBuiltinSkillsDir } from './builtin-skills.js';

Expand Down Expand Up @@ -71,10 +80,16 @@ export async function listSkills(deps: ListCmdDeps): Promise<SkillRow[]> {

export async function runPluginsCommand(sub: string[], deps: ListCmdDeps): Promise<number> {
const out = deps.output ?? process.stdout;
if (sub[0] && sub[0] !== 'list') {
out.write('Usage: deepcode plugins list [--json]\n');
const err = deps.errOutput ?? process.stderr;
const cmd = sub[0];

if (cmd === 'install') return pluginInstall(sub.slice(1), deps, out, err);
if (cmd === 'uninstall' || cmd === 'remove') return pluginUninstall(sub[1], deps, out, err);
if (cmd && cmd !== 'list') {
out.write('Usage: deepcode plugins [list [--json] | install <spec> | uninstall <name>]\n');
return 2;
}

const { rows, issues } = await listPlugins(deps);
if (deps.json || sub.includes('--json')) {
out.write(JSON.stringify({ plugins: rows, issues }, null, 2) + '\n');
Expand All @@ -98,6 +113,50 @@ export async function runPluginsCommand(sub: string[], deps: ListCmdDeps): Promi
return 0;
}

async function pluginInstall(
args: string[],
deps: ListCmdDeps,
out: Writable,
err: Writable,
): Promise<number> {
const spec = args[0];
if (!spec) {
err.write(
'Usage: deepcode plugins install <gh:owner/repo[@ref] | <name>@npm | ./local/path>\n',
);
return 2;
}
try {
const installed = spec.startsWith('gh:')
? await installFromGithub(spec, { home: deps.home })
: /@npm$/.test(spec)
? await installFromNpm(spec, { home: deps.home })
: await installLocal({ sourcePath: resolve(deps.cwd, spec), home: deps.home });
out.write(
`✓ Installed ${installed.manifest.name}@${installed.manifest.version} (trusted: user).\n`,
);
return 0;
} catch (e) {
err.write(`Install failed: ${(e as Error).message}\n`);
return 1;
}
}

async function pluginUninstall(
name: string | undefined,
deps: ListCmdDeps,
out: Writable,
err: Writable,
): Promise<number> {
if (!name) {
err.write('Usage: deepcode plugins uninstall <name>\n');
return 2;
}
const removed = await uninstallPlugin(name, deps.home);
out.write(removed ? `✓ Uninstalled ${name}.\n` : `No plugin named "${name}".\n`);
return removed ? 0 : 1;
}

export async function runSkillsCommand(sub: string[], deps: ListCmdDeps): Promise<number> {
const out = deps.output ?? process.stdout;
if (sub[0] && sub[0] !== 'list') {
Expand Down
2 changes: 2 additions & 0 deletions apps/cli/src/parse-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,8 @@ USAGE
deepcode mcp serve Expose DeepCode tools as an MCP server (stdio)
deepcode trust [--plan-only] Trust this directory's project config (hooks/MCP/...)
deepcode plugins list [--json] List installed plugins
deepcode plugins install <spec> Install a plugin (gh:owner/repo | name@npm | ./path)
deepcode plugins uninstall <name> Remove an installed plugin
deepcode skills list [--json] List available skills

MODE
Expand Down
Loading