Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
7 changes: 6 additions & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@connectrpc/connect-node": "^1.4.0",
"@graphql-tools/utils": "^10.2.2",
"@modelcontextprotocol/sdk": "^1.9.0",
"@octokit/rest": "^22.0.0",
"@wundergraph/composition": "workspace:*",
"@wundergraph/cosmo-connect": "workspace:*",
"@wundergraph/cosmo-shared": "workspace:*",
Expand All @@ -58,6 +59,7 @@
"env-ci": "^11.1.0",
"env-paths": "^3.0.0",
"execa": "^9.5.2",
"fs-extra": "^11.3.0",
"graphql": "^16.9.0",
"https-proxy-agent": "^7.0.5",
"inquirer": "^9.2.7",
Expand All @@ -74,6 +76,7 @@
"prompts": "^2.4.2",
"pupa": "^3.1.0",
"semver": "^7.7.1",
"tar": "^7.4.3",
"trieve-ts-sdk": "^0.0.80",
"undici": "^6.21.1",
"zod": "^3.22.4"
Expand All @@ -84,12 +87,14 @@
"@types/cli-table": "^0.3.1",
"@types/decompress": "^4.2.7",
"@types/env-ci": "^3.1.4",
"@types/fs-extra": "^11.0.4",
"@types/inquirer": "^9.0.3",
"@types/js-yaml": "^4.0.5",
"@types/lodash-es": "4.17.12",
"@types/node": "^20.3.1",
"@types/prompts": "^2.4.9",
"@types/semver": "^7.7.0",
"@types/tar": "^6.1.13",
"del-cli": "^5.0.0",
"eslint": "^8.57.1",
"eslint-config-unjs": "^0.2.1",
Expand All @@ -100,4 +105,4 @@
"vitest": "^3.1.2"
},
"gitHead": "c37aed755e1b19ed91d30f9b5f7041e15c56901a"
}
}
129 changes: 129 additions & 0 deletions cli/src/commands/grpc-service/commands/init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { existsSync, readdirSync } from 'node:fs';
import { mkdir } from 'node:fs/promises';
Comment thread
jensneuse marked this conversation as resolved.
Outdated
import os from 'node:os';
import { Command, program } from 'commander';
import { resolve, join } from 'pathe';
import Spinner from 'ora';
import { Octokit } from '@octokit/rest';
import { extract, t } from 'tar';
import fs from 'fs-extra';
import pc from 'picocolors';
import { BaseCommandOptions } from '../../../core/types/types.js';
import { fetchAvailableTemplates } from './list-templates.js';

