diff --git a/api/projects.ts b/api/projects.ts index fa77b2c..5dec5b1 100644 --- a/api/projects.ts +++ b/api/projects.ts @@ -15,3 +15,9 @@ export const projectSecrets = { update: client("/projects/{id}/secrets/{name}").patch, delete: client("/projects/{id}/secrets/{name}").delete, }; + +export const projectTags = { + list: client("/projects/{id}/tags").get, + create: client("/projects/{id}/tags").post, + delete: client("/projects/{id}/tags").delete, +}; diff --git a/commands/project/tags/create/mod.ts b/commands/project/tags/create/mod.ts new file mode 100644 index 0000000..fd65ea0 --- /dev/null +++ b/commands/project/tags/create/mod.ts @@ -0,0 +1,85 @@ +import { command, flags } from "../../../../zcli.ts"; +import { args, z } from "../../../../zcli.ts"; +import { input } from "../../../../prompts/input.ts"; +import { asserts } from "../../../../lib/asserts.ts"; +import { loading } from "../../../../lib/loading.ts"; +import { dataTable } from "../../../../lib/data-table.ts"; +import { pickJson } from "../../../../lib/pick-json.ts"; +import * as psFlags from "../../../../flags.ts"; +import { defaultFields } from "../mod.ts"; +import { projects, projectTags } from "../../../../api/projects.ts"; +import { select } from "../../../../prompts/select.ts"; + +/** + * This variable is automatically generated by `zcli add`. Do not remove this + * or change its name unless you're no longer using `zcli add`. + */ +const subCommands: ReturnType[] = []; + +export const create = command("create", { + short: "Create a new project tag", + long: ` + Create a new project tag. This command will prompt you for a name if you don't + provide one. + `, + commands: subCommands, + + args: args({ short: "Create a project tag with these properties." }).tuple([ + z.string().describe("The project ID to create a tag for."), + z.string().describe("The name of the project tag to create."), + ]).optional(), + + flags: flags({ + fields: psFlags.fields, + }), + + // We use command metadata in the `persistentPreRun` function to check if a + // command requires an API key. If it does, we'll check to see if one is + // set. If not, we'll throw an error. + meta: { + requireApiKey: true, + }, +}).run(async function* ({ args, flags }) { + let [projectId, name] = args; + + if (!projectId) { + const existingProjects = await loading(projects.list({ limit: 50 })); + asserts(existingProjects.ok, existingProjects); + + const selected = await select( + "Select a project:", + existingProjects.data.items, + { + filter(input, option) { + return option.name.toLowerCase().startsWith(input); + }, + renderOption(option, isSelected) { + return `${isSelected ? ">" : " "} ${option.name}`; + }, + }, + ); + + asserts(selected, "No project selected."); + projectId = selected.id; + } + + if (!name) { + name = await input("Tag name:"); + asserts(name, "You must provide a name for the project tag."); + } + + const result = await loading(projectTags.create({ id: projectId, name }), { + enabled: !flags.json, + }); + asserts(result.ok, result); + + if (!flags.json) { + for await ( + const line of dataTable([result.data], flags.fields ?? defaultFields) + ) { + yield line; + } + } else { + yield pickJson(result.data, flags.fields); + } +}); diff --git a/commands/project/tags/delete/mod.ts b/commands/project/tags/delete/mod.ts new file mode 100644 index 0000000..d245e83 --- /dev/null +++ b/commands/project/tags/delete/mod.ts @@ -0,0 +1,102 @@ +import { projects, projectTags } from "../../../../api/projects.ts"; +import { asserts } from "../../../../lib/asserts.ts"; +import { args, command, flag, flags, z } from "../../../../zcli.ts"; +import { select } from "../../../../prompts/select.ts"; +import { dataTable } from "../../../../lib/data-table.ts"; +import { pickJson } from "../../../../lib/pick-json.ts"; +import { loading } from "../../../../lib/loading.ts"; +import { defaultFields } from "../mod.ts"; + +/** + * This variable is automatically generated by `zcli add`. Do not remove this + * or change its name unless you're no longer using `zcli add`. + */ +const subCommands: ReturnType[] = []; + +export const delete_ = command("delete", { + short: "Delete a project tag.", + long: ` + Delete a project tag by its name. If you don't provide a name, this command + will prompt you for one. + `, + commands: subCommands, + args: args().tuple([ + z.string().describe("The project ID to delete from."), + z.string().describe("The project tag to delete."), + ]) + .optional(), + flags: flags({ + fields: flag({ + short: "The fields to include in the response.", + aliases: ["F"], + }).array(z.string()).optional(), + }), + // We use command metadata in the `persistentPreRun` function to check if a + // command requires an API key. If it does, we'll check to see if one is + // set. If not, we'll throw an error. + meta: { + requireApiKey: true, + }, +}).run(async function* ({ args, flags }) { + let [projectId, name] = args; + + if (!projectId) { + const existingProjects = await loading(projects.list({ limit: 50 })); + asserts(existingProjects.ok, existingProjects); + + const selected = await select( + "Select a project:", + existingProjects.data.items, + { + filter(input, option) { + return option.name.toLowerCase().startsWith(input); + }, + renderOption(option, isSelected) { + return `${isSelected ? ">" : " "} ${option.name}`; + }, + }, + ); + + asserts(selected, "No project selected."); + projectId = selected.id; + } + + if (!name) { + const existingProjects = await loading( + projectTags.list({ id: projectId, limit: 50 }), + ); + asserts(existingProjects.ok, existingProjects); + + const selected = await select( + "Select a tag:", + existingProjects.data.items, + { + filter(input, option) { + return option.name.toLowerCase().startsWith(input); + }, + renderOption(option, isSelected) { + return `${isSelected ? ">" : " "} ${option.name}`; + }, + }, + ); + + asserts(selected, "No tag selected."); + name = selected.name; + } + + const result = await loading(projectTags.delete({ id: projectId, name }), { + enabled: !flags.json, + }); + + asserts(result.ok, result); + + if (!flags.json) { + for await ( + const line of dataTable([result.data], flags.fields ?? defaultFields) + ) { + yield line; + } + } else { + yield pickJson(result.data, flags.fields); + } +}); diff --git a/commands/project/tags/list/mod.ts b/commands/project/tags/list/mod.ts new file mode 100644 index 0000000..8315b64 --- /dev/null +++ b/commands/project/tags/list/mod.ts @@ -0,0 +1,85 @@ +import { args, command } from "../../../../zcli.ts"; +import { asserts } from "../../../../lib/asserts.ts"; +import { dataTable } from "../../../../lib/data-table.ts"; +import { loading } from "../../../../lib/loading.ts"; +import * as psFlags from "../../../../flags.ts"; +import { pickJson } from "../../../../lib/pick-json.ts"; +import { defaultFields } from "../mod.ts"; +import { projects } from "../../../../api/projects.ts"; +import { projectTags } from "../../../../api/projects.ts"; +import { select } from "../../../../prompts/select.ts"; +import { z } from "../../../../zcli.ts"; + +/** + * This variable is automatically generated by `zcli add`. Do not remove this + * or change its name unless you're no longer using `zcli add`. + */ +const subCommands: ReturnType[] = []; + +export const list = command("list", { + short: "List project tags.", + long: ({ root }) => ` + List project tags in your team. + + Pick a subset of fields to display: + \`\`\` + ${root.name} project tags list -F id -F name -F dtCreated + \`\`\` + `, + commands: subCommands, + args: args().tuple([ + z.string().describe("The project ID to list tags for."), + ]) + .optional(), + flags: psFlags.paginator, + // We use command metadata in the `persistentPreRun` function to check if a + // command requires an API key. If it does, we'll check to see if one is + // set. If not, we'll throw an error. + meta: { + requireApiKey: true, + }, +}).run(async function* ({ args, flags }) { + let [projectId] = args; + + if (!projectId) { + const existingProjects = await loading(projects.list({ limit: 50 })); + asserts(existingProjects.ok, existingProjects); + + const selected = await select( + "Select a project:", + existingProjects.data.items, + { + filter(input, option) { + return option.name.toLowerCase().startsWith(input); + }, + renderOption(option, isSelected) { + return `${isSelected ? ">" : " "} ${option.name}`; + }, + }, + ); + + asserts(selected, "No project selected."); + projectId = selected.id; + } + + const result = await loading( + projectTags.list({ + id: projectId, + ...flags, + order: flags.asc ? "asc" : undefined, + }), + { enabled: !flags.json }, + ); + + asserts(result.ok, result); + + if (!flags.json) { + for await ( + const line of dataTable(result.data.items, flags.fields ?? defaultFields) + ) { + yield line; + } + } else { + yield pickJson(result.data, flags.fields); + } +}); diff --git a/commands/project/tags/mod.ts b/commands/project/tags/mod.ts new file mode 100644 index 0000000..cc47222 --- /dev/null +++ b/commands/project/tags/mod.ts @@ -0,0 +1,28 @@ +import { command } from "../../../zcli.ts"; +import { create } from "./create/mod.ts"; +import { list } from "./list/mod.ts"; +import { delete_ } from "./delete/mod.ts"; + +export const defaultFields = [ + "id", + "name", +]; + +/** + * This variable is automatically generated by `zcli add`. Do not remove this + * or change its name unless you're no longer using `zcli add`. + */ +const subCommands: ReturnType[] = [ + list, + create, + delete_, +]; + +export const tag = command("tag", { + short: "Manage your Paperspace project tags.", + commands: subCommands, +}).run(function* ({ ctx }) { + for (const line of tag.help(ctx)) { + yield line; + } +});