diff --git a/.eslintrc.json b/.eslintrc.json index e698178..1dfcfc4 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,3 +1,3 @@ { - "extends": ["oclif", "prettier"] + "extends": ["oclif", "oclif-typescript", "prettier"] } diff --git a/config.json b/config.json index cc30a3c..e5834e2 100644 --- a/config.json +++ b/config.json @@ -1,4 +1,4 @@ { - "token": "hola", + "token": "5qdderpq2ejrjlu90hrnbjohvgc8j1u1k7i00um4", "url": "http://localhost:3000" } \ No newline at end of file diff --git a/package.json b/package.json index 510a583..b49cab2 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@oclif/plugin-plugins": "^5", "axios": "^1.7.2", "chalk": "^5.3.0", + "cli-table3": "^0.6.5", "inquirer": "^9.2.23", "superjson": "^2.2.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0db1304..1e509c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ dependencies: chalk: specifier: ^5.3.0 version: 5.3.0 + cli-table3: + specifier: ^0.6.5 + version: 0.6.5 inquirer: specifier: ^9.2.23 version: 9.2.23 @@ -783,6 +786,13 @@ packages: js-tokens: 4.0.0 picocolors: 1.0.1 + /@colors/colors@1.5.0: + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + requiresBuild: true + dev: false + optional: true + /@cspotcode/source-map-support@0.8.1: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -2263,6 +2273,15 @@ packages: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} + /cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + dev: false + /cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} diff --git a/src/commands/authenticate.ts b/src/commands/authenticate.ts index 05ca5e3..9e58baa 100644 --- a/src/commands/authenticate.ts +++ b/src/commands/authenticate.ts @@ -83,23 +83,17 @@ export default class Authenticate extends Command { try { console.log(`\n${chalk.blue("Validating server...")}`); - const response = await axios.post( + await axios.post( `${url}/api/trpc/auth.verifyToken`, - { - json: { - token, - }, - }, + {}, { headers: { + Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, }, ); - if (!response.data.result.data.json) { - this.error(chalk.red("Invalid token")); - } fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); this.log(chalk.green("Authentication details saved successfully.")); } catch (error) { diff --git a/src/commands/project/create.ts b/src/commands/project/create.ts new file mode 100644 index 0000000..2b3988e --- /dev/null +++ b/src/commands/project/create.ts @@ -0,0 +1,94 @@ +import { Command, Flags } from "@oclif/core"; +import axios from "axios"; +import chalk from "chalk"; +import inquirer, { type Answers, type QuestionCollection } from "inquirer"; + +import { readAuthConfig } from "../../utils/utils.js"; + +export default class ProjectCreate extends Command { + static override description = + "Create a new project with an optional description."; + + static override examples = [ + "$ <%= config.bin %> <%= command.id %> -n MyProject -d 'This is my project description'", + "$ <%= config.bin %> <%= command.id %> -n MyProject", + "$ <%= config.bin %> <%= command.id %>", + ]; + + static override flags = { + description: Flags.string({ + char: "d", + description: "Description of the project", + required: false, + }), + name: Flags.string({ + char: "n", + description: "Name of the project", + required: false, + }), + }; + + public async run(): Promise { + const auth = await readAuthConfig(this); + + console.log(chalk.blue.bold("\n Create a New Project \n")); + + const { flags } = await this.parse(ProjectCreate); + + let answers: Answers = {}; + + const questions: QuestionCollection[] = []; + + if (!flags.name) { + questions.push({ + message: chalk.green("Enter the project name:"), + name: "name", + type: "input", + validate: (input) => (input ? true : "Project name is required"), + }); + } + + if (!flags.description) { + questions.push({ + default: "", + message: chalk.green("Enter the project description (optional):"), + name: "description", + type: "input", + }); + } + + if (questions.length > 0) { + answers = await inquirer.prompt(questions); + } + + const name = flags.name || answers.name; + const description = flags.description || answers.description; + + try { + const response = await axios.post( + `${auth.url}/api/trpc/project.createCLI`, + { + json: { + description, + name, + }, + }, + { + headers: { + Authorization: `Bearer ${auth.token}`, + "Content-Type": "application/json", + }, + }, + ); + + if (!response.data.result.data.json) { + this.error(chalk.red("Error`")); + } + + this.log(chalk.green(`Project '${name}' created successfully.`)); + } catch (error) { + // @ts-expect-error hola + this.error(chalk.red(`Failed to create project: ${error.message}`)); + } + } +} diff --git a/src/commands/project/list.ts b/src/commands/project/list.ts new file mode 100644 index 0000000..fcf355e --- /dev/null +++ b/src/commands/project/list.ts @@ -0,0 +1,60 @@ +import { Command } from "@oclif/core"; +import axios from "axios"; +import chalk from "chalk"; +import Table from "cli-table3"; + +import { readAuthConfig } from "../../utils/utils.js"; + +export default class ProjectList extends Command { + static description = "List all projects."; + + static examples = ["$ <%= config.bin %> project list"]; + + public async run(): Promise { + const auth = await readAuthConfig(this); + + console.log(chalk.blue.bold("\n Listing all Projects \n")); + + try { + const response = await axios.get(`${auth.url}/api/trpc/project.all`, { + headers: { + Authorization: `Bearer ${auth.token}`, + "Content-Type": "application/json", + }, + }); + + if (!response.data.result.data.json) { + this.error(chalk.red("Error fetching projects")); + } + + const projects = response.data.result.data.json; + + if (projects.length === 0) { + this.log(chalk.yellow("No projects found.")); + } else { + this.log(chalk.green("Projects:")); + const table = new Table({ + colWidths: [10, 30, 50], + head: [ + chalk.cyan("Index"), + chalk.cyan("Name"), + chalk.cyan("Description"), + ], + }); + const index = 1; + for (const project of projects) { + table.push([ + chalk.white(index + 1), + chalk.white(project.name), + chalk.gray(project.description || "No description"), + ]); + } + + this.log(table.toString()); + } + } catch { + // @ts-expect-error error is not defined + this.error(chalk.red(`Failed to list projects: ${error.message}`)); + } + } +} diff --git a/src/commands/verify.ts b/src/commands/verify.ts index 2da837f..d77da35 100644 --- a/src/commands/verify.ts +++ b/src/commands/verify.ts @@ -42,13 +42,10 @@ export default class Verify extends Command { const response = await axios.post( `${url}/api/trpc/auth.verifyToken`, - { - json: { - token, - }, - }, + {}, { headers: { + Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, }, diff --git a/src/utils/http.ts b/src/utils/http.ts new file mode 100644 index 0000000..b27d636 --- /dev/null +++ b/src/utils/http.ts @@ -0,0 +1,4 @@ +export const headers = { + "Content-Type": "application/json", + "User-Agent": "Dokploy CLI", +}; diff --git a/src/utils/utils.ts b/src/utils/utils.ts new file mode 100644 index 0000000..419678f --- /dev/null +++ b/src/utils/utils.ts @@ -0,0 +1,36 @@ +import type { Command } from "@oclif/core"; + +import chalk from "chalk"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const configPath = path.join(__dirname, "..", "..", "config.json"); + +export const readAuthConfig = async ( + command: Command, +): Promise<{ token: string; url: string }> => { + if (!fs.existsSync(configPath)) { + command.error( + chalk.red( + "No configuration file found. Please authenticate first using the 'authenticate' command.", + ), + ); + } + + const configFileContent = fs.readFileSync(configPath, "utf8"); + const config = JSON.parse(configFileContent); + const { token, url } = config; + + if (!url || !token) { + command.error( + chalk.red( + "Incomplete authentication details. Please authenticate again using the 'authenticate' command.", + ), + ); + } + + return { token, url }; +}; diff --git a/test/commands/project/create.test.ts b/test/commands/project/create.test.ts new file mode 100644 index 0000000..cd9e123 --- /dev/null +++ b/test/commands/project/create.test.ts @@ -0,0 +1,14 @@ +import {runCommand} from '@oclif/test' +import {expect} from 'chai' + +describe('project:create', () => { + it('runs project:create cmd', async () => { + const {stdout} = await runCommand('project:create') + expect(stdout).to.contain('hello world') + }) + + it('runs project:create --name oclif', async () => { + const {stdout} = await runCommand('project:create --name oclif') + expect(stdout).to.contain('hello oclif') + }) +}) diff --git a/test/commands/project/list.test.ts b/test/commands/project/list.test.ts new file mode 100644 index 0000000..c21edbb --- /dev/null +++ b/test/commands/project/list.test.ts @@ -0,0 +1,14 @@ +import {runCommand} from '@oclif/test' +import {expect} from 'chai' + +describe('project:list', () => { + it('runs project:list cmd', async () => { + const {stdout} = await runCommand('project:list') + expect(stdout).to.contain('hello world') + }) + + it('runs project:list --name oclif', async () => { + const {stdout} = await runCommand('project:list --name oclif') + expect(stdout).to.contain('hello oclif') + }) +})