From f3058901c4e20467b5591f417170291f3a9cc95c Mon Sep 17 00:00:00 2001 From: Bryan Robinson Date: Thu, 23 Apr 2026 15:35:34 -0400 Subject: [PATCH 1/9] Working posthog logging --- .gitignore | 3 + package-lock.json | 91 ++++++++++++++++++++++++++++- package.json | 1 + src/commands/check-common-config.ts | 4 +- src/commands/email-create.ts | 3 + src/commands/email-download.ts | 3 +- src/commands/email-duplicate.ts | 3 + src/commands/email-html-to-text.ts | 3 + src/commands/email-upload.ts | 4 +- src/commands/email-watch.ts | 4 +- src/commands/import-generate.ts | 4 +- src/commands/index.ts | 2 + src/commands/kickstart-install.ts | 4 +- src/commands/kickstart-kill.ts | 3 +- src/commands/kickstart-start.ts | 3 +- src/commands/kickstart-stop.ts | 3 +- src/commands/lambda-delete.ts | 4 +- src/commands/lambda-retrieve.ts | 4 +- src/commands/lambda-update.ts | 4 +- src/commands/message-download.ts | 5 +- src/commands/message-upload.ts | 4 +- src/commands/telemetry-disable.ts | 19 ++++++ src/commands/telemetry-enable.ts | 19 ++++++ src/commands/theme-download.ts | 4 +- src/commands/theme-upload.ts | 4 +- src/commands/theme-watch.ts | 4 +- src/utils.ts | 39 +++++++++++++ 27 files changed, 228 insertions(+), 20 deletions(-) create mode 100644 src/commands/telemetry-disable.ts create mode 100644 src/commands/telemetry-enable.ts diff --git a/.gitignore b/.gitignore index 584b6cf..b0e9a05 100644 --- a/.gitignore +++ b/.gitignore @@ -215,3 +215,6 @@ lambdas/ emails/ *.swp + + +.fa/ \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4213275..1b8eeb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@fusionauth/cli", - "version": "1.6.0", + "version": "1.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@fusionauth/cli", - "version": "1.6.0", + "version": "1.7.0", "license": "Apache-2.0", "dependencies": { "@comandeer/cli-spinner": "^1.0.2", @@ -27,6 +27,7 @@ "log-symbols": "5.1.0", "log-update": "5.0.1", "merge": "2.1.1", + "posthog-node": "^5.21.2", "queue": "7.0.0", "remove-undefined-objects": "3.0.0", "uuid": "9.0.0", @@ -480,6 +481,15 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@posthog/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.10.0.tgz", + "integrity": "sha512-Xk3JQ+cdychsvftrV3G9ZrN9W329lbyFW0pGJXFGKFQf8qr4upw2SgNg9BVorjSrfhoXZRnJGt/uNF4nGFBL5A==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.6" + } + }, "node_modules/@selderee/plugin-htmlparser2": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", @@ -917,6 +927,20 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -1267,6 +1291,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -1400,6 +1430,15 @@ "url": "https://ko-fi.com/killymxi" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/peberminta": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", @@ -1419,6 +1458,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/posthog-node": { + "version": "5.21.2", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.21.2.tgz", + "integrity": "sha512-Jehlu0KguL1LLyUczCt86OtA5INmeStK3zcgbv1BSyMcNxs0HP3GQogBrYhwhqHsk6JopiFFVpJyZEoXOUMhGw==", + "license": "MIT", + "dependencies": { + "@posthog/core": "1.10.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/queue": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/queue/-/queue-7.0.0.tgz", @@ -1493,6 +1544,27 @@ "url": "https://ko-fi.com/killymxi" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -1670,6 +1742,21 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/widest-line": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", diff --git a/package.json b/package.json index f5feacc..7b39441 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "log-symbols": "5.1.0", "log-update": "5.0.1", "merge": "2.1.1", + "posthog-node": "^5.21.2", "queue": "7.0.0", "remove-undefined-objects": "3.0.0", "uuid": "9.0.0", diff --git a/src/commands/check-common-config.ts b/src/commands/check-common-config.ts index 8c051a0..fa7ffcf 100644 --- a/src/commands/check-common-config.ts +++ b/src/commands/check-common-config.ts @@ -1,7 +1,7 @@ import {Command, Option} from "@commander-js/extra-typings"; import {FusionAuthClient} from '@fusionauth/typescript-client'; import chalk from "chalk"; -import {errorAndExit} from '../utils.js'; +import {errorAndExit, logEvent} from '../utils.js'; import {apiKeyOption, hostOption} from "../options.js"; interface CheckResult { @@ -19,6 +19,8 @@ const action = async function ({key: apiKey, host, skipLicenseCheck}: { host: string; skipLicenseCheck: boolean; }) { + logEvent('cli command check:common-config') + console.log(chalk.blue(`Checking common configuration on ${host}...`)); const results: CheckResult[] = []; diff --git a/src/commands/email-create.ts b/src/commands/email-create.ts index 0cf2aa5..c1c2a9f 100644 --- a/src/commands/email-create.ts +++ b/src/commands/email-create.ts @@ -2,6 +2,7 @@ import {Command} from "@commander-js/extra-typings"; import {ensureDir, ensureFile} from "fs-extra"; import {v4} from "uuid"; import chalk from "chalk"; +import { logEvent } from "../utils.js"; // noinspection JSUnusedGlobalSymbols export const emailCreate = new Command('email:create') @@ -9,6 +10,8 @@ export const emailCreate = new Command('email:create') .option('-o, --output ', 'The output directory', './emails/') .option('-l, --locales ', 'The locales to create.', []) .action(async ({output, locales}) => { + logEvent('cli command email:create') + console.log(`Creating email template in ${output}`); const emailTemplateId = v4(); diff --git a/src/commands/email-download.ts b/src/commands/email-download.ts index 6d64f57..2ffe178 100644 --- a/src/commands/email-download.ts +++ b/src/commands/email-download.ts @@ -1,5 +1,5 @@ import {Command} from "@commander-js/extra-typings"; -import {getEmailErrorMessage, getEmailSuccessMessage, reportError} from "../utils.js"; +import {getEmailErrorMessage, getEmailSuccessMessage, logEvent, reportError} from "../utils.js"; import {EmailTemplate, FusionAuthClient} from "@fusionauth/typescript-client"; import {mkdir, writeFile} from "fs/promises"; import chalk from "chalk"; @@ -16,6 +16,7 @@ export const emailDownload = new Command('email:download') .addOption(hostOption) .option('-c, --clean', 'Clean the output directory before downloading', false) .action(async (emailTemplateId, {output, key: apiKey, host, clean}) => { + logEvent('cli command email:download') let clientResponse; const errorMessage = getEmailErrorMessage('download', emailTemplateId); diff --git a/src/commands/email-duplicate.ts b/src/commands/email-duplicate.ts index e4631b2..587846e 100644 --- a/src/commands/email-duplicate.ts +++ b/src/commands/email-duplicate.ts @@ -2,6 +2,7 @@ import {Command} from "@commander-js/extra-typings"; import {copy} from "fs-extra"; import {v4} from "uuid"; import chalk from "chalk"; +import { logEvent } from "../utils.js"; // noinspection JSUnusedGlobalSymbols export const emailDuplicate = new Command('email:duplicate') @@ -9,6 +10,8 @@ export const emailDuplicate = new Command('email:duplicate') .argument('', 'The email template id to duplicate') .option('-o, --output ', 'The output directory', './emails/') .action(async (emailTemplateId: string, {output}) => { + logEvent('cli command email:duplicate') + console.log(`Duplicating email template ${emailTemplateId} in ${output}`); const newEmailTemplateId = v4(); diff --git a/src/commands/email-html-to-text.ts b/src/commands/email-html-to-text.ts index da44cf9..d609cb6 100644 --- a/src/commands/email-html-to-text.ts +++ b/src/commands/email-html-to-text.ts @@ -3,6 +3,7 @@ import {validate as isUUID} from "uuid"; import chalk from "chalk"; import {lstat, readdir, readFile, writeFile} from "fs/promises"; import {compile} from "html-to-text"; +import { logEvent } from "../utils.js"; const htmlToText = compile({ wordwrap: false, @@ -17,6 +18,8 @@ export const emailHtmlToText = new Command('email:html-to-text') .argument('[emailTemplateId]', 'The email template id to convert. If not provided, all email templates will be converted') .option('-o, --output ', 'The output directory', './emails/') .action(async (emailTemplateId: string | undefined, {output}) => { + logEvent('cli command email:html-to-text') + if (!emailTemplateId) { console.log(`Converting all email templates in ${output}`); } else { diff --git a/src/commands/email-upload.ts b/src/commands/email-upload.ts index 16e8200..c745107 100644 --- a/src/commands/email-upload.ts +++ b/src/commands/email-upload.ts @@ -1,5 +1,5 @@ import {Command} from "@commander-js/extra-typings"; -import {getEmailErrorMessage, reportError} from "../utils.js"; +import {getEmailErrorMessage, logEvent, reportError} from "../utils.js"; import {EmailTemplate, EmailTemplateRequest, FusionAuthClient} from "@fusionauth/typescript-client"; import {pathExists} from "fs-extra"; import {lstat, readdir, readFile} from "fs/promises"; @@ -24,6 +24,8 @@ export const emailUpload = new Command('email:upload') .option('-o, --overwrite', 'Overwrite the existing email template with the new one. F.e. locales that are not defined in the directory, but on the FusionAuth server will be removed.', false) .option('--no-create', 'Create the email template if it does not exist') .action(async (emailTemplateId, {input, key: apiKey, host, overwrite, create}) => { + logEvent('cli command email:upload') + const errorMessage = getEmailErrorMessage('uploading', emailTemplateId); if (emailTemplateId) { diff --git a/src/commands/email-watch.ts b/src/commands/email-watch.ts index 09295ea..a583222 100644 --- a/src/commands/email-watch.ts +++ b/src/commands/email-watch.ts @@ -1,5 +1,5 @@ import {Command} from "@commander-js/extra-typings"; -import {reportError} from "../utils.js"; +import {logEvent, reportError} from "../utils.js"; import {watch} from "chokidar"; import Queue from "queue"; import logUpdate from "log-update"; @@ -40,6 +40,8 @@ export const emailWatch = new Command('email:watch') .addOption(apiKeyOption) .addOption(hostOption) .action(async ({input, key: apiKey, host}) => { + logEvent('cli command email:watch') + console.log(`Watching email templates in ${input}`); const watchedFiles = [ diff --git a/src/commands/import-generate.ts b/src/commands/import-generate.ts index a0e635a..ab79e76 100644 --- a/src/commands/import-generate.ts +++ b/src/commands/import-generate.ts @@ -3,7 +3,7 @@ import {FusionAuthClient} from '@fusionauth/typescript-client'; import {readFile} from 'fs/promises'; import chalk from 'chalk'; import {join} from 'path'; -import {errorAndExit} from '../utils.js'; +import {errorAndExit, logEvent} from '../utils.js'; import { faker } from '@faker-js/faker'; import * as fs from 'fs'; @@ -17,6 +17,8 @@ const action = async function ({numberOfFiles, countPerFile, applicationId, grou filePrefix?: string | undefined; } ): Promise { + logEvent('cli command import:generate') + console.log(`Generating users`); try { const finalNumberOfFiles = (numberOfFiles !== undefined ? parseInt(numberOfFiles) : 10); diff --git a/src/commands/index.ts b/src/commands/index.ts index 0bd1da9..9a819e0 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -14,6 +14,8 @@ export * from './lambda-delete.js'; export * from './lambda-retrieve.js'; export * from './message-download.js'; export * from './message-upload.js'; +export * from './telemetry-disable.js'; +export * from './telemetry-enable.js'; export * from './theme-watch.js'; export * from './theme-upload.js'; export * from './theme-download.js'; diff --git a/src/commands/kickstart-install.ts b/src/commands/kickstart-install.ts index 8317913..206797a 100644 --- a/src/commands/kickstart-install.ts +++ b/src/commands/kickstart-install.ts @@ -8,7 +8,7 @@ import fs from 'node:fs' import path from "node:path"; import { dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { betaWarning, isDirEmpty, isDockerInstalled } from "../utils.js"; +import { betaWarning, isDirEmpty, isDockerInstalled, logEvent } from "../utils.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -32,6 +32,8 @@ async function createKickstart(kickstartPath: string, answers: any, newDir: stri const action = async function (dir: string) { const dockerInstalled = isDockerInstalled(); const directory = path.resolve(dir) + logEvent('cli command kickstart:install') + betaWarning() try { diff --git a/src/commands/kickstart-kill.ts b/src/commands/kickstart-kill.ts index 0099f0c..cff80fc 100644 --- a/src/commands/kickstart-kill.ts +++ b/src/commands/kickstart-kill.ts @@ -2,7 +2,7 @@ import { Command } from "@commander-js/extra-typings"; import chalk from "chalk"; import { spawn } from 'node:child_process'; -import { betaWarning, isDockerInstalled } from "../utils.js"; +import { betaWarning, isDockerInstalled, logEvent } from "../utils.js"; import boxen from "boxen"; import inquirer from "inquirer"; @@ -14,6 +14,7 @@ const action = async function () { if (!isDockerInstalled()) throw (chalk.red('Error: You need Docker to run.')) if (process.cwd() != process.env.CLI_DIR) throw(chalk.red('Error: Current directory was not kickstarted.')) + logEvent('cli command kickstart:kill') inquirer.prompt([ { diff --git a/src/commands/kickstart-start.ts b/src/commands/kickstart-start.ts index 0f18c9d..e3853cb 100644 --- a/src/commands/kickstart-start.ts +++ b/src/commands/kickstart-start.ts @@ -2,7 +2,7 @@ import { Command } from "@commander-js/extra-typings"; import chalk from "chalk"; import { spawn } from 'node:child_process'; -import { betaWarning, isDockerInstalled } from "../utils.js"; +import { betaWarning, isDockerInstalled, logEvent } from "../utils.js"; import 'dotenv/config'; import boxen from "boxen"; import yoctoSpinner from "yocto-spinner"; @@ -16,6 +16,7 @@ const action = async function () { if (process.cwd() != process.env.CLI_DIR) throw (chalk.red('Error: Current directory was not kickstarted.')) if (!isDockerInstalled()) console.error(chalk.red('Error: You need Docker to run.')) + logEvent('cli command kickstart:start') const starting = spawn('docker compose up -d', { shell: true, stdio: 'inherit' }) starting.on('error', e => { diff --git a/src/commands/kickstart-stop.ts b/src/commands/kickstart-stop.ts index 9b8ba8d..932d607 100644 --- a/src/commands/kickstart-stop.ts +++ b/src/commands/kickstart-stop.ts @@ -2,7 +2,7 @@ import { Command } from "@commander-js/extra-typings"; import chalk from "chalk"; import { spawn } from 'node:child_process'; -import { betaWarning, isDockerInstalled } from "../utils.js"; +import { betaWarning, isDockerInstalled, logEvent } from "../utils.js"; import boxen from "boxen"; @@ -12,6 +12,7 @@ const action = async function () { try { if (process.cwd() != process.env.CLI_DIR) throw (chalk.red('Error: Current directory was not kickstarted.')) if (!isDockerInstalled()) throw (chalk.red('Error: You need Docker to run.')) + logEvent('cli command kickstart:stop') console.log(chalk.yellow('Stopping FusionAuth...\n')) const starting = spawn('docker compose stop', { shell: true, stdio: 'inherit' }) diff --git a/src/commands/lambda-delete.ts b/src/commands/lambda-delete.ts index a22b261..45dab09 100644 --- a/src/commands/lambda-delete.ts +++ b/src/commands/lambda-delete.ts @@ -1,13 +1,15 @@ import {Command} from '@commander-js/extra-typings'; import {FusionAuthClient} from '@fusionauth/typescript-client'; import chalk from 'chalk'; -import {errorAndExit} from '../utils.js'; +import {errorAndExit, logEvent} from '../utils.js'; import {apiKeyOption, hostOption} from "../options.js"; const action = async function (lambdaId: string, {key: apiKey, host}: { key: string; host: string }) { + logEvent('cli command lambda:delete') + console.log(`Deleting lambda ${lambdaId} from ${host}`); try { const fusionAuthClient = new FusionAuthClient(apiKey, host); diff --git a/src/commands/lambda-retrieve.ts b/src/commands/lambda-retrieve.ts index 4a99b44..b53a527 100644 --- a/src/commands/lambda-retrieve.ts +++ b/src/commands/lambda-retrieve.ts @@ -4,7 +4,7 @@ import chalk from 'chalk'; import {existsSync} from 'fs'; import {join} from 'path'; import {mkdir, writeFile} from 'fs/promises'; -import {errorAndExit, toJson} from '../utils.js'; +import {errorAndExit, logEvent, toJson} from '../utils.js'; import {apiKeyOption, hostOption} from "../options.js"; const action = async function (lambdaId: string, {output, key: apiKey, host}: { @@ -12,6 +12,8 @@ const action = async function (lambdaId: string, {output, key: apiKey, host}: { key: string; host: string }) { + logEvent('cli command lambda:retrieve') + console.log(`Retrieving lambda ${lambdaId} from ${host}`); try { const fusionAuthClient = new FusionAuthClient(apiKey, host); diff --git a/src/commands/lambda-update.ts b/src/commands/lambda-update.ts index c6e7ec4..f7d48cb 100644 --- a/src/commands/lambda-update.ts +++ b/src/commands/lambda-update.ts @@ -3,7 +3,7 @@ import {FusionAuthClient} from '@fusionauth/typescript-client'; import {readFile} from 'fs/promises'; import chalk from 'chalk'; import {join} from 'path'; -import {errorAndExit} from '../utils.js'; +import {errorAndExit, logEvent} from '../utils.js'; import {apiKeyOption, hostOption} from "../options.js"; const action = async function (lambdaId: string, {input, key: apiKey, host}: { @@ -11,6 +11,8 @@ const action = async function (lambdaId: string, {input, key: apiKey, host}: { key: string; host: string }): Promise { + logEvent('cli command lambda:update') + console.log(`Updating lambda ${lambdaId} on ${host}`); try { const filename = join(input, lambdaId + ".json"); diff --git a/src/commands/message-download.ts b/src/commands/message-download.ts index 5535c4d..0ff3724 100644 --- a/src/commands/message-download.ts +++ b/src/commands/message-download.ts @@ -1,5 +1,5 @@ import {Command} from "@commander-js/extra-typings"; -import {getMessageErrorMessage, getMessageSuccessMessage, reportError} from "../utils.js"; +import {getMessageErrorMessage, getMessageSuccessMessage, logEvent, reportError} from "../utils.js"; import {MessageTemplate, SMSMessageTemplate, FusionAuthClient} from "@fusionauth/typescript-client"; import {mkdir, writeFile} from "fs/promises"; import chalk from "chalk"; @@ -16,7 +16,8 @@ export const messageDownload = new Command('message:download') .addOption(hostOption) .option('-c, --clean', 'Clean the output directory before downloading', false) .action(async (messageTemplateId, {output, key: apiKey, host, clean}) => { - + logEvent('cli command message:download') + let clientResponse; const errorMessage = getMessageErrorMessage('download', messageTemplateId); diff --git a/src/commands/message-upload.ts b/src/commands/message-upload.ts index 3d433df..5654934 100644 --- a/src/commands/message-upload.ts +++ b/src/commands/message-upload.ts @@ -1,5 +1,5 @@ import {Command} from "@commander-js/extra-typings"; -import {getMessageErrorMessage, reportError} from "../utils.js"; +import {getMessageErrorMessage, logEvent, reportError} from "../utils.js"; import {MessageTemplate, MessageTemplateRequest, SMSMessageTemplate, FusionAuthClient} from "@fusionauth/typescript-client"; import {pathExists} from "fs-extra"; import {lstat, readdir, readFile} from "fs/promises"; @@ -21,6 +21,8 @@ export const messageUpload = new Command('message:upload') .option('-o, --overwrite', 'Overwrite the existing message template with the new one. F.e. locales that are not defined in the directory, but on the FusionAuth server will be removed.', false) .option('--no-create', 'Create the message template if it does not exist') .action(async (messageTemplateId, {input, key: apiKey, host, overwrite, create}) => { + logEvent('cli command message:upload') + const errorMessage = getMessageErrorMessage('uploading', messageTemplateId); if (messageTemplateId) { diff --git a/src/commands/telemetry-disable.ts b/src/commands/telemetry-disable.ts new file mode 100644 index 0000000..df6bb73 --- /dev/null +++ b/src/commands/telemetry-disable.ts @@ -0,0 +1,19 @@ +import { Command } from "@commander-js/extra-typings"; +import { __dirname, loadConfig } from '../utils.js' +import fs from 'node:fs' +const action = async function () { + + try { + let config = loadConfig() + config.globalConfig.telemetry = false + fs.writeFileSync(__dirname + '/.fa/config.json', JSON.stringify(config.globalConfig, null, 2)) + } catch (err) { + console.log(err) + } + +} + +export const telemetryDisable = new Command() + .description('Sets a global config value to disallow telemetry from being collected') + .command('telemetry:disable') + .action(action) diff --git a/src/commands/telemetry-enable.ts b/src/commands/telemetry-enable.ts new file mode 100644 index 0000000..307a534 --- /dev/null +++ b/src/commands/telemetry-enable.ts @@ -0,0 +1,19 @@ +import { Command } from "@commander-js/extra-typings"; +import { __dirname, loadConfig } from '../utils.js' +import fs from 'node:fs' +const action = async function () { + + try { + let config = loadConfig() + config.globalConfig.telemetry = true + fs.writeFileSync(__dirname + '/.fa/config.json', JSON.stringify(config.globalConfig, null, 2)) + } catch (err) { + console.log(err) + } + +} + +export const telemetryEnable = new Command() + .description('Sets a global config value to allow telemetry to be collected') + .command('telemetry:enable') + .action(action) diff --git a/src/commands/theme-download.ts b/src/commands/theme-download.ts index 9d13fcb..625088e 100644 --- a/src/commands/theme-download.ts +++ b/src/commands/theme-download.ts @@ -3,7 +3,7 @@ import {FusionAuthClient} from '@fusionauth/typescript-client'; import chalk from 'chalk'; import {existsSync} from 'fs'; import {mkdir, writeFile} from 'fs/promises'; -import {errorAndExit, toString} from '../utils.js'; +import {errorAndExit, logEvent, toString} from '../utils.js'; import {apiKeyOption, hostOption, themeTypeOption} from "../options.js"; // noinspection JSUnusedGlobalSymbols @@ -15,6 +15,8 @@ export const themeDownload = new Command('theme:download') .addOption(hostOption) .addOption(themeTypeOption) .action(async (themeId: string, {output, key: apiKey, host, types}) => { + logEvent('cli command theme:download') + console.log(`Downloading theme ${themeId} to ${output}`); try { diff --git a/src/commands/theme-upload.ts b/src/commands/theme-upload.ts index 9c723e1..702118d 100644 --- a/src/commands/theme-upload.ts +++ b/src/commands/theme-upload.ts @@ -2,7 +2,7 @@ import {Command} from '@commander-js/extra-typings'; import {FusionAuthClient, Templates, Theme} from '@fusionauth/typescript-client'; import chalk from 'chalk'; import {readdir, readFile} from 'fs/promises'; -import {errorAndExit, getLocaleFromLocalizedMessageFileName} from '../utils.js'; +import {errorAndExit, getLocaleFromLocalizedMessageFileName, logEvent} from '../utils.js'; import {apiKeyOption, hostOption, themeTypeOption} from "../options.js"; // noinspection JSUnusedGlobalSymbols @@ -14,6 +14,8 @@ export const themeUpload = new Command('theme:upload') .addOption(hostOption) .addOption(themeTypeOption) .action(async (themeId: string, {input, key: apiKey, host, types}) => { + logEvent('cli command theme:upload') + console.log(`Uploading theme ${themeId} from ${input}`); try { diff --git a/src/commands/theme-watch.ts b/src/commands/theme-watch.ts index 5d203eb..eb2918d 100644 --- a/src/commands/theme-watch.ts +++ b/src/commands/theme-watch.ts @@ -1,6 +1,6 @@ import {Command} from '@commander-js/extra-typings'; import {watch} from 'chokidar'; -import {getLocaleFromLocalizedMessageFileName, reportError} from '../utils.js'; +import {getLocaleFromLocalizedMessageFileName, logEvent, reportError} from '../utils.js'; import Queue from 'queue'; import {FusionAuthClient, Theme} from '@fusionauth/typescript-client'; import {readFile} from 'fs/promises'; @@ -22,6 +22,8 @@ export const themeWatch = new Command('theme:watch') .addOption(hostOption) .addOption(themeTypeOption) .action((themeId: string, {input, key: apiKey, host, types}) => { + logEvent('cli command theme:watch') + console.log(`Watching theme directory ${input} for changes and uploading to ${themeId}`); const watchedFiles: string[] = []; diff --git a/src/utils.ts b/src/utils.ts index 45b32f2..faa1f03 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,10 +1,24 @@ import ClientResponse from '@fusionauth/typescript-client/build/src/ClientResponse.js'; import {Errors} from '@fusionauth/typescript-client'; import fs from 'node:fs' +import { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; import chalk from 'chalk'; import boxen from 'boxen'; import { execSync } from 'node:child_process'; + +import { PostHog } from 'posthog-node' + +export const posthogClient = new PostHog( + 'phc_nB6C2uZX2LA6ce6VAaWZxBYPtq1wYH5x8A3n36DaLzQ', + { host: 'https://us.i.posthog.com' } +) + + + +export const __dirname = dirname(fileURLToPath(import.meta.url)); + /** * Checks if the response is a client response * @param response @@ -188,4 +202,29 @@ export function isDirEmpty(path: string) { } else { return true } +} + +export function loadConfig() { + const globalConfig = JSON.parse(fs.readFileSync(__dirname + '/.fa/config.json').toString()) + // TODO: Combine this with a local-project config + return {globalConfig} +} + +export function allowsTelemetry() { + const {globalConfig} = loadConfig() + + return globalConfig.telemetry +} + + +export async function logEvent(eventName:string, eventDetails:any = {}) { + const config = loadConfig() + if (allowsTelemetry()) { + posthogClient.capture({ + distinctId: config.globalConfig.id, + event: eventName, + properties: eventDetails + }) + await posthogClient.shutdown() + } } \ No newline at end of file From d499e9ef653e47a8c2036b3eca998db9fa776469 Mon Sep 17 00:00:00 2001 From: Bryan Robinson Date: Fri, 24 Apr 2026 13:42:07 -0400 Subject: [PATCH 2/9] Adds postinstall for configuration --- package.json | 1 + src/postinstall.ts | 20 ++++++++++++++++++++ src/utils.ts | 1 + 3 files changed, 22 insertions(+) create mode 100755 src/postinstall.ts diff --git a/package.json b/package.json index 7b39441..f3918fd 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "build": "tsc && npm run copy-files", "copy-files": "cp -r ./src/resources/ ./dist/commands/resources/", "prepublishOnly": "npm run build", + "postinstall": "node dist/postinstall.js", "start": "node --no-warnings=ExperimentalWarning --loader ts-node/esm src/index.ts", "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/src/postinstall.ts b/src/postinstall.ts new file mode 100755 index 0000000..c236a94 --- /dev/null +++ b/src/postinstall.ts @@ -0,0 +1,20 @@ +import { randomUUID } from 'node:crypto' +import fs from 'node:fs' + +function run() { + const dir = 'dist/.fa' + if (!fs.existsSync(dir)) { + const configObject = { + id: randomUUID(), + telemetry: true + } + + fs.mkdirSync('dist/.fa') + if (!fs.existsSync(dir + '/config.json')){ + fs.writeFileSync(dir + '/config.json', JSON.stringify(configObject, null, 2)) + } + fs.chmodSync(dir, '00020') + } +} + +run() \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index faa1f03..3c7cd76 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -225,6 +225,7 @@ export async function logEvent(eventName:string, eventDetails:any = {}) { event: eventName, properties: eventDetails }) + console.log('logged: ' + eventName) await posthogClient.shutdown() } } \ No newline at end of file From f2dce574587caccc8fcc20c39fb5f41923892b71 Mon Sep 17 00:00:00 2001 From: Bryan Robinson Date: Fri, 24 Apr 2026 13:56:20 -0400 Subject: [PATCH 3/9] Adds messages for telemetry enable/disable --- src/commands/kickstart-kill.ts | 2 +- src/commands/telemetry-disable.ts | 5 ++++- src/commands/telemetry-enable.ts | 6 +++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/commands/kickstart-kill.ts b/src/commands/kickstart-kill.ts index cff80fc..11e4863 100644 --- a/src/commands/kickstart-kill.ts +++ b/src/commands/kickstart-kill.ts @@ -61,6 +61,6 @@ const action = async function () { } export const kickstartKill = new Command() - .description('Runs docker compose down in current directory') .command('kickstart:kill') + .description('Runs docker compose down in current directory') .action(action) diff --git a/src/commands/telemetry-disable.ts b/src/commands/telemetry-disable.ts index df6bb73..37d5fcb 100644 --- a/src/commands/telemetry-disable.ts +++ b/src/commands/telemetry-disable.ts @@ -1,12 +1,15 @@ import { Command } from "@commander-js/extra-typings"; import { __dirname, loadConfig } from '../utils.js' import fs from 'node:fs' +import chalk from "chalk"; const action = async function () { try { let config = loadConfig() config.globalConfig.telemetry = false fs.writeFileSync(__dirname + '/.fa/config.json', JSON.stringify(config.globalConfig, null, 2)) + + console.log(chalk.green(`Usage data will no longer be collected. To re-enable, run ${chalk.bold.bgWhite(' npx fusionauth telemetry:enable ')}.`)) } catch (err) { console.log(err) } @@ -14,6 +17,6 @@ const action = async function () { } export const telemetryDisable = new Command() - .description('Sets a global config value to disallow telemetry from being collected') .command('telemetry:disable') + .description('Sets a global config value to disallow telemetry from being collected') .action(action) diff --git a/src/commands/telemetry-enable.ts b/src/commands/telemetry-enable.ts index 307a534..c615683 100644 --- a/src/commands/telemetry-enable.ts +++ b/src/commands/telemetry-enable.ts @@ -1,12 +1,16 @@ import { Command } from "@commander-js/extra-typings"; import { __dirname, loadConfig } from '../utils.js' import fs from 'node:fs' +import chalk from "chalk"; const action = async function () { try { let config = loadConfig() config.globalConfig.telemetry = true fs.writeFileSync(__dirname + '/.fa/config.json', JSON.stringify(config.globalConfig, null, 2)) + + console.log(chalk.green(`Sharing usage data has been re-enabled. To disable, run ${chalk.bold.bgWhite(' npx fusionauth telemetry:disable ')}.`)) + } catch (err) { console.log(err) } @@ -14,6 +18,6 @@ const action = async function () { } export const telemetryEnable = new Command() - .description('Sets a global config value to allow telemetry to be collected') .command('telemetry:enable') + .description('Sets a global config value to allow telemetry to be collected') .action(action) From 7dfbf1340927ab92b1b6d9a38d9f2c0cb6674324 Mon Sep 17 00:00:00 2001 From: Bryan Robinson Date: Fri, 1 May 2026 08:26:26 -0400 Subject: [PATCH 4/9] Adds tracking to telemetry:enable and an event to telemetry:disable --- src/commands/telemetry-disable.ts | 3 ++- src/commands/telemetry-enable.ts | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/commands/telemetry-disable.ts b/src/commands/telemetry-disable.ts index 37d5fcb..14cfe5b 100644 --- a/src/commands/telemetry-disable.ts +++ b/src/commands/telemetry-disable.ts @@ -1,8 +1,9 @@ import { Command } from "@commander-js/extra-typings"; -import { __dirname, loadConfig } from '../utils.js' +import { __dirname, loadConfig, logEvent } from '../utils.js' import fs from 'node:fs' import chalk from "chalk"; const action = async function () { + logEvent('cli do not track') try { let config = loadConfig() diff --git a/src/commands/telemetry-enable.ts b/src/commands/telemetry-enable.ts index c615683..40baf9b 100644 --- a/src/commands/telemetry-enable.ts +++ b/src/commands/telemetry-enable.ts @@ -1,5 +1,5 @@ import { Command } from "@commander-js/extra-typings"; -import { __dirname, loadConfig } from '../utils.js' +import { __dirname, loadConfig, logEvent } from '../utils.js' import fs from 'node:fs' import chalk from "chalk"; const action = async function () { @@ -9,7 +9,9 @@ const action = async function () { config.globalConfig.telemetry = true fs.writeFileSync(__dirname + '/.fa/config.json', JSON.stringify(config.globalConfig, null, 2)) - console.log(chalk.green(`Sharing usage data has been re-enabled. To disable, run ${chalk.bold.bgWhite(' npx fusionauth telemetry:disable ')}.`)) + logEvent('cli command telemetry:enable') + + console.log(chalk.green(`Sharing usage data has been re-enabled. To disable, run ${chalk.bold.bgWhite(' npx fusionauth telemetry:disable ')}.`)) } catch (err) { console.log(err) From 450f684781d197782d1bb6564746868c3d89549a Mon Sep 17 00:00:00 2001 From: Bryan Robinson Date: Fri, 1 May 2026 08:36:01 -0400 Subject: [PATCH 5/9] Adds check for if config exists (in case postinstall didn't fire properly) and adds a default config --- package-lock.json | 1 + src/utils.ts | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1b8eeb4..9b04027 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "@fusionauth/cli", "version": "1.7.0", + "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@comandeer/cli-spinner": "^1.0.2", diff --git a/src/utils.ts b/src/utils.ts index 3c7cd76..3d81f3b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -205,9 +205,21 @@ export function isDirEmpty(path: string) { } export function loadConfig() { - const globalConfig = JSON.parse(fs.readFileSync(__dirname + '/.fa/config.json').toString()) - // TODO: Combine this with a local-project config - return {globalConfig} + const defaultConfig = { + telemetry: false, + id: '' + } + const configPath = __dirname + '/.fa/config.json' + try { + if (!fs.existsSync(configPath)) { + return {globalConfig: defaultConfig} + } + const globalConfig = JSON.parse(fs.readFileSync(configPath).toString()) + // TODO: Combine this with a local-project config + return {globalConfig: {...defaultConfig, ...globalConfig}} + } catch (e) { + return {globalConfig: defaultConfig} + } } export function allowsTelemetry() { @@ -225,7 +237,6 @@ export async function logEvent(eventName:string, eventDetails:any = {}) { event: eventName, properties: eventDetails }) - console.log('logged: ' + eventName) await posthogClient.shutdown() } } \ No newline at end of file From 762ee715a109869e1ac40412ea1d645054457e15 Mon Sep 17 00:00:00 2001 From: Bryan Robinson Date: Fri, 1 May 2026 08:36:43 -0400 Subject: [PATCH 6/9] Makes default ID obviously a problem so we can debug if things are going wrong --- src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index 3d81f3b..913740a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -207,7 +207,7 @@ export function isDirEmpty(path: string) { export function loadConfig() { const defaultConfig = { telemetry: false, - id: '' + id: 'id-unavailable' } const configPath = __dirname + '/.fa/config.json' try { From 6fa0fd3f92fb8fbbd10dd33177a617bd39d22c49 Mon Sep 17 00:00:00 2001 From: Bryan Robinson Date: Fri, 1 May 2026 08:56:27 -0400 Subject: [PATCH 7/9] More stable postinstall. recommendation from Copilot --- src/postinstall.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/postinstall.ts b/src/postinstall.ts index c236a94..d5b64f1 100755 --- a/src/postinstall.ts +++ b/src/postinstall.ts @@ -3,18 +3,14 @@ import fs from 'node:fs' function run() { const dir = 'dist/.fa' - if (!fs.existsSync(dir)) { + const configPath = dir + '/config.json' + fs.mkdirSync(dir, { recursive: true }) + if (!fs.existsSync(configPath)) { const configObject = { id: randomUUID(), telemetry: true } - - fs.mkdirSync('dist/.fa') - if (!fs.existsSync(dir + '/config.json')){ - fs.writeFileSync(dir + '/config.json', JSON.stringify(configObject, null, 2)) - } - fs.chmodSync(dir, '00020') - } + fs.writeFileSync(configPath, JSON.stringify(configObject, null, 2)) } } run() \ No newline at end of file From 9d53223de249249e5c7a06c6d3f7c8e90ec0ca3a Mon Sep 17 00:00:00 2001 From: Bryan Robinson Date: Fri, 1 May 2026 09:02:58 -0400 Subject: [PATCH 8/9] Removes premature optimization for allowTelemetry() --- src/utils.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 913740a..985344e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -222,16 +222,9 @@ export function loadConfig() { } } -export function allowsTelemetry() { - const {globalConfig} = loadConfig() - - return globalConfig.telemetry -} - - export async function logEvent(eventName:string, eventDetails:any = {}) { const config = loadConfig() - if (allowsTelemetry()) { + if (config.globalConfig.telemetry) { posthogClient.capture({ distinctId: config.globalConfig.id, event: eventName, From 551dbb9038b6e33f09637ef08e2beb2d9232e0aa Mon Sep 17 00:00:00 2001 From: Bryan Robinson Date: Fri, 1 May 2026 09:57:00 -0400 Subject: [PATCH 9/9] Guardrail for contributor local install instead of user install Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f3918fd..87b3ec5 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "build": "tsc && npm run copy-files", "copy-files": "cp -r ./src/resources/ ./dist/commands/resources/", "prepublishOnly": "npm run build", - "postinstall": "node dist/postinstall.js", + "postinstall": "test -f dist/postinstall.js && node dist/postinstall.js || true", "start": "node --no-warnings=ExperimentalWarning --loader ts-node/esm src/index.ts", "test": "echo \"Error: no test specified\" && exit 1" },