diff --git a/src/lib/posthog.ts b/src/lib/posthog.ts index 171a9f5..f61d55b 100644 --- a/src/lib/posthog.ts +++ b/src/lib/posthog.ts @@ -2,18 +2,18 @@ import process from "node:process"; import { PostHog } from "posthog-node"; import { loadConfig, saveConfig } from "../helpers/config.ts"; import { - cacheFeatureFlag, - getCachedFeatureFlag, + cacheFeatureFlag, + getCachedFeatureFlag, } from "../helpers/feature-flags.ts"; import { getApiUrl } from "../helpers/urls.ts"; const postHogClient = new PostHog( - "phc_ErsIQYNj6gPFTkHfupfuUGeKjabwtk3WTPdkTDktbU4", - { - host: "https://us.posthog.com", - flushAt: 1, - flushInterval: 0, - } + "phc_ErsIQYNj6gPFTkHfupfuUGeKjabwtk3WTPdkTDktbU4", + { + host: "https://us.posthog.com", + flushAt: 1, + flushInterval: 0, + }, ); // Uncomment this out to see Posthog debugging logs. // postHogClient.debug(); @@ -22,49 +22,49 @@ const postHogClient = new PostHog( * Whether the user has opted out of telemetry collection. */ export const IS_TRACKING_DISABLED = - process.env.SF_CLI_TELEMETRY_OPTOUT === "1" || - process.env.SF_CLI_TELEMETRY_OPTOUT === "true"; + process.env.SF_CLI_TELEMETRY_OPTOUT === "1" || + process.env.SF_CLI_TELEMETRY_OPTOUT === "true"; type EventMessage = Parameters[0]; const trackEvent = ({ - properties, - event, - ...payload + properties, + event, + ...payload }: Omit) => { - const runner = async () => { - const config = await loadConfig(); - let exchangeAccountId = config.account_id; - - if (!exchangeAccountId) { - const response = await fetch(await getApiUrl("me"), { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${config.auth_token}`, - }, - }); - - const data = await response.json(); - if (data.id) { - exchangeAccountId = data.id; - saveConfig({ ...config, account_id: data.id }); - } - } - - if (exchangeAccountId) { - postHogClient.capture({ - ...payload, - distinctId: exchangeAccountId, - event: `cli_sf_${event}`, - properties: { ...properties, source: "cli" }, - }); - } - }; - - if (!IS_TRACKING_DISABLED) { - runner(); - } + const runner = async () => { + const config = await loadConfig(); + let exchangeAccountId = config.account_id; + + if (!exchangeAccountId) { + const response = await fetch(await getApiUrl("me"), { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${config.auth_token}`, + }, + }); + + const data = await response.json(); + if (data.id) { + exchangeAccountId = data.id; + saveConfig({ ...config, account_id: data.id }); + } + } + + if (exchangeAccountId) { + postHogClient.capture({ + ...payload, + distinctId: exchangeAccountId, + event: `cli_sf_${event}`, + properties: { ...properties, source: "cli" }, + }); + } + }; + + if (!IS_TRACKING_DISABLED) { + runner(); + } }; type FeatureFlags = "vms"; @@ -73,33 +73,33 @@ type FeatureFlags = "vms"; * Checks if a feature is enabled for the current user. */ export const isFeatureEnabled = async (feature: FeatureFlags) => { - const config = await loadConfig(); - const exchangeAccountId = config.account_id; - - if (!exchangeAccountId) { - return false; - } - - // Check cache first - const cachedFlag = await getCachedFeatureFlag(feature, exchangeAccountId); - if (cachedFlag) { - return cachedFlag.value; - } - - // If not in cache or expired, fetch from PostHog - const result = await postHogClient.isFeatureEnabled( - feature, - exchangeAccountId - ); - - // Cache the result (PostHog returns undefined if there's an error, default to false) - const finalResult = result ?? false; - await cacheFeatureFlag(feature, exchangeAccountId, finalResult); - - return finalResult; + const config = await loadConfig(); + const exchangeAccountId = config.account_id; + + if (!exchangeAccountId) { + return false; + } + + // Check cache first + const cachedFlag = await getCachedFeatureFlag(feature, exchangeAccountId); + if (cachedFlag) { + return cachedFlag.value; + } + + // If not in cache or expired, fetch from PostHog + const result = await postHogClient.isFeatureEnabled( + feature, + exchangeAccountId, + ); + + // Cache the result (PostHog returns undefined if there's an error, default to false) + const finalResult = result ?? false; + await cacheFeatureFlag(feature, exchangeAccountId, finalResult); + + return finalResult; }; export const analytics = { - track: trackEvent, - shutdown: () => postHogClient.shutdown(), + track: trackEvent, + shutdown: () => postHogClient.shutdown(), }; diff --git a/src/lib/vm.ts b/src/lib/vm.ts index 058d90e..471667d 100644 --- a/src/lib/vm.ts +++ b/src/lib/vm.ts @@ -1,19 +1,95 @@ import type { Command } from "@commander-js/extra-typings"; import { isFeatureEnabled } from "./posthog.ts"; +import { apiClient } from "../apiClient.ts"; +import { readFileSync } from "node:fs"; +import { + logAndQuit, + logSessionTokenExpiredAndQuit, +} from "../helpers/errors.ts"; export async function registerVM(program: Command) { - const isEnabled = await isFeatureEnabled("vms"); + const isEnabled = await isFeatureEnabled("vms"); - if (!isEnabled) { - return; - } + if (!isEnabled) { + return; + } - program - .command("vm") - .description("Manage virtual machines") - .action(async () => { - console.log("VMs!!!"); + const vm = program + .command("vm") + .aliases(["v", "vms"]) + .description("Manage virtual machines"); - process.exit(0); - }); + const client = await apiClient(); + + vm.command("list") + .description("List all virtual machines") + .action(async () => { + const { data, error, response } = await client.GET("/v0/vm/nodes"); + + if (!response.ok) { + if (response.status === 401) { + await logSessionTokenExpiredAndQuit(); + } + logAndQuit(`Failed to list VMs: ${error?.message}`); + } + + if (!data?.data) { + logAndQuit("No VMs found"); + } + + console.table( + data.data.map((node) => ({ + id: node.id, + status: node.status, + created_at: node.created_at, + })), + ); + }); + + vm.command("script") + .description("Push a startup script to VMs") + .requiredOption("-f, --file ", "Path to startup script file") + .action(async (options) => { + let script: string; + try { + script = readFileSync(options.file, "utf-8"); + } catch (err) { + logAndQuit(`Failed to read script file: ${err.message}`); + } + + const { error, response } = await client.POST("/v0/vm/script", { + body: { script }, + }); + + if (!response.ok) { + if (response.status === 401) { + await logSessionTokenExpiredAndQuit(); + } + logAndQuit(`Failed to upload script: ${error?.message}`); + } + + console.log("Successfully uploaded startup script"); + }); + + vm.command("logs") + .description("View VM logs") + .action(async () => { + const { data, error, response } = await client.GET("/v0/vm/logs"); + + if (!response.ok) { + if (response.status === 401) { + await logSessionTokenExpiredAndQuit(); + } + logAndQuit(`Failed to fetch logs: ${error?.message}`); + } + + if (!data?.data?.length) { + console.log("No logs found"); + return; + } + + for (const log of data.data) { + console.log(`[${log.timestamp}] ${log.message}`); + } + }); }