@kitn.ai/cli 0.3.0 → 0.4.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,4 +1,5 @@
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
  {
@@ -239,6 +240,23 @@ function sourceFiles(dir, limit = 400) {
239
240
  if (statSync(dir, { throwIfNoEntry: false })?.isDirectory()) walk(dir);
240
241
  return out;
241
242
  }
243
+ function baselineDrift(cwd, files) {
244
+ const changed = [];
245
+ const gone = [];
246
+ let same = 0;
247
+ for (const [file, recorded] of Object.entries(files)) {
248
+ let text;
249
+ try {
250
+ text = readFileSync(join(cwd, file), "utf8");
251
+ } catch {
252
+ gone.push(file);
253
+ continue;
254
+ }
255
+ if (createHash("sha256").update(text, "utf8").digest("hex") === recorded) same += 1;
256
+ else changed.push(file);
257
+ }
258
+ return { changed: changed.sort(), gone: gone.sort(), same };
259
+ }
242
260
  const readAll = (files) => files.flatMap((file) => {
243
261
  try {
244
262
  return [{ file, text: readFileSync(file, "utf8") }];
@@ -305,7 +323,38 @@ function diagnose(input) {
305
323
  const framework = kaiJson.framework ?? "?";
306
324
  const built = kaiJson.kitBuiltAgainst ?? "?";
307
325
  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}` });
326
+ findings.push({
327
+ severity: "ok",
328
+ title: `${KAI_JSON}: framework ${framework}, features ${features}`,
329
+ detail: `scaffolded against kit ${built}`
330
+ });
331
+ const baseline = kaiJson.files;
332
+ if (baseline !== null && typeof baseline === "object" && Object.keys(baseline).length > 0) {
333
+ const files = baseline;
334
+ const { changed, gone, same } = baselineDrift(input.cwd, files);
335
+ if (changed.length === 0 && gone.length === 0) {
336
+ findings.push({
337
+ severity: "ok",
338
+ title: `${KAI_JSON}'s baseline: all ${same} scaffolded file(s) are exactly as written`
339
+ });
340
+ } else {
341
+ const names = [...changed, ...gone];
342
+ const shown = names.slice(0, 3).join(", ");
343
+ const more = names.length > 3 ? ` (and ${names.length - 3} more)` : "";
344
+ findings.push({
345
+ severity: "info",
346
+ title: `${KAI_JSON}'s baseline: ${same} of ${Object.keys(files).length} scaffolded file(s) are as written, ${changed.length} changed, ${gone.length} gone`,
347
+ detail: `${shown}${more}
348
+ 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.`
349
+ });
350
+ }
351
+ } else {
352
+ findings.push({
353
+ severity: "info",
354
+ title: `${KAI_JSON} has no baseline`,
355
+ 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."
356
+ });
357
+ }
309
358
  } else {
310
359
  findings.push({
311
360
  severity: "info",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitn.ai/cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.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.8.0",
60
60
  "zod": "^4.4.3"
61
61
  },
62
62
  "devDependencies": {