Skip to content

Commit

Permalink
Add a "project delete" command.
Browse files Browse the repository at this point in the history
  • Loading branch information
sangaline committed Feb 27, 2025
1 parent 4ba3d9f commit e3c5cd6
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { deployCommand } from "cli/deploy";
import { lintCommand } from "cli/lint";
import { loginCommand } from "cli/login";
import { logoutCommand } from "cli/logout";
import { projectCommand } from "cli/project";
import { proofCommand } from "cli/proof";
import { whoamiCommand } from "cli/whoami";
import { loadPackageJson } from "cli/utils";
Expand Down Expand Up @@ -38,6 +39,7 @@ export const program = new Command()
.addCommand(lintCommand)
.addCommand(loginCommand)
.addCommand(logoutCommand)
.addCommand(projectCommand)
.addCommand(proofCommand)
.addCommand(whoamiCommand)
// Parse the base command options and respond to them before invoking the subcommand.
Expand Down
156 changes: 156 additions & 0 deletions src/cli/project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { existsSync, readFileSync } from "node:fs";
import path from "node:path";

import { Command } from "@commander-js/extra-typings";

import { findFileUpwards } from "cli/utils";
import sindri from "lib";
import { ApiError } from "lib/api";

async function deleteProject(projectIdentifier: string): Promise<void> {
try {
await sindri._client.internal.projectDelete(projectIdentifier);
sindri.logger.debug(`Deleted project "${projectIdentifier}".`);
} catch (error) {
if (error instanceof ApiError && error.status === 401) {
sindri.logger.error(
"Your credentials are invalid. Please log in again with `sindri login`.",
);
} else if (error instanceof ApiError && error.status === 404) {
sindri.logger.error(`The project "${projectIdentifier}" does not exist.`);
} else {
sindri.logger.fatal("An unknown error occurred.");
sindri.logger.error(error);
}
return process.exit(1);
}
}

function findProjectName(directory: string): string {
// Find `sindri.json` and move into the root of the project directory.
const directoryPath = path.resolve(directory);
if (!existsSync(directoryPath)) {
sindri.logger.error(
`The "${directoryPath}" directory does not exist. Aborting.`,
);
return process.exit(1);
}
const sindriJsonPath = findFileUpwards(/^sindri.json$/i, directoryPath);
if (!sindriJsonPath) {
sindri.logger.error(
`No "sindri.json" file was found in or above "${directoryPath}". Aborting.`,
);
return process.exit(1);
}
sindri.logger.debug(`Found "sindri.json" at "${sindriJsonPath}".`);
const rootDirectory = path.dirname(sindriJsonPath);
sindri.logger.debug(`Changing current directory to "${rootDirectory}".`);
process.chdir(rootDirectory);

// Load `sindri.json`.
let sindriJson: object = {};
try {
const sindriJsonContent = readFileSync(sindriJsonPath, {
encoding: "utf-8",
});
sindriJson = JSON.parse(sindriJsonContent);
sindri.logger.debug(
`Successfully loaded "sindri.json" from "${sindriJsonPath}":`,
);
sindri.logger.debug(sindriJson);
} catch (error) {
sindri.logger.fatal(
`Error loading "${sindriJsonPath}", perhaps it is not valid JSON?`,
);
sindri.logger.error(error);
return process.exit(1);
}
if (!("name" in sindriJson) || typeof sindriJson.name !== "string") {
sindri.logger.error('No "name" field found in "sindri.json". Aborting.');
return process.exit(1);
}
return sindriJson.name;
}

async function getSomeProjectNames(teamName: string): Promise<string[]> {
try {
const projects = await sindri._client.internal.projectListPaginated({
team_name: teamName,
});
return projects.items.map(({ name }) => name);
} catch (error) {
if (error instanceof ApiError && error.status === 401) {
sindri.logger.error(
"Your credentials are invalid. Please log in again with `sindri login`.",
);
} else {
sindri.logger.fatal("An unknown error occurred.");
sindri.logger.error(error);
}
return process.exit(1);
}
}

async function getTeamName(): Promise<string> {
try {
const response = await sindri._client.internal.teamMe();
return response.team.slug;
} catch (error) {
if (error instanceof ApiError && error.status === 401) {
sindri.logger.error(
"Your credentials are invalid. Please log in again with `sindri login`.",
);
} else {
sindri.logger.fatal("An unknown error occurred.");
sindri.logger.error(error);
}
return process.exit(1);
}
}

const projectDeleteCommand = new Command()
.name("delete")
.description("Delete a project and all associated builds and proofs.")
.option(
"-a, --all",
"Delete *all* projects. Overrides the `--name` and directory options.",
)
.option(
"-n, --name <name>",
"The name of the project to delete. Overrides the directory option.",
)
.argument("[directory]", "The location of the Sindri project to delete.", ".")
.action(async (directory, { all, name }) => {
// Check that the API client is authorized.
if (!sindri.apiKey || !sindri.baseUrl) {
sindri.logger.warn("You must login first with `sindri login`.");
return process.exit(1);
}

let deletedCount = 0;
if (!all) {
name = name || findProjectName(directory);
await deleteProject(name);
} else {
const teamName = await getTeamName();
while (true) {
const projectNames = await getSomeProjectNames(teamName);
if (projectNames.length === 0) {
break;
}

// Iterate through project names.
for (const projectName of projectNames) {
await deleteProject(projectName);
deletedCount++;
}
}
}

sindri.logger.info(`Successfully deleted ${deletedCount} projects.`);
});

export const projectCommand = new Command()
.name("project")
.description("Commands related to managing projects.")
.addCommand(projectDeleteCommand);

0 comments on commit e3c5cd6

Please sign in to comment.