diff --git a/package.json b/package.json index 1747328..3ab4569 100644 --- a/package.json +++ b/package.json @@ -38,13 +38,13 @@ }, "dependencies": { "@nuxt/kit": "^3.14.1592", + "citty": "^0.1.6", "consola": "^3.3.0", "destr": "^2.0.3", "dotenv": "^16.4.7", "git-url-parse": "^16.0.0", "is-docker": "^3.0.0", "jiti": "^2.4.2", - "mri": "^1.2.0", "ofetch": "^1.4.1", "package-manager-detector": "^0.2.7", "parse-git-config": "^3.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55768ed..721c879 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@nuxt/kit': specifier: ^3.14.1592 version: 3.14.1592(magicast@0.3.5)(rollup@4.28.1) + citty: + specifier: ^0.1.6 + version: 0.1.6 consola: specifier: ^3.3.0 version: 3.3.0 @@ -29,9 +32,6 @@ importers: jiti: specifier: ^2.4.2 version: 2.4.2 - mri: - specifier: ^1.2.0 - version: 1.2.0 ofetch: specifier: ^1.4.1 version: 1.4.1 @@ -1429,10 +1429,6 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/scope-manager@8.18.0': - resolution: {integrity: sha512-PNGcHop0jkK2WVYGotk/hxj+UFLhXtGPiGtiaWgVBVP1jhMoMCHlTyJA+hEj4rszoSdLTK3fN4oOatrL0Cp+Xw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.18.1': resolution: {integrity: sha512-HxfHo2b090M5s2+/9Z3gkBhI6xBH8OJCFjH9MhQ+nnoZqxU3wNxkLT+VWXWSFWc3UF3Z+CfPAyqdCTdoXtDPCQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1444,33 +1440,16 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/types@8.18.0': - resolution: {integrity: sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.18.1': resolution: {integrity: sha512-7uoAUsCj66qdNQNpH2G8MyTFlgerum8ubf21s3TSM3XmKXuIn+H2Sifh/ES2nPOPiYSRJWAk0fDkW0APBWcpfw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.18.0': - resolution: {integrity: sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/typescript-estree@8.18.1': resolution: {integrity: sha512-z8U21WI5txzl2XYOW7i9hJhxoKKNG1kcU4RzyNvKrdZDmbjkmLBo8bgeiOJmA06kizLI76/CCBAAGlTlEeUfyg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/utils@8.18.0': - resolution: {integrity: sha512-p6GLdY383i7h5b0Qrfbix3Vc3+J2k6QWw6UMUeY5JGfm3C5LbZ4QIZzJNoNOfgyRe0uuYKjvVOsO/jD4SJO+xg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/utils@8.18.1': resolution: {integrity: sha512-8vikiIj2ebrC4WRdcAdDcmnu9Q/MXXwg+STf40BVfT8exDqBCUPdypvzcUPxEqRGKg9ALagZ0UWcYCtn+4W2iQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1478,10 +1457,6 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/visitor-keys@8.18.0': - resolution: {integrity: sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.18.1': resolution: {integrity: sha512-Vj0WLm5/ZsD013YeUKn+K0y8p1M0jPpxOkKdbD1wB0ns53a5piVY02zjf072TblEweAbcYiFiPoSMF3kp+VhhQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1989,10 +1964,6 @@ packages: confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - consola@3.2.3: - resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} - engines: {node: ^14.18.0 || >=16.10.0} - consola@3.3.0: resolution: {integrity: sha512-kxltocVQCwQNFvw40dlVRYeAkAvtYjMFZYNlOcsF5wExPpGwPxMwgx4IfDJvBRPtBpnQwItd5WkTaR0ZwT/TmQ==} engines: {node: ^14.18.0 || >=16.10.0} @@ -5884,7 +5855,7 @@ snapshots: '@stylistic/eslint-plugin@2.12.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.6.3)': dependencies: - '@typescript-eslint/utils': 8.18.0(eslint@9.17.0(jiti@2.4.2))(typescript@5.6.3) + '@typescript-eslint/utils': 8.18.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.6.3) eslint: 9.17.0(jiti@2.4.2) eslint-visitor-keys: 4.2.0 espree: 10.3.0 @@ -5947,11 +5918,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.18.0': - dependencies: - '@typescript-eslint/types': 8.18.0 - '@typescript-eslint/visitor-keys': 8.18.0 - '@typescript-eslint/scope-manager@8.18.1': dependencies: '@typescript-eslint/types': 8.18.1 @@ -5968,24 +5934,8 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.18.0': {} - '@typescript-eslint/types@8.18.1': {} - '@typescript-eslint/typescript-estree@8.18.0(typescript@5.6.3)': - dependencies: - '@typescript-eslint/types': 8.18.0 - '@typescript-eslint/visitor-keys': 8.18.0 - debug: 4.3.7(supports-color@9.4.0) - fast-glob: 3.3.2 - is-glob: 4.0.3 - minimatch: 9.0.4 - semver: 7.6.3 - ts-api-utils: 1.3.0(typescript@5.6.3) - typescript: 5.6.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/typescript-estree@8.18.1(typescript@5.6.3)': dependencies: '@typescript-eslint/types': 8.18.1 @@ -6000,17 +5950,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.18.0(eslint@9.17.0(jiti@2.4.2))(typescript@5.6.3)': - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.17.0(jiti@2.4.2)) - '@typescript-eslint/scope-manager': 8.18.0 - '@typescript-eslint/types': 8.18.0 - '@typescript-eslint/typescript-estree': 8.18.0(typescript@5.6.3) - eslint: 9.17.0(jiti@2.4.2) - typescript: 5.6.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/utils@8.18.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.6.3)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@9.17.0(jiti@2.4.2)) @@ -6022,11 +5961,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.18.0': - dependencies: - '@typescript-eslint/types': 8.18.0 - eslint-visitor-keys: 4.2.0 - '@typescript-eslint/visitor-keys@8.18.1': dependencies: '@typescript-eslint/types': 8.18.1 @@ -6663,8 +6597,6 @@ snapshots: confbox@0.1.8: {} - consola@3.2.3: {} - consola@3.3.0: {} console-control-strings@1.1.0: {} @@ -7057,8 +6989,8 @@ snapshots: eslint-plugin-import-x@4.6.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.6.3): dependencies: '@types/doctrine': 0.0.9 - '@typescript-eslint/scope-manager': 8.18.0 - '@typescript-eslint/utils': 8.18.0(eslint@9.17.0(jiti@2.4.2))(typescript@5.6.3) + '@typescript-eslint/scope-manager': 8.18.1 + '@typescript-eslint/utils': 8.18.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.6.3) debug: 4.3.7(supports-color@9.4.0) doctrine: 3.0.0 enhanced-resolve: 5.17.1 @@ -8191,7 +8123,7 @@ snapshots: c12: 2.0.1(magicast@0.3.5) chokidar: 4.0.1 compatx: 0.1.8 - consola: 3.2.3 + consola: 3.3.0 cookie-es: 1.2.2 defu: 6.1.4 destr: 2.0.3 diff --git a/src/cli.ts b/src/cli.ts index 5972f08..77cfce7 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,148 +1,180 @@ import { existsSync, readFileSync } from 'node:fs' import { homedir } from 'node:os' + import { resolve } from 'pathe' import { destr } from 'destr' -import mri from 'mri' import * as rc from 'rc9' import { colors as c } from 'consola/utils' import { consola } from 'consola' -import jiti from 'jiti' +import { createJiti } from 'jiti' import { isTest } from 'std-env' -import { parse as praseDotenv } from 'dotenv' +import { parse as parseDotenv } from 'dotenv' +import { createMain, defineCommand } from 'citty' + +import { version } from '../package.json' import { consentVersion } from './meta' import { ensureUserconsent } from './consent' -export const usage = 'npx nuxt-telemetry `status`|`enable`|`disable`|`consent` [`-g`,`--global`] [`dir`]' const RC_FILENAME = '.nuxtrc' -function _run() { - const _argv = process.argv.slice(2) - const args = mri(_argv, { - boolean: [ - '--global', - ], - alias: { - '-g': '--global', - }, - }) - const [command, _dir = '.'] = args._ - const dir = resolve(process.cwd(), _dir) - const global = args['--global'] - - if (!global && !existsSync(resolve(dir, 'nuxt.config.js')) - && !existsSync(resolve(dir, 'nuxt.config.ts'))) { - consola.error('It seems you are not in a nuxt project!') - consola.info('You can try with providing dir or using `-g`') - showUsage() +const sharedArgs = { + global: { + type: 'boolean', + alias: 'g', + description: 'Apply globally', + }, + dir: { + type: 'positional', + default: '.', + }, +} as const + +const cli = createMain({ + meta: { + name: 'nuxt-telemetry', + description: 'Manage consent for Nuxt collecting anonymous telemetry data about general usage.', + version, + }, + subCommands: { + status: defineCommand({ + meta: { + name: 'status', + description: 'Show telemetry status', + }, + args: sharedArgs, + async run({ args }) { + ensureNuxtProject(args) + const dir = resolve(args.dir) + await showStatus(dir, args.global) + }, + }), + enable: defineCommand({ + meta: { + name: 'enable', + description: 'Enable telemetry', + }, + args: sharedArgs, + async run({ args }) { + ensureNuxtProject(args) + const dir = resolve(args.dir) + setRC(dir, 'telemetry.enabled', true, args.global) + setRC(dir, 'telemetry.consent', consentVersion, args.global) + await showStatus(dir, args.global) + consola.info('You can disable telemetry with `npx nuxt-telemetry disable' + (args.global ? ' --global' : (args.dir ? ' ' + args.dir : '')) + '`') + }, + }), + disable: defineCommand({ + meta: { + name: 'disable', + description: 'Disable telemetry', + }, + args: sharedArgs, + async run({ args }) { + ensureNuxtProject(args) + const dir = resolve(args.dir) + setRC(dir, 'telemetry.enabled', false, args.global) + setRC(dir, 'telemetry.consent', 0, args.global) + await showStatus(dir, args.global) + consola.info('You can enable telemetry with `npx nuxt-telemetry enable' + (args.global ? ' --global' : (args.dir ? ' ' + args.dir : '')) + '`') + }, + }), + consent: defineCommand({ + meta: { + name: 'consent', + description: 'Prompt for user consent', + }, + args: sharedArgs, + async run({ args }) { + ensureNuxtProject(args) + const dir = resolve(args.dir) + const accepted = await ensureUserconsent({} as any) + if (accepted && !args.global) { + setRC(dir, 'telemetry.enabled', true, args.global) + setRC(dir, 'telemetry.consent', consentVersion, args.global) + } + await showStatus(dir, args.global) + }, + }), + }, +}) + +async function _checkDisabled(dir: string): Promise { + if (isTest) { + return 'because you are running in a test environment' } - switch (command) { - case 'enable': - setRC('telemetry.enabled', true) - setRC('telemetry.consent', consentVersion) - showStatus() - consola.info('You can disable telemetry with `npx nuxt-telemetry disable ' + (global ? '-g' : _dir)) - return - case 'disable': - setRC('telemetry.enabled', false) - setRC('telemetry.consent', 0) - showStatus() - consola.info('You can enable telemetry with `npx nuxt-telemetry enable ' + (global ? '-g' : _dir) + '`') - return - case 'status': - return showStatus() - case 'consent': - return _prompt() - default: - showUsage() + if (destr(process.env.NUXT_TELEMETRY_DISABLED)) { + return 'by the `NUXT_TELEMETRY_DISABLED` environment variable' } - async function _prompt() { - const accepted = await ensureUserconsent({} as any) // <-- always sets global - if (accepted && !global) { - setRC('telemetry.enabled', true) - setRC('telemetry.consent', consentVersion) + const dotenvFile = resolve(dir, '.env') + if (existsSync(dotenvFile)) { + const _env = parseDotenv(readFileSync(dotenvFile)) + if (destr(_env.NUXT_TELEMETRY_DISABLED)) { + return 'by the `NUXT_TELEMETRY_DISABLED` environment variable set in ' + dotenvFile } - showStatus() } - function _checkDisabled(): string | false | undefined { - // test - if (isTest) { - return 'Because running in test environment' - } - - // env - if (destr(process.env.NUXT_TELEMETRY_DISABLED)) { - return 'by `NUXT_TELEMETRY_DISABLED` environment variable' - } - - // dotenv - const dotenvFile = resolve(dir, '.env') - if (existsSync(dotenvFile)) { - const _env = praseDotenv(readFileSync(dotenvFile)) - if (destr(_env.NUXT_TELEMETRY_DISABLED)) { - return 'by `NUXT_TELEMETRY_DISABLED` from ' + dotenvFile - } - } - - const disabledByConf = (conf: any) => conf.telemetry === false - || (conf.telemetry && conf.telemetry.enabled === false) - - // nuxt.config - try { - const _require = jiti(dir) - if (disabledByConf(_require('./nuxt.config'))) { - return 'by ' + _require.resolve('./nuxt.config') - } - } - catch { - // Ignore if we do not have `nuxt.config` - } + const disabledByConf = (conf: any) => conf.telemetry === false + || (conf.telemetry && conf.telemetry.enabled === false) - // Projct .nuxtrc - if (disabledByConf(rc.read({ name: RC_FILENAME, dir }))) { - return 'by ' + resolve(dir, RC_FILENAME) + try { + const configPath = resolveNuxtConfigPath(dir) + if (configPath && disabledByConf(createJiti(dir).import(configPath, { default: true }))) { + return 'by ' + configPath } + } + catch { + // Ignore if we do not have `nuxt.config` + } - // Global .nuxtrc - if (disabledByConf(rc.readUser({ name: RC_FILENAME }))) { - return 'by ' + resolve(homedir(), RC_FILENAME) - } + if (disabledByConf(rc.read({ name: RC_FILENAME, dir }))) { + return 'by ' + resolve(dir, RC_FILENAME) } - function showStatus() { - const disabled = _checkDisabled() - if (disabled) { - consola.info(`Nuxt telemetry is ${c.yellow('disabled')} ${disabled}`) - } - else { - consola.info(`Nuxt telemetry is ${c.green('enabled')}`, global ? 'on machine' : 'on current project') - } + if (disabledByConf(rc.readUser({ name: RC_FILENAME }))) { + return 'by ' + resolve(homedir(), RC_FILENAME) } +} - function showUsage() { - consola.info(`Usage: ${usage}`) - process.exit(0) +async function showStatus(dir: string, global: boolean) { + const disabled = await _checkDisabled(dir) + if (disabled) { + consola.info(`Nuxt telemetry is ${c.yellow('disabled')} ${disabled}.`) } + else { + consola.info(`Nuxt telemetry is ${c.green('enabled')}`, global ? 'on your machine.' : 'in the current project.') + } +} - function setRC(key: any, val: any) { - const update = { [key]: val } - if (global) { - rc.updateUser(update, RC_FILENAME) - } - else { - rc.update(update, { name: RC_FILENAME, dir }) - } +function setRC(dir: string, key: any, val: any, global: boolean) { + const update = { [key]: val } + if (global) { + rc.updateUser(update, RC_FILENAME) + } + else { + rc.update(update, { name: RC_FILENAME, dir }) } } -export function main() { - try { - _run() +function resolveNuxtConfigPath(dir: string) { + const jiti = createJiti(dir) + return jiti.esmResolve('./nuxt.config', { try: true }) || jiti.esmResolve('./.config/nuxt', { try: true }) +} + +function ensureNuxtProject(args: { global: boolean, dir: string }) { + if (args.global) { + return } - catch (err) { - consola.fatal(err) - process.exit(1) + const dir = resolve(args.dir) + if (!resolveNuxtConfigPath(dir)) { + consola.error('You are not in a Nuxt project.') + consola.info('You can try specifying a directory or by using the `--global` flag to configure telemetry for your machine.') + process.exit() } } + +cli().catch((error) => { + console.error(error) + process.exit(1) +})