UNPKG

casualos

Version:
288 lines 10.3 kB
import { z } from 'zod'; import prompts from 'prompts'; import { resolve } from 'path'; import { mkdir, writeFile } from 'fs/promises'; import { createOAuthDeviceAuth } from '@octokit/auth-oauth-device'; import { Octokit } from 'octokit'; import simpleGit from 'simple-git'; import { exec as nodeExec } from 'child_process'; import { existsSync } from 'fs'; import { v4 as uuid } from 'uuid'; import { promisify } from 'util'; import { getRepoName } from './infra-utils'; const exec = promisify(nodeExec); export function setupInfraCommands(program, config) { // new infra repo program .command('new [name]') .description('Create a new infrastructure project') .action(async (name) => { console.log('Creating a new repository for storing infrastructure'); if (!name) { const res = await prompts({ type: 'text', name: 'name', message: 'Enter the name of the repository', }); name = res.name; } if (!name) { console.error('No repository name provided'); process.exit(1); return; } const fullPath = getRepoPath(name); if (existsSync(fullPath)) { console.error('Directory already exists:', fullPath); process.exit(1); return; } console.log('Creating repository in:', fullPath); await mkdir(fullPath, { recursive: true }); const git = simpleGit(fullPath); await git.init(); console.log('Local Git Repository created:', fullPath); const { createGithub } = await prompts({ type: 'confirm', name: 'createGithub', message: 'Create a GitHub repository for the project?', }); const repoName = getRepoName(name); if (createGithub) { const kit = await getOctokit(fullPath, config); const orgs = (await kit.rest.apps.listInstallationsForAuthenticatedUser({})).data.installations.filter((i) => i.target_type === 'Organization'); const { org } = await prompts({ type: 'autocomplete', name: 'org', message: 'Select the organization to create the repository in', choices: orgs.map((org) => ({ title: org.account.login, value: org.account.login, })), }); if (!org) { console.error('No organization selected'); process.exit(1); return; } const { enteredName } = await prompts({ type: 'text', name: 'enteredName', message: 'Enter the name of the repository', initial: repoName, }); if (!enteredName) { console.error('No repository name provided'); process.exit(1); return; } const repo = await kit.rest.repos.createInOrg({ org: org, name: enteredName, visibility: 'private', }); console.log('GitHub Repository created:', repo.data.html_url); await git.addRemote('origin', repo.data.ssh_url); } const projectId = uuid(); const projectMeta = { id: projectId, name: repoName, }; await git.checkoutLocalBranch('main'); await writeFile(resolve(fullPath, '.infra.json'), JSON.stringify(projectMeta, null, 2)); // const casualOsPath = require.resolve('casualos'); // const templatePath = resolve(casualOsPath, 'templates'); // await cp(templatePath, fullPath, { recursive: true }) await git.add('.'); await git.commit('Initial commit'); if (createGithub) { await git.push('origin', 'main', ['--set-upstream']); } await exec('pulumi login file://' + getLoginPath(fullPath)); await git.add('.'); await git.commit('Add Pulumi Metadata'); if (createGithub) { await git.push('origin', 'main', ['--set-upstream']); } console.log('Repository setup complete!'); }); program .command('clone [url]') .description('Clone an infrastructure project') .action(async (url) => { if (!url) { const res = await prompts({ type: 'text', name: 'url', message: 'Enter the url of the repository', }); url = res.url; } const regex = /\/([\w-.\s]+)\.git$/; const match = regex.exec(url); if (!match) { console.error('Invalid repository URL'); process.exit(1); return; } const repoName = match[1]; const fullPath = getRepoPath(repoName); if (existsSync(fullPath)) { console.error('Directory already exists:', fullPath); process.exit(1); return; } console.log('Cloning repository:', url); await simpleGit().clone(url, fullPath); await exec('pulumi login file://' + getLoginPath(fullPath)); console.log('Done.'); }); program .command('switch <name>') .description('Switch to a different infrastructure project') .action(async (name) => { const fullPath = getRepoPath(name); await exec('pulumi login file://' + getLoginPath(fullPath)); }); program .command('status <name>') .description('Get info about a infrastructure project') .action(async (name) => { const fullPath = getRepoPath(name); const git = simpleGit(fullPath); const status = await git.status(); console.log('Project Info:'); console.log('Path:', fullPath); console.log('Status:', status.isClean() ? 'Clean' : 'Dirty'); if (!status.isClean()) { console.log('Changes:'); console.log(status.files.map((f) => f.path).join('\n')); } }); program .command('save <name>') .description('Get info about a infrastructure project') .option('-m, --message <message>', 'Commit message') .action(async (name, options) => { const fullPath = getRepoPath(name); const git = simpleGit(fullPath); await git.add('.'); const message = options.message || 'Save changes'; await git.commit(message); console.log('Changes saved'); const { push } = await prompts({ type: 'confirm', name: 'push', message: 'Push changes to remote?', initial: true, }); if (push) { await git.push(); } }); } const INFRA_CONFIG_SCHEMA = z.object({ githubToken: z .object({ token: z.string(), refreshToken: z.string().optional().nullable(), expiresAt: z.coerce.date(), }) .optional() .nullable(), }); async function getOctokit(cwd, config) { const token = await getOrRequestGithubToken(cwd, config); const kit = new Octokit({ auth: token.token, }); return kit; } const CLIENT_ID = 'Iv23li10jiTgXpOGRrWJ'; async function getOrRequestGithubToken(cwd, config) { const infra = getInfraConfig(cwd, config); if (!infra.githubToken || infra.githubToken.expiresAt < new Date()) { if (infra.githubToken && infra.githubToken.expiresAt < new Date()) { console.log('Token expired, refreshing'); } const token = await requestGithubToken(); config.set(`infra.githubToken`, token); return token; } return infra.githubToken; } async function requestGithubToken() { const auth = createOAuthDeviceAuth({ clientType: 'github-app', clientId: CLIENT_ID, onVerification: (verification) => { console.log('\nVerification required.\n'); console.log('Open this URL:', verification.verification_uri); console.log('Enter code:', verification.user_code); }, }); const token = await auth({ type: 'oauth', }); console.log('Token:', token); return token; } function getInfraConfig(cwd, config) { return INFRA_CONFIG_SCHEMA.parse({ githubToken: config.get(`infra.githubToken`), }); } // async function getAndSaveSshKey( // cwd: string, // config: CliConfig, // infra: InfraConfig // ) { // const sshKey = await getInfraSshKey(cwd, infra); // config.set(`${cwd}.infra.sshKey`, sshKey); // return sshKey; // } // async function getInfraSshKey(cwd: string, config: InfraConfig) { // if (!config.sshKey) { // const home = homedir(); // const sshDir = resolve(home, '.ssh'); // // get list of files in .ssh directory // let sshFiles: string[] = []; // try { // sshFiles = await readdir(sshDir); // } catch (e) { // // ignore // console.warn('Unable to read .ssh directory:'); // } // const { selectOrEnter } = await prompts({ // type: 'autocomplete', // name: 'selectOrEnter', // message: `Select an SSH key for the repository`, // choices: [ // { title: 'None', value: 'none' }, // { title: 'Enter SSH Key Path', value: 'enter' }, // ...sshFiles.map((file) => ({ title: file, value: file })), // ], // }); // if (selectOrEnter === 'none') { // return null; // } else if (selectOrEnter === 'enter') { // const { path } = await prompts({ // type: 'text', // name: 'path', // message: 'Enter the path to the SSH key file', // }); // return resolve(cwd, path); // } else { // return resolve(sshDir, selectOrEnter); // } // } // return config.sshKey; // } export function getRepoPath(name) { return resolve(name); } export function getLoginPath(name) { return resolve(name, '.state'); } //# sourceMappingURL=infra.js.map