Update gemini extensions new (#10629)

This commit is contained in:
christine betts
2025-10-07 09:10:21 -04:00
committed by GitHub
parent 5a0b21b1d1
commit 69f93f852e
3 changed files with 43 additions and 19 deletions

View File

@@ -1,5 +1,4 @@
{ {
"name": "context-example", "name": "context-example",
"version": "1.0.0", "version": "1.0.0"
"contextFileName": "GEMINI.md"
} }

View File

@@ -30,15 +30,22 @@ describe('extensions new command', () => {
it('should fail if no path is provided', async () => { it('should fail if no path is provided', async () => {
const parser = yargs([]).command(newCommand).fail(false).locale('en'); const parser = yargs([]).command(newCommand).fail(false).locale('en');
await expect(parser.parseAsync('new')).rejects.toThrow( await expect(parser.parseAsync('new')).rejects.toThrow(
'Not enough non-option arguments: got 0, need at least 2', 'Not enough non-option arguments: got 0, need at least 1',
); );
}); });
it('should fail if no template is provided', async () => { it('should create directory when no template is provided', async () => {
const parser = yargs([]).command(newCommand).fail(false).locale('en'); mockedFs.access.mockRejectedValue(new Error('ENOENT'));
await expect(parser.parseAsync('new /some/path')).rejects.toThrow( mockedFs.mkdir.mockResolvedValue(undefined);
'Not enough non-option arguments: got 1, need at least 2',
); const parser = yargs([]).command(newCommand).fail(false);
await parser.parseAsync('new /some/path');
expect(mockedFs.mkdir).toHaveBeenCalledWith('/some/path', {
recursive: true,
});
expect(mockedFs.cp).not.toHaveBeenCalled();
}); });
it('should create directory and copy files when path does not exist', async () => { it('should create directory and copy files when path does not exist', async () => {

View File

@@ -4,15 +4,15 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { access, cp, mkdir, readdir } from 'node:fs/promises'; import { access, cp, mkdir, readdir, writeFile } from 'node:fs/promises';
import { join, dirname } from 'node:path'; import { join, dirname, basename } from 'node:path';
import type { CommandModule } from 'yargs'; import type { CommandModule } from 'yargs';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { getErrorMessage } from '../../utils/errors.js'; import { getErrorMessage } from '../../utils/errors.js';
interface NewArgs { interface NewArgs {
path: string; path: string;
template: string; template?: string;
} }
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
@@ -29,13 +29,17 @@ async function pathExists(path: string) {
} }
} }
async function copyDirectory(template: string, path: string) { async function createDirectory(path: string) {
if (await pathExists(path)) { if (await pathExists(path)) {
throw new Error(`Path already exists: ${path}`); throw new Error(`Path already exists: ${path}`);
} }
await mkdir(path, { recursive: true });
}
async function copyDirectory(template: string, path: string) {
await createDirectory(path);
const examplePath = join(EXAMPLES_PATH, template); const examplePath = join(EXAMPLES_PATH, template);
await mkdir(path, { recursive: true });
const entries = await readdir(examplePath, { withFileTypes: true }); const entries = await readdir(examplePath, { withFileTypes: true });
for (const entry of entries) { for (const entry of entries) {
const srcPath = join(examplePath, entry.name); const srcPath = join(examplePath, entry.name);
@@ -46,10 +50,24 @@ async function copyDirectory(template: string, path: string) {
async function handleNew(args: NewArgs) { async function handleNew(args: NewArgs) {
try { try {
await copyDirectory(args.template, args.path); if (args.template) {
console.log( await copyDirectory(args.template, args.path);
`Successfully created new extension from template "${args.template}" at ${args.path}.`, console.log(
); `Successfully created new extension from template "${args.template}" at ${args.path}.`,
);
} else {
await createDirectory(args.path);
const extensionName = basename(args.path);
const manifest = {
name: extensionName,
version: '1.0.0',
};
await writeFile(
join(args.path, 'gemini-extension.json'),
JSON.stringify(manifest, null, 2),
);
console.log(`Successfully created new extension at ${args.path}.`);
}
console.log( console.log(
`You can install this using "gemini extensions link ${args.path}" to test it out.`, `You can install this using "gemini extensions link ${args.path}" to test it out.`,
); );
@@ -67,7 +85,7 @@ async function getBoilerplateChoices() {
} }
export const newCommand: CommandModule = { export const newCommand: CommandModule = {
command: 'new <path> <template>', command: 'new <path> [template]',
describe: 'Create a new extension from a boilerplate example.', describe: 'Create a new extension from a boilerplate example.',
builder: async (yargs) => { builder: async (yargs) => {
const choices = await getBoilerplateChoices(); const choices = await getBoilerplateChoices();
@@ -85,7 +103,7 @@ export const newCommand: CommandModule = {
handler: async (args) => { handler: async (args) => {
await handleNew({ await handleNew({
path: args['path'] as string, path: args['path'] as string,
template: args['template'] as string, template: args['template'] as string | undefined,
}); });
}, },
}; };