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..9b04027 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,13 @@ { "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", + "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@comandeer/cli-spinner": "^1.0.2", @@ -27,6 +28,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 +482,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 +928,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 +1292,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 +1431,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 +1459,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 +1545,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 +1743,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..87b3ec5 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": "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" }, @@ -49,6 +50,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..11e4863 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([ { @@ -60,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/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..14cfe5b --- /dev/null +++ b/src/commands/telemetry-disable.ts @@ -0,0 +1,23 @@ +import { Command } from "@commander-js/extra-typings"; +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() + 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) + } + +} + +export const telemetryDisable = new Command() + .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 new file mode 100644 index 0000000..40baf9b --- /dev/null +++ b/src/commands/telemetry-enable.ts @@ -0,0 +1,25 @@ +import { Command } from "@commander-js/extra-typings"; +import { __dirname, loadConfig, logEvent } 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)) + + 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) + } + +} + +export const telemetryEnable = new Command() + .command('telemetry:enable') + .description('Sets a global config value to allow telemetry to be collected') + .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/postinstall.ts b/src/postinstall.ts new file mode 100755 index 0000000..d5b64f1 --- /dev/null +++ b/src/postinstall.ts @@ -0,0 +1,16 @@ +import { randomUUID } from 'node:crypto' +import fs from 'node:fs' + +function run() { + const dir = 'dist/.fa' + const configPath = dir + '/config.json' + fs.mkdirSync(dir, { recursive: true }) + if (!fs.existsSync(configPath)) { + const configObject = { + id: randomUUID(), + telemetry: true + } + fs.writeFileSync(configPath, JSON.stringify(configObject, null, 2)) } +} + +run() \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index 45b32f2..985344e 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,34 @@ export function isDirEmpty(path: string) { } else { return true } +} + +export function loadConfig() { + const defaultConfig = { + telemetry: false, + id: 'id-unavailable' + } + 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 async function logEvent(eventName:string, eventDetails:any = {}) { + const config = loadConfig() + if (config.globalConfig.telemetry) { + posthogClient.capture({ + distinctId: config.globalConfig.id, + event: eventName, + properties: eventDetails + }) + await posthogClient.shutdown() + } } \ No newline at end of file