assistant-ui 0.0.94 → 0.0.95
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/commands/doctor.d.ts +26 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +219 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/doctor.ts +372 -0
- package/src/index.ts +2 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
|
|
3
|
+
//#region src/commands/doctor.d.ts
|
|
4
|
+
interface DiscoveredPackage {
|
|
5
|
+
name: string;
|
|
6
|
+
version: string;
|
|
7
|
+
installPath: string;
|
|
8
|
+
}
|
|
9
|
+
declare function discoverInstalledPackages(cwd: string): DiscoveredPackage[];
|
|
10
|
+
interface DuplicateGroup {
|
|
11
|
+
name: string;
|
|
12
|
+
installations: DiscoveredPackage[];
|
|
13
|
+
}
|
|
14
|
+
declare function findDuplicates(packages: DiscoveredPackage[]): DuplicateGroup[];
|
|
15
|
+
declare function uniquePackageNames(packages: DiscoveredPackage[]): string[];
|
|
16
|
+
declare function compareSemver(a: string, b: string): number;
|
|
17
|
+
interface OutdatedPackage {
|
|
18
|
+
name: string;
|
|
19
|
+
current: string;
|
|
20
|
+
latest: string;
|
|
21
|
+
}
|
|
22
|
+
declare function findOutdated(packages: DiscoveredPackage[], latest: Map<string, string | null>): OutdatedPackage[];
|
|
23
|
+
declare const doctor: Command;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { DiscoveredPackage, DuplicateGroup, OutdatedPackage, compareSemver, discoverInstalledPackages, doctor, findDuplicates, findOutdated, uniquePackageNames };
|
|
26
|
+
//# sourceMappingURL=doctor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","names":[],"sources":["../../src/commands/doctor.ts"],"mappings":";;;UAiBiB,iBAAA;EACf,IAAA;EACA,OAAA;EACA,WAAA;AAAA;AAAA,iBA2Fc,yBAAA,CAA0B,GAAA,WAAc,iBAAiB;AAAA,UAOxD,cAAA;EACf,IAAA;EACA,aAAA,EAAe,iBAAiB;AAAA;AAAA,iBAGlB,cAAA,CACd,QAAA,EAAU,iBAAA,KACT,cAAc;AAAA,iBAmBD,kBAAA,CAAmB,QAA6B,EAAnB,iBAAiB;AAAA,iBAqD9C,aAAA,CAAc,CAAA,UAAW,CAAS;AAAA,UAkBjC,eAAA;EACf,IAAA;EACA,OAAA;EACA,MAAA;AAAA;AAAA,iBAGc,YAAA,CACd,QAAA,EAAU,iBAAA,IACV,MAAA,EAAQ,GAAA,0BACP,eAAA;AAAA,cAsFU,MAAA,EAAM,OA6Df"}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import * as fs$1 from "node:fs";
|
|
4
|
+
import * as path$1 from "node:path";
|
|
5
|
+
//#region src/commands/doctor.ts
|
|
6
|
+
const ASSISTANT_UI_PACKAGE_NAMES = new Set([
|
|
7
|
+
"assistant-stream",
|
|
8
|
+
"assistant-cloud",
|
|
9
|
+
"assistant-ui"
|
|
10
|
+
]);
|
|
11
|
+
function isTrackedPackage(name) {
|
|
12
|
+
if (!name) return false;
|
|
13
|
+
if (name.startsWith("@assistant-ui/")) return true;
|
|
14
|
+
return ASSISTANT_UI_PACKAGE_NAMES.has(name);
|
|
15
|
+
}
|
|
16
|
+
function readJson(file) {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(fs$1.readFileSync(file, "utf8"));
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function processPackageDir(pkgDir, results, visited) {
|
|
24
|
+
const real = (() => {
|
|
25
|
+
try {
|
|
26
|
+
return fs$1.realpathSync(pkgDir);
|
|
27
|
+
} catch {
|
|
28
|
+
return pkgDir;
|
|
29
|
+
}
|
|
30
|
+
})();
|
|
31
|
+
if (visited.set.has(real)) return;
|
|
32
|
+
visited.set.add(real);
|
|
33
|
+
const pkgJson = readJson(path$1.join(pkgDir, "package.json"));
|
|
34
|
+
let isTracked = false;
|
|
35
|
+
if (pkgJson) {
|
|
36
|
+
const name = pkgJson.name;
|
|
37
|
+
const version = pkgJson.version;
|
|
38
|
+
if (name && version && isTrackedPackage(name)) {
|
|
39
|
+
results.push({
|
|
40
|
+
name,
|
|
41
|
+
version,
|
|
42
|
+
installPath: pkgDir
|
|
43
|
+
});
|
|
44
|
+
isTracked = true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (isTracked) walkNodeModulesAt(pkgDir, results, visited);
|
|
48
|
+
}
|
|
49
|
+
function walkNodeModulesAt(baseDir, results, visited) {
|
|
50
|
+
const nm = path$1.join(baseDir, "node_modules");
|
|
51
|
+
if (!fs$1.existsSync(nm)) return;
|
|
52
|
+
let entries;
|
|
53
|
+
try {
|
|
54
|
+
entries = fs$1.readdirSync(nm, { withFileTypes: true });
|
|
55
|
+
} catch {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
if (entry.name.startsWith(".")) continue;
|
|
60
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
61
|
+
if (entry.name.startsWith("@")) {
|
|
62
|
+
const scopeDir = path$1.join(nm, entry.name);
|
|
63
|
+
let scoped;
|
|
64
|
+
try {
|
|
65
|
+
scoped = fs$1.readdirSync(scopeDir, { withFileTypes: true });
|
|
66
|
+
} catch {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
for (const s of scoped) {
|
|
70
|
+
if (!s.isDirectory() && !s.isSymbolicLink()) continue;
|
|
71
|
+
processPackageDir(path$1.join(scopeDir, s.name), results, visited);
|
|
72
|
+
}
|
|
73
|
+
} else processPackageDir(path$1.join(nm, entry.name), results, visited);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function discoverInstalledPackages(cwd) {
|
|
77
|
+
const results = [];
|
|
78
|
+
walkNodeModulesAt(cwd, results, { set: /* @__PURE__ */ new Set() });
|
|
79
|
+
return results;
|
|
80
|
+
}
|
|
81
|
+
function findDuplicates(packages) {
|
|
82
|
+
const byName = /* @__PURE__ */ new Map();
|
|
83
|
+
for (const pkg of packages) {
|
|
84
|
+
const list = byName.get(pkg.name) ?? [];
|
|
85
|
+
list.push(pkg);
|
|
86
|
+
byName.set(pkg.name, list);
|
|
87
|
+
}
|
|
88
|
+
const duplicates = [];
|
|
89
|
+
for (const [name, installations] of byName) if (new Set(installations.map((i) => i.version)).size > 1) duplicates.push({
|
|
90
|
+
name,
|
|
91
|
+
installations
|
|
92
|
+
});
|
|
93
|
+
duplicates.sort((a, b) => a.name.localeCompare(b.name));
|
|
94
|
+
return duplicates;
|
|
95
|
+
}
|
|
96
|
+
function uniquePackageNames(packages) {
|
|
97
|
+
return Array.from(new Set(packages.map((p) => p.name))).sort();
|
|
98
|
+
}
|
|
99
|
+
const VALID_NPM_NAME = /^(@[a-z0-9._~-]+\/)?[a-z0-9._~-]+$/;
|
|
100
|
+
async function fetchLatestVersion(name) {
|
|
101
|
+
if (!VALID_NPM_NAME.test(name)) return null;
|
|
102
|
+
try {
|
|
103
|
+
const res = await fetch(`https://registry.npmjs.org/${name}/latest`, { headers: { Accept: "application/json" } });
|
|
104
|
+
if (!res.ok) return null;
|
|
105
|
+
return (await res.json()).version ?? null;
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function fetchAllLatestVersions(names) {
|
|
111
|
+
const entries = await Promise.all(names.map(async (n) => [n, await fetchLatestVersion(n)]));
|
|
112
|
+
return new Map(entries);
|
|
113
|
+
}
|
|
114
|
+
function parseSemver(v) {
|
|
115
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(?:-([^+\s]+))?/.exec(v);
|
|
116
|
+
if (!m) return null;
|
|
117
|
+
return {
|
|
118
|
+
major: parseInt(m[1], 10),
|
|
119
|
+
minor: parseInt(m[2], 10),
|
|
120
|
+
patch: parseInt(m[3], 10),
|
|
121
|
+
prerelease: m[4] ?? ""
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function compareSemver(a, b) {
|
|
125
|
+
const pa = parseSemver(a);
|
|
126
|
+
const pb = parseSemver(b);
|
|
127
|
+
if (!pa || !pb) return a.localeCompare(b);
|
|
128
|
+
if (pa.major !== pb.major) return pa.major - pb.major;
|
|
129
|
+
if (pa.minor !== pb.minor) return pa.minor - pb.minor;
|
|
130
|
+
if (pa.patch !== pb.patch) return pa.patch - pb.patch;
|
|
131
|
+
if (pa.prerelease === pb.prerelease) return 0;
|
|
132
|
+
if (!pa.prerelease) return 1;
|
|
133
|
+
if (!pb.prerelease) return -1;
|
|
134
|
+
return pa.prerelease.localeCompare(pb.prerelease);
|
|
135
|
+
}
|
|
136
|
+
function findOutdated(packages, latest) {
|
|
137
|
+
const newestByName = /* @__PURE__ */ new Map();
|
|
138
|
+
for (const pkg of packages) {
|
|
139
|
+
const existing = newestByName.get(pkg.name);
|
|
140
|
+
if (!existing || compareSemver(pkg.version, existing) > 0) newestByName.set(pkg.name, pkg.version);
|
|
141
|
+
}
|
|
142
|
+
const result = [];
|
|
143
|
+
for (const [name, current] of newestByName) {
|
|
144
|
+
const latestVersion = latest.get(name);
|
|
145
|
+
if (!latestVersion) continue;
|
|
146
|
+
if (compareSemver(current, latestVersion) < 0) result.push({
|
|
147
|
+
name,
|
|
148
|
+
current,
|
|
149
|
+
latest: latestVersion
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
result.sort((a, b) => a.name.localeCompare(b.name));
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
function relativeInstallPath(installPath, cwd) {
|
|
156
|
+
const rel = path$1.relative(cwd, installPath);
|
|
157
|
+
return rel.startsWith("..") ? installPath : rel;
|
|
158
|
+
}
|
|
159
|
+
function reportDuplicates(duplicates, cwd, lines) {
|
|
160
|
+
if (duplicates.length === 0) {
|
|
161
|
+
lines.push(chalk.green("✓ No duplicate versions detected."));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
lines.push(chalk.red.bold("✗ Duplicate versions detected:"));
|
|
165
|
+
for (const dup of duplicates) {
|
|
166
|
+
const versions = Array.from(new Set(dup.installations.map((i) => i.version))).sort(compareSemver).join(", ");
|
|
167
|
+
lines.push(chalk.red(` ${dup.name} → ${versions}`));
|
|
168
|
+
for (const inst of dup.installations) lines.push(chalk.dim(` ${inst.version} ${relativeInstallPath(inst.installPath, cwd)}`));
|
|
169
|
+
}
|
|
170
|
+
lines.push("");
|
|
171
|
+
lines.push(chalk.yellow("Duplicates almost always cause subtle runtime bugs (see https://github.com/assistant-ui/assistant-ui/issues/4101)."));
|
|
172
|
+
lines.push(chalk.yellow("Fix by aligning all @assistant-ui/* packages to compatible versions — run:"));
|
|
173
|
+
lines.push(chalk.cyan(" npx assistant-ui update"));
|
|
174
|
+
}
|
|
175
|
+
function reportOutdated(outdated, lines) {
|
|
176
|
+
if (outdated.length === 0) {
|
|
177
|
+
lines.push(chalk.green("✓ All assistant-ui packages are up to date."));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
lines.push(chalk.yellow.bold("! Outdated packages:"));
|
|
181
|
+
const maxLen = Math.max(...outdated.map((o) => o.name.length));
|
|
182
|
+
for (const o of outdated) lines.push(chalk.yellow(` ${o.name.padEnd(maxLen)} ${o.current} → ${o.latest} (latest)`));
|
|
183
|
+
lines.push("");
|
|
184
|
+
lines.push(chalk.yellow("Run the following to upgrade everything:"));
|
|
185
|
+
lines.push(chalk.cyan(" npx assistant-ui update"));
|
|
186
|
+
}
|
|
187
|
+
const doctor = new Command().name("doctor").description("Diagnose mismatched or outdated assistant-ui packages (including transitive ones).").option("-c, --cwd <cwd>", "the working directory. defaults to the current directory.", process.cwd()).option("--no-network", "Skip the npm registry check for latest versions.").action(async (opts) => {
|
|
188
|
+
const cwd = path$1.resolve(opts.cwd);
|
|
189
|
+
const packageJsonPath = path$1.join(cwd, "package.json");
|
|
190
|
+
if (!fs$1.existsSync(packageJsonPath)) {
|
|
191
|
+
console.error(chalk.red("No package.json found in the current directory."));
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
console.log("");
|
|
195
|
+
console.log(chalk.bold("Running assistant-ui doctor..."));
|
|
196
|
+
console.log("");
|
|
197
|
+
const installed = discoverInstalledPackages(cwd);
|
|
198
|
+
if (installed.length === 0) {
|
|
199
|
+
console.log(chalk.yellow("No assistant-ui packages found in node_modules. Did you run `npm install`?"));
|
|
200
|
+
console.log("");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const duplicates = findDuplicates(installed);
|
|
204
|
+
let latest = /* @__PURE__ */ new Map();
|
|
205
|
+
if (opts.network) latest = await fetchAllLatestVersions(uniquePackageNames(installed));
|
|
206
|
+
const outdated = findOutdated(installed, latest);
|
|
207
|
+
const lines = [];
|
|
208
|
+
reportDuplicates(duplicates, cwd, lines);
|
|
209
|
+
lines.push("");
|
|
210
|
+
if (opts.network) reportOutdated(outdated, lines);
|
|
211
|
+
else lines.push(chalk.dim("Skipped npm registry check (--no-network)."));
|
|
212
|
+
for (const line of lines) console.log(line);
|
|
213
|
+
console.log("");
|
|
214
|
+
if (duplicates.length > 0) process.exitCode = 1;
|
|
215
|
+
});
|
|
216
|
+
//#endregion
|
|
217
|
+
export { compareSemver, discoverInstalledPackages, doctor, findDuplicates, findOutdated, uniquePackageNames };
|
|
218
|
+
|
|
219
|
+
//# sourceMappingURL=doctor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor.js","names":["fs","path"],"sources":["../../src/commands/doctor.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport chalk from \"chalk\";\n\nconst ASSISTANT_UI_PACKAGE_NAMES = new Set([\n \"assistant-stream\",\n \"assistant-cloud\",\n \"assistant-ui\",\n]);\n\nfunction isTrackedPackage(name: string | undefined): boolean {\n if (!name) return false;\n if (name.startsWith(\"@assistant-ui/\")) return true;\n return ASSISTANT_UI_PACKAGE_NAMES.has(name);\n}\n\nexport interface DiscoveredPackage {\n name: string;\n version: string;\n installPath: string;\n}\n\ninterface ProcessedDir {\n set: Set<string>;\n}\n\nfunction readJson(file: string): Record<string, unknown> | null {\n try {\n return JSON.parse(fs.readFileSync(file, \"utf8\"));\n } catch {\n return null;\n }\n}\n\nfunction processPackageDir(\n pkgDir: string,\n results: DiscoveredPackage[],\n visited: ProcessedDir,\n): void {\n const real = (() => {\n try {\n return fs.realpathSync(pkgDir);\n } catch {\n return pkgDir;\n }\n })();\n if (visited.set.has(real)) return;\n visited.set.add(real);\n\n const pkgJson = readJson(path.join(pkgDir, \"package.json\"));\n let isTracked = false;\n if (pkgJson) {\n const name = pkgJson.name as string | undefined;\n const version = pkgJson.version as string | undefined;\n if (name && version && isTrackedPackage(name)) {\n results.push({ name, version, installPath: pkgDir });\n isTracked = true;\n }\n }\n\n // Only descend into nested node_modules of tracked packages. Transitive\n // copies of @assistant-ui/* live inside packages that depend on them,\n // which are themselves tracked. Walking every unrelated package's\n // subtree turns a doctor run on a large repo into thousands of stat\n // calls for no gain.\n if (isTracked) {\n walkNodeModulesAt(pkgDir, results, visited);\n }\n}\n\nfunction walkNodeModulesAt(\n baseDir: string,\n results: DiscoveredPackage[],\n visited: ProcessedDir,\n): void {\n const nm = path.join(baseDir, \"node_modules\");\n if (!fs.existsSync(nm)) return;\n\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(nm, { withFileTypes: true });\n } catch {\n return;\n }\n\n for (const entry of entries) {\n if (entry.name.startsWith(\".\")) continue;\n if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;\n\n if (entry.name.startsWith(\"@\")) {\n const scopeDir = path.join(nm, entry.name);\n let scoped: fs.Dirent[];\n try {\n scoped = fs.readdirSync(scopeDir, { withFileTypes: true });\n } catch {\n continue;\n }\n for (const s of scoped) {\n if (!s.isDirectory() && !s.isSymbolicLink()) continue;\n processPackageDir(path.join(scopeDir, s.name), results, visited);\n }\n } else {\n processPackageDir(path.join(nm, entry.name), results, visited);\n }\n }\n}\n\n// Discover every installation of an assistant-ui-family package reachable\n// from `cwd`. Recurses into nested node_modules so transitive copies\n// (the real source of duplicate-version bugs) are not missed.\nexport function discoverInstalledPackages(cwd: string): DiscoveredPackage[] {\n const results: DiscoveredPackage[] = [];\n const visited: ProcessedDir = { set: new Set() };\n walkNodeModulesAt(cwd, results, visited);\n return results;\n}\n\nexport interface DuplicateGroup {\n name: string;\n installations: DiscoveredPackage[];\n}\n\nexport function findDuplicates(\n packages: DiscoveredPackage[],\n): DuplicateGroup[] {\n const byName = new Map<string, DiscoveredPackage[]>();\n for (const pkg of packages) {\n const list = byName.get(pkg.name) ?? [];\n list.push(pkg);\n byName.set(pkg.name, list);\n }\n\n const duplicates: DuplicateGroup[] = [];\n for (const [name, installations] of byName) {\n const versions = new Set(installations.map((i) => i.version));\n if (versions.size > 1) {\n duplicates.push({ name, installations });\n }\n }\n duplicates.sort((a, b) => a.name.localeCompare(b.name));\n return duplicates;\n}\n\nexport function uniquePackageNames(packages: DiscoveredPackage[]): string[] {\n return Array.from(new Set(packages.map((p) => p.name))).sort();\n}\n\n// Package names use a restricted character set (`[a-z0-9._~-]` plus a\n// leading `@scope/` for scoped packages — see the npm package-name spec)\n// and the npm registry expects the scope's `@` and `/` un-encoded. So a\n// simple validation + concatenation is both correct and avoids the\n// CodeQL \"incomplete string escaping\" foot-gun of `encodeURIComponent`\n// + targeted un-escape.\nconst VALID_NPM_NAME = /^(@[a-z0-9._~-]+\\/)?[a-z0-9._~-]+$/;\n\nasync function fetchLatestVersion(name: string): Promise<string | null> {\n if (!VALID_NPM_NAME.test(name)) return null;\n try {\n const res = await fetch(`https://registry.npmjs.org/${name}/latest`, {\n headers: { Accept: \"application/json\" },\n });\n if (!res.ok) return null;\n const data = (await res.json()) as { version?: string };\n return data.version ?? null;\n } catch {\n return null;\n }\n}\n\nasync function fetchAllLatestVersions(\n names: string[],\n): Promise<Map<string, string | null>> {\n const entries = await Promise.all(\n names.map(async (n) => [n, await fetchLatestVersion(n)] as const),\n );\n return new Map(entries);\n}\n\ninterface SemverParts {\n major: number;\n minor: number;\n patch: number;\n prerelease: string;\n}\n\nfunction parseSemver(v: string): SemverParts | null {\n const m = /^(\\d+)\\.(\\d+)\\.(\\d+)(?:-([^+\\s]+))?/.exec(v);\n if (!m) return null;\n return {\n major: parseInt(m[1]!, 10),\n minor: parseInt(m[2]!, 10),\n patch: parseInt(m[3]!, 10),\n prerelease: m[4] ?? \"\",\n };\n}\n\nexport function compareSemver(a: string, b: string): number {\n const pa = parseSemver(a);\n const pb = parseSemver(b);\n if (!pa || !pb) return a.localeCompare(b);\n if (pa.major !== pb.major) return pa.major - pb.major;\n if (pa.minor !== pb.minor) return pa.minor - pb.minor;\n if (pa.patch !== pb.patch) return pa.patch - pb.patch;\n\n // Per SemVer §11: a version with a prerelease tag is *less than* the\n // same x.y.z without one. We compare tags lexically for a stable\n // ordering across prereleases — good enough for doctor's \"is X older\n // than the npm latest\" check.\n if (pa.prerelease === pb.prerelease) return 0;\n if (!pa.prerelease) return 1;\n if (!pb.prerelease) return -1;\n return pa.prerelease.localeCompare(pb.prerelease);\n}\n\nexport interface OutdatedPackage {\n name: string;\n current: string;\n latest: string;\n}\n\nexport function findOutdated(\n packages: DiscoveredPackage[],\n latest: Map<string, string | null>,\n): OutdatedPackage[] {\n const newestByName = new Map<string, string>();\n for (const pkg of packages) {\n const existing = newestByName.get(pkg.name);\n if (!existing || compareSemver(pkg.version, existing) > 0) {\n newestByName.set(pkg.name, pkg.version);\n }\n }\n\n const result: OutdatedPackage[] = [];\n for (const [name, current] of newestByName) {\n const latestVersion = latest.get(name);\n if (!latestVersion) continue;\n if (compareSemver(current, latestVersion) < 0) {\n result.push({ name, current, latest: latestVersion });\n }\n }\n result.sort((a, b) => a.name.localeCompare(b.name));\n return result;\n}\n\nfunction relativeInstallPath(installPath: string, cwd: string): string {\n const rel = path.relative(cwd, installPath);\n return rel.startsWith(\"..\") ? installPath : rel;\n}\n\nfunction reportDuplicates(\n duplicates: DuplicateGroup[],\n cwd: string,\n lines: string[],\n): void {\n if (duplicates.length === 0) {\n lines.push(chalk.green(\"✓ No duplicate versions detected.\"));\n return;\n }\n\n lines.push(chalk.red.bold(\"✗ Duplicate versions detected:\"));\n for (const dup of duplicates) {\n const versions = Array.from(\n new Set(dup.installations.map((i) => i.version)),\n )\n .sort(compareSemver)\n .join(\", \");\n lines.push(chalk.red(` ${dup.name} → ${versions}`));\n for (const inst of dup.installations) {\n lines.push(\n chalk.dim(\n ` ${inst.version} ${relativeInstallPath(inst.installPath, cwd)}`,\n ),\n );\n }\n }\n lines.push(\"\");\n lines.push(\n chalk.yellow(\n \"Duplicates almost always cause subtle runtime bugs (see https://github.com/assistant-ui/assistant-ui/issues/4101).\",\n ),\n );\n lines.push(\n chalk.yellow(\n \"Fix by aligning all @assistant-ui/* packages to compatible versions — run:\",\n ),\n );\n lines.push(chalk.cyan(\" npx assistant-ui update\"));\n}\n\nfunction reportOutdated(outdated: OutdatedPackage[], lines: string[]): void {\n if (outdated.length === 0) {\n lines.push(chalk.green(\"✓ All assistant-ui packages are up to date.\"));\n return;\n }\n\n lines.push(chalk.yellow.bold(\"! Outdated packages:\"));\n const maxLen = Math.max(...outdated.map((o) => o.name.length));\n for (const o of outdated) {\n lines.push(\n chalk.yellow(\n ` ${o.name.padEnd(maxLen)} ${o.current} → ${o.latest} (latest)`,\n ),\n );\n }\n lines.push(\"\");\n lines.push(chalk.yellow(\"Run the following to upgrade everything:\"));\n lines.push(chalk.cyan(\" npx assistant-ui update\"));\n}\n\nexport const doctor = new Command()\n .name(\"doctor\")\n .description(\n \"Diagnose mismatched or outdated assistant-ui packages (including transitive ones).\",\n )\n .option(\n \"-c, --cwd <cwd>\",\n \"the working directory. defaults to the current directory.\",\n process.cwd(),\n )\n .option(\"--no-network\", \"Skip the npm registry check for latest versions.\")\n .action(async (opts: { cwd: string; network: boolean }) => {\n const cwd = path.resolve(opts.cwd);\n const packageJsonPath = path.join(cwd, \"package.json\");\n\n if (!fs.existsSync(packageJsonPath)) {\n console.error(\n chalk.red(\"No package.json found in the current directory.\"),\n );\n process.exit(1);\n }\n\n console.log(\"\");\n console.log(chalk.bold(\"Running assistant-ui doctor...\"));\n console.log(\"\");\n\n const installed = discoverInstalledPackages(cwd);\n\n if (installed.length === 0) {\n console.log(\n chalk.yellow(\n \"No assistant-ui packages found in node_modules. Did you run `npm install`?\",\n ),\n );\n console.log(\"\");\n return;\n }\n\n const duplicates = findDuplicates(installed);\n\n let latest = new Map<string, string | null>();\n if (opts.network) {\n latest = await fetchAllLatestVersions(uniquePackageNames(installed));\n }\n const outdated = findOutdated(installed, latest);\n\n const lines: string[] = [];\n reportDuplicates(duplicates, cwd, lines);\n lines.push(\"\");\n if (opts.network) {\n reportOutdated(outdated, lines);\n } else {\n lines.push(chalk.dim(\"Skipped npm registry check (--no-network).\"));\n }\n\n for (const line of lines) console.log(line);\n console.log(\"\");\n\n if (duplicates.length > 0) {\n process.exitCode = 1;\n }\n });\n"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import { update } from "./commands/update.js";
|
|
|
7
7
|
import { mcp } from "./commands/mcp.js";
|
|
8
8
|
import { agent } from "./commands/agent.js";
|
|
9
9
|
import { info } from "./commands/info.js";
|
|
10
|
+
import { doctor } from "./commands/doctor.js";
|
|
10
11
|
import { Command } from "commander";
|
|
11
12
|
//#region src/index.ts
|
|
12
13
|
process.on("SIGINT", () => process.exit(0));
|
|
@@ -22,6 +23,7 @@ function main() {
|
|
|
22
23
|
program.addCommand(update);
|
|
23
24
|
program.addCommand(agent);
|
|
24
25
|
program.addCommand(info);
|
|
26
|
+
program.addCommand(doctor);
|
|
25
27
|
program.parse();
|
|
26
28
|
}
|
|
27
29
|
main();
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { Command } from \"commander\";\nimport { create } from \"./commands/create\";\nimport { add } from \"./commands/add\";\nimport { codemodCommand, upgradeCommand } from \"./commands/upgrade\";\nimport { init } from \"./commands/init\";\nimport { update } from \"./commands/update\";\nimport { mcp } from \"./commands/mcp\";\nimport { agent } from \"./commands/agent\";\nimport { info } from \"./commands/info\";\n\nprocess.on(\"SIGINT\", () => process.exit(0));\nprocess.on(\"SIGTERM\", () => process.exit(0));\n\nfunction main() {\n const program = new Command()\n .name(\"assistant-ui\")\n .description(\"add components and dependencies to your project\");\n\n program.addCommand(add);\n program.addCommand(create);\n program.addCommand(init);\n program.addCommand(mcp);\n program.addCommand(codemodCommand);\n program.addCommand(upgradeCommand);\n program.addCommand(update);\n program.addCommand(agent);\n program.addCommand(info);\n\n program.parse();\n}\n\nmain();\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { Command } from \"commander\";\nimport { create } from \"./commands/create\";\nimport { add } from \"./commands/add\";\nimport { codemodCommand, upgradeCommand } from \"./commands/upgrade\";\nimport { init } from \"./commands/init\";\nimport { update } from \"./commands/update\";\nimport { mcp } from \"./commands/mcp\";\nimport { agent } from \"./commands/agent\";\nimport { info } from \"./commands/info\";\nimport { doctor } from \"./commands/doctor\";\n\nprocess.on(\"SIGINT\", () => process.exit(0));\nprocess.on(\"SIGTERM\", () => process.exit(0));\n\nfunction main() {\n const program = new Command()\n .name(\"assistant-ui\")\n .description(\"add components and dependencies to your project\");\n\n program.addCommand(add);\n program.addCommand(create);\n program.addCommand(init);\n program.addCommand(mcp);\n program.addCommand(codemodCommand);\n program.addCommand(upgradeCommand);\n program.addCommand(update);\n program.addCommand(agent);\n program.addCommand(info);\n program.addCommand(doctor);\n\n program.parse();\n}\n\nmain();\n"],"mappings":";;;;;;;;;;;;AAaA,QAAQ,GAAG,gBAAgB,QAAQ,KAAK,CAAC,CAAC;AAC1C,QAAQ,GAAG,iBAAiB,QAAQ,KAAK,CAAC,CAAC;AAE3C,SAAS,OAAO;CACd,MAAM,UAAU,IAAI,QAAQ,EACzB,KAAK,cAAc,EACnB,YAAY,iDAAiD;CAEhE,QAAQ,WAAW,GAAG;CACtB,QAAQ,WAAW,MAAM;CACzB,QAAQ,WAAW,IAAI;CACvB,QAAQ,WAAW,GAAG;CACtB,QAAQ,WAAW,cAAc;CACjC,QAAQ,WAAW,cAAc;CACjC,QAAQ,WAAW,MAAM;CACzB,QAAQ,WAAW,KAAK;CACxB,QAAQ,WAAW,IAAI;CACvB,QAAQ,WAAW,MAAM;CAEzB,QAAQ,MAAM;AAChB;AAEA,KAAK"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
|
|
6
|
+
const ASSISTANT_UI_PACKAGE_NAMES = new Set([
|
|
7
|
+
"assistant-stream",
|
|
8
|
+
"assistant-cloud",
|
|
9
|
+
"assistant-ui",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
function isTrackedPackage(name: string | undefined): boolean {
|
|
13
|
+
if (!name) return false;
|
|
14
|
+
if (name.startsWith("@assistant-ui/")) return true;
|
|
15
|
+
return ASSISTANT_UI_PACKAGE_NAMES.has(name);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DiscoveredPackage {
|
|
19
|
+
name: string;
|
|
20
|
+
version: string;
|
|
21
|
+
installPath: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ProcessedDir {
|
|
25
|
+
set: Set<string>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readJson(file: string): Record<string, unknown> | null {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function processPackageDir(
|
|
37
|
+
pkgDir: string,
|
|
38
|
+
results: DiscoveredPackage[],
|
|
39
|
+
visited: ProcessedDir,
|
|
40
|
+
): void {
|
|
41
|
+
const real = (() => {
|
|
42
|
+
try {
|
|
43
|
+
return fs.realpathSync(pkgDir);
|
|
44
|
+
} catch {
|
|
45
|
+
return pkgDir;
|
|
46
|
+
}
|
|
47
|
+
})();
|
|
48
|
+
if (visited.set.has(real)) return;
|
|
49
|
+
visited.set.add(real);
|
|
50
|
+
|
|
51
|
+
const pkgJson = readJson(path.join(pkgDir, "package.json"));
|
|
52
|
+
let isTracked = false;
|
|
53
|
+
if (pkgJson) {
|
|
54
|
+
const name = pkgJson.name as string | undefined;
|
|
55
|
+
const version = pkgJson.version as string | undefined;
|
|
56
|
+
if (name && version && isTrackedPackage(name)) {
|
|
57
|
+
results.push({ name, version, installPath: pkgDir });
|
|
58
|
+
isTracked = true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Only descend into nested node_modules of tracked packages. Transitive
|
|
63
|
+
// copies of @assistant-ui/* live inside packages that depend on them,
|
|
64
|
+
// which are themselves tracked. Walking every unrelated package's
|
|
65
|
+
// subtree turns a doctor run on a large repo into thousands of stat
|
|
66
|
+
// calls for no gain.
|
|
67
|
+
if (isTracked) {
|
|
68
|
+
walkNodeModulesAt(pkgDir, results, visited);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function walkNodeModulesAt(
|
|
73
|
+
baseDir: string,
|
|
74
|
+
results: DiscoveredPackage[],
|
|
75
|
+
visited: ProcessedDir,
|
|
76
|
+
): void {
|
|
77
|
+
const nm = path.join(baseDir, "node_modules");
|
|
78
|
+
if (!fs.existsSync(nm)) return;
|
|
79
|
+
|
|
80
|
+
let entries: fs.Dirent[];
|
|
81
|
+
try {
|
|
82
|
+
entries = fs.readdirSync(nm, { withFileTypes: true });
|
|
83
|
+
} catch {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
if (entry.name.startsWith(".")) continue;
|
|
89
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
90
|
+
|
|
91
|
+
if (entry.name.startsWith("@")) {
|
|
92
|
+
const scopeDir = path.join(nm, entry.name);
|
|
93
|
+
let scoped: fs.Dirent[];
|
|
94
|
+
try {
|
|
95
|
+
scoped = fs.readdirSync(scopeDir, { withFileTypes: true });
|
|
96
|
+
} catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
for (const s of scoped) {
|
|
100
|
+
if (!s.isDirectory() && !s.isSymbolicLink()) continue;
|
|
101
|
+
processPackageDir(path.join(scopeDir, s.name), results, visited);
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
processPackageDir(path.join(nm, entry.name), results, visited);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Discover every installation of an assistant-ui-family package reachable
|
|
110
|
+
// from `cwd`. Recurses into nested node_modules so transitive copies
|
|
111
|
+
// (the real source of duplicate-version bugs) are not missed.
|
|
112
|
+
export function discoverInstalledPackages(cwd: string): DiscoveredPackage[] {
|
|
113
|
+
const results: DiscoveredPackage[] = [];
|
|
114
|
+
const visited: ProcessedDir = { set: new Set() };
|
|
115
|
+
walkNodeModulesAt(cwd, results, visited);
|
|
116
|
+
return results;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface DuplicateGroup {
|
|
120
|
+
name: string;
|
|
121
|
+
installations: DiscoveredPackage[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function findDuplicates(
|
|
125
|
+
packages: DiscoveredPackage[],
|
|
126
|
+
): DuplicateGroup[] {
|
|
127
|
+
const byName = new Map<string, DiscoveredPackage[]>();
|
|
128
|
+
for (const pkg of packages) {
|
|
129
|
+
const list = byName.get(pkg.name) ?? [];
|
|
130
|
+
list.push(pkg);
|
|
131
|
+
byName.set(pkg.name, list);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const duplicates: DuplicateGroup[] = [];
|
|
135
|
+
for (const [name, installations] of byName) {
|
|
136
|
+
const versions = new Set(installations.map((i) => i.version));
|
|
137
|
+
if (versions.size > 1) {
|
|
138
|
+
duplicates.push({ name, installations });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
duplicates.sort((a, b) => a.name.localeCompare(b.name));
|
|
142
|
+
return duplicates;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function uniquePackageNames(packages: DiscoveredPackage[]): string[] {
|
|
146
|
+
return Array.from(new Set(packages.map((p) => p.name))).sort();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Package names use a restricted character set (`[a-z0-9._~-]` plus a
|
|
150
|
+
// leading `@scope/` for scoped packages — see the npm package-name spec)
|
|
151
|
+
// and the npm registry expects the scope's `@` and `/` un-encoded. So a
|
|
152
|
+
// simple validation + concatenation is both correct and avoids the
|
|
153
|
+
// CodeQL "incomplete string escaping" foot-gun of `encodeURIComponent`
|
|
154
|
+
// + targeted un-escape.
|
|
155
|
+
const VALID_NPM_NAME = /^(@[a-z0-9._~-]+\/)?[a-z0-9._~-]+$/;
|
|
156
|
+
|
|
157
|
+
async function fetchLatestVersion(name: string): Promise<string | null> {
|
|
158
|
+
if (!VALID_NPM_NAME.test(name)) return null;
|
|
159
|
+
try {
|
|
160
|
+
const res = await fetch(`https://registry.npmjs.org/${name}/latest`, {
|
|
161
|
+
headers: { Accept: "application/json" },
|
|
162
|
+
});
|
|
163
|
+
if (!res.ok) return null;
|
|
164
|
+
const data = (await res.json()) as { version?: string };
|
|
165
|
+
return data.version ?? null;
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function fetchAllLatestVersions(
|
|
172
|
+
names: string[],
|
|
173
|
+
): Promise<Map<string, string | null>> {
|
|
174
|
+
const entries = await Promise.all(
|
|
175
|
+
names.map(async (n) => [n, await fetchLatestVersion(n)] as const),
|
|
176
|
+
);
|
|
177
|
+
return new Map(entries);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
interface SemverParts {
|
|
181
|
+
major: number;
|
|
182
|
+
minor: number;
|
|
183
|
+
patch: number;
|
|
184
|
+
prerelease: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function parseSemver(v: string): SemverParts | null {
|
|
188
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(?:-([^+\s]+))?/.exec(v);
|
|
189
|
+
if (!m) return null;
|
|
190
|
+
return {
|
|
191
|
+
major: parseInt(m[1]!, 10),
|
|
192
|
+
minor: parseInt(m[2]!, 10),
|
|
193
|
+
patch: parseInt(m[3]!, 10),
|
|
194
|
+
prerelease: m[4] ?? "",
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function compareSemver(a: string, b: string): number {
|
|
199
|
+
const pa = parseSemver(a);
|
|
200
|
+
const pb = parseSemver(b);
|
|
201
|
+
if (!pa || !pb) return a.localeCompare(b);
|
|
202
|
+
if (pa.major !== pb.major) return pa.major - pb.major;
|
|
203
|
+
if (pa.minor !== pb.minor) return pa.minor - pb.minor;
|
|
204
|
+
if (pa.patch !== pb.patch) return pa.patch - pb.patch;
|
|
205
|
+
|
|
206
|
+
// Per SemVer §11: a version with a prerelease tag is *less than* the
|
|
207
|
+
// same x.y.z without one. We compare tags lexically for a stable
|
|
208
|
+
// ordering across prereleases — good enough for doctor's "is X older
|
|
209
|
+
// than the npm latest" check.
|
|
210
|
+
if (pa.prerelease === pb.prerelease) return 0;
|
|
211
|
+
if (!pa.prerelease) return 1;
|
|
212
|
+
if (!pb.prerelease) return -1;
|
|
213
|
+
return pa.prerelease.localeCompare(pb.prerelease);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface OutdatedPackage {
|
|
217
|
+
name: string;
|
|
218
|
+
current: string;
|
|
219
|
+
latest: string;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function findOutdated(
|
|
223
|
+
packages: DiscoveredPackage[],
|
|
224
|
+
latest: Map<string, string | null>,
|
|
225
|
+
): OutdatedPackage[] {
|
|
226
|
+
const newestByName = new Map<string, string>();
|
|
227
|
+
for (const pkg of packages) {
|
|
228
|
+
const existing = newestByName.get(pkg.name);
|
|
229
|
+
if (!existing || compareSemver(pkg.version, existing) > 0) {
|
|
230
|
+
newestByName.set(pkg.name, pkg.version);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const result: OutdatedPackage[] = [];
|
|
235
|
+
for (const [name, current] of newestByName) {
|
|
236
|
+
const latestVersion = latest.get(name);
|
|
237
|
+
if (!latestVersion) continue;
|
|
238
|
+
if (compareSemver(current, latestVersion) < 0) {
|
|
239
|
+
result.push({ name, current, latest: latestVersion });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
result.sort((a, b) => a.name.localeCompare(b.name));
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function relativeInstallPath(installPath: string, cwd: string): string {
|
|
247
|
+
const rel = path.relative(cwd, installPath);
|
|
248
|
+
return rel.startsWith("..") ? installPath : rel;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function reportDuplicates(
|
|
252
|
+
duplicates: DuplicateGroup[],
|
|
253
|
+
cwd: string,
|
|
254
|
+
lines: string[],
|
|
255
|
+
): void {
|
|
256
|
+
if (duplicates.length === 0) {
|
|
257
|
+
lines.push(chalk.green("✓ No duplicate versions detected."));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
lines.push(chalk.red.bold("✗ Duplicate versions detected:"));
|
|
262
|
+
for (const dup of duplicates) {
|
|
263
|
+
const versions = Array.from(
|
|
264
|
+
new Set(dup.installations.map((i) => i.version)),
|
|
265
|
+
)
|
|
266
|
+
.sort(compareSemver)
|
|
267
|
+
.join(", ");
|
|
268
|
+
lines.push(chalk.red(` ${dup.name} → ${versions}`));
|
|
269
|
+
for (const inst of dup.installations) {
|
|
270
|
+
lines.push(
|
|
271
|
+
chalk.dim(
|
|
272
|
+
` ${inst.version} ${relativeInstallPath(inst.installPath, cwd)}`,
|
|
273
|
+
),
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
lines.push("");
|
|
278
|
+
lines.push(
|
|
279
|
+
chalk.yellow(
|
|
280
|
+
"Duplicates almost always cause subtle runtime bugs (see https://github.com/assistant-ui/assistant-ui/issues/4101).",
|
|
281
|
+
),
|
|
282
|
+
);
|
|
283
|
+
lines.push(
|
|
284
|
+
chalk.yellow(
|
|
285
|
+
"Fix by aligning all @assistant-ui/* packages to compatible versions — run:",
|
|
286
|
+
),
|
|
287
|
+
);
|
|
288
|
+
lines.push(chalk.cyan(" npx assistant-ui update"));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function reportOutdated(outdated: OutdatedPackage[], lines: string[]): void {
|
|
292
|
+
if (outdated.length === 0) {
|
|
293
|
+
lines.push(chalk.green("✓ All assistant-ui packages are up to date."));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
lines.push(chalk.yellow.bold("! Outdated packages:"));
|
|
298
|
+
const maxLen = Math.max(...outdated.map((o) => o.name.length));
|
|
299
|
+
for (const o of outdated) {
|
|
300
|
+
lines.push(
|
|
301
|
+
chalk.yellow(
|
|
302
|
+
` ${o.name.padEnd(maxLen)} ${o.current} → ${o.latest} (latest)`,
|
|
303
|
+
),
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
lines.push("");
|
|
307
|
+
lines.push(chalk.yellow("Run the following to upgrade everything:"));
|
|
308
|
+
lines.push(chalk.cyan(" npx assistant-ui update"));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export const doctor = new Command()
|
|
312
|
+
.name("doctor")
|
|
313
|
+
.description(
|
|
314
|
+
"Diagnose mismatched or outdated assistant-ui packages (including transitive ones).",
|
|
315
|
+
)
|
|
316
|
+
.option(
|
|
317
|
+
"-c, --cwd <cwd>",
|
|
318
|
+
"the working directory. defaults to the current directory.",
|
|
319
|
+
process.cwd(),
|
|
320
|
+
)
|
|
321
|
+
.option("--no-network", "Skip the npm registry check for latest versions.")
|
|
322
|
+
.action(async (opts: { cwd: string; network: boolean }) => {
|
|
323
|
+
const cwd = path.resolve(opts.cwd);
|
|
324
|
+
const packageJsonPath = path.join(cwd, "package.json");
|
|
325
|
+
|
|
326
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
327
|
+
console.error(
|
|
328
|
+
chalk.red("No package.json found in the current directory."),
|
|
329
|
+
);
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
console.log("");
|
|
334
|
+
console.log(chalk.bold("Running assistant-ui doctor..."));
|
|
335
|
+
console.log("");
|
|
336
|
+
|
|
337
|
+
const installed = discoverInstalledPackages(cwd);
|
|
338
|
+
|
|
339
|
+
if (installed.length === 0) {
|
|
340
|
+
console.log(
|
|
341
|
+
chalk.yellow(
|
|
342
|
+
"No assistant-ui packages found in node_modules. Did you run `npm install`?",
|
|
343
|
+
),
|
|
344
|
+
);
|
|
345
|
+
console.log("");
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const duplicates = findDuplicates(installed);
|
|
350
|
+
|
|
351
|
+
let latest = new Map<string, string | null>();
|
|
352
|
+
if (opts.network) {
|
|
353
|
+
latest = await fetchAllLatestVersions(uniquePackageNames(installed));
|
|
354
|
+
}
|
|
355
|
+
const outdated = findOutdated(installed, latest);
|
|
356
|
+
|
|
357
|
+
const lines: string[] = [];
|
|
358
|
+
reportDuplicates(duplicates, cwd, lines);
|
|
359
|
+
lines.push("");
|
|
360
|
+
if (opts.network) {
|
|
361
|
+
reportOutdated(outdated, lines);
|
|
362
|
+
} else {
|
|
363
|
+
lines.push(chalk.dim("Skipped npm registry check (--no-network)."));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
for (const line of lines) console.log(line);
|
|
367
|
+
console.log("");
|
|
368
|
+
|
|
369
|
+
if (duplicates.length > 0) {
|
|
370
|
+
process.exitCode = 1;
|
|
371
|
+
}
|
|
372
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { update } from "./commands/update";
|
|
|
9
9
|
import { mcp } from "./commands/mcp";
|
|
10
10
|
import { agent } from "./commands/agent";
|
|
11
11
|
import { info } from "./commands/info";
|
|
12
|
+
import { doctor } from "./commands/doctor";
|
|
12
13
|
|
|
13
14
|
process.on("SIGINT", () => process.exit(0));
|
|
14
15
|
process.on("SIGTERM", () => process.exit(0));
|
|
@@ -27,6 +28,7 @@ function main() {
|
|
|
27
28
|
program.addCommand(update);
|
|
28
29
|
program.addCommand(agent);
|
|
29
30
|
program.addCommand(info);
|
|
31
|
+
program.addCommand(doctor);
|
|
30
32
|
|
|
31
33
|
program.parse();
|
|
32
34
|
}
|