Skip to content

Commit 41da38c

Browse files
author
Mahima Shanware
committed
feat(extensions): add support for plan directory in extension manifest
1 parent dd9ccc9 commit 41da38c

6 files changed

Lines changed: 130 additions & 7 deletions

File tree

packages/cli/src/config/config.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import {
1919
debugLogger,
2020
ApprovalMode,
2121
type MCPServerConfig,
22+
type GeminiCLIExtension,
23+
Storage,
2224
} from '@google/gemini-cli-core';
2325
import { loadCliConfig, parseArguments, type CliArgs } from './config.js';
2426
import {
@@ -3524,4 +3526,75 @@ describe('loadCliConfig mcpEnabled', () => {
35243526
expect(config.getAllowedMcpServers()).toEqual(['serverA']);
35253527
expect(config.getBlockedMcpServers()).toEqual(['serverB']);
35263528
});
3529+
3530+
describe('extension plan settings', () => {
3531+
beforeEach(() => {
3532+
vi.spyOn(Storage.prototype, 'getProjectTempDir').mockReturnValue(
3533+
'/tmp/test-project',
3534+
);
3535+
});
3536+
3537+
it('should use plan directory from active extension when user has not specified one', async () => {
3538+
process.argv = ['node', 'script.js'];
3539+
const settings = createTestMergedSettings({
3540+
experimental: { plan: true },
3541+
});
3542+
const argv = await parseArguments(settings);
3543+
3544+
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([
3545+
{
3546+
name: 'ext-plan',
3547+
isActive: true,
3548+
plan: { directory: 'ext-plans-dir' },
3549+
} as unknown as GeminiCLIExtension,
3550+
]);
3551+
3552+
const config = await loadCliConfig(settings, 'test-session', argv);
3553+
expect(config.storage.getPlansDir()).toContain('ext-plans-dir');
3554+
});
3555+
3556+
it('should NOT use plan directory from active extension when user has specified one', async () => {
3557+
process.argv = ['node', 'script.js'];
3558+
const settings = createTestMergedSettings({
3559+
experimental: { plan: true },
3560+
general: {
3561+
plan: { directory: 'user-plans-dir' },
3562+
},
3563+
});
3564+
const argv = await parseArguments(settings);
3565+
3566+
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([
3567+
{
3568+
name: 'ext-plan',
3569+
isActive: true,
3570+
plan: { directory: 'ext-plans-dir' },
3571+
} as unknown as GeminiCLIExtension,
3572+
]);
3573+
3574+
const config = await loadCliConfig(settings, 'test-session', argv);
3575+
expect(config.storage.getPlansDir()).toContain('user-plans-dir');
3576+
expect(config.storage.getPlansDir()).not.toContain('ext-plans-dir');
3577+
});
3578+
3579+
it('should NOT use plan directory from inactive extension', async () => {
3580+
process.argv = ['node', 'script.js'];
3581+
const settings = createTestMergedSettings({
3582+
experimental: { plan: true },
3583+
});
3584+
const argv = await parseArguments(settings);
3585+
3586+
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([
3587+
{
3588+
name: 'ext-plan',
3589+
isActive: false,
3590+
plan: { directory: 'ext-plans-dir-inactive' },
3591+
} as unknown as GeminiCLIExtension,
3592+
]);
3593+
3594+
const config = await loadCliConfig(settings, 'test-session', argv);
3595+
expect(config.storage.getPlansDir()).not.toContain(
3596+
'ext-plans-dir-inactive',
3597+
);
3598+
});
3599+
});
35273600
});

packages/cli/src/config/config.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,10 @@ export async function loadCliConfig(
511511
});
512512
await extensionManager.loadExtensions();
513513

514+
const extensionPlanSettings = extensionManager
515+
.getExtensions()
516+
.find((ext) => ext.isActive && ext.plan?.directory)?.plan;
517+
514518
const experimentalJitContext = settings.experimental?.jitContext ?? false;
515519

516520
let memoryContent: string | HierarchicalMemory = '';
@@ -827,7 +831,9 @@ export async function loadCliConfig(
827831
enableAgents: settings.experimental?.enableAgents,
828832
plan: settings.experimental?.plan,
829833
directWebFetch: settings.experimental?.directWebFetch,
830-
planSettings: settings.general?.plan,
834+
planSettings: settings.general?.plan?.directory
835+
? settings.general.plan
836+
: (extensionPlanSettings ?? settings.general?.plan),
831837
enableEventDrivenScheduler: true,
832838
skillsSupport: settings.skills?.enabled ?? true,
833839
disabledSkills: settings.skills?.disabled,

packages/cli/src/config/extension-manager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -886,6 +886,7 @@ Would you like to attempt to install via "git clone" instead?`,
886886
themes: config.themes,
887887
rules,
888888
checkers,
889+
plan: config.plan,
889890
};
890891
} catch (e) {
891892
debugLogger.error(

packages/cli/src/config/extension.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ export interface ExtensionConfig {
3333
* These themes will be registered when the extension is activated.
3434
*/
3535
themes?: CustomTheme[];
36+
/**
37+
* Planning features configuration contributed by this extension.
38+
*/
39+
plan?: {
40+
/**
41+
* The directory where planning artifacts are stored.
42+
*/
43+
directory?: string;
44+
};
3645
}
3746

3847
export interface ExtensionUpdateInfo {

packages/core/src/config/config.test.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2930,9 +2930,11 @@ describe('Plans Directory Initialization', () => {
29302930

29312931
afterEach(() => {
29322932
vi.mocked(fs.promises.mkdir).mockRestore();
2933+
vi.mocked(fs.promises.access).mockRestore?.();
29332934
});
29342935

2935-
it('should create plans directory and add it to workspace context when plan is enabled', async () => {
2936+
it('should add plans directory to workspace context if it exists', async () => {
2937+
vi.spyOn(fs.promises, 'access').mockResolvedValue(undefined);
29362938
const config = new Config({
29372939
...baseParams,
29382940
plan: true,
@@ -2941,14 +2943,32 @@ describe('Plans Directory Initialization', () => {
29412943
await config.initialize();
29422944

29432945
const plansDir = config.storage.getPlansDir();
2944-
expect(fs.promises.mkdir).toHaveBeenCalledWith(plansDir, {
2945-
recursive: true,
2946-
});
2946+
// Should NOT create the directory eagerly
2947+
expect(fs.promises.mkdir).not.toHaveBeenCalled();
2948+
// Should check if it exists
2949+
expect(fs.promises.access).toHaveBeenCalledWith(plansDir);
29472950

29482951
const context = config.getWorkspaceContext();
29492952
expect(context.getDirectories()).toContain(plansDir);
29502953
});
29512954

2955+
it('should NOT add plans directory to workspace context if it does not exist', async () => {
2956+
vi.spyOn(fs.promises, 'access').mockRejectedValue({ code: 'ENOENT' });
2957+
const config = new Config({
2958+
...baseParams,
2959+
plan: true,
2960+
});
2961+
2962+
await config.initialize();
2963+
2964+
const plansDir = config.storage.getPlansDir();
2965+
expect(fs.promises.mkdir).not.toHaveBeenCalled();
2966+
expect(fs.promises.access).toHaveBeenCalledWith(plansDir);
2967+
2968+
const context = config.getWorkspaceContext();
2969+
expect(context.getDirectories()).not.toContain(plansDir);
2970+
});
2971+
29522972
it('should NOT create plans directory or add it to workspace context when plan is disabled', async () => {
29532973
const config = new Config({
29542974
...baseParams,

packages/core/src/config/config.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,15 @@ export interface GeminiCLIExtension {
339339
* Safety checkers contributed by this extension.
340340
*/
341341
checkers?: SafetyCheckerRule[];
342+
/**
343+
* Planning features configuration contributed by this extension.
344+
*/
345+
plan?: {
346+
/**
347+
* The directory where planning artifacts are stored.
348+
*/
349+
directory?: string;
350+
};
342351
}
343352

344353
export interface ExtensionInstallMetadata {
@@ -1097,8 +1106,13 @@ export class Config implements McpContext {
10971106
// Add plans directory to workspace context for plan file storage
10981107
if (this.planEnabled) {
10991108
const plansDir = this.storage.getPlansDir();
1100-
await fs.promises.mkdir(plansDir, { recursive: true });
1101-
this.workspaceContext.addDirectory(plansDir);
1109+
try {
1110+
await fs.promises.access(plansDir);
1111+
this.workspaceContext.addDirectory(plansDir);
1112+
} catch {
1113+
// Directory does not exist yet, so we don't add it to the workspace context.
1114+
// It will be created when the first plan is written.
1115+
}
11021116
}
11031117

11041118
// Initialize centralized FileDiscoveryService

0 commit comments

Comments
 (0)