@kitn.ai/cli 0.4.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/dist/doctor.es.js +153 -5
- package/package.json +2 -2
package/dist/doctor.es.js
CHANGED
|
@@ -6,7 +6,14 @@ const RULES = [
|
|
|
6
6
|
// Rule 1 — array/object data set as an HTML attribute
|
|
7
7
|
// Source: for-ai-agents.mdx §1; context7.json rule 2
|
|
8
8
|
id: "array-as-attribute",
|
|
9
|
-
|
|
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),
|
|
10
17
|
title: "Array/object prop set as an HTML attribute (silent failure)",
|
|
11
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.",
|
|
12
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```"
|
|
@@ -43,10 +50,25 @@ const RULES = [
|
|
|
43
50
|
{
|
|
44
51
|
// Rule 6 — web components not registered / renders nothing (React #1 failure)
|
|
45
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.
|
|
46
66
|
id: "web-components-not-registered",
|
|
47
67
|
test: (t) => {
|
|
48
|
-
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))
|
|
49
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;
|
|
50
72
|
if (/\b(empty|blank)\b/.test(t) && /render|element|component|kai-|<[a-z]+-|shadow/.test(t))
|
|
51
73
|
return true;
|
|
52
74
|
if (/doesn'?t\s+render|won'?t\s+render/.test(t) && /kai-|element|component|custom.?element/.test(t))
|
|
@@ -55,7 +77,7 @@ const RULES = [
|
|
|
55
77
|
},
|
|
56
78
|
title: "Web components not registered — renders nothing / empty box",
|
|
57
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`.",
|
|
58
|
-
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\"."
|
|
59
81
|
},
|
|
60
82
|
{
|
|
61
83
|
// Rule 7 — tsc errors inside node_modules/@kitn.ai/ui/src (SolidJS source pulled in)
|
|
@@ -194,6 +216,113 @@ const RULES = [
|
|
|
194
216
|
function matchRules(text) {
|
|
195
217
|
return RULES.filter((rule) => rule.test(text));
|
|
196
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
|
+
}
|
|
197
326
|
const KIT = "@kitn.ai/ui";
|
|
198
327
|
const MCP = "@kitn.ai/mcp";
|
|
199
328
|
const KAI_JSON = "kai.json";
|
|
@@ -377,7 +506,7 @@ Run \`kai upgrade\` to see what the template this CLI emits would change (it rep
|
|
|
377
506
|
}
|
|
378
507
|
const hitByRule = /* @__PURE__ */ new Map();
|
|
379
508
|
for (const { file, text } of contents) {
|
|
380
|
-
for (const rule of matchRules(text)) {
|
|
509
|
+
for (const rule of matchRules(sourceCode(text))) {
|
|
381
510
|
if (!hitByRule.has(rule.id)) hitByRule.set(rule.id, { rule, files: [] });
|
|
382
511
|
hitByRule.get(rule.id).files.push(relative(input.cwd, file));
|
|
383
512
|
}
|
|
@@ -392,6 +521,25 @@ Run \`kai upgrade\` to see what the template this CLI emits would change (it rep
|
|
|
392
521
|
${rule.fix}`
|
|
393
522
|
});
|
|
394
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
|
+
}
|
|
395
543
|
const styled = contents.some((c) => /theme\.tokens\.css|theme\.css|solid\.css/.test(c.text));
|
|
396
544
|
if (files.length > 0 && !styled) {
|
|
397
545
|
findings.push({
|
|
@@ -441,7 +589,7 @@ async function runDoctor(argv = [], io = {}) {
|
|
|
441
589
|
const findings = diagnose({
|
|
442
590
|
cwd: io.cwd ?? process.cwd(),
|
|
443
591
|
cliVersion: cliVersion(),
|
|
444
|
-
builtAgainstKit: "0.
|
|
592
|
+
builtAgainstKit: "0.36.0"
|
|
445
593
|
});
|
|
446
594
|
const strict = argv.includes("--strict");
|
|
447
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": {
|