async function checkTemplateExists(template: string): Promise<boolean> {
const url = `https://api.github.com/repos/wundergraph/cosmo-templates/contents/grpc-service/${template}`;
const res = await fetch(url, { headers: { Accept: 'application/vnd.github.v3+json' } });
Comment thread
jensneuse marked this conversation as resolved.
Outdated
if (res.status === 200) {
const data: any = await res.json();
return Array.isArray(data); // Should be an array if it's a directory
}
return false;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

async function downloadAndExtractTemplate(template: string, outputDir: string, spinner: any) {
const octokit = new Octokit();
const owner = 'wundergraph';
const repo = 'cosmo-templates';
const ref = 'main'; // You may want to make this configurable
const tempTarPath = join(os.tmpdir(), `cosmo-templates-${Date.now()}.tar.gz`);
const tempExtractDir = join(os.tmpdir(), `cosmo-templates-extract-${Date.now()}`);
await fs.ensureDir(tempExtractDir);

spinner.text = 'Downloading template from GitHub...';
const response = await octokit.repos.downloadTarballArchive({ owner, repo, ref });
if (response.data instanceof ArrayBuffer) {
await fs.writeFile(tempTarPath, new Uint8Array(Buffer.from(response.data)));
} else {
throw new TypeError('Unexpected tarball response type');
}

spinner.text = 'Extracting template files...';
// The tarball will have a top-level directory like cosmo-templates-<sha>/grpc-service/<template>/
// We want to extract only grpc-service/<template> and copy its contents to outputDir
let topLevelDir = '';
await t({
file: tempTarPath,
onentry: (entry: { path: string }) => {
if (!topLevelDir && entry.path.includes('/')) {
topLevelDir = entry.path.split('/')[0];
}
},
});
const templatePathInTar = `${topLevelDir}/grpc-service/${template}`;
await extract({
file: tempTarPath,
cwd: tempExtractDir,
filter: (p: string) => p.startsWith(templatePathInTar + '/'),
strip: templatePathInTar.split('/').length,
});

// Copy extracted files to outputDir
await fs.copy(tempExtractDir, outputDir, { overwrite: true });

// Cleanup
await fs.remove(tempTarPath);
await fs.remove(tempExtractDir);
}
Comment thread
jensneuse marked this conversation as resolved.

export default (opts: BaseCommandOptions) => {
const command = new Command('init');
command.description('Scaffold a new gRPC service project from a template');
command.option('-t, --template <template>', 'Template to use', 'typescript-connect-rpc-fastify');
command.option('-d, --directory <directory>', 'Output directory', '.');
command.action(async (options) => {
const spinner = Spinner();
const template = options.template || 'typescript-connect-rpc-fastify';
Comment thread
jensneuse marked this conversation as resolved.
Outdated
const outputDir = resolve(process.cwd(), options.directory || '.');

spinner.start(`Checking if template '${template}' exists...`);
const exists = await checkTemplateExists(template);
if (!exists) {
spinner.start('Fetching available templates...');
const templates = await fetchAvailableTemplates();
spinner.stop();
if (templates.length > 0) {
console.log(pc.yellow('Available templates:'));
for (const t of templates) {
console.log(` - ${t}`);
}
console.log('');
console.log(pc.yellow('To use a template, run:'));
console.log(` wgc grpc-service init --template ${templates[0]} --directory ./output`);
console.log('');
Comment thread
jensneuse marked this conversation as resolved.
Outdated
} else {
console.log(pc.red('No templates found in wundergraph/cosmo-templates under grpc-service.'));
}
program.error(
`Template '${template}' does not exist in wundergraph/cosmo-templates under grpc-service. Please check the template name and try again.`,
);
}

spinner.text = `Scaffolding gRPC service using template '${template}'...`;

try {
if (existsSync(outputDir)) {
const files = readdirSync(outputDir);
if (files.length > 0) {
spinner.fail(pc.red('Output directory is not empty.'));
program.error(
`The directory '${outputDir}' is not empty. Please use the --directory argument to specify an empty or new directory.`,
);
}
} else {
await mkdir(outputDir, { recursive: true });
}
Comment thread
jensneuse marked this conversation as resolved.
await downloadAndExtractTemplate(template, outputDir, spinner);
spinner.succeed(pc.green(`gRPC service scaffolded in ${outputDir}`));
console.log('');
console.log(
` Checkout the ${pc.bold(pc.italic('README.md'))} file for instructions on how to use your service.`,
);
console.log('');
} catch (error: any) {
spinner.fail(pc.red('Failed to scaffold gRPC service'));
program.error(error.message || String(error));
}
});
return command;
Comment thread
jensneuse marked this conversation as resolved.
Outdated
};
45 changes: 45 additions & 0 deletions cli/src/commands/grpc-service/commands/list-templates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Command } from 'commander';
import pc from 'picocolors';
import Spinner from 'ora';
import { Octokit } from '@octokit/rest';
import { BaseCommandOptions } from '../../../core/types/types.js';

export async function fetchAvailableTemplates(): Promise<string[]> {
const octokit = new Octokit();
const owner = 'wundergraph';
const repo = 'cosmo-templates';
const path = 'grpc-service';

try {
const res = await octokit.repos.getContent({ owner, repo, path });
if (Array.isArray(res.data)) {
return res.data.filter((item: any) => item.type === 'dir').map((item: any) => item.name);
}
} catch {
console.error('Error listing templates from https://github.com/wundergraph/cosmo-templates/');
}
return [];
}
Comment thread
jensneuse marked this conversation as resolved.

export default (_opts: BaseCommandOptions) => {
const command = new Command('list-templates');
command.description('List all available gRPC service templates');
command.action(async () => {
const spinner = Spinner('Fetching available templates...').start();
const templates = await fetchAvailableTemplates();
spinner.stop();
Comment thread
jensneuse marked this conversation as resolved.
Outdated
if (templates.length > 0) {
console.log(pc.yellow('Available templates:'));
for (const t of templates) {
console.log(` - ${t}`);
}
console.log('');
console.log(pc.yellow('To use a template, run:'));
console.log(` wgc grpc-service init --template ${templates[0]} --directory ./output`);
console.log('');
} else {
console.log(pc.red('No templates found in wundergraph/cosmo-templates under grpc-service.'));
}
});
return command;
};
4 changes: 4 additions & 0 deletions cli/src/commands/grpc-service/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { Command } from 'commander';
import { BaseCommandOptions } from '../../core/types/types.js';
import generateCommand from './commands/generate.js';
import initCommand from './commands/init.js';
import listTemplatesCommand from './commands/list-templates.js';

export default (opts: BaseCommandOptions) => {
const command = new Command('grpc-service');
command.description('Manage protobuf schemas for remote gRPC services');
command.addCommand(generateCommand(opts));
command.addCommand(initCommand(opts));
command.addCommand(listTemplatesCommand(opts));

return command;
};
Loading
Loading