@kitn.ai/cli 0.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.
@@ -0,0 +1,193 @@
1
+ import { readFileSync, statSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ const KIT = "@kitn.ai/ui";
4
+ const MCP = "@kitn.ai/mcp";
5
+ const KAI_JSON = "kai.json";
6
+ function readJson(file) {
7
+ try {
8
+ return JSON.parse(readFileSync(file, "utf8"));
9
+ } catch {
10
+ return void 0;
11
+ }
12
+ }
13
+ function compareVersions(a, b) {
14
+ const parts = (v) => v.split(/[.+-]/).map((p) => /^\d+$/.test(p) ? Number(p) : p);
15
+ const left = parts(a);
16
+ const right = parts(b);
17
+ for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
18
+ const l = left[i] ?? 0;
19
+ const r = right[i] ?? 0;
20
+ if (l === r) continue;
21
+ if (typeof l !== typeof r) return typeof l === "number" ? 1 : -1;
22
+ return l < r ? -1 : 1;
23
+ }
24
+ return 0;
25
+ }
26
+ function sourceFiles(dir, limit = 400) {
27
+ const out = [];
28
+ const skip = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".git", ".next", ".svelte-kit", "coverage", ".astro", ".output"]);
29
+ const walk = (current) => {
30
+ if (out.length >= limit) return;
31
+ let entries;
32
+ try {
33
+ entries = readdirSync(current, { withFileTypes: true });
34
+ } catch {
35
+ return;
36
+ }
37
+ for (const entry of entries) {
38
+ if (out.length >= limit) return;
39
+ if (entry.isDirectory()) {
40
+ if (!skip.has(entry.name)) walk(join(current, entry.name));
41
+ } else if (/\.(?:[cm]?[jt]sx?|mdx?|astro|svelte|vue|html|css)$/.test(entry.name)) {
42
+ out.push(join(current, entry.name));
43
+ }
44
+ }
45
+ };
46
+ if (statSync(dir, { throwIfNoEntry: false })?.isDirectory()) walk(dir);
47
+ return out;
48
+ }
49
+ const readAll = (files) => files.flatMap((f) => {
50
+ try {
51
+ return [readFileSync(f, "utf8")];
52
+ } catch {
53
+ return [];
54
+ }
55
+ });
56
+ function diagnose(input) {
57
+ const findings = [];
58
+ const manifest = readJson(join(input.cwd, "package.json"));
59
+ if (!manifest) {
60
+ return [
61
+ {
62
+ severity: "error",
63
+ title: `no package.json in ${input.cwd}`,
64
+ detail: "kai doctor diagnoses a project, so run it from the project root."
65
+ }
66
+ ];
67
+ }
68
+ findings.push({
69
+ severity: "ok",
70
+ title: `kai ${input.cliVersion}`,
71
+ detail: `this CLI was built against @kitn.ai/ui ${input.builtAgainstKit}`
72
+ });
73
+ const deps = manifest.dependencies ?? {};
74
+ const devDeps = manifest.devDependencies ?? {};
75
+ const declared = deps[KIT] ?? devDeps[KIT];
76
+ if (declared === void 0) {
77
+ findings.push({
78
+ severity: "warn",
79
+ title: `this project does not depend on ${KIT}`,
80
+ detail: "nothing in package.json declares it, so the components cannot be imported yet."
81
+ });
82
+ } else {
83
+ findings.push({ severity: "ok", title: `${KIT} is declared as ${declared}` });
84
+ }
85
+ const installed = readJson(join(input.cwd, "node_modules", KIT, "package.json"));
86
+ const installedVersion = typeof installed?.version === "string" ? installed.version : void 0;
87
+ if (declared !== void 0 && installedVersion === void 0) {
88
+ findings.push({
89
+ severity: "error",
90
+ title: `${KIT} is declared but not installed`,
91
+ detail: "run your package manager's install (npm install / pnpm install / yarn)."
92
+ });
93
+ } else if (installedVersion !== void 0) {
94
+ findings.push({ severity: "ok", title: `${KIT} ${installedVersion} is installed` });
95
+ const cmp = compareVersions(installedVersion, input.builtAgainstKit);
96
+ if (cmp < 0) {
97
+ findings.push({
98
+ severity: "warn",
99
+ title: `this project's kit (${installedVersion}) is older than the kit this CLI was built against (${input.builtAgainstKit})`,
100
+ detail: "upgrade the kit, or install a CLI that matches it."
101
+ });
102
+ } else if (cmp > 0) {
103
+ findings.push({
104
+ severity: "info",
105
+ title: `this project's kit (${installedVersion}) is newer than the one this CLI was built against (${input.builtAgainstKit})`,
106
+ detail: "the CLI's own commands may lag the API it is describing; a newer CLI would match."
107
+ });
108
+ }
109
+ }
110
+ const kaiJson = readJson(join(input.cwd, KAI_JSON));
111
+ if (kaiJson) {
112
+ const framework = kaiJson.framework ?? "?";
113
+ const built = kaiJson.kitBuiltAgainst ?? "?";
114
+ const features = Array.isArray(kaiJson.features) ? kaiJson.features.join(", ") : "?";
115
+ findings.push({ severity: "ok", title: `${KAI_JSON}: framework ${framework}, features ${features}`, detail: `scaffolded against kit ${built}` });
116
+ } else {
117
+ findings.push({
118
+ severity: "info",
119
+ title: `no ${KAI_JSON}`,
120
+ detail: "this project was not scaffolded with `npm create kai`, which is fine for a hand-built app: nothing here depends on it."
121
+ });
122
+ }
123
+ if (declared !== void 0) {
124
+ const files = sourceFiles(join(input.cwd, "src"));
125
+ const contents = readAll(files);
126
+ const referencing = contents.filter((text) => text.includes(KIT)).length;
127
+ if (files.length > 0 && referencing === 0) {
128
+ findings.push({
129
+ severity: "warn",
130
+ title: `nothing under src/ references ${KIT}`,
131
+ detail: "the dependency is declared but unused, so no chat surface is rendering."
132
+ });
133
+ } else if (referencing > 0) {
134
+ findings.push({ severity: "ok", title: `${referencing} file(s) under src/ reference ${KIT}` });
135
+ }
136
+ const styled = contents.some((text) => /theme\.tokens\.css|theme\.css|solid\.css/.test(text));
137
+ if (files.length > 0 && !styled) {
138
+ findings.push({
139
+ severity: "info",
140
+ title: "no kit stylesheet is referenced",
141
+ detail: "import @kitn.ai/ui/theme.tokens.css (or theme.css inside a Tailwind build), or the components render unstyled."
142
+ });
143
+ }
144
+ }
145
+ const mcp = readJson(join(input.cwd, "node_modules", MCP, "package.json"));
146
+ findings.push(
147
+ mcp ? { severity: "ok", title: `${MCP} ${String(mcp.version)} is installed locally` } : {
148
+ severity: "info",
149
+ title: `${MCP} is not installed in this project`,
150
+ detail: "normal: an MCP client config runs it as `npx -y @kitn.ai/mcp`, so it needs no local install."
151
+ }
152
+ );
153
+ return findings;
154
+ }
155
+ function exitCodeFor(findings) {
156
+ return findings.some((f) => f.severity === "error") ? 1 : 0;
157
+ }
158
+ const MARK = { ok: "✓", info: "·", warn: "!", error: "✗" };
159
+ function render(findings, out = console.log) {
160
+ for (const finding of findings) {
161
+ out(`${MARK[finding.severity]} ${finding.title}`);
162
+ if (finding.detail) out(` ${finding.detail}`);
163
+ }
164
+ const errors = findings.filter((f) => f.severity === "error").length;
165
+ const warns = findings.filter((f) => f.severity === "warn").length;
166
+ out("");
167
+ out(
168
+ errors > 0 ? `✗ kai doctor: ${errors} problem(s)${warns > 0 ? `, ${warns} warning(s)` : ""}.` : `✓ kai doctor: no problems${warns > 0 ? `, ${warns} warning(s)` : ""}.`
169
+ );
170
+ return exitCodeFor(findings);
171
+ }
172
+ function cliVersion() {
173
+ try {
174
+ return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version ?? "unknown";
175
+ } catch {
176
+ return "unknown";
177
+ }
178
+ }
179
+ async function runDoctor(argv = [], io = {}) {
180
+ const findings = diagnose({
181
+ cwd: io.cwd ?? process.cwd(),
182
+ cliVersion: cliVersion(),
183
+ builtAgainstKit: "0.33.0"
184
+ });
185
+ if (argv.includes("--json")) {
186
+ (io.out ?? console.log)(JSON.stringify({ findings }, null, 2));
187
+ return findings.some((f) => f.severity === "error") ? 1 : 0;
188
+ }
189
+ return render(findings, io.out);
190
+ }
191
+ export {
192
+ runDoctor
193
+ };