react-doctor 0.0.28 → 0.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/dist/cli.js +121 -29
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +66 -13
- package/dist/index.js.map +1 -1
- package/dist/react-doctor-plugin.js +8 -0
- package/dist/react-doctor-plugin.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,6 +48,29 @@ curl -fsSL https://react.doctor/install-skill.sh | bash
|
|
|
48
48
|
|
|
49
49
|
Supports Cursor, Claude Code, Amp Code, Codex, Gemini CLI, OpenCode, Windsurf, and Antigravity.
|
|
50
50
|
|
|
51
|
+
## GitHub Actions
|
|
52
|
+
|
|
53
|
+
```yaml
|
|
54
|
+
- uses: actions/checkout@v5
|
|
55
|
+
with:
|
|
56
|
+
fetch-depth: 0 # required for --diff
|
|
57
|
+
- uses: millionco/react-doctor@main
|
|
58
|
+
with:
|
|
59
|
+
diff: main
|
|
60
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
| Input | Default | Description |
|
|
64
|
+
| -------------- | ------- | ----------------------------------------------------------------- |
|
|
65
|
+
| `directory` | `.` | Project directory to scan |
|
|
66
|
+
| `verbose` | `true` | Show file details per rule |
|
|
67
|
+
| `project` | | Workspace project(s) to scan (comma-separated) |
|
|
68
|
+
| `diff` | | Base branch for diff mode. Only changed files are scanned |
|
|
69
|
+
| `github-token` | | When set on `pull_request` events, posts findings as a PR comment |
|
|
70
|
+
| `node-version` | `20` | Node.js version to use |
|
|
71
|
+
|
|
72
|
+
The action outputs a `score` (0–100) you can use in subsequent steps.
|
|
73
|
+
|
|
51
74
|
## Options
|
|
52
75
|
|
|
53
76
|
```
|
package/dist/cli.js
CHANGED
|
@@ -283,6 +283,7 @@ const combineDiagnostics = (lintDiagnostics, deadCodeDiagnostics, directory, isD
|
|
|
283
283
|
//#region src/utils/find-monorepo-root.ts
|
|
284
284
|
const isMonorepoRoot = (directory) => {
|
|
285
285
|
if (fs.existsSync(path.join(directory, "pnpm-workspace.yaml"))) return true;
|
|
286
|
+
if (fs.existsSync(path.join(directory, "nx.json"))) return true;
|
|
286
287
|
const packageJsonPath = path.join(directory, "package.json");
|
|
287
288
|
if (!fs.existsSync(packageJsonPath)) return false;
|
|
288
289
|
const packageJson = readPackageJson(packageJsonPath);
|
|
@@ -335,7 +336,9 @@ const FRAMEWORK_PACKAGES = {
|
|
|
335
336
|
vite: "vite",
|
|
336
337
|
"react-scripts": "cra",
|
|
337
338
|
"@remix-run/react": "remix",
|
|
338
|
-
gatsby: "gatsby"
|
|
339
|
+
gatsby: "gatsby",
|
|
340
|
+
expo: "expo",
|
|
341
|
+
"react-native": "react-native"
|
|
339
342
|
};
|
|
340
343
|
const FRAMEWORK_DISPLAY_NAMES = {
|
|
341
344
|
nextjs: "Next.js",
|
|
@@ -343,10 +346,34 @@ const FRAMEWORK_DISPLAY_NAMES = {
|
|
|
343
346
|
cra: "Create React App",
|
|
344
347
|
remix: "Remix",
|
|
345
348
|
gatsby: "Gatsby",
|
|
349
|
+
expo: "Expo",
|
|
350
|
+
"react-native": "React Native",
|
|
346
351
|
unknown: "React"
|
|
347
352
|
};
|
|
348
353
|
const formatFrameworkName = (framework) => FRAMEWORK_DISPLAY_NAMES[framework];
|
|
349
|
-
const
|
|
354
|
+
const IGNORED_DIRECTORIES = new Set([
|
|
355
|
+
"node_modules",
|
|
356
|
+
"dist",
|
|
357
|
+
"build",
|
|
358
|
+
"coverage"
|
|
359
|
+
]);
|
|
360
|
+
const countSourceFilesViaFilesystem = (rootDirectory) => {
|
|
361
|
+
let count = 0;
|
|
362
|
+
const stack = [rootDirectory];
|
|
363
|
+
while (stack.length > 0) {
|
|
364
|
+
const currentDirectory = stack.pop();
|
|
365
|
+
const entries = fs.readdirSync(currentDirectory, { withFileTypes: true });
|
|
366
|
+
for (const entry of entries) {
|
|
367
|
+
if (entry.isDirectory()) {
|
|
368
|
+
if (!entry.name.startsWith(".") && !IGNORED_DIRECTORIES.has(entry.name)) stack.push(path.join(currentDirectory, entry.name));
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (entry.isFile() && SOURCE_FILE_PATTERN.test(entry.name)) count++;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return count;
|
|
375
|
+
};
|
|
376
|
+
const countSourceFilesViaGit = (rootDirectory) => {
|
|
350
377
|
const result = spawnSync("git", [
|
|
351
378
|
"ls-files",
|
|
352
379
|
"--cached",
|
|
@@ -357,9 +384,10 @@ const countSourceFiles = (rootDirectory) => {
|
|
|
357
384
|
encoding: "utf-8",
|
|
358
385
|
maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES
|
|
359
386
|
});
|
|
360
|
-
if (result.error || result.status !== 0) return
|
|
387
|
+
if (result.error || result.status !== 0) return null;
|
|
361
388
|
return result.stdout.split("\n").filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath)).length;
|
|
362
389
|
};
|
|
390
|
+
const countSourceFiles = (rootDirectory) => countSourceFilesViaGit(rootDirectory) ?? countSourceFilesViaFilesystem(rootDirectory);
|
|
363
391
|
const collectAllDependencies = (packageJson) => ({
|
|
364
392
|
...packageJson.peerDependencies,
|
|
365
393
|
...packageJson.dependencies,
|
|
@@ -444,14 +472,30 @@ const findReactInWorkspaces = (rootDirectory, packageJson) => {
|
|
|
444
472
|
}
|
|
445
473
|
return result;
|
|
446
474
|
};
|
|
475
|
+
const REACT_DEPENDENCY_NAMES = new Set([
|
|
476
|
+
"react",
|
|
477
|
+
"react-native",
|
|
478
|
+
"next"
|
|
479
|
+
]);
|
|
447
480
|
const hasReactDependency = (packageJson) => {
|
|
448
481
|
const allDependencies = collectAllDependencies(packageJson);
|
|
449
|
-
return Object.keys(allDependencies).some((packageName) =>
|
|
482
|
+
return Object.keys(allDependencies).some((packageName) => REACT_DEPENDENCY_NAMES.has(packageName));
|
|
450
483
|
};
|
|
451
484
|
const discoverReactSubprojects = (rootDirectory) => {
|
|
452
485
|
if (!fs.existsSync(rootDirectory) || !fs.statSync(rootDirectory).isDirectory()) return [];
|
|
453
|
-
const entries = fs.readdirSync(rootDirectory, { withFileTypes: true });
|
|
454
486
|
const packages = [];
|
|
487
|
+
const rootPackageJsonPath = path.join(rootDirectory, "package.json");
|
|
488
|
+
if (fs.existsSync(rootPackageJsonPath)) {
|
|
489
|
+
const rootPackageJson = readPackageJson(rootPackageJsonPath);
|
|
490
|
+
if (hasReactDependency(rootPackageJson)) {
|
|
491
|
+
const name = rootPackageJson.name ?? path.basename(rootDirectory);
|
|
492
|
+
packages.push({
|
|
493
|
+
name,
|
|
494
|
+
directory: rootDirectory
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const entries = fs.readdirSync(rootDirectory, { withFileTypes: true });
|
|
455
499
|
for (const entry of entries) {
|
|
456
500
|
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
457
501
|
const subdirectory = path.join(rootDirectory, entry.name);
|
|
@@ -623,14 +667,10 @@ const loadConfig = (rootDirectory) => {
|
|
|
623
667
|
if (fs.existsSync(configFilePath)) try {
|
|
624
668
|
const fileContent = fs.readFileSync(configFilePath, "utf-8");
|
|
625
669
|
const parsed = JSON.parse(fileContent);
|
|
626
|
-
if (
|
|
627
|
-
|
|
628
|
-
return null;
|
|
629
|
-
}
|
|
630
|
-
return parsed;
|
|
670
|
+
if (isPlainObject(parsed)) return parsed;
|
|
671
|
+
console.warn(`Warning: ${CONFIG_FILENAME} must be a JSON object, ignoring.`);
|
|
631
672
|
} catch (error) {
|
|
632
673
|
console.warn(`Warning: Failed to parse ${CONFIG_FILENAME}: ${error instanceof Error ? error.message : String(error)}`);
|
|
633
|
-
return null;
|
|
634
674
|
}
|
|
635
675
|
const packageJsonPath = path.join(rootDirectory, "package.json");
|
|
636
676
|
if (fs.existsSync(packageJsonPath)) try {
|
|
@@ -862,11 +902,15 @@ const CONFIG_LOADING_ERROR_PATTERN = /Error loading .*\/([a-z-]+)\.config\./;
|
|
|
862
902
|
const extractFailedPluginName = (error) => {
|
|
863
903
|
return String(error).match(CONFIG_LOADING_ERROR_PATTERN)?.[1] ?? null;
|
|
864
904
|
};
|
|
905
|
+
const TSCONFIG_FILENAMES = ["tsconfig.base.json", "tsconfig.json"];
|
|
906
|
+
const resolveTsConfigFile = (directory) => TSCONFIG_FILENAMES.find((filename) => fs.existsSync(path.join(directory, filename)));
|
|
865
907
|
const runKnipWithOptions = async (knipCwd, workspaceName) => {
|
|
908
|
+
const tsConfigFile = resolveTsConfigFile(knipCwd);
|
|
866
909
|
const options = await silenced(() => createOptions({
|
|
867
910
|
cwd: knipCwd,
|
|
868
911
|
isShowProgress: false,
|
|
869
|
-
...workspaceName ? { workspace: workspaceName } : {}
|
|
912
|
+
...workspaceName ? { workspace: workspaceName } : {},
|
|
913
|
+
...tsConfigFile ? { tsConfigFile } : {}
|
|
870
914
|
}));
|
|
871
915
|
const parsedConfig = options.parsedConfig;
|
|
872
916
|
for (let attempt = 0; attempt <= MAX_KNIP_RETRIES; attempt++) try {
|
|
@@ -938,6 +982,16 @@ const NEXTJS_RULES = {
|
|
|
938
982
|
"react-doctor/nextjs-no-head-import": "error",
|
|
939
983
|
"react-doctor/nextjs-no-side-effect-in-get-handler": "error"
|
|
940
984
|
};
|
|
985
|
+
const REACT_NATIVE_RULES = {
|
|
986
|
+
"react-doctor/rn-no-raw-text": "error",
|
|
987
|
+
"react-doctor/rn-no-deprecated-modules": "error",
|
|
988
|
+
"react-doctor/rn-no-legacy-expo-packages": "warn",
|
|
989
|
+
"react-doctor/rn-no-dimensions-get": "warn",
|
|
990
|
+
"react-doctor/rn-no-inline-flatlist-renderitem": "warn",
|
|
991
|
+
"react-doctor/rn-no-legacy-shadow-styles": "warn",
|
|
992
|
+
"react-doctor/rn-prefer-reanimated": "warn",
|
|
993
|
+
"react-doctor/rn-no-single-element-style-array": "warn"
|
|
994
|
+
};
|
|
941
995
|
const REACT_COMPILER_RULES = {
|
|
942
996
|
"react-hooks-js/set-state-in-render": "error",
|
|
943
997
|
"react-hooks-js/immutability": "error",
|
|
@@ -1041,7 +1095,8 @@ const createOxlintConfig = ({ pluginPath, framework, hasReactCompiler }) => ({
|
|
|
1041
1095
|
"react-doctor/server-after-nonblocking": "warn",
|
|
1042
1096
|
"react-doctor/client-passive-event-listeners": "warn",
|
|
1043
1097
|
"react-doctor/async-parallel": "warn",
|
|
1044
|
-
...framework === "nextjs" ? NEXTJS_RULES : {}
|
|
1098
|
+
...framework === "nextjs" ? NEXTJS_RULES : {},
|
|
1099
|
+
...framework === "expo" || framework === "react-native" ? REACT_NATIVE_RULES : {}
|
|
1045
1100
|
}
|
|
1046
1101
|
});
|
|
1047
1102
|
|
|
@@ -1149,7 +1204,15 @@ const RULE_CATEGORY_MAP = {
|
|
|
1149
1204
|
"react-doctor/server-auth-actions": "Server",
|
|
1150
1205
|
"react-doctor/server-after-nonblocking": "Server",
|
|
1151
1206
|
"react-doctor/client-passive-event-listeners": "Performance",
|
|
1152
|
-
"react-doctor/async-parallel": "Performance"
|
|
1207
|
+
"react-doctor/async-parallel": "Performance",
|
|
1208
|
+
"react-doctor/rn-no-raw-text": "React Native",
|
|
1209
|
+
"react-doctor/rn-no-deprecated-modules": "React Native",
|
|
1210
|
+
"react-doctor/rn-no-legacy-expo-packages": "React Native",
|
|
1211
|
+
"react-doctor/rn-no-dimensions-get": "React Native",
|
|
1212
|
+
"react-doctor/rn-no-inline-flatlist-renderitem": "React Native",
|
|
1213
|
+
"react-doctor/rn-no-legacy-shadow-styles": "React Native",
|
|
1214
|
+
"react-doctor/rn-prefer-reanimated": "React Native",
|
|
1215
|
+
"react-doctor/rn-no-single-element-style-array": "React Native"
|
|
1153
1216
|
};
|
|
1154
1217
|
const RULE_HELP_MAP = {
|
|
1155
1218
|
"no-derived-state-effect": "For derived state, compute inline: `const x = fn(dep)`. For state resets on prop change, use a key prop: `<Component key={prop} />`",
|
|
@@ -1205,7 +1268,15 @@ const RULE_HELP_MAP = {
|
|
|
1205
1268
|
"server-auth-actions": "Add `const session = await auth()` at the top and throw/redirect if unauthorized before any data access",
|
|
1206
1269
|
"server-after-nonblocking": "`import { after } from 'next/server'` then wrap: `after(() => analytics.track(...))` — response isn't blocked",
|
|
1207
1270
|
"client-passive-event-listeners": "Add `{ passive: true }` as the third argument: `addEventListener('scroll', handler, { passive: true })`",
|
|
1208
|
-
"async-parallel": "Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` to run independent operations concurrently"
|
|
1271
|
+
"async-parallel": "Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` to run independent operations concurrently",
|
|
1272
|
+
"rn-no-raw-text": "Wrap text in a `<Text>` component: `<Text>{value}</Text>` — raw strings outside `<Text>` crash on React Native",
|
|
1273
|
+
"rn-no-deprecated-modules": "Import from the community package instead — deprecated modules were removed from the react-native core",
|
|
1274
|
+
"rn-no-legacy-expo-packages": "Migrate to the recommended replacement package — legacy Expo packages are no longer maintained",
|
|
1275
|
+
"rn-no-dimensions-get": "Use `const { width, height } = useWindowDimensions()` — it updates reactively on rotation and resize",
|
|
1276
|
+
"rn-no-inline-flatlist-renderitem": "Extract renderItem to a named function or wrap in useCallback to avoid re-creating on every render",
|
|
1277
|
+
"rn-no-legacy-shadow-styles": "Use `boxShadow` for cross-platform shadows on the new architecture instead of platform-specific shadow properties",
|
|
1278
|
+
"rn-prefer-reanimated": "Use `import Animated from 'react-native-reanimated'` — animations run on the UI thread instead of the JS thread",
|
|
1279
|
+
"rn-no-single-element-style-array": "Use `style={value}` instead of `style={[value]}` — single-element arrays add unnecessary allocation"
|
|
1209
1280
|
};
|
|
1210
1281
|
const FILEPATH_WITH_LOCATION_PATTERN = /\S+\.\w+:\d+:\d+[\s\S]*$/;
|
|
1211
1282
|
const REACT_COMPILER_MESSAGE = "React Compiler can't optimize this code";
|
|
@@ -1639,13 +1710,15 @@ const scan = async (directory, inputOptions = {}) => {
|
|
|
1639
1710
|
return lintDiagnostics;
|
|
1640
1711
|
} catch (error) {
|
|
1641
1712
|
didLintFail = true;
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1713
|
+
if (!options.scoreOnly) {
|
|
1714
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1715
|
+
if (errorMessage.includes("native binding")) {
|
|
1716
|
+
lintSpinner?.fail(`Lint checks failed — oxlint native binding not found (Node ${process.version}).`);
|
|
1717
|
+
logger.dim(` Upgrade to Node ${OXLINT_NODE_REQUIREMENT} or run: npx -p oxlint@latest react-doctor@latest`);
|
|
1718
|
+
} else {
|
|
1719
|
+
lintSpinner?.fail("Lint checks failed (non-fatal, skipping).");
|
|
1720
|
+
logger.error(errorMessage);
|
|
1721
|
+
}
|
|
1649
1722
|
}
|
|
1650
1723
|
return [];
|
|
1651
1724
|
}
|
|
@@ -1658,8 +1731,10 @@ const scan = async (directory, inputOptions = {}) => {
|
|
|
1658
1731
|
return knipDiagnostics;
|
|
1659
1732
|
} catch (error) {
|
|
1660
1733
|
didDeadCodeFail = true;
|
|
1661
|
-
|
|
1662
|
-
|
|
1734
|
+
if (!options.scoreOnly) {
|
|
1735
|
+
deadCodeSpinner?.fail("Dead code detection failed (non-fatal, skipping).");
|
|
1736
|
+
logger.error(String(error));
|
|
1737
|
+
}
|
|
1663
1738
|
return [];
|
|
1664
1739
|
}
|
|
1665
1740
|
})() : Promise.resolve([]);
|
|
@@ -2029,7 +2104,18 @@ const maybePromptSkillInstall = async (shouldSkipPrompts) => {
|
|
|
2029
2104
|
|
|
2030
2105
|
//#endregion
|
|
2031
2106
|
//#region src/cli.ts
|
|
2032
|
-
const VERSION = "0.0.
|
|
2107
|
+
const VERSION = "0.0.29";
|
|
2108
|
+
const VALID_FAIL_ON_LEVELS = new Set([
|
|
2109
|
+
"error",
|
|
2110
|
+
"warning",
|
|
2111
|
+
"none"
|
|
2112
|
+
]);
|
|
2113
|
+
const isValidFailOnLevel = (level) => VALID_FAIL_ON_LEVELS.has(level);
|
|
2114
|
+
const shouldFailForDiagnostics = (diagnostics, failOnLevel) => {
|
|
2115
|
+
if (failOnLevel === "none") return false;
|
|
2116
|
+
if (failOnLevel === "warning") return diagnostics.length > 0;
|
|
2117
|
+
return diagnostics.some((diagnostic) => diagnostic.severity === "error");
|
|
2118
|
+
};
|
|
2033
2119
|
const exitWithFixHint = () => {
|
|
2034
2120
|
logger.break();
|
|
2035
2121
|
logger.log("Cancelled.");
|
|
@@ -2052,8 +2138,8 @@ const isAutomatedEnvironment = () => AUTOMATED_ENVIRONMENT_VARIABLES.some((envVa
|
|
|
2052
2138
|
const resolveCliScanOptions = (flags, userConfig, programInstance) => {
|
|
2053
2139
|
const isCliOverride = (optionName) => programInstance.getOptionValueSource(optionName) === "cli";
|
|
2054
2140
|
return {
|
|
2055
|
-
lint: isCliOverride("lint") ? flags.lint : userConfig?.lint ??
|
|
2056
|
-
deadCode: isCliOverride("deadCode") ? flags.deadCode : userConfig?.deadCode ??
|
|
2141
|
+
lint: isCliOverride("lint") ? flags.lint : userConfig?.lint ?? true,
|
|
2142
|
+
deadCode: isCliOverride("deadCode") ? flags.deadCode : userConfig?.deadCode ?? true,
|
|
2057
2143
|
verbose: isCliOverride("verbose") ? Boolean(flags.verbose) : userConfig?.verbose ?? false,
|
|
2058
2144
|
scoreOnly: flags.score,
|
|
2059
2145
|
offline: flags.offline
|
|
@@ -2081,7 +2167,7 @@ const resolveDiffMode = async (diffInfo, effectiveDiff, shouldSkipPrompts, isSco
|
|
|
2081
2167
|
});
|
|
2082
2168
|
return Boolean(shouldScanChangedOnly);
|
|
2083
2169
|
};
|
|
2084
|
-
const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--no-lint", "skip linting").option("--no-dead-code", "skip dead code detection").option("--verbose", "show file details per rule").option("--score", "output only the score").option("-y, --yes", "skip prompts, scan all workspace projects").option("--project <name>", "select workspace project (comma-separated for multiple)").option("--diff [base]", "scan only files changed vs base branch").option("--offline", "skip telemetry (anonymous, not stored, only used to calculate score)").option("--no-ami", "skip Ami-related prompts").option("--fix", "open Ami to auto-fix all issues").action(async (directory, flags) => {
|
|
2170
|
+
const program = new Command().name("react-doctor").description("Diagnose React codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--lint", "enable linting").option("--no-lint", "skip linting").option("--dead-code", "enable dead code detection").option("--no-dead-code", "skip dead code detection").option("--verbose", "show file details per rule").option("--score", "output only the score").option("-y, --yes", "skip prompts, scan all workspace projects").option("--project <name>", "select workspace project (comma-separated for multiple)").option("--diff [base]", "scan only files changed vs base branch").option("--offline", "skip telemetry (anonymous, not stored, only used to calculate score)").option("--no-ami", "skip Ami-related prompts").option("--fail-on <level>", "exit with error code on diagnostics: error, warning, none", "none").option("--fix", "open Ami to auto-fix all issues").action(async (directory, flags) => {
|
|
2085
2171
|
const isScoreOnly = flags.score;
|
|
2086
2172
|
try {
|
|
2087
2173
|
const resolvedDirectory = path.resolve(directory);
|
|
@@ -2131,6 +2217,8 @@ const program = new Command().name("react-doctor").description("Diagnose React c
|
|
|
2131
2217
|
allDiagnostics.push(...scanResult.diagnostics);
|
|
2132
2218
|
if (!isScoreOnly) logger.break();
|
|
2133
2219
|
}
|
|
2220
|
+
const resolvedFailOn = program.getOptionValueSource("failOn") === "cli" ? flags.failOn : userConfig?.failOn ?? flags.failOn;
|
|
2221
|
+
if (shouldFailForDiagnostics(allDiagnostics, isValidFailOnLevel(resolvedFailOn) ? resolvedFailOn : "none")) process.exitCode = 1;
|
|
2134
2222
|
if (flags.fix) openAmiToFix(resolvedDirectory);
|
|
2135
2223
|
if (!isScoreOnly && !shouldSkipAmiPrompts && !flags.fix) {
|
|
2136
2224
|
await maybePromptSkillInstall(shouldSkipAmiPrompts);
|
|
@@ -2193,7 +2281,11 @@ const openAmiToFix = (directory) => {
|
|
|
2193
2281
|
if (!isInstalled) {
|
|
2194
2282
|
if (process.platform === "darwin") {
|
|
2195
2283
|
installAmi();
|
|
2196
|
-
logger.success("Ami installed successfully.");
|
|
2284
|
+
if (isAmiInstalled()) logger.success("Ami installed successfully.");
|
|
2285
|
+
else {
|
|
2286
|
+
logger.error("Installation could not be verified.");
|
|
2287
|
+
logger.dim(`Install manually at ${highlighter.info(AMI_WEBSITE_URL)}`);
|
|
2288
|
+
}
|
|
2197
2289
|
} else {
|
|
2198
2290
|
logger.error("Ami is not installed.");
|
|
2199
2291
|
logger.dim(`Download at ${highlighter.info(AMI_RELEASES_URL)}`);
|