diff --git a/apps/cli/cli-core/src/open-file.ts b/apps/cli/cli-core/src/open-file.ts new file mode 100644 index 00000000..5756410b --- /dev/null +++ b/apps/cli/cli-core/src/open-file.ts @@ -0,0 +1,59 @@ +import childProcess from "child_process"; +import { promisify } from "util"; +import os from "os"; +import fs from "fs/promises"; +import path from "path"; + +import logger from "@captain/logger"; + +const promisifiedExecFile = promisify(childProcess.execFile); + +const getCommand = () => { + const plat = os + .platform() + .toLowerCase() + .replace(/[0-9]/g, ``) + .replace(`darwin`, `macos`); + + if (plat === "win") return "start"; + if (plat === "linux") return "xdg-open"; + if (plat === "macos") return "open"; + + throw new Error("Idk what os this is"); +}; + +const pathExists = async (path: string) => { + try { + await fs.access(path); + return true; + } catch { + return false; + } +}; + +const writeFile = async (filePath: string, data: string) => { + try { + const dirname = path.dirname(filePath); + const exist = await pathExists(dirname); + if (!exist) { + await fs.mkdir(dirname, { recursive: true }); + } + + await fs.writeFile(filePath, data, "utf8"); + } catch (err) { + throw err; + } +}; + +export async function openFile(path: string) { + const exists = await pathExists(path); + const maybeFileName = path.split("/").pop(); + + // create file if it doesn't exist + if (!exists) { + logger.warn(`${maybeFileName ?? "file"} does not exist, creating it now!`); + await writeFile(path, "{}"); + } + + return await promisifiedExecFile(getCommand(), [path]); +} diff --git a/apps/cli/cli-core/src/open-folder.ts b/apps/cli/cli-core/src/open-folder.ts index f69f9567..50ecdc18 100644 --- a/apps/cli/cli-core/src/open-folder.ts +++ b/apps/cli/cli-core/src/open-folder.ts @@ -21,6 +21,7 @@ const getCommand = () => { throw new Error("Idk what os this is"); }; +// TODO: IGOR add openFile, or a flag to create file instead of dir if it doesn't exist. Trying to "Open .config file" in fileRunner brought me here 💀 // Ripped from https://www.npmjs.com/package/open-file-explorer export async function openInExplorer(path: string) { // Create the directory if it doesn't exist @@ -31,3 +32,4 @@ export async function openInExplorer(path: string) { return await promisifiedExecFile(getCommand(), [path]); } + diff --git a/apps/cli/cli-core/src/trpc.ts b/apps/cli/cli-core/src/trpc.ts index 2f3be660..cfb9ef7d 100644 --- a/apps/cli/cli-core/src/trpc.ts +++ b/apps/cli/cli-core/src/trpc.ts @@ -1,4 +1,3 @@ -import { ConfigValidatorType } from "./update-config"; import { initTRPC } from "@trpc/server"; import superjson from "superjson"; import { z } from "zod"; @@ -6,12 +5,15 @@ import fetch from "node-fetch"; import fsPromises from "fs/promises"; import fs from "fs"; import path from "path"; +import { observable } from "@trpc/server/observable"; import { openInExplorer } from "./open-folder"; +import { openFile } from "./open-file"; import { getSampleHooks } from "./get-sample-hooks"; import { HOOK_PATH } from "./constants"; import { configValidator, updateConfig } from "./update-config"; import { substituteTemplate } from "./templateSubstitution"; +import { getFullPath, getRoute } from "./utils/get-full-path"; export type { ConfigValidatorType } from "./update-config"; type ExtendedConfigValidatorType = ConfigValidatorType & { @@ -19,10 +21,9 @@ type ExtendedConfigValidatorType = ConfigValidatorType & { }; import logger from "@captain/logger"; -import { observable } from "@trpc/server/observable"; import type { LogLevels } from "@captain/logger"; -import { getFullPath, getRoute } from "./utils/get-full-path"; +import type { ConfigValidatorType } from "./update-config"; export const t = initTRPC.create({ transformer: superjson, @@ -164,6 +165,40 @@ export const cliApiRouter = t.router({ } }), + openFile: t.procedure + .input(z.object({ path: z.string() })) + .mutation(async ({ input }) => { + // if running in codespace, early return + // eslint-disable-next-line turbo/no-undeclared-env-vars + if (process.env.CODESPACES) { + throw new Error( + "Sorry, opening files in codespaces is not supported yet." + ); + } + // if running over ssh, early return + if ( + // eslint-disable-next-line turbo/no-undeclared-env-vars + process.env.SSH_CONNECTION || + // eslint-disable-next-line turbo/no-undeclared-env-vars + process.env.SSH_CLIENT || + // eslint-disable-next-line turbo/no-undeclared-env-vars + process.env.SSH_TTY + ) { + throw new Error( + "Sorry, opening files on remote connections is not supported yet." + ); + } + + try { + await openFile(path.join(HOOK_PATH, input.path)); + } catch (e) { + logger.error( + "Failed to open file (unless you're on Windows, then this just happens)", + e + ); + } + }), + getSampleHooks: t.procedure.mutation(async () => { await getSampleHooks(); }), diff --git a/apps/cli/cli-web/package.json b/apps/cli/cli-web/package.json index da84caf3..07916cc0 100644 --- a/apps/cli/cli-web/package.json +++ b/apps/cli/cli-web/package.json @@ -29,6 +29,7 @@ "react-hook-form": "^7.43.0", "react-hot-toast": "^2.4.0", "superjson": "^1.12.1", + "tailwind-merge": "^1.12.0", "wouter": "^2.10.0", "zod": "^3.19.1" }, diff --git a/apps/cli/cli-web/src/App.tsx b/apps/cli/cli-web/src/App.tsx index bfebf8fa..3b3e2735 100644 --- a/apps/cli/cli-web/src/App.tsx +++ b/apps/cli/cli-web/src/App.tsx @@ -17,6 +17,7 @@ import { classNames } from "./utils/classnames"; import { useFileRoute } from "./utils/useRoute"; import { FileRunner } from "./components/filerunner"; import { cliApi } from "./utils/api"; +import { ButtonDropdown } from "./components/common/button"; const SubscriptionsHelper = () => { useConnectionStateToasts(); @@ -121,70 +122,31 @@ export default function AppCore() { const NavMenu = () => { return ( -
+ } + items={[ + { + name: "Support", + href: "https://discord.gg/4wD3CNdsf6", + icon:48 + ? "hidden" + : "hidden sm:inline" + )} + > + {page} +
+ +