@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 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 wizard and block registry (the same implementation
12
- // `npm create kai` runs), and mcp is `@kitn.ai/mcp`'s server. They are
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
- test: (t) => /\b(messages|models|context|suggestions|triggers)\s*=\s*["']/.test(t),
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|unregistered|not\s+upgraded|unknown\s+element|customElements\.get|undefined\s+element|no\s+shadow\s+root/.test(t))
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: "Import the web-components bundle for its side effect BEFORE your first render it must run before the component mounts.\n\n```tsx\nimport '@kitn.ai/ui/web-components' // registers <kai-*> REQUIRED, must come first\nimport { Chat } from '@kitn.ai/ui/react'\nimport '@kitn.ai/ui/theme.css'\n```\n\nIn plain HTML: `import '@kitn.ai/ui/web-components'` in your module script. The import is a side effect keep it even if your linter flags it as \"unused\"."
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({ severity: "ok", title: `${KAI_JSON}: framework ${framework}, features ${features}`, detail: `scaffolded against kit ${built}` });
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.35.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.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.7.1",
59
+ "create-kai": "^0.9.0",
60
60
  "zod": "^4.4.3"
61
61
  },
62
62
  "devDependencies": {