@funeste38/allmight 1.0.0 → 1.1.0

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/dist/cli/index.js CHANGED
@@ -3,40 +3,80 @@ import path from "node:path";
3
3
  import { scanDuplicateFamilies } from "../core/scanner/scanDuplicateFamilies.js";
4
4
  import { applySafePatches } from "../core/patcher/applySafePatches.js";
5
5
  import { writeReportArtifacts } from "../core/report/writeReportArtifacts.js";
6
+ import { analyzeRepoFamilies } from "../core/multi/analyzeRepoFamilies.js";
7
+ import { analyzePublishDoctor } from "../core/doctor/analyzePublishDoctor.js";
8
+ import { writeDoctorArtifacts, writeMultiRootArtifacts } from "../core/report/writeMultiRootArtifacts.js";
6
9
  function usage() {
7
10
  console.log([
8
- "allmight <command> [root] [--output <dir>]",
11
+ "allmight <command> [args] [--output <dir>]",
9
12
  "",
10
13
  "Commands:",
11
14
  " scan scan repo and write report artifacts",
12
15
  " report alias of scan",
13
16
  " propose scan repo and write diff proposals",
14
17
  " fix-safe scan repo, apply safe patches, then write artifacts",
15
- " canonicalize scan repo and print canonical map"
18
+ " canonicalize scan repo and print canonical map",
19
+ " multi-scan scan multiple roots and write repo family + publish risk artifacts",
20
+ " doctor publish verify package/dist/bin/global-install coherence",
21
+ " doctor bin focus on bin targets and install shadows",
22
+ " doctor install-shadow detect global/local install mismatch for a package"
16
23
  ].join("\n"));
17
24
  }
18
25
  function parseArgs(argv) {
19
- const command = argv[0];
20
- const positional = argv.filter((arg) => !arg.startsWith("--"));
26
+ const positional = argv.filter((arg, index) => !(arg === "--output" || argv[index - 1] === "--output") && !arg.startsWith("--output="));
21
27
  const outputIndex = argv.indexOf("--output");
22
- const outputDir = outputIndex !== -1 ? argv[outputIndex + 1] ?? "" : "";
23
- const root = positional[1] ? path.resolve(positional[1]) : process.cwd();
28
+ const inlineOutput = argv.find((arg) => arg.startsWith("--output="));
29
+ const outputDir = outputIndex !== -1
30
+ ? argv[outputIndex + 1] ?? ""
31
+ : inlineOutput
32
+ ? inlineOutput.slice("--output=".length)
33
+ : "";
24
34
  return {
25
- command,
26
- root,
35
+ command: positional[0],
36
+ positional,
27
37
  outputDir: outputDir || path.resolve(process.cwd(), "allmight-output")
28
38
  };
29
39
  }
30
40
  export async function runCli(argv = process.argv.slice(2)) {
31
- const { command, root, outputDir } = parseArgs(argv);
41
+ const { command, positional, outputDir } = parseArgs(argv);
32
42
  if (!command || command === "--help" || command === "-h") {
33
43
  usage();
34
44
  return 0;
35
45
  }
36
- const report = await scanDuplicateFamilies(root, { outputDir });
46
+ if (command === "multi-scan") {
47
+ const roots = positional.slice(1).map((root) => path.resolve(root));
48
+ if (roots.length === 0) {
49
+ usage();
50
+ return 1;
51
+ }
52
+ const report = await analyzeRepoFamilies(roots);
53
+ await writeMultiRootArtifacts(report, outputDir);
54
+ console.log(`Scanned ${roots.length} roots.`);
55
+ console.log(`Repo families: ${report.repoFamilies.length}`);
56
+ console.log(`Command shadows: ${report.commandShadows.length}`);
57
+ console.log(`Artifacts: ${outputDir}`);
58
+ return 0;
59
+ }
60
+ if (command === "doctor") {
61
+ const mode = positional[1];
62
+ if (!mode || !["publish", "bin", "install-shadow"].includes(mode)) {
63
+ usage();
64
+ return 1;
65
+ }
66
+ const root = positional[2] ? path.resolve(positional[2]) : process.cwd();
67
+ const report = await analyzePublishDoctor(root);
68
+ await writeDoctorArtifacts(report, outputDir, mode);
69
+ console.log(`Doctor ${mode} -> ${report.publishRisk}`);
70
+ console.log(`Artifacts: ${outputDir}`);
71
+ return 0;
72
+ }
73
+ const root = positional[1] ? path.resolve(positional[1]) : process.cwd();
74
+ let report = await scanDuplicateFamilies(root, { outputDir });
37
75
  await writeReportArtifacts(report, outputDir);
38
76
  if (command === "fix-safe") {
39
77
  const applied = await applySafePatches(report);
78
+ report = await scanDuplicateFamilies(root, { outputDir });
79
+ await writeReportArtifacts(report, outputDir);
40
80
  console.log(`Applied ${applied.length} safe patches.`);
41
81
  return 0;
42
82
  }
@@ -0,0 +1,2 @@
1
+ import { PublishDoctorReport } from "../../types.js";
2
+ export declare function analyzePublishDoctor(root: string): Promise<PublishDoctorReport>;
@@ -0,0 +1,80 @@
1
+ import path from "node:path";
2
+ import { readGlobalNpmState, findCommandPaths } from "../../utils/system.js";
3
+ import { toPosix } from "../../utils/text.js";
4
+ import { readRepoRootMeta } from "../repo/readRepoRootMeta.js";
5
+ function toRisk(reasons) {
6
+ if (reasons.some((reason) => /mismatch|missing|shadow|different/i.test(reason))) {
7
+ return "dangerous";
8
+ }
9
+ if (reasons.length > 0) {
10
+ return "review";
11
+ }
12
+ return "safe";
13
+ }
14
+ function buildInstallShadowFinding(root, packageName, packageVersion, bin, globalInfo, globalPrefix) {
15
+ const commandPaths = findCommandPaths(bin);
16
+ const reasons = [];
17
+ const normalizedRoot = toPosix(path.resolve(root)).toLowerCase();
18
+ const normalizedResolved = toPosix(String(globalInfo?.resolved || "").replace(/^file:/i, "")).toLowerCase();
19
+ if (commandPaths.length === 0) {
20
+ reasons.push("bin is not available on PATH");
21
+ }
22
+ if (globalInfo?.resolved && normalizedResolved && normalizedResolved !== normalizedRoot) {
23
+ reasons.push("global install resolves to a different root");
24
+ }
25
+ if (globalInfo?.version && packageVersion && globalInfo.version !== packageVersion) {
26
+ reasons.push("global install version differs from package.json");
27
+ }
28
+ if (commandPaths.some((entry) => !toPosix(entry).toLowerCase().startsWith(toPosix(globalPrefix).toLowerCase()))) {
29
+ reasons.push("command shadow detected outside the npm global prefix");
30
+ }
31
+ if (!globalInfo && packageName) {
32
+ reasons.push("package is not installed globally");
33
+ }
34
+ return {
35
+ bin,
36
+ commandPaths,
37
+ globalPackageVersion: globalInfo?.version,
38
+ globalPackageResolved: globalInfo?.resolved,
39
+ risk: toRisk(reasons),
40
+ reason: reasons.join(" | ") || "install path looks coherent"
41
+ };
42
+ }
43
+ export async function analyzePublishDoctor(root) {
44
+ const meta = await readRepoRootMeta(root);
45
+ const npmState = readGlobalNpmState();
46
+ const globalPackageInfo = meta.packageName ? npmState.packages[meta.packageName] : undefined;
47
+ const installShadow = meta.binEntries.map((entry) => (buildInstallShadowFinding(meta.root, meta.packageName, meta.packageVersion, entry.name, globalPackageInfo, npmState.prefix)));
48
+ const reasons = [];
49
+ if (!meta.packageName)
50
+ reasons.push("missing package.json name");
51
+ if (meta.packagePrivate === true)
52
+ reasons.push("package is marked private");
53
+ if (!meta.hasDist)
54
+ reasons.push("dist/ is missing");
55
+ if (!meta.hasSrc)
56
+ reasons.push("src/ is missing");
57
+ if (meta.packageLockVersion && meta.packageVersion && meta.packageLockVersion !== meta.packageVersion) {
58
+ reasons.push("package-lock version mismatch");
59
+ }
60
+ if (meta.binEntries.some((entry) => !entry.exists)) {
61
+ reasons.push("at least one bin target is missing");
62
+ }
63
+ if (installShadow.some((entry) => entry.risk === "dangerous")) {
64
+ reasons.push("install shadow or global mismatch detected");
65
+ }
66
+ return {
67
+ createdAt: new Date().toISOString(),
68
+ root: meta.root,
69
+ packageName: meta.packageName,
70
+ packageVersion: meta.packageVersion,
71
+ packagePrivate: meta.packagePrivate,
72
+ packageLockVersion: meta.packageLockVersion,
73
+ hasDist: meta.hasDist,
74
+ hasSrc: meta.hasSrc,
75
+ bins: meta.binEntries,
76
+ installShadow,
77
+ publishRisk: toRisk(reasons),
78
+ reasons
79
+ };
80
+ }
@@ -0,0 +1,2 @@
1
+ import { MultiRepoReport } from "../../types.js";
2
+ export declare function analyzeRepoFamilies(roots: string[]): Promise<MultiRepoReport>;
@@ -0,0 +1,215 @@
1
+ import path from "node:path";
2
+ import { levenshteinDistance, toPosix } from "../../utils/text.js";
3
+ import { readRepoRootMeta } from "../repo/readRepoRootMeta.js";
4
+ function pathPenalty(root) {
5
+ const normalized = toPosix(root).toLowerCase();
6
+ let penalty = 0;
7
+ if (normalized.includes("/poubele/"))
8
+ penalty += 80;
9
+ if (normalized.includes("/tmp/"))
10
+ penalty += 60;
11
+ if (normalized.includes("/dump/"))
12
+ penalty += 40;
13
+ if (normalized.includes("/backup/"))
14
+ penalty += 45;
15
+ if (normalized.includes("/copy/"))
16
+ penalty += 35;
17
+ if (normalized.includes("/old/"))
18
+ penalty += 35;
19
+ if (normalized.includes("/gpt6.0/"))
20
+ penalty += 20;
21
+ return penalty;
22
+ }
23
+ function versionScore(version) {
24
+ if (!version)
25
+ return 0;
26
+ const [major = 0, minor = 0, patch = 0] = version.split(".").map((value) => Number(value) || 0);
27
+ return major * 30 + minor * 4 + patch * 0.2;
28
+ }
29
+ function canonicalScore(meta, family) {
30
+ const basename = meta.basename.toLowerCase();
31
+ const packageName = meta.packageName?.toLowerCase() ?? "";
32
+ let score = 0;
33
+ if (basename === family)
34
+ score += 70;
35
+ if (packageName.endsWith(`/${family}`) || packageName === family)
36
+ score += 45;
37
+ if (basename.includes(family))
38
+ score += 18;
39
+ if (meta.hasSrc)
40
+ score += 20;
41
+ if (meta.hasDist)
42
+ score += 10;
43
+ if (meta.binEntries.length > 0)
44
+ score += 12;
45
+ if (meta.binEntries.every((entry) => entry.exists))
46
+ score += 10;
47
+ if (meta.packagePrivate === true)
48
+ score -= 30;
49
+ score += versionScore(meta.packageVersion);
50
+ score -= pathPenalty(meta.root);
51
+ return score;
52
+ }
53
+ function memberRole(root, canonicalRoot) {
54
+ if (root === canonicalRoot) {
55
+ return "canonical";
56
+ }
57
+ const normalized = toPosix(root).toLowerCase();
58
+ if (normalized.includes("/funesterie/") || normalized.includes("/a11/")) {
59
+ return "transition";
60
+ }
61
+ return "mirror";
62
+ }
63
+ function collectCollisions(members, selector) {
64
+ const groups = new Map();
65
+ for (const meta of members) {
66
+ for (const entry of selector(meta)) {
67
+ const existing = groups.get(entry.name) ?? {
68
+ name: entry.name,
69
+ roots: [],
70
+ packages: []
71
+ };
72
+ existing.roots.push(meta.root);
73
+ if (entry.packageName) {
74
+ existing.packages.push(entry.packageName);
75
+ }
76
+ groups.set(entry.name, existing);
77
+ }
78
+ }
79
+ return Array.from(groups.values())
80
+ .filter((entry) => entry.roots.length > 1)
81
+ .map((entry) => ({
82
+ ...entry,
83
+ roots: Array.from(new Set(entry.roots)),
84
+ packages: Array.from(new Set(entry.packages))
85
+ }))
86
+ .sort((left, right) => right.roots.length - left.roots.length || left.name.localeCompare(right.name));
87
+ }
88
+ function publishRiskLevel(score) {
89
+ if (score >= 60)
90
+ return "dangerous";
91
+ if (score >= 30)
92
+ return "review";
93
+ return "safe";
94
+ }
95
+ function familyReasons(family, canonicalRoot, members, packageCollisions, binCollisions) {
96
+ const reasons = [`canonical source proposed for ${family}: ${canonicalRoot}`];
97
+ if (members.some((meta) => pathPenalty(meta.root) > 0 && meta.packagePrivate !== true)) {
98
+ reasons.push("legacy or trash-area copy still looks publishable");
99
+ }
100
+ if (packageCollisions.length > 0) {
101
+ reasons.push("same package name appears in multiple roots");
102
+ }
103
+ if (binCollisions.length > 0) {
104
+ reasons.push("same CLI bin is exposed by multiple roots");
105
+ }
106
+ if (members.some((meta) => meta.binEntries.some((entry) => !entry.exists))) {
107
+ reasons.push("at least one bin target is missing");
108
+ }
109
+ return reasons;
110
+ }
111
+ function buildRepoFamilyReport(family, members) {
112
+ const scoredMembers = members
113
+ .map((meta) => ({ meta, score: canonicalScore(meta, family) }))
114
+ .sort((left, right) => right.score - left.score || left.meta.root.localeCompare(right.meta.root));
115
+ const canonicalRoot = scoredMembers[0]?.meta.root ?? members[0].root;
116
+ const packageCollisions = collectCollisions(members, (meta) => (meta.packageName ? [{ name: meta.packageName, packageName: meta.packageName }] : []));
117
+ const binCollisions = collectCollisions(members, (meta) => (meta.binEntries.map((entry) => ({ name: entry.name, packageName: meta.packageName }))));
118
+ const reasons = familyReasons(family, canonicalRoot, members, packageCollisions, binCollisions);
119
+ let publishRiskScore = 0;
120
+ publishRiskScore += packageCollisions.length * 28;
121
+ publishRiskScore += binCollisions.length * 24;
122
+ publishRiskScore += members.filter((meta) => pathPenalty(meta.root) > 0 && meta.packagePrivate !== true).length * 12;
123
+ publishRiskScore += members.filter((meta) => meta.binEntries.some((entry) => !entry.exists)).length * 12;
124
+ publishRiskScore = Math.min(100, publishRiskScore);
125
+ const memberReports = scoredMembers.map(({ meta, score }) => ({
126
+ root: meta.root,
127
+ packageName: meta.packageName,
128
+ packageVersion: meta.packageVersion,
129
+ role: memberRole(meta.root, canonicalRoot),
130
+ score,
131
+ bins: meta.binEntries.map((entry) => entry.name).sort()
132
+ }));
133
+ return {
134
+ family,
135
+ canonicalRoot,
136
+ canonicalPackage: scoredMembers[0]?.meta.packageName,
137
+ members: memberReports,
138
+ mirrors: memberReports.filter((member) => member.role === "mirror").map((member) => member.root),
139
+ transitions: memberReports.filter((member) => member.role === "transition").map((member) => member.root),
140
+ packageCollisions,
141
+ binCollisions,
142
+ publishRiskScore,
143
+ publishRisk: publishRiskLevel(publishRiskScore),
144
+ reasons
145
+ };
146
+ }
147
+ function isSimilarCommand(left, right) {
148
+ if (left === right)
149
+ return false;
150
+ const distance = levenshteinDistance(left, right);
151
+ if (distance <= 1)
152
+ return true;
153
+ return left.startsWith(right) || right.startsWith(left);
154
+ }
155
+ function commandShadowRisk(left, right, roots) {
156
+ if (levenshteinDistance(left, right) <= 1 || roots.length > 1) {
157
+ return "dangerous";
158
+ }
159
+ return "review";
160
+ }
161
+ function buildCommandShadows(roots) {
162
+ const commandToRoots = new Map();
163
+ for (const meta of roots) {
164
+ for (const entry of meta.binEntries) {
165
+ const existing = commandToRoots.get(entry.name) ?? new Set();
166
+ existing.add(meta.root);
167
+ commandToRoots.set(entry.name, existing);
168
+ }
169
+ }
170
+ const commands = Array.from(commandToRoots.keys()).sort();
171
+ const shadows = [];
172
+ for (let index = 0; index < commands.length; index += 1) {
173
+ for (let other = index + 1; other < commands.length; other += 1) {
174
+ const left = commands[index];
175
+ const right = commands[other];
176
+ if (!isSimilarCommand(left, right))
177
+ continue;
178
+ const rootsForPair = Array.from(new Set([
179
+ ...(commandToRoots.get(left) ?? new Set()),
180
+ ...(commandToRoots.get(right) ?? new Set())
181
+ ])).sort();
182
+ shadows.push({
183
+ left,
184
+ right,
185
+ distance: levenshteinDistance(left, right),
186
+ roots: rootsForPair,
187
+ risk: commandShadowRisk(left, right, rootsForPair),
188
+ reason: `commands ${left} and ${right} are visually close and may be mistaken during install or publish`
189
+ });
190
+ }
191
+ }
192
+ return shadows.sort((left, right) => left.distance - right.distance || left.left.localeCompare(right.left));
193
+ }
194
+ export async function analyzeRepoFamilies(roots) {
195
+ const uniqueRoots = Array.from(new Set(roots.map((root) => path.resolve(root))));
196
+ const metas = await Promise.all(uniqueRoots.map((root) => readRepoRootMeta(root)));
197
+ const familyBuckets = new Map();
198
+ for (const meta of metas) {
199
+ for (const family of meta.familyTags) {
200
+ const existing = familyBuckets.get(family) ?? [];
201
+ existing.push(meta);
202
+ familyBuckets.set(family, existing);
203
+ }
204
+ }
205
+ const repoFamilies = Array.from(familyBuckets.entries())
206
+ .filter(([, members]) => members.length >= 2)
207
+ .map(([family, members]) => buildRepoFamilyReport(family, members))
208
+ .sort((left, right) => right.publishRiskScore - left.publishRiskScore || left.family.localeCompare(right.family));
209
+ return {
210
+ createdAt: new Date().toISOString(),
211
+ roots: metas,
212
+ repoFamilies,
213
+ commandShadows: buildCommandShadows(metas)
214
+ };
215
+ }
@@ -0,0 +1,2 @@
1
+ import { RepoRootMeta } from "../../types.js";
2
+ export declare function readRepoRootMeta(root: string): Promise<RepoRootMeta>;
@@ -0,0 +1,120 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { toPosix } from "../../utils/text.js";
4
+ function normalizeLower(value) {
5
+ return value.trim().toLowerCase();
6
+ }
7
+ function hasFamilyHint(value, hints) {
8
+ const normalized = normalizeLower(value);
9
+ return hints.some((hint) => normalized.includes(hint));
10
+ }
11
+ function detectFamilyTags(root, packageName) {
12
+ const sources = [path.basename(root), packageName ?? ""].filter(Boolean);
13
+ const tags = new Set();
14
+ if (sources.some((value) => hasFamilyHint(value, ["qflush", "qflash"]))) {
15
+ tags.add("qflush");
16
+ }
17
+ if (sources.some((value) => hasFamilyHint(value, ["envapt", "envaptex"]))) {
18
+ tags.add("envapt");
19
+ }
20
+ if (sources.some((value) => hasFamilyHint(value, ["allmight"]))) {
21
+ tags.add("allmight");
22
+ }
23
+ if (sources.some((value) => hasFamilyHint(value, ["spyder"]))) {
24
+ tags.add("spyder");
25
+ }
26
+ return Array.from(tags).sort();
27
+ }
28
+ function readPackageLockVersion(parsed) {
29
+ if (typeof parsed.version === "string" && parsed.version.trim()) {
30
+ return parsed.version;
31
+ }
32
+ const packages = parsed.packages;
33
+ if (!packages || typeof packages !== "object") {
34
+ return undefined;
35
+ }
36
+ const rootPackage = packages[""];
37
+ if (!rootPackage || typeof rootPackage !== "object") {
38
+ return undefined;
39
+ }
40
+ const version = rootPackage.version;
41
+ return typeof version === "string" && version.trim() ? version : undefined;
42
+ }
43
+ function normalizeBinEntries(root, rawBin) {
44
+ const entries = new Map();
45
+ if (typeof rawBin === "string" && rawBin.trim()) {
46
+ entries.set(path.basename(root), rawBin.trim());
47
+ }
48
+ else if (rawBin && typeof rawBin === "object") {
49
+ for (const [name, target] of Object.entries(rawBin)) {
50
+ if (typeof target === "string" && target.trim()) {
51
+ entries.set(name, target.trim());
52
+ }
53
+ }
54
+ }
55
+ return Array.from(entries.entries())
56
+ .map(([name, target]) => {
57
+ const absoluteTarget = path.resolve(root, target);
58
+ return fs.access(absoluteTarget)
59
+ .then(() => ({
60
+ name,
61
+ target,
62
+ absoluteTarget,
63
+ exists: true
64
+ }))
65
+ .catch(() => ({
66
+ name,
67
+ target,
68
+ absoluteTarget,
69
+ exists: false
70
+ }));
71
+ });
72
+ }
73
+ export async function readRepoRootMeta(root) {
74
+ const resolvedRoot = path.resolve(root);
75
+ const packageJsonPath = path.join(resolvedRoot, "package.json");
76
+ const packageLockPath = path.join(resolvedRoot, "package-lock.json");
77
+ const srcPath = path.join(resolvedRoot, "src");
78
+ const distPath = path.join(resolvedRoot, "dist");
79
+ let packageName;
80
+ let packageVersion;
81
+ let packagePrivate;
82
+ let rawBin;
83
+ try {
84
+ const rawPackageJson = await fs.readFile(packageJsonPath, "utf8");
85
+ const parsed = JSON.parse(rawPackageJson);
86
+ packageName = typeof parsed.name === "string" ? parsed.name : undefined;
87
+ packageVersion = typeof parsed.version === "string" ? parsed.version : undefined;
88
+ packagePrivate = typeof parsed.private === "boolean" ? parsed.private : undefined;
89
+ rawBin = parsed.bin;
90
+ }
91
+ catch {
92
+ // root without package.json is still a valid scan root
93
+ }
94
+ let packageLockVersion;
95
+ try {
96
+ const rawPackageLock = await fs.readFile(packageLockPath, "utf8");
97
+ const parsed = JSON.parse(rawPackageLock);
98
+ packageLockVersion = readPackageLockVersion(parsed);
99
+ }
100
+ catch {
101
+ packageLockVersion = undefined;
102
+ }
103
+ const [hasSrc, hasDist] = await Promise.all([
104
+ fs.access(srcPath).then(() => true).catch(() => false),
105
+ fs.access(distPath).then(() => true).catch(() => false)
106
+ ]);
107
+ const binEntries = await Promise.all(normalizeBinEntries(resolvedRoot, rawBin));
108
+ return {
109
+ root: resolvedRoot,
110
+ basename: path.basename(resolvedRoot),
111
+ packageName,
112
+ packageVersion,
113
+ packagePrivate,
114
+ packageLockVersion,
115
+ familyTags: detectFamilyTags(toPosix(resolvedRoot), packageName),
116
+ hasSrc,
117
+ hasDist,
118
+ binEntries
119
+ };
120
+ }
@@ -0,0 +1,3 @@
1
+ import { MultiRepoReport, PublishDoctorReport } from "../../types.js";
2
+ export declare function writeMultiRootArtifacts(report: MultiRepoReport, outputDir: string): Promise<void>;
3
+ export declare function writeDoctorArtifacts(report: PublishDoctorReport, outputDir: string, mode: string): Promise<void>;
@@ -0,0 +1,73 @@
1
+ import path from "node:path";
2
+ import { ensureDir, writeJson, writeText } from "../../utils/fs.js";
3
+ function buildMultiScanSummary(report) {
4
+ const lines = [
5
+ "# Allmight Multi Scan",
6
+ "",
7
+ `Created at: ${report.createdAt}`,
8
+ `Roots scanned: ${report.roots.length}`,
9
+ `Families detected: ${report.repoFamilies.length}`,
10
+ `Command shadows: ${report.commandShadows.length}`,
11
+ "",
12
+ "## Families",
13
+ ""
14
+ ];
15
+ for (const family of report.repoFamilies) {
16
+ lines.push(`- ${family.family}: canonical=${family.canonicalRoot}`);
17
+ lines.push(` risk=${family.publishRisk} (${family.publishRiskScore})`);
18
+ }
19
+ if (report.commandShadows.length > 0) {
20
+ lines.push("", "## Command Shadows", "");
21
+ for (const shadow of report.commandShadows) {
22
+ lines.push(`- ${shadow.left} vs ${shadow.right}: ${shadow.risk}`);
23
+ }
24
+ }
25
+ return lines.join("\n");
26
+ }
27
+ function buildDoctorSummary(report, mode) {
28
+ const lines = [
29
+ "# Allmight Doctor",
30
+ "",
31
+ `Mode: ${mode}`,
32
+ `Root: ${report.root}`,
33
+ `Package: ${report.packageName ?? "unknown"}`,
34
+ `Version: ${report.packageVersion ?? "unknown"}`,
35
+ `Risk: ${report.publishRisk}`,
36
+ ""
37
+ ];
38
+ if (report.reasons.length > 0) {
39
+ lines.push("## Reasons", "");
40
+ for (const reason of report.reasons) {
41
+ lines.push(`- ${reason}`);
42
+ }
43
+ }
44
+ if (report.installShadow.length > 0) {
45
+ lines.push("", "## Install Shadow", "");
46
+ for (const entry of report.installShadow) {
47
+ lines.push(`- ${entry.bin}: ${entry.reason}`);
48
+ }
49
+ }
50
+ return lines.join("\n");
51
+ }
52
+ export async function writeMultiRootArtifacts(report, outputDir) {
53
+ await ensureDir(outputDir);
54
+ await writeJson(path.join(outputDir, "repo-families.json"), report.repoFamilies);
55
+ await writeJson(path.join(outputDir, "publish-risk-report.json"), {
56
+ createdAt: report.createdAt,
57
+ roots: report.roots,
58
+ repoFamilies: report.repoFamilies,
59
+ commandShadows: report.commandShadows
60
+ });
61
+ await writeJson(path.join(outputDir, "command-shadow-report.json"), report.commandShadows);
62
+ await writeText(path.join(outputDir, "summary.md"), buildMultiScanSummary(report));
63
+ }
64
+ export async function writeDoctorArtifacts(report, outputDir, mode) {
65
+ await ensureDir(outputDir);
66
+ const fileName = mode === "install-shadow"
67
+ ? "install-shadow-report.json"
68
+ : mode === "bin"
69
+ ? "bin-risk-report.json"
70
+ : "publish-risk-report.json";
71
+ await writeJson(path.join(outputDir, fileName), report);
72
+ await writeText(path.join(outputDir, "summary.md"), buildDoctorSummary(report, mode));
73
+ }
package/dist/index.d.ts CHANGED
@@ -6,4 +6,7 @@ export { applySafePatches } from "./core/patcher/applySafePatches.js";
6
6
  export { buildProposedPatches } from "./core/patcher/buildProposedPatches.js";
7
7
  export { rewriteImportsToCanonicalSource, createLegacyReexportShim } from "./core/patcher/rewriters.js";
8
8
  export { writeReportArtifacts } from "./core/report/writeReportArtifacts.js";
9
- export type { CanonicalMapEntry, DuplicateFamily, DuplicateType, FileFingerprint, PatchProposal, ScanOptions, ScanReport } from "./types.js";
9
+ export { analyzeRepoFamilies } from "./core/multi/analyzeRepoFamilies.js";
10
+ export { analyzePublishDoctor } from "./core/doctor/analyzePublishDoctor.js";
11
+ export { writeMultiRootArtifacts, writeDoctorArtifacts } from "./core/report/writeMultiRootArtifacts.js";
12
+ export type { CanonicalMapEntry, CommandShadow, DuplicateFamily, DuplicateType, FileFingerprint, InstallShadowFinding, MultiRepoReport, PatchProposal, PublishDoctorReport, RepoFamilyReport, RepoRootMeta, ScanOptions, ScanReport } from "./types.js";
package/dist/index.js CHANGED
@@ -6,3 +6,6 @@ export { applySafePatches } from "./core/patcher/applySafePatches.js";
6
6
  export { buildProposedPatches } from "./core/patcher/buildProposedPatches.js";
7
7
  export { rewriteImportsToCanonicalSource, createLegacyReexportShim } from "./core/patcher/rewriters.js";
8
8
  export { writeReportArtifacts } from "./core/report/writeReportArtifacts.js";
9
+ export { analyzeRepoFamilies } from "./core/multi/analyzeRepoFamilies.js";
10
+ export { analyzePublishDoctor } from "./core/doctor/analyzePublishDoctor.js";
11
+ export { writeMultiRootArtifacts, writeDoctorArtifacts } from "./core/report/writeMultiRootArtifacts.js";
package/dist/types.d.ts CHANGED
@@ -74,3 +74,88 @@ export type ScanOptions = {
74
74
  outputDir?: string;
75
75
  includeContent?: boolean;
76
76
  };
77
+ export type RepoBinEntry = {
78
+ name: string;
79
+ target: string;
80
+ exists: boolean;
81
+ absoluteTarget: string;
82
+ };
83
+ export type RepoRootMeta = {
84
+ root: string;
85
+ basename: string;
86
+ packageName?: string;
87
+ packageVersion?: string;
88
+ packagePrivate?: boolean;
89
+ packageLockVersion?: string;
90
+ familyTags: string[];
91
+ hasSrc: boolean;
92
+ hasDist: boolean;
93
+ binEntries: RepoBinEntry[];
94
+ };
95
+ export type RepoFamilyMemberRole = "canonical" | "transition" | "mirror";
96
+ export type RepoFamilyMember = {
97
+ root: string;
98
+ packageName?: string;
99
+ packageVersion?: string;
100
+ role: RepoFamilyMemberRole;
101
+ score: number;
102
+ bins: string[];
103
+ };
104
+ export type RepoCollision = {
105
+ name: string;
106
+ roots: string[];
107
+ packages: string[];
108
+ };
109
+ export type CommandShadow = {
110
+ left: string;
111
+ right: string;
112
+ distance: number;
113
+ roots: string[];
114
+ risk: RiskLevel;
115
+ reason: string;
116
+ };
117
+ export type RepoFamilyReport = {
118
+ family: string;
119
+ canonicalRoot: string;
120
+ canonicalPackage?: string;
121
+ members: RepoFamilyMember[];
122
+ mirrors: string[];
123
+ transitions: string[];
124
+ packageCollisions: RepoCollision[];
125
+ binCollisions: RepoCollision[];
126
+ publishRiskScore: number;
127
+ publishRisk: RiskLevel;
128
+ reasons: string[];
129
+ };
130
+ export type MultiRepoReport = {
131
+ createdAt: string;
132
+ roots: RepoRootMeta[];
133
+ repoFamilies: RepoFamilyReport[];
134
+ commandShadows: CommandShadow[];
135
+ };
136
+ export type GlobalPackageInfo = {
137
+ version?: string;
138
+ resolved?: string;
139
+ };
140
+ export type InstallShadowFinding = {
141
+ bin: string;
142
+ commandPaths: string[];
143
+ globalPackageVersion?: string;
144
+ globalPackageResolved?: string;
145
+ risk: RiskLevel;
146
+ reason: string;
147
+ };
148
+ export type PublishDoctorReport = {
149
+ createdAt: string;
150
+ root: string;
151
+ packageName?: string;
152
+ packageVersion?: string;
153
+ packagePrivate?: boolean;
154
+ packageLockVersion?: string;
155
+ hasDist: boolean;
156
+ hasSrc: boolean;
157
+ bins: RepoBinEntry[];
158
+ installShadow: InstallShadowFinding[];
159
+ publishRisk: RiskLevel;
160
+ reasons: string[];
161
+ };
@@ -0,0 +1,9 @@
1
+ export declare function findCommandPaths(commandName: string): string[];
2
+ export declare function readGlobalNpmState(): {
3
+ prefix: string;
4
+ root: string;
5
+ packages: Record<string, {
6
+ version?: string;
7
+ resolved?: string;
8
+ }>;
9
+ };
@@ -0,0 +1,48 @@
1
+ import { spawnSync } from "node:child_process";
2
+ function runCommand(command, args) {
3
+ try {
4
+ const result = spawnSync(command, args, {
5
+ encoding: "utf8",
6
+ windowsHide: true,
7
+ shell: process.platform === "win32"
8
+ });
9
+ return {
10
+ status: result.status,
11
+ stdout: String(result.stdout ?? ""),
12
+ stderr: String(result.stderr ?? "")
13
+ };
14
+ }
15
+ catch (error) {
16
+ return {
17
+ status: 1,
18
+ stdout: "",
19
+ stderr: String(error?.message ?? error)
20
+ };
21
+ }
22
+ }
23
+ export function findCommandPaths(commandName) {
24
+ const result = process.platform === "win32"
25
+ ? runCommand("where.exe", [commandName])
26
+ : runCommand("which", ["-a", commandName]);
27
+ if (result.status !== 0) {
28
+ return [];
29
+ }
30
+ return result.stdout
31
+ .split(/\r?\n/g)
32
+ .map((line) => line.trim())
33
+ .filter(Boolean);
34
+ }
35
+ export function readGlobalNpmState() {
36
+ const prefix = runCommand("npm", ["prefix", "-g"]).stdout.trim();
37
+ const root = runCommand("npm", ["root", "-g"]).stdout.trim();
38
+ const listResult = runCommand("npm", ["list", "-g", "--depth=0", "--json"]);
39
+ let packages = {};
40
+ try {
41
+ const parsed = JSON.parse(listResult.stdout);
42
+ packages = parsed.dependencies ?? {};
43
+ }
44
+ catch {
45
+ packages = {};
46
+ }
47
+ return { prefix, root, packages };
48
+ }
@@ -6,3 +6,4 @@ export declare function jaccardSimilarity(a: Set<string>, b: Set<string>): numbe
6
6
  export declare function sharedValues(a: string[], b: string[]): string[];
7
7
  export declare function slugify(value: string): string;
8
8
  export declare function removeExtension(filePath: string): string;
9
+ export declare function levenshteinDistance(left: string, right: string): number;
@@ -49,3 +49,27 @@ export function removeExtension(filePath) {
49
49
  const ext = path.extname(filePath);
50
50
  return filePath.slice(0, filePath.length - ext.length);
51
51
  }
52
+ export function levenshteinDistance(left, right) {
53
+ if (left === right)
54
+ return 0;
55
+ if (!left.length)
56
+ return right.length;
57
+ if (!right.length)
58
+ return left.length;
59
+ const rows = right.length + 1;
60
+ const cols = left.length + 1;
61
+ const matrix = Array.from({ length: rows }, () => Array(cols).fill(0));
62
+ for (let row = 0; row < rows; row += 1) {
63
+ matrix[row][0] = row;
64
+ }
65
+ for (let col = 0; col < cols; col += 1) {
66
+ matrix[0][col] = col;
67
+ }
68
+ for (let row = 1; row < rows; row += 1) {
69
+ for (let col = 1; col < cols; col += 1) {
70
+ const substitutionCost = right[row - 1] === left[col - 1] ? 0 : 1;
71
+ matrix[row][col] = Math.min(matrix[row - 1][col] + 1, matrix[row][col - 1] + 1, matrix[row - 1][col - 1] + substitutionCost);
72
+ }
73
+ }
74
+ return matrix[rows - 1][cols - 1];
75
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funeste38/allmight",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Audit-first duplicate detector and canonicalization helper for Funesterie repositories.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",