From aa36ce3c4784c91024c4e8001c236e28f02396e7 Mon Sep 17 00:00:00 2001 From: Manuel <30572287+manuel-rw@users.noreply.github.com> Date: Thu, 12 Mar 2026 08:47:08 +0100 Subject: [PATCH] feat: expand cli (#5159) * feat: expand cli * fix: lint issue --------- Co-authored-by: Meier Lukas --- packages/cli/package.json | 4 +- .../cli/src/commands/integrations-delete.ts | 39 +++++++++++++++ .../cli/src/commands/integrations-list.ts | 21 ++++++++ packages/cli/src/commands/integrations.ts | 10 ++++ packages/cli/src/commands/users-delete.ts | 39 +++++++++++++++ packages/cli/src/commands/users-list.ts | 22 ++++++++ .../cli/src/commands/users-update-password.ts | 50 +++++++++++++++++++ packages/cli/src/commands/users.ts | 11 ++++ packages/cli/src/index.ts | 4 +- 9 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 packages/cli/src/commands/integrations-delete.ts create mode 100644 packages/cli/src/commands/integrations-list.ts create mode 100644 packages/cli/src/commands/integrations.ts create mode 100644 packages/cli/src/commands/users-delete.ts create mode 100644 packages/cli/src/commands/users-list.ts create mode 100644 packages/cli/src/commands/users-update-password.ts create mode 100644 packages/cli/src/commands/users.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 00d1cf5af..a13ec75b9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -19,7 +19,9 @@ "clean": "rm -rf .turbo node_modules", "format": "prettier --check . --ignore-path ../../.gitignore", "lint": "eslint", - "typecheck": "tsc --noEmit" + "run": "pnpm with-env node cli.cjs", + "typecheck": "tsc --noEmit", + "with-env": "dotenv -e .env --" }, "prettier": "@homarr/prettier-config", "dependencies": { diff --git a/packages/cli/src/commands/integrations-delete.ts b/packages/cli/src/commands/integrations-delete.ts new file mode 100644 index 000000000..47ff12c22 --- /dev/null +++ b/packages/cli/src/commands/integrations-delete.ts @@ -0,0 +1,39 @@ +import { command, string } from "@drizzle-team/brocli"; + +import { db, eq } from "@homarr/db"; +import { integrations } from "@homarr/db/schema"; + +export const integrationsDelete = command({ + name: "delete", + desc: "Delete an integration by id or name", + options: { + id: string("id").alias("i").desc("ID of the integration"), + name: string("name").alias("n").desc("Name of the integration"), + }, + // eslint-disable-next-line no-restricted-syntax + handler: async (options) => { + if (!options.id && !options.name) { + console.error("Either --id or --name must be provided"); + return; + } + + let entry; + if (options.id) { + entry = await db.query.integrations.findFirst({ + where: eq(integrations.id, options.id), + }); + } else if (options.name) { + entry = await db.query.integrations.findFirst({ + where: eq(integrations.name, options.name), + }); + } + + if (!entry) { + console.error("Integration not found"); + return; + } + + await db.delete(integrations).where(eq(integrations.id, entry.id)); + console.log(`Integration ${entry.name} (${entry.id}) deleted`); + }, +}); diff --git a/packages/cli/src/commands/integrations-list.ts b/packages/cli/src/commands/integrations-list.ts new file mode 100644 index 000000000..3899ba5b9 --- /dev/null +++ b/packages/cli/src/commands/integrations-list.ts @@ -0,0 +1,21 @@ +import { command } from "@drizzle-team/brocli"; + +import { db } from "@homarr/db"; + +export const integrationsList = command({ + name: "list", + desc: "List all integrations (id and name)", + // eslint-disable-next-line no-restricted-syntax + handler: async () => { + const all = await db.query.integrations.findMany(); + if (all.length === 0) { + console.log("No integrations found"); + return; + } + + console.log("ID\t\t\t\tName"); + for (const i of all) { + console.log(`${i.id}\t${i.name}`); + } + }, +}); diff --git a/packages/cli/src/commands/integrations.ts b/packages/cli/src/commands/integrations.ts new file mode 100644 index 000000000..cef6cd910 --- /dev/null +++ b/packages/cli/src/commands/integrations.ts @@ -0,0 +1,10 @@ +import { command } from "@drizzle-team/brocli"; + +import { integrationsDelete } from "./integrations-delete"; +import { integrationsList } from "./integrations-list"; + +export const integrationsRoot = command({ + name: "integrations", + desc: "Group of commands to manage integrations", + subcommands: [integrationsList, integrationsDelete], +}); diff --git a/packages/cli/src/commands/users-delete.ts b/packages/cli/src/commands/users-delete.ts new file mode 100644 index 000000000..8e0cc5799 --- /dev/null +++ b/packages/cli/src/commands/users-delete.ts @@ -0,0 +1,39 @@ +import { command, string } from "@drizzle-team/brocli"; + +import { db, eq } from "@homarr/db"; +import { users } from "@homarr/db/schema"; + +export const usersDelete = command({ + name: "delete", + desc: "Delete a user by id or username", + options: { + id: string("id").alias("i").desc("ID of the user"), + username: string("username").alias("u").desc("Name of the user"), + }, + // eslint-disable-next-line no-restricted-syntax + handler: async (options) => { + if (!options.id && !options.username) { + console.error("Either --id or --username must be provided"); + return; + } + + let user; + if (options.id) { + user = await db.query.users.findFirst({ + where: eq(users.id, options.id), + }); + } else if (options.username) { + user = await db.query.users.findFirst({ + where: eq(users.name, options.username), + }); + } + + if (!user) { + console.error("User not found"); + return; + } + + await db.delete(users).where(eq(users.id, user.id)); + console.log(`User ${user.name ?? user.id} (${user.id}) deleted`); + }, +}); diff --git a/packages/cli/src/commands/users-list.ts b/packages/cli/src/commands/users-list.ts new file mode 100644 index 000000000..74ac50c2b --- /dev/null +++ b/packages/cli/src/commands/users-list.ts @@ -0,0 +1,22 @@ +import { command } from "@drizzle-team/brocli"; + +import { db } from "@homarr/db"; + +export const usersList = command({ + name: "list", + desc: "List all users (id, username, email)", + // eslint-disable-next-line no-restricted-syntax + handler: async () => { + const allUsers = await db.query.users.findMany(); + + if (allUsers.length === 0) { + console.log("No users found"); + return; + } + + console.log("ID\t\t\t\tUsername\tEmail"); + for (const user of allUsers) { + console.log(`${user.id}\t${user.name ?? ""}\t${user.email ?? ""}`); + } + }, +}); diff --git a/packages/cli/src/commands/users-update-password.ts b/packages/cli/src/commands/users-update-password.ts new file mode 100644 index 000000000..c579f0d24 --- /dev/null +++ b/packages/cli/src/commands/users-update-password.ts @@ -0,0 +1,50 @@ +import { command, string } from "@drizzle-team/brocli"; + +import { hashPasswordAsync } from "@homarr/auth"; +import { db, eq } from "@homarr/db"; +import { sessions, users } from "@homarr/db/schema"; + +export const usersUpdatePassword = command({ + name: "update-password", + desc: "Update the password for a user (credentials provider only)", + options: { + id: string("id").alias("i").desc("ID of the user"), + username: string("username").alias("u").desc("Name of the user"), + password: string("password").alias("p").required().desc("New password"), + }, + // eslint-disable-next-line no-restricted-syntax + handler: async (options) => { + if (!process.env.AUTH_PROVIDERS?.toLowerCase().includes("credentials")) { + console.error("Credentials provider is not enabled"); + return; + } + + if (!options.id && !options.username) { + console.error("Either --id or --username must be provided"); + return; + } + + const user = await db.query.users.findFirst({ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + where: options.id ? eq(users.id, options.id) : eq(users.name, options.username!), + }); + + if (!user?.salt) { + console.error("User not found or has no credentials record"); + return; + } + + await db + .update(users) + .set({ + password: await hashPasswordAsync(options.password, user.salt), + }) + .where(eq(users.id, user.id)); + + // invalidate existing sessions + await db.delete(sessions).where(eq(sessions.userId, user.id)); + console.log(`All sessions for user ${user.name ?? user.id} have been deleted`); + + console.log("Password updated successfully"); + }, +}); diff --git a/packages/cli/src/commands/users.ts b/packages/cli/src/commands/users.ts new file mode 100644 index 000000000..26655931f --- /dev/null +++ b/packages/cli/src/commands/users.ts @@ -0,0 +1,11 @@ +import { command } from "@drizzle-team/brocli"; + +import { usersDelete } from "./users-delete"; +import { usersList } from "./users-list"; +import { usersUpdatePassword } from "./users-update-password"; + +export const usersRoot = command({ + name: "users", + desc: "Group of commands to manage users", + subcommands: [usersList, usersDelete, usersUpdatePassword], +}); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 0041651b5..f1b0c658d 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,10 +1,12 @@ import { run } from "@drizzle-team/brocli"; import { fixUsernames } from "./commands/fix-usernames"; +import { integrationsRoot } from "./commands/integrations"; import { recreateAdmin } from "./commands/recreate-admin"; import { resetPassword } from "./commands/reset-password"; +import { usersRoot } from "./commands/users"; -const commands = [resetPassword, fixUsernames, recreateAdmin]; +const commands = [usersRoot, integrationsRoot, resetPassword, fixUsernames, recreateAdmin]; void run(commands, { name: "homarr-cli",