Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 26 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
15 changes: 10 additions & 5 deletions src/analyze/attw.ts
Original file line number Diff line number Diff line change
@@ -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<ReportPluginResult> {
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) {
Expand Down
5 changes: 5 additions & 0 deletions src/commands/analyze.meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
35 changes: 34 additions & 1 deletion src/commands/analyze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand All @@ -24,6 +29,7 @@ export async function run(ctx: CommandContext<typeof meta.args>) {
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
Expand All @@ -33,6 +39,32 @@ export async function run(ctx: CommandContext<typeof meta.args>) {

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<string> = meta.args.pack.choices;
if (typeof pack === 'string' && !allowedPackChoices.includes(pack)) {
prompts.cancel(
Expand Down Expand Up @@ -83,7 +115,8 @@ export async function run(ctx: CommandContext<typeof meta.args>) {
const {stats, messages} = await report({
root,
pack,
manifest: customManifests
manifest: customManifests,
attw: attwEnabled
});

prompts.log.info('Summary');
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface Options {
root?: string;
pack?: PackType;
manifest?: string[];
attw?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is what i was trying to explain in discord the other day: we shouldn't be exposing any of this as "attw".

we should have a feature to enable "type checking". right now we happen to use attw under the hood, but we may not in future. it shouldn't matter to the user

Copy link
Contributor Author

@dreyfus92 dreyfus92 Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how should we handle this? i gotta think about that.

}

export interface StatLike<T> {
Expand Down
79 changes: 79 additions & 0 deletions src/utils/dependency-check.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
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<boolean> {
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);
}
});
});
}
Loading