@kitn.ai/cli 0.3.0 → 0.5.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/README.md +16 -0
- package/bin/kai.js +2 -0
- package/bin/route.js +10 -3
- package/dist/doctor.es.js +203 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -21,6 +21,8 @@ npx -y @kitn.ai/cli add support-widget # no install at all
|
|
|
21
21
|
| `kai create [dir]` | the scaffolder wizard (the same one `npm create kai` runs) |
|
|
22
22
|
| `kai add <block>` | writes a block from the registry into an existing project |
|
|
23
23
|
| `kai add --list` | prints the blocks this release ships |
|
|
24
|
+
| `kai init [--form <id>]` | makes an EXISTING project kai-aware: adds the kit at this CLI's pin and prints the wiring that framework needs |
|
|
25
|
+
| `kai upgrade [--write]` | brings a SCAFFOLDED project up to the template this CLI emits: it replaces the files you never touched and reports the ones you edited. `--strict` exits non-zero on drift |
|
|
24
26
|
| `kai doctor` | diagnoses this project's kit wiring, versions and registration |
|
|
25
27
|
| `kai mcp` | runs the MCP server for an AI coding harness, if that package is installed |
|
|
26
28
|
| `kai dev <construct.json>` | live preview with reload-on-edit |
|
|
@@ -29,6 +31,20 @@ npx -y @kitn.ai/cli add support-widget # no install at all
|
|
|
29
31
|
| `kai eject <construct.json> <outDir>` | writes the generated Solid project out; the source is yours |
|
|
30
32
|
| `kai validate <construct.json>` | checks a construct and prints problems with paths |
|
|
31
33
|
|
|
34
|
+
## upgrade
|
|
35
|
+
|
|
36
|
+
A project made with `npm create kai` is a copy of a template, and the templates move. `kai upgrade` brings that copy up to what the current CLI emits and **never overwrites something you wrote**:
|
|
37
|
+
|
|
38
|
+
| | verdict | `--write` |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `^` | outdated: untouched since you scaffolded it, so the template moved | replaces it |
|
|
41
|
+
| `+` | missing: the template emits it and you do not have it | adds it |
|
|
42
|
+
| `!` | edited: you changed it | nothing, ever |
|
|
43
|
+
| `?` | unknown: it differs, and there is no baseline to say whose change it is | nothing |
|
|
44
|
+
| `=` | same: already current | nothing |
|
|
45
|
+
|
|
46
|
+
`kai.json` records a sha256 of every file the scaffolder wrote, which is what makes that distinction possible. A project scaffolded before that was recorded has no baseline, so `upgrade` reports the drift and refuses to write. It renders into a temp directory with the same code the scaffolder runs, it deletes nothing, and `--strict` makes drift exit non-zero for CI. `doctor` reads the same recorded hashes and reports how far your copy has moved, without rendering anything.
|
|
47
|
+
|
|
32
48
|
## doctor
|
|
33
49
|
|
|
34
50
|
```bash
|
package/bin/kai.js
CHANGED
|
@@ -38,6 +38,8 @@ Usage
|
|
|
38
38
|
kai create [dir] scaffold a project (the same wizard as \`npm create kai\`)
|
|
39
39
|
kai add <block> write a block from the registry into an existing project
|
|
40
40
|
kai add --list print the blocks this release ships
|
|
41
|
+
kai init [--form <id>] make an EXISTING project kai-aware: add the kit and print the wiring
|
|
42
|
+
kai upgrade [--write] bring a SCAFFOLDED project up to this CLI's template (never your edits)
|
|
41
43
|
|
|
42
44
|
kai doctor diagnose this project's kit wiring, versions and registration
|
|
43
45
|
kai doctor --strict the same, but warnings fail the run (for CI)
|
package/bin/route.js
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
// 'local' -- a bundle inside THIS package. dev/compile/eject/validate are the
|
|
8
8
|
// construct engine; doctor is the wiring diagnosis.
|
|
9
9
|
// 'forward' -- a SEPARATE published program, launched by resolving that package's
|
|
10
|
-
// bin and spawning it with this process's stdio. create/add are
|
|
11
|
-
// `create-kai`'s
|
|
12
|
-
//
|
|
10
|
+
// bin and spawning it with this process's stdio. create/add/init are
|
|
11
|
+
// `create-kai`'s (the same implementation `npm create kai` runs), and
|
|
12
|
+
// mcp is `@kitn.ai/mcp`'s server. They are
|
|
13
13
|
// not bundled into this package because neither belongs to its install
|
|
14
14
|
// weight: create-kai is the scaffolder npm's own `create` convention
|
|
15
15
|
// reaches, and the MCP is the only thing carrying the 5.9 MB SDK.
|
|
@@ -27,6 +27,8 @@ export const CONSTRUCT_COMMANDS = ['dev', 'compile', 'eject', 'validate'];
|
|
|
27
27
|
export const KNOWN_COMMANDS = [
|
|
28
28
|
'create',
|
|
29
29
|
'add',
|
|
30
|
+
'init',
|
|
31
|
+
'upgrade',
|
|
30
32
|
'doctor',
|
|
31
33
|
'mcp',
|
|
32
34
|
...CONSTRUCT_COMMANDS,
|
|
@@ -51,6 +53,11 @@ export function decideEntry(command, rest = []) {
|
|
|
51
53
|
// wizard is the from-scratch door, `add` the into-an-existing-project door.
|
|
52
54
|
if (command === 'create') return { kind: 'forward', pkg: 'create-kai', args: rest };
|
|
53
55
|
if (command === 'add') return { kind: 'forward', pkg: 'create-kai', args: ['add', ...rest] };
|
|
56
|
+
// `init` makes an EXISTING project kai-aware: it merges the dependency and prints the wiring.
|
|
57
|
+
if (command === 'init') return { kind: 'forward', pkg: 'create-kai', args: ['init', ...rest] };
|
|
58
|
+
// `upgrade` re-diffs a scaffolded project against the template this CLI emits; it replaces the
|
|
59
|
+
// files the user never touched and reports the ones they did.
|
|
60
|
+
if (command === 'upgrade') return { kind: 'forward', pkg: 'create-kai', args: ['upgrade', ...rest] };
|
|
54
61
|
if (command === 'mcp') return { kind: 'forward', pkg: '@kitn.ai/mcp', args: [] };
|
|
55
62
|
if (command === 'doctor') return { kind: 'local', verb: 'doctor' };
|
|
56
63
|
if (CONSTRUCT_COMMANDS.includes(command)) return { kind: 'local', verb: 'construct' };
|
package/dist/doctor.es.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { readFileSync, statSync, readdirSync } from "node:fs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { join, relative } from "node:path";
|
|
3
4
|
const RULES = [
|
|
4
5
|
{
|
|
5
6
|
// Rule 1 — array/object data set as an HTML attribute
|
|
6
7
|
// Source: for-ai-agents.mdx §1; context7.json rule 2
|
|
7
8
|
id: "array-as-attribute",
|
|
8
|
-
|
|
9
|
+
// A LEADING `:` OR `[` IS A PROPERTY BINDING, WHICH IS THE CORRECT FORM, and this rule is about
|
|
10
|
+
// HTML ATTRIBUTES. Vue writes the right thing as `:messages="messages"` (or `v-bind:`), Angular
|
|
11
|
+
// as `[messages]="messages"`, and the bare `\b` matched the tail of both: measured, `kai doctor`
|
|
12
|
+
// warned on a freshly scaffolded vue app whose only `messages` occurrence was `:messages=`,
|
|
13
|
+
// telling the reader to do what they had already done. The lookbehind excludes a binding, a
|
|
14
|
+
// member access (`x.messages=`) and a compound attribute name (`data-messages=`), so a real
|
|
15
|
+
// `<kai-chat messages="[...]">` still fires.
|
|
16
|
+
test: (t) => /(?<![:.\[\w-])(messages|models|context|suggestions|triggers)\s*=\s*["']/.test(t),
|
|
9
17
|
title: "Array/object prop set as an HTML attribute (silent failure)",
|
|
10
18
|
cause: "An HTML attribute is always a string. Passing `messages`, `models`, `context`, `suggestions`, or `triggers` as an HTML attribute silently fails — the element receives a stringified value it cannot parse.",
|
|
11
19
|
fix: "Set the property in JavaScript, not as an HTML attribute. Only scalar props (`placeholder`, `loading`, `theme`) work as attributes.\n\n```js\n// ✅ Works — set messages in JavaScript as a property\nconst chat = document.querySelector('kai-chat');\nchat.messages = [{ id: '1', role: 'assistant', parts: [{ type: 'text', text: 'Hello!' }] }];\n```\n\n```html\n<!-- ❌ Fails — messages cannot be an HTML attribute -->\n<kai-chat messages=\"[...]\"></kai-chat>\n```"
|
|
@@ -42,10 +50,25 @@ const RULES = [
|
|
|
42
50
|
{
|
|
43
51
|
// Rule 6 — web components not registered / renders nothing (React #1 failure)
|
|
44
52
|
// Source: field-test reports; for-ai-agents.mdx §"Import order matters"
|
|
53
|
+
//
|
|
54
|
+
// THE SIGNALS MUST BE A REPORT'S VOCABULARY, NOT AN IDIOM WIRING USES. `kai doctor` runs this
|
|
55
|
+
// same rule set over a project's FILES (doctor.ts), so a signal that a correct app can contain
|
|
56
|
+
// reports a registered app as broken. Two were both, and the four raw-tag starters carry them
|
|
57
|
+
// verbatim: their upgrade gate reads
|
|
58
|
+
// const unregistered = TAGS.filter((tag) => !customElements.get(tag));
|
|
59
|
+
// A bare `unregistered` therefore matched an identifier, and the bare `customElements.get`
|
|
60
|
+
// matched a guard. Measured: `kai doctor` on a freshly scaffolded vue app warned "Web components
|
|
61
|
+
// not registered" at src/main.ts, about an app that registers every tag it places.
|
|
62
|
+
//
|
|
63
|
+
// So the bare token now needs a noun after it (`unregistered element`, not `unregistered =`),
|
|
64
|
+
// and the idiom needs its undefined COMPARISON, which is what a pasted diagnostic carries
|
|
65
|
+
// (`customElements.get('kai-chat') === undefined`) and what a guard does not.
|
|
45
66
|
id: "web-components-not-registered",
|
|
46
67
|
test: (t) => {
|
|
47
|
-
if (/renders?\s+nothing|nothing\s+renders?|not\s+registered|
|
|
68
|
+
if (/renders?\s+nothing|nothing\s+renders?|not\s+registered|not\s+upgraded|unknown\s+element|undefined\s+element|no\s+shadow\s+root/.test(t))
|
|
48
69
|
return true;
|
|
70
|
+
if (/\bunregistered\s+(?:kai|element|component|tag|web)/.test(t)) return true;
|
|
71
|
+
if (/customElements\.get\s*\([^)\n]*\)\s*(?:={2,3}|!={1,2})?\s*undefined/.test(t)) return true;
|
|
49
72
|
if (/\b(empty|blank)\b/.test(t) && /render|element|component|kai-|<[a-z]+-|shadow/.test(t))
|
|
50
73
|
return true;
|
|
51
74
|
if (/doesn'?t\s+render|won'?t\s+render/.test(t) && /kai-|element|component|custom.?element/.test(t))
|
|
@@ -54,7 +77,7 @@ const RULES = [
|
|
|
54
77
|
},
|
|
55
78
|
title: "Web components not registered — renders nothing / empty box",
|
|
56
79
|
cause: "The `@kitn.ai/ui/react` wrappers (and bare `<kai-*>` tags) do NOT register the web components by themselves. Without the registration side-effect import, `<kai-chat>` / `<Chat>` is an un-upgraded unknown element — an empty box. `customElements.get('kai-chat') === undefined`.",
|
|
57
|
-
fix: "
|
|
80
|
+
fix: "Register the web components on the client BEFORE your first render: the import has to run before the component mounts. A scaffolded app takes the narrowest form that covers what it renders, ONE ENTRY PER TAG it places.\n\n```tsx\nimport '@kitn.ai/ui/web-components/chat'; // registers <kai-chat>, and kai-message rides in with it\nimport { Chat } from '@kitn.ai/ui/react'\nimport '@kitn.ai/ui/theme.css'\n```\n\nThe register-all barrel `import '@kitn.ai/ui/web-components'` is still the right form when you want every tag, and it is the SSR-import-safe default: a per-web-component entry is client-only, so an SSR entry point wants the single barrel line instead of one import per tag. It is also the barrel the imperative `toast()` helper is exported from.\n\nIn plain HTML: either form in your module script. The import is a side effect; keep it even if your linter flags it as \"unused\"."
|
|
58
81
|
},
|
|
59
82
|
{
|
|
60
83
|
// Rule 7 — tsc errors inside node_modules/@kitn.ai/ui/src (SolidJS source pulled in)
|
|
@@ -193,6 +216,113 @@ const RULES = [
|
|
|
193
216
|
function matchRules(text) {
|
|
194
217
|
return RULES.filter((rule) => rule.test(text));
|
|
195
218
|
}
|
|
219
|
+
const tags = { "kai-conversations": "conversation-list", "kai-conversation-item": "conversation-item", "kai-prompt-input": "prompt-input", "kai-chat": "chat", "kai-workspace": "chat-workspace", "kai-thread": "thread", "kai-thinking-bar": "thinking-bar", "kai-model-switcher": "model-switcher", "kai-attachments": "attachments", "kai-message": "message", "kai-markdown": "markdown", "kai-code-block": "code-block", "kai-reasoning": "reasoning", "kai-tool": "tool", "kai-context": "context-meter", "kai-feedback-bar": "feedback-bar", "kai-scope-picker": "chat-scope-picker", "kai-suggestions": "prompt-suggestions", "kai-file-upload": "file-upload", "kai-voice-input": "voice-input", "kai-audio-visualizer": "audio-visualizer", "kai-loader": "loader", "kai-text-shimmer": "text-shimmer", "kai-image": "image", "kai-checkpoint": "checkpoint", "kai-skills": "message-skills", "kai-source": "source", "kai-sources": "source", "kai-response-stream": "response-stream", "kai-empty": "empty", "kai-status": "status", "kai-nav": "nav", "kai-progress-bar": "progress-bar", "kai-coachmark": "coachmark", "kai-tabs": "tabs", "kai-voice-output": "voice-output", "kai-screen": "screen", "kai-chain-of-thought": "chain-of-thought", "kai-resizable": "resizable", "kai-resizable-item": "resizable", "kai-file-tree": "file-tree", "kai-artifact": "artifact", "kai-scroll-button": "scroll-button", "kai-popover": "popover", "kai-switch": "switch", "kai-checkbox": "checkbox", "kai-checkbox-group": "checkbox-group", "kai-radio-group": "radio-group", "kai-slider": "slider", "kai-select": "select", "kai-button": "button", "kai-avatar": "avatar", "kai-badge": "badge", "kai-tooltip": "tooltip", "kai-notice": "notice", "kai-icon": "icon", "kai-separator": "separator", "kai-scroll-area": "scroll-area", "kai-hover-card": "hover-card", "kai-skeleton": "skeleton", "kai-toast-region": "toast", "kai-card": "card", "kai-form": "form", "kai-link-preview": "link-preview", "kai-embed": "embed", "kai-confirm": "confirm-card", "kai-tasks": "tasks", "kai-choice": "choice", "kai-cards": "cards", "kai-compare": "compare", "kai-composer": "composer", "kai-menu": "menu", "kai-dropdown": "dropdown", "kai-command": "command", "kai-prompt-dock": "prompt-dock", "kai-segmented": "segmented", "kai-settings-group": "settings-group", "kai-setting-item": "setting-item", "kai-pane": "pane", "kai-pane-group": "pane-group", "kai-pane-grid": "pane-grid", "kai-agent-card": "agent-card", "kai-dialog": "dialog", "kai-dock": "dock", "kai-input": "input", "kai-search": "search", "kai-kbd": "kbd", "kai-editable-label": "editable-label", "kai-panel": "panel", "kai-panel-header": "panel", "kai-tab-bar": "tab-bar", "kai-tab-bar-item": "tab-bar-item", "kai-view-stack": "view-stack", "kai-view": "view", "kai-row": "row", "kai-row-group": "row-group" };
|
|
220
|
+
const WEB_COMPONENTS_ENTRY = "@kitn.ai/ui/web-components";
|
|
221
|
+
function entryForTag(tag) {
|
|
222
|
+
return tags[tag];
|
|
223
|
+
}
|
|
224
|
+
const WORD = /[\w$]/;
|
|
225
|
+
const PLACED_TAG = /<(kai-[a-z][a-z0-9-]*)/g;
|
|
226
|
+
const CREATED_TAG = /\b(?:el|createElement)\s*\(\s*['"](kai-[a-z][a-z0-9-]*)['"]/g;
|
|
227
|
+
const REGISTRATION_IMPORT = /(?:import\s*\(\s*|import(?!\s*type\b)\s*([^;'"]*?\bfrom\s*)?)['"]@kitn\.ai\/ui\/web-components(?:\/([a-z][a-z0-9-]*))?['"]/g;
|
|
228
|
+
const SELF_DEFINE = /customElements\s*\.\s*define\s*\(\s*['"](kai-[a-z][a-z0-9-]*)['"]/g;
|
|
229
|
+
function past(text, from, terminator) {
|
|
230
|
+
const at = text.indexOf(terminator, from);
|
|
231
|
+
return at === -1 ? text.length : at + terminator.length;
|
|
232
|
+
}
|
|
233
|
+
function stringEnd(text, open) {
|
|
234
|
+
const quote = text[open];
|
|
235
|
+
for (let i = open + 1; i < text.length; i += 1) {
|
|
236
|
+
const ch = text[i];
|
|
237
|
+
if (ch === "\\") {
|
|
238
|
+
i += 1;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (ch === quote) return i;
|
|
242
|
+
if (ch === "\n" && quote !== "`") return i;
|
|
243
|
+
}
|
|
244
|
+
return text.length;
|
|
245
|
+
}
|
|
246
|
+
function blank(text) {
|
|
247
|
+
const code = [];
|
|
248
|
+
const markup = [];
|
|
249
|
+
const put = (chunk, visibleInMarkup = true) => {
|
|
250
|
+
code.push(chunk);
|
|
251
|
+
markup.push(visibleInMarkup ? chunk : " ".repeat(chunk.length));
|
|
252
|
+
};
|
|
253
|
+
const blanked = (count) => put(" ".repeat(count));
|
|
254
|
+
let i = 0;
|
|
255
|
+
while (i < text.length) {
|
|
256
|
+
if (text.startsWith("<!--", i)) {
|
|
257
|
+
const end = past(text, i + 4, "-->");
|
|
258
|
+
blanked(end - i);
|
|
259
|
+
i = end;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (text.startsWith("//", i) && text[i - 1] !== ":") {
|
|
263
|
+
const stop = text.indexOf("\n", i);
|
|
264
|
+
const end = stop === -1 ? text.length : stop;
|
|
265
|
+
blanked(end - i);
|
|
266
|
+
i = end;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (text.startsWith("/*", i)) {
|
|
270
|
+
const end = past(text, i + 2, "*/");
|
|
271
|
+
blanked(end - i);
|
|
272
|
+
i = end;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const ch = text[i];
|
|
276
|
+
if ((ch === "'" || ch === '"' || ch === "`") && !WORD.test(text[i - 1] ?? " ")) {
|
|
277
|
+
const end = stringEnd(text, i);
|
|
278
|
+
put(ch);
|
|
279
|
+
put(text.slice(i + 1, end), false);
|
|
280
|
+
if (text[end] === ch) {
|
|
281
|
+
put(ch);
|
|
282
|
+
i = end + 1;
|
|
283
|
+
} else {
|
|
284
|
+
i = end;
|
|
285
|
+
}
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
put(ch);
|
|
289
|
+
i += 1;
|
|
290
|
+
}
|
|
291
|
+
return { code: code.join(""), markup: markup.join("") };
|
|
292
|
+
}
|
|
293
|
+
function sourceCode(text) {
|
|
294
|
+
return blank(text).code;
|
|
295
|
+
}
|
|
296
|
+
function unregisteredPlacedTags(files) {
|
|
297
|
+
const placed = /* @__PURE__ */ new Map();
|
|
298
|
+
const imported = /* @__PURE__ */ new Set();
|
|
299
|
+
const defined = /* @__PURE__ */ new Set();
|
|
300
|
+
let barrel = false;
|
|
301
|
+
const note = (tag, file) => {
|
|
302
|
+
const seen = placed.get(tag);
|
|
303
|
+
if (seen === void 0) placed.set(tag, [file]);
|
|
304
|
+
else if (!seen.includes(file)) seen.push(file);
|
|
305
|
+
};
|
|
306
|
+
for (const { file, text } of files) {
|
|
307
|
+
const { code, markup } = blank(text);
|
|
308
|
+
for (const match of markup.matchAll(PLACED_TAG)) note(match[1], file);
|
|
309
|
+
for (const match of code.matchAll(CREATED_TAG)) note(match[1], file);
|
|
310
|
+
for (const match of code.matchAll(REGISTRATION_IMPORT)) {
|
|
311
|
+
const entry = match[2];
|
|
312
|
+
if (entry === void 0) barrel = true;
|
|
313
|
+
else imported.add(entry);
|
|
314
|
+
}
|
|
315
|
+
for (const match of code.matchAll(SELF_DEFINE)) defined.add(match[1]);
|
|
316
|
+
}
|
|
317
|
+
if (barrel) return [];
|
|
318
|
+
const out = [];
|
|
319
|
+
for (const [tag, names] of placed) {
|
|
320
|
+
const entry = entryForTag(tag);
|
|
321
|
+
if (entry === void 0 || imported.has(entry) || defined.has(tag)) continue;
|
|
322
|
+
out.push({ tag, entry, files: names });
|
|
323
|
+
}
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
196
326
|
const KIT = "@kitn.ai/ui";
|
|
197
327
|
const MCP = "@kitn.ai/mcp";
|
|
198
328
|
const KAI_JSON = "kai.json";
|
|
@@ -239,6 +369,23 @@ function sourceFiles(dir, limit = 400) {
|
|
|
239
369
|
if (statSync(dir, { throwIfNoEntry: false })?.isDirectory()) walk(dir);
|
|
240
370
|
return out;
|
|
241
371
|
}
|
|
372
|
+
function baselineDrift(cwd, files) {
|
|
373
|
+
const changed = [];
|
|
374
|
+
const gone = [];
|
|
375
|
+
let same = 0;
|
|
376
|
+
for (const [file, recorded] of Object.entries(files)) {
|
|
377
|
+
let text;
|
|
378
|
+
try {
|
|
379
|
+
text = readFileSync(join(cwd, file), "utf8");
|
|
380
|
+
} catch {
|
|
381
|
+
gone.push(file);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (createHash("sha256").update(text, "utf8").digest("hex") === recorded) same += 1;
|
|
385
|
+
else changed.push(file);
|
|
386
|
+
}
|
|
387
|
+
return { changed: changed.sort(), gone: gone.sort(), same };
|
|
388
|
+
}
|
|
242
389
|
const readAll = (files) => files.flatMap((file) => {
|
|
243
390
|
try {
|
|
244
391
|
return [{ file, text: readFileSync(file, "utf8") }];
|
|
@@ -305,7 +452,38 @@ function diagnose(input) {
|
|
|
305
452
|
const framework = kaiJson.framework ?? "?";
|
|
306
453
|
const built = kaiJson.kitBuiltAgainst ?? "?";
|
|
307
454
|
const features = Array.isArray(kaiJson.features) ? kaiJson.features.join(", ") : "?";
|
|
308
|
-
findings.push({
|
|
455
|
+
findings.push({
|
|
456
|
+
severity: "ok",
|
|
457
|
+
title: `${KAI_JSON}: framework ${framework}, features ${features}`,
|
|
458
|
+
detail: `scaffolded against kit ${built}`
|
|
459
|
+
});
|
|
460
|
+
const baseline = kaiJson.files;
|
|
461
|
+
if (baseline !== null && typeof baseline === "object" && Object.keys(baseline).length > 0) {
|
|
462
|
+
const files = baseline;
|
|
463
|
+
const { changed, gone, same } = baselineDrift(input.cwd, files);
|
|
464
|
+
if (changed.length === 0 && gone.length === 0) {
|
|
465
|
+
findings.push({
|
|
466
|
+
severity: "ok",
|
|
467
|
+
title: `${KAI_JSON}'s baseline: all ${same} scaffolded file(s) are exactly as written`
|
|
468
|
+
});
|
|
469
|
+
} else {
|
|
470
|
+
const names = [...changed, ...gone];
|
|
471
|
+
const shown = names.slice(0, 3).join(", ");
|
|
472
|
+
const more = names.length > 3 ? ` (and ${names.length - 3} more)` : "";
|
|
473
|
+
findings.push({
|
|
474
|
+
severity: "info",
|
|
475
|
+
title: `${KAI_JSON}'s baseline: ${same} of ${Object.keys(files).length} scaffolded file(s) are as written, ${changed.length} changed, ${gone.length} gone`,
|
|
476
|
+
detail: `${shown}${more}
|
|
477
|
+
Run \`kai upgrade\` to see what the template this CLI emits would change (it replaces only the files you never touched), or \`kai upgrade --strict\` in CI.`
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
} else {
|
|
481
|
+
findings.push({
|
|
482
|
+
severity: "info",
|
|
483
|
+
title: `${KAI_JSON} has no baseline`,
|
|
484
|
+
detail: "it predates the recorded hashes, so this cannot tell your edits from a template change. `kai upgrade` still diffs the project against the template this CLI emits, and will not write without a baseline."
|
|
485
|
+
});
|
|
486
|
+
}
|
|
309
487
|
} else {
|
|
310
488
|
findings.push({
|
|
311
489
|
severity: "info",
|
|
@@ -328,7 +506,7 @@ function diagnose(input) {
|
|
|
328
506
|
}
|
|
329
507
|
const hitByRule = /* @__PURE__ */ new Map();
|
|
330
508
|
for (const { file, text } of contents) {
|
|
331
|
-
for (const rule of matchRules(text)) {
|
|
509
|
+
for (const rule of matchRules(sourceCode(text))) {
|
|
332
510
|
if (!hitByRule.has(rule.id)) hitByRule.set(rule.id, { rule, files: [] });
|
|
333
511
|
hitByRule.get(rule.id).files.push(relative(input.cwd, file));
|
|
334
512
|
}
|
|
@@ -343,6 +521,25 @@ function diagnose(input) {
|
|
|
343
521
|
${rule.fix}`
|
|
344
522
|
});
|
|
345
523
|
}
|
|
524
|
+
const unregistered = unregisteredPlacedTags(contents);
|
|
525
|
+
if (unregistered.length > 0) {
|
|
526
|
+
const shown = unregistered.slice(0, 3);
|
|
527
|
+
const lines = shown.map(({ tag, entry, files: placing }) => {
|
|
528
|
+
const where = placing.slice(0, 2).map((file) => relative(input.cwd, file)).join(", ");
|
|
529
|
+
const more = placing.length > 2 ? ` (+${placing.length - 2})` : "";
|
|
530
|
+
return `<${tag}> in ${where}${more} -> add: import '${WEB_COMPONENTS_ENTRY}/${entry}'`;
|
|
531
|
+
});
|
|
532
|
+
if (unregistered.length > shown.length) lines.push(`(and ${unregistered.length - shown.length} more)`);
|
|
533
|
+
lines.push(
|
|
534
|
+
`Per-web-component entries are client-only: on an SSR target, put the import inside \`if (typeof window !== 'undefined')\`.`,
|
|
535
|
+
`Importing the register-all barrel '${WEB_COMPONENTS_ENTRY}' instead is the SSR-import-safe form and registers every tag at once.`
|
|
536
|
+
);
|
|
537
|
+
findings.push({
|
|
538
|
+
severity: "warn",
|
|
539
|
+
title: `${unregistered.length} placed <kai-*> tag(s) have nothing registering them`,
|
|
540
|
+
detail: lines.join("\n")
|
|
541
|
+
});
|
|
542
|
+
}
|
|
346
543
|
const styled = contents.some((c) => /theme\.tokens\.css|theme\.css|solid\.css/.test(c.text));
|
|
347
544
|
if (files.length > 0 && !styled) {
|
|
348
545
|
findings.push({
|
|
@@ -392,7 +589,7 @@ async function runDoctor(argv = [], io = {}) {
|
|
|
392
589
|
const findings = diagnose({
|
|
393
590
|
cwd: io.cwd ?? process.cwd(),
|
|
394
591
|
cliVersion: cliVersion(),
|
|
395
|
-
builtAgainstKit: "0.
|
|
592
|
+
builtAgainstKit: "0.36.0"
|
|
396
593
|
});
|
|
397
594
|
const strict = argv.includes("--strict");
|
|
398
595
|
if (argv.includes("--json")) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kitn.ai/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The kai command line for @kitn.ai/ui: scaffold a project or add a block to one, diagnose its wiring, and run the construct dev/eject/compile tooling.",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"lint:cli-invocations": "node scripts/lint-cli-invocations.mjs --self-test && node scripts/lint-cli-invocations.mjs"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"create-kai": "^0.
|
|
59
|
+
"create-kai": "^0.9.0",
|
|
60
60
|
"zod": "^4.4.3"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|