diff --git a/package-lock.json b/package-lock.json index d322505..8237a8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "0.0.1", "license": "MIT", "dependencies": { - "@arethetypeswrong/core": "^0.18.2", "@clack/prompts": "^1.0.0-alpha.6", "@publint/pack": "^0.1.2", "debug": "^4.4.3", @@ -27,6 +26,7 @@ "e18e-cli": "cli.js" }, "devDependencies": { + "@arethetypeswrong/core": "^0.18.2", "@eslint/js": "^9.37.0", "@types/debug": "^4.1.12", "@types/node": "^24.7.2", @@ -40,6 +40,14 @@ "typescript": "^5.9.3", "typescript-eslint": "^8.46.0", "vitest": "^3.2.3" + }, + "peerDependencies": { + "@arethetypeswrong/core": "^0.18.2" + }, + "peerDependenciesMeta": { + "@arethetypeswrong/core": { + "optional": true + } } }, "node_modules/@actions/core": { @@ -110,12 +118,14 @@ "node_modules/@andrewbranch/untar.js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@andrewbranch/untar.js/-/untar.js-1.0.3.tgz", - "integrity": "sha512-Jh15/qVmrLGhkKJBdXlK1+9tY4lZruYjsgkDFj08ZmDiWVBLJcqkok7Z0/R0In+i1rScBpJlSvrTS2Lm41Pbnw==" + "integrity": "sha512-Jh15/qVmrLGhkKJBdXlK1+9tY4lZruYjsgkDFj08ZmDiWVBLJcqkok7Z0/R0In+i1rScBpJlSvrTS2Lm41Pbnw==", + "dev": true }, "node_modules/@arethetypeswrong/core": { "version": "0.18.2", "resolved": "https://registry.npmjs.org/@arethetypeswrong/core/-/core-0.18.2.tgz", "integrity": "sha512-GiwTmBFOU1/+UVNqqCGzFJYfBXEytUkiI+iRZ6Qx7KmUVtLm00sYySkfe203C9QtPG11yOz1ZaMek8dT/xnlgg==", + "dev": true, "license": "MIT", "dependencies": { "@andrewbranch/untar.js": "^1.0.3", @@ -135,6 +145,7 @@ "version": "5.6.1-rc", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.1-rc.tgz", "integrity": "sha512-E3b2+1zEFu84jB0YQi9BORDjz9+jGbwwy1Zi3G0LUNw7a7cePUrHMRNy8aPh53nXpkFGVHSxIZo5vKTfYaFiBQ==", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -918,9 +929,11 @@ } }, "node_modules/@braidai/lang": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@braidai/lang/-/lang-1.1.1.tgz", - "integrity": "sha512-5uM+no3i3DafVgkoW7ayPhEGHNNBZCSj5TrGDQt0ayEKQda5f3lAXlmQg0MR5E0gKgmTzUUEtSWHsEC3h9jUcg==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@braidai/lang/-/lang-1.1.2.tgz", + "integrity": "sha512-qBcknbBufNHlui137Hft8xauQMTZDKdophmLFv05r2eNmdIv/MlPuP4TdUknHG68UdWLgVZwgxVe735HzJNIwA==", + "dev": true, + "license": "ISC" }, "node_modules/@clack/core": { "version": "1.0.0-alpha.6", @@ -1682,6 +1695,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/@loaderkit/resolve/-/resolve-1.0.4.tgz", "integrity": "sha512-rJzYKVcV4dxJv+vW6jlvagF8zvGxHJ2+HTr1e2qOejfmGhAApgJHl8Aog4mMszxceTRiKTTbnpgmTO1bEZHV/A==", + "dev": true, "license": "ISC", "dependencies": { "@braidai/lang": "^1.0.0" @@ -3067,6 +3081,7 @@ "version": "1.4.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, "license": "MIT" }, "node_modules/clone-deep": { @@ -3552,6 +3567,7 @@ "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, "license": "MIT" }, "node_modules/file-entry-cache": { @@ -4186,9 +4202,10 @@ "license": "MIT" }, "node_modules/lru-cache": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", - "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, "license": "ISC", "engines": { "node": "20 || >=22" @@ -5611,6 +5628,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "dev": true, "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" diff --git a/package.json b/package.json index 7e8df15..7998691 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ }, "homepage": "https://github.com/e18e/cli#readme", "dependencies": { - "@arethetypeswrong/core": "^0.18.2", "@clack/prompts": "^1.0.0-alpha.6", "@publint/pack": "^0.1.2", "debug": "^4.4.3", @@ -68,7 +67,16 @@ "semver": "^7.7.3", "tinyglobby": "^0.2.15" }, + "peerDependencies": { + "@arethetypeswrong/core": "^0.18.2" + }, + "peerDependenciesMeta": { + "@arethetypeswrong/core": { + "optional": true + } + }, "devDependencies": { + "@arethetypeswrong/core": "^0.18.2", "@eslint/js": "^9.37.0", "@types/debug": "^4.1.12", "@types/node": "^24.7.2", diff --git a/src/analyze/attw.ts b/src/analyze/attw.ts index 7918245..dbfeae1 100644 --- a/src/analyze/attw.ts +++ b/src/analyze/attw.ts @@ -1,22 +1,27 @@ +import type {ReportPluginResult, Options} from '../types.js'; +import type {FileSystem} from '../file-system.js'; +import type {ResolutionKind} from '@arethetypeswrong/core'; +import {TarballFileSystem} from '../tarball-file-system.js'; import { checkPackage, createPackageFromTarballData } from '@arethetypeswrong/core'; import {groupProblemsByKind} from '@arethetypeswrong/core/utils'; -import type {ResolutionKind} from '@arethetypeswrong/core'; import {filterProblems, problemKindInfo} from '@arethetypeswrong/core/problems'; -import type {ReportPluginResult, Options} from '../types.js'; -import type {FileSystem} from '../file-system.js'; -import {TarballFileSystem} from '../tarball-file-system.js'; export async function runAttw( fileSystem: FileSystem, - _options?: Options + options?: Options ): Promise { const result: ReportPluginResult = { messages: [] }; + // Only run attw when explicitly enabled + if (!options?.attw) { + return result; + } + // Only run attw when TypeScript is configured const hasTypeScriptConfig = await fileSystem.fileExists('/tsconfig.json'); if (!hasTypeScriptConfig) { diff --git a/src/commands/analyze.meta.ts b/src/commands/analyze.meta.ts index 4071898..048b4a3 100644 --- a/src/commands/analyze.meta.ts +++ b/src/commands/analyze.meta.ts @@ -28,6 +28,11 @@ export const meta = { array: true, description: 'Path(s) to custom manifest file(s) for module replacements analysis' + }, + attw: { + type: 'boolean', + default: false, + description: 'Enable Are The Types Wrong analysis' } } } as const; diff --git a/src/commands/analyze.ts b/src/commands/analyze.ts index eba0235..f17b3d1 100644 --- a/src/commands/analyze.ts +++ b/src/commands/analyze.ts @@ -6,6 +6,11 @@ import {meta} from './analyze.meta.js'; import {report} from '../index.js'; import type {PackType} from '../types.js'; import {enableDebug} from '../logger.js'; +import { + isPackageInstalled, + promptToInstall, + installPackage +} from '../utils/dependency-check.js'; function formatBytes(bytes: number) { const units = ['B', 'KB', 'MB', 'GB']; @@ -24,6 +29,7 @@ export async function run(ctx: CommandContext) { const [_commandName, providedPath] = ctx.positionals; let pack: PackType = ctx.values.pack; const logLevel = ctx.values['log-level']; + let attwEnabled = ctx.values.attw; let root: string | undefined = undefined; // Enable debug output based on log level @@ -33,6 +39,32 @@ export async function run(ctx: CommandContext) { prompts.intro('Analyzing...'); + // Check for optional dependencies if features are enabled + if (attwEnabled) { + const attwPackage = '@arethetypeswrong/core'; + if (!isPackageInstalled(attwPackage)) { + const shouldInstall = await promptToInstall(attwPackage); + + if (shouldInstall) { + const installed = await installPackage(attwPackage); + if (installed) { + prompts.log.info('Restarting analysis with ATTW enabled...'); + // Continue with analysis now that ATTW is installed + } else { + prompts.log.warn( + 'Installation failed. Please install the package manually and run the command again.' + ); + process.exit(1); + } + } else { + prompts.log.warn( + `ATTW analysis will be skipped because ${c.cyan(attwPackage)} is not installed.` + ); + attwEnabled = false; + } + } + } + const allowedPackChoices: ReadonlyArray = meta.args.pack.choices; if (typeof pack === 'string' && !allowedPackChoices.includes(pack)) { prompts.cancel( @@ -83,7 +115,8 @@ export async function run(ctx: CommandContext) { const {stats, messages} = await report({ root, pack, - manifest: customManifests + manifest: customManifests, + attw: attwEnabled }); prompts.log.info('Summary'); diff --git a/src/types.ts b/src/types.ts index fd92fb8..ffbd3a4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,6 +14,7 @@ export interface Options { root?: string; pack?: PackType; manifest?: string[]; + attw?: boolean; } export interface StatLike { diff --git a/src/utils/dependency-check.ts b/src/utils/dependency-check.ts new file mode 100644 index 0000000..1f2cc43 --- /dev/null +++ b/src/utils/dependency-check.ts @@ -0,0 +1,79 @@ +import {createRequire} from 'node:module'; +import {spawn} from 'node:child_process'; +import {detect, resolveCommand} from 'package-manager-detector'; +import * as p from '@clack/prompts'; +import c from 'picocolors'; +import {resolve} from 'node:path'; + +const require = createRequire(import.meta.url); + +/** + * Checks if a package is installed and can be imported + */ +export function isPackageInstalled(packageName: string): boolean { + try { + // Check only in current directory's node_modules, not parent directories + const localPath = resolve('./node_modules', packageName); + require.resolve(localPath); + return true; + } catch { + return false; + } +} + +/** + * Prompts the user to install a missing package + * Returns true if user wants to proceed with installation + */ +export async function promptToInstall(packageName: string): Promise { + p.log.warn( + `${c.yellow('Optional dependency not found:')} ${c.bold(packageName)}` + ); + p.log.message(`This package is required for the feature you've enabled.`, { + spacing: 0 + }); + + const shouldInstall = await p.confirm({ + message: `Would you like to install ${c.cyan(packageName)} now?` + }); + + if (p.isCancel(shouldInstall)) { + return false; + } + + return shouldInstall; +} + +/** + * Automatically installs a package using the detected package manager + */ +export async function installPackage(packageName: string): Promise { + const detected = await detect(); + const agent = detected?.agent || 'npm'; + + const resolved = resolveCommand(agent, 'add', [packageName]); + + if (!resolved) { + p.log.error(`Failed to resolve install command for ${c.cyan(agent)}`); + return false; + } + + p.log.info(`Installing ${c.cyan(packageName)} with ${c.cyan(agent)}...`); + + return new Promise((resolve) => { + const child = spawn(resolved.command, resolved.args, { + stdio: 'inherit', + cwd: process.cwd() + }); + + child.on('close', (code) => { + if (code === 0) { + p.log.success(`Successfully installed ${c.cyan(packageName)}!`); + resolve(true); + } else { + p.log.error(`Failed to install ${c.cyan(packageName)}`); + resolve(false); + } + }); + }); +}