create-kudzu 0.1.155 → 0.1.157

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.
Files changed (4) hide show
  1. package/README.md +77 -1
  2. package/ai.mjs +126 -0
  3. package/index.mjs +29 -2
  4. package/package.json +3 -2
package/README.md CHANGED
@@ -10,6 +10,82 @@ cd my-app
10
10
  npm run dev
11
11
  ```
12
12
 
13
- The generated project is a working showcase on `@kudzujs/core@^0.16.30` with reusable components, an interactive state example, a zero-JavaScript static route, page metadata, responsive source CSS, and `npm run check`. Configuration remains optional until the app needs custom assets, document defaults, navigation, or build hooks.
13
+ The generated project is a working showcase on `@kudzujs/core@^0.16.36` with reusable components, an interactive state example, a zero-JavaScript static route, page metadata, responsive CSS, and `npm run check`. Configuration remains optional until the app needs custom assets, document defaults, navigation, or build hooks.
14
14
 
15
15
  Use `--no-install` to create the files without installing dependencies.
16
+
17
+ ## Optional AI authoring guidance
18
+
19
+ `create-kudzu@0.1.157` provides optional local AI developer tools with managed check cancellation:
20
+
21
+ ```bash
22
+ npm create kudzu@latest my-app -- --ai
23
+ ```
24
+
25
+ From a source checkout:
26
+
27
+ ```bash
28
+ node /path/to/kudzu/packages/create-kudzu/index.mjs my-app --ai
29
+ ```
30
+
31
+ This adds a short `AGENTS.md`, a standard-library-only `kudzu-ai.mjs` developer
32
+ tool, and an `ai` npm script. Check logs are ignored through `.kudzu-ai/`.
33
+ Combine with `--no-install` to generate files without installing packages.
34
+ Without `--ai`, the starter stays unchanged.
35
+
36
+ ### Developer tools
37
+
38
+ ```bash
39
+ npm run ai -- docs
40
+ npm run ai -- docs Authoring
41
+ npm run ai -- check
42
+ npm run ai -- check --timeout-ms 600000
43
+ ```
44
+
45
+ - `docs` lists level-two headings from the installed core README, including the
46
+ installed version and file path. Supply one exact, case-insensitive heading
47
+ (quote multi-word headings) to return that section. Sections are capped at
48
+ 6,000 characters with explicit truncation and original line ranges. Follow the
49
+ file reference if more is needed. This uses the generated npm dependency layout;
50
+ install dependencies before reading docs. It makes no network or model calls.
51
+ - `check` executes the app's existing `npm run check`, including its npm lifecycle
52
+ hooks, from the project root. It returns JSON status, exit code, timing and a
53
+ log excerpt. Failure stays nonzero. The default timeout is five minutes; choose
54
+ an integer from 1 to 1,200,000 ms. Timeout/cancellation terminates the spawned
55
+ process group on Unix or process tree on Windows and reports failure.
56
+ Under the existing `KUDZU_AI_DELIVERY_GROUP=1` managed-runner contract, Unix
57
+ checks inherit their outer group so a hard group cancellation also reaches
58
+ their ordinary descendants. A check-local timeout or interruption uses native
59
+ `ps` PID/parent-PID inspection to stop only that check's descendant snapshot,
60
+ preserving sibling work; inspection errors are reported as failures.
61
+ - Full stdout/stderr are retained in a unique `.kudzu-ai/check-*/output.log`.
62
+ Large logs show the first/last 2,048 bytes with an omission marker; an error may
63
+ be in the omitted middle. Read the full log when needed. Old logs are not deleted
64
+ automatically; remove `.kudzu-ai/` when no longer needed.
65
+ - The tool does not cache checks or certify browser behavior. Later edits require
66
+ a new check. Scripts that intentionally detach background jobs or custom
67
+ deployment rules need their own lifecycle/asset handling. Use
68
+ `npm run --silent ai -- check` to suppress npm's outer lifecycle banner.
69
+
70
+ Start a new [OpenCode](https://opencode.ai/docs/rules/) or
71
+ [Codex](https://developers.openai.com/codex/agent-configuration/agents-md) session
72
+ from the app root to use their documented project-instruction discovery. Other
73
+ agents need support for `AGENTS.md` or an explicit request to read it. Editor
74
+ installation alone does not load instructions. Overrides, disabled discovery,
75
+ context limits, and headless modes can change loading; verify the effective
76
+ instructions in the agent you actually use.
77
+
78
+ AGENTS.md supplies guidance; kudzu-ai.mjs performs real document lookup and check
79
+ execution. The host's existing shell tool can invoke it without an MCP connection
80
+ or plugin. It does not intercept every agent action or force use of these commands.
81
+ There is no additional dependency, agent installation, or model call for these
82
+ tools. Both files stay outside `src` and `public` and are excluded by the default
83
+ Kudzu build. Custom asset-copy/deployment settings can still publish root files.
84
+ Instructions and tool responses add input context; lower total AI cost is unmeasured.
85
+
86
+ For an existing app, generate a disposable `--ai --no-install` project, copy
87
+ `kudzu-ai.mjs`, add `"ai": "node kudzu-ai.mjs"` to your scripts and `.kudzu-ai/` to
88
+ your ignore rules, and merge the applicable instructions into your existing rules.
89
+ Keep your real `check` script; do not make it invoke `ai check` recursively.
90
+ The generator rejects nonempty targets. Generated tools/rules are snapshots, not
91
+ auto-updated when Kudzu is upgraded; review them with your existing app setup.
package/ai.mjs ADDED
@@ -0,0 +1,126 @@
1
+ import { spawn, spawnSync } from "node:child_process"
2
+ import { mkdir, mkdtemp, open, readFile, realpath } from "node:fs/promises"
3
+ import { dirname, join } from "node:path"
4
+ import { fileURLToPath } from "node:url"
5
+ import { parseArgs } from "node:util"
6
+
7
+ export async function docs(root, heading) {
8
+ const directory = join(root, "node_modules/@kudzujs/core")
9
+ const { version } = JSON.parse(await readFile(join(directory, "package.json"), "utf8"))
10
+ const path = join(directory, "README.md")
11
+ const lines = (await readFile(path, "utf8")).split(/\r?\n/)
12
+ const sections = []
13
+ let fence
14
+ for (const [index, line] of lines.entries()) {
15
+ const delimiter = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/)
16
+ if (delimiter) {
17
+ if (!fence) fence = delimiter[1]
18
+ else if (delimiter[1][0] === fence[0] && delimiter[1].length >= fence.length && !delimiter[2].trim()) fence = undefined
19
+ continue
20
+ }
21
+ const title = !fence && line.match(/^##[ \t]+(.+?)(?:[ \t]+#+)?[ \t]*$/)
22
+ if (title) sections.push({ heading: title[1], start: index })
23
+ }
24
+ const result = { version, path, headings: sections.map(section => section.heading) }
25
+ if (heading === undefined) return result
26
+ const matches = sections.filter(section => section.heading.toLowerCase() === heading.toLowerCase())
27
+ if (matches.length !== 1) throw new Error(`Expected one exact README heading. Available: ${result.headings.join(", ")}`)
28
+ const section = matches[0], end = sections[sections.indexOf(section) + 1]?.start ?? lines.length
29
+ const text = lines.slice(section.start, end).join("\n")
30
+ return { version, path, heading: section.heading, startLine: section.start + 1, endLine: end, text: text.slice(0, 6000), truncated: text.length > 6000 }
31
+ }
32
+
33
+ export async function check(root, timeoutMs = 300000) {
34
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 1200000) throw new Error("timeout-ms must be an integer from 1 to 1200000")
35
+ root = await realpath(root)
36
+ if (process.env.KUDZU_AI_CHECK_ROOT === root) throw new Error("scripts.check must not invoke ai check recursively")
37
+ const manifest = JSON.parse(await readFile(join(root, "package.json"), "utf8"))
38
+ if (typeof manifest.scripts?.check !== "string" || !manifest.scripts.check.trim()) throw new Error("package.json must define scripts.check")
39
+ const logs = join(root, ".kudzu-ai")
40
+ await mkdir(logs, { recursive: true })
41
+ const directory = await mkdtemp(join(logs, "check-")), path = join(directory, "output.log")
42
+ const log = await open(path, "wx+")
43
+ const start = performance.now()
44
+ const managed = process.env.KUDZU_AI_DELIVERY_GROUP === "1"
45
+ try {
46
+ const result = await new Promise(resolveRun => {
47
+ const child = spawn(process.platform === "win32" ? "npm.cmd" : "npm", ["run", "check"], {
48
+ cwd: root, detached: process.platform !== "win32" && !managed, shell: process.platform === "win32",
49
+ stdio: ["ignore", log.fd, log.fd], env: { ...process.env, FORCE_COLOR: "0", KUDZU_AI_CHECK_ROOT: root },
50
+ })
51
+ let timedOut = false, interrupted = null, error = null
52
+ const terminate = () => {
53
+ if (!child.pid || child.exitCode !== null || child.signalCode !== null) return
54
+ if (process.platform === "win32") {
55
+ const killed = spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { encoding: "utf8" })
56
+ if (killed.status !== 0) error = killed.error?.message ?? killed.stderr ?? "Could not terminate check tree"
57
+ } else {
58
+ const pids = [managed ? child.pid : -child.pid]
59
+ if (managed) {
60
+ // Keep the outer group alive on this tool's timeout; stop only its ordinary descendants.
61
+ const table = spawnSync("ps", ["-A", "-o", "pid=", "-o", "ppid="], { encoding: "utf8", timeout: 1000, maxBuffer: 1024 * 1024 })
62
+ if (table.status !== 0) error = table.error?.message || table.stderr?.trim() || "Could not inspect managed check descendants"
63
+ else {
64
+ const parents = table.stdout.trim().split("\n").map(line => line.trim().split(/\s+/).map(Number))
65
+ if (parents.some(row => row.length !== 2 || row.some(id => !Number.isSafeInteger(id) || id < 0))) error = "Invalid managed check process table"
66
+ else for (let index = 0; index < pids.length; index++) for (const [pid, parent] of parents) {
67
+ if (parent === pids[index] && pid > 1 && pid !== process.pid && !pids.includes(pid)) pids.push(pid)
68
+ }
69
+ }
70
+ }
71
+ for (const pid of pids.reverse()) try { process.kill(pid, "SIGKILL") } catch (failure) { if (failure.code !== "ESRCH") error = failure.message }
72
+ }
73
+ }
74
+ const onInterrupt = signal => { interrupted = signal; terminate() }
75
+ const onSigint = () => onInterrupt("SIGINT"), onSigterm = () => onInterrupt("SIGTERM")
76
+ process.once("SIGINT", onSigint)
77
+ process.once("SIGTERM", onSigterm)
78
+ const timer = setTimeout(() => { timedOut = true; terminate() }, timeoutMs)
79
+ child.once("error", failure => { error = failure.message })
80
+ child.once("close", (exitCode, signal) => {
81
+ clearTimeout(timer)
82
+ process.removeListener("SIGINT", onSigint)
83
+ process.removeListener("SIGTERM", onSigterm)
84
+ resolveRun({ exitCode, signal, timedOut, interrupted, error })
85
+ })
86
+ })
87
+ const { size } = await log.stat()
88
+ const buffer = Buffer.alloc(Math.min(size, 4096))
89
+ // ponytail: bounded head/tail is an excerpt, never a substitute for the retained full log.
90
+ let excerpt
91
+ if (size <= 4096) {
92
+ const { bytesRead } = await log.read(buffer, 0, buffer.length, 0)
93
+ excerpt = buffer.subarray(0, bytesRead).toString("utf8")
94
+ } else {
95
+ await log.read(buffer, 0, 2048, 0)
96
+ await log.read(buffer, 2048, 2048, size - 2048)
97
+ excerpt = buffer.subarray(0, 2048).toString("utf8") + "\n[... omitted; read full output.log ...]\n" + buffer.subarray(2048).toString("utf8")
98
+ }
99
+ return {
100
+ command: "npm run check", passed: result.exitCode === 0 && !result.timedOut && !result.interrupted && !result.error,
101
+ ...result, elapsedMs: Math.round(performance.now() - start),
102
+ log: { path, bytes: size, excerpt, truncated: size > 4096 },
103
+ browserVerified: false, note: "Fresh check result only; later edits invalidate it. Compiler success is not browser or accessibility proof.",
104
+ }
105
+ } finally {
106
+ await log.close()
107
+ }
108
+ }
109
+
110
+ if (process.argv[1] && await realpath(process.argv[1]) === fileURLToPath(import.meta.url)) {
111
+ try {
112
+ const { positionals, values } = parseArgs({ allowPositionals: true, strict: true, options: { "timeout-ms": { type: "string" } } })
113
+ const root = dirname(fileURLToPath(import.meta.url)), [command, heading] = positionals
114
+ let result
115
+ if (command === "docs" && positionals.length <= 2 && values["timeout-ms"] === undefined) result = await docs(root, heading)
116
+ else if (command === "check" && positionals.length === 1) {
117
+ if (values["timeout-ms"] !== undefined && !/^\d+$/.test(values["timeout-ms"])) throw new Error("timeout-ms must be a decimal integer")
118
+ result = await check(root, values["timeout-ms"] === undefined ? undefined : Number(values["timeout-ms"]))
119
+ if (!result.passed) process.exitCode = result.exitCode || 1
120
+ } else throw new Error("Use: npm run ai -- docs [heading] | npm run ai -- check [--timeout-ms 300000]")
121
+ process.stdout.write(JSON.stringify(result) + "\n")
122
+ } catch (error) {
123
+ process.stderr.write(JSON.stringify({ error: error.message }) + "\n")
124
+ process.exitCode = 1
125
+ }
126
+ }
package/index.mjs CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawnSync } from "node:child_process"
4
- import { mkdir, readdir, writeFile } from "node:fs/promises"
4
+ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"
5
5
  import { basename, resolve } from "node:path"
6
6
 
7
7
  const args = process.argv.slice(2)
8
8
  const skipInstall = args.includes("--no-install")
9
+ const ai = args.includes("--ai")
9
10
  const target = args.find(argument => !argument.startsWith("-")) ?? "kudzu-app"
10
11
  const root = resolve(target)
11
12
 
@@ -33,7 +34,7 @@ const files = {
33
34
  check: "tsc --noEmit && kudzu build"
34
35
  },
35
36
  dependencies: {
36
- "@kudzujs/core": "^0.16.30"
37
+ "@kudzujs/core": "^0.16.36"
37
38
  },
38
39
  devDependencies: {
39
40
  typescript: "^5.9.2"
@@ -233,6 +234,31 @@ Documentation: https://kudzujs.cloud/docs
233
234
  `
234
235
  }
235
236
 
237
+ if (ai) {
238
+ files["kudzu-ai.mjs"] = await readFile(new URL("./ai.mjs", import.meta.url), "utf8")
239
+ const manifest = JSON.parse(files["package.json"])
240
+ manifest.scripts.ai = "node kudzu-ai.mjs"
241
+ files["package.json"] = `${JSON.stringify(manifest, null, 2)}\n`
242
+ files[".gitignore"] += ".kudzu-ai/\n"
243
+ files["AGENTS.md"] = `# Kudzu application
244
+
245
+ ## Write
246
+ - This is a Kudzu app, not the compiler repository. Routes are in src/pages; index.tsx maps to /. Reuse the existing components, data, and CSS relevant to the task.
247
+ - Use ordinary function components and declarative TSX. Import hooks from @kudzujs/core. React-shaped syntax is compiler input, not full React runtime compatibility; do not add React, hydration, or a client router as a workaround.
248
+ - Prefer native HTML controls, anchors, and events. Read input values through event.currentTarget.value inside the handler. State setters update logical state immediately; DOM writes are batched.
249
+
250
+ ## Find answers when needed
251
+ - npm run ai -- docs lists installed-version README headings. npm run ai -- docs Authoring reads one section. Full local guide: node_modules/@kudzujs/core/README.md; public types: node_modules/@kudzujs/core/framework/core.d.ts. Read what the task needs.
252
+ - Full guide and current limits: https://kudzujs.cloud/docs. Online docs can differ from the installed version; use local build diagnostics to confirm support.
253
+
254
+ ## Verify
255
+ - npm run ai -- check runs npm run check (typecheck + build) with a timeout, JSON status, bounded excerpt, and full log in .kudzu-ai. Read that log if the excerpt is insufficient. This already performs the check; subsequent edits require a fresh check. npm run dev starts the dev server. Fix source, not generated output.
256
+ - Build success does not prove browser behavior or accessibility. Exercise affected interactions in a browser and check labels, keyboard use, and visible results. Report unavailable checks instead of claiming a pass.
257
+ - Static routes must remain JavaScript-free. Raw HTML text scans and compiler reports are not proof of visible DOM or complete script exclusion. Preserve required checks; avoid repeatedly dumping minified assets.
258
+ `
259
+ files["README.md"] += "\nAI authoring guidance: [AGENTS.md](AGENTS.md). Start a supporting coding agent from this project root.\n\nRun `npm run ai -- docs` to list installed README headings, `npm run ai -- docs Authoring` to read a section, and `npm run ai -- check` to typecheck/build with a bounded result. Full check logs stay in `.kudzu-ai/`; remove that directory when no longer needed. Check results do not verify browser behavior.\n"
260
+ }
261
+
236
262
  await Promise.all(Object.entries(files).map(async ([file, content]) => {
237
263
  await writeFile(resolve(root, file), content)
238
264
  }))
@@ -243,3 +269,4 @@ if (!skipInstall) {
243
269
  }
244
270
 
245
271
  console.log(`\nCreated ${name} in ${root}${skipInstall ? "" : " with dependencies installed"}\n\n cd ${target}\n${skipInstall ? " npm install\n" : ""} npm run dev\n`)
272
+ if (ai) console.log("AI authoring guidance: AGENTS.md. Start OpenCode or Codex from the project root in a new session.")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-kudzu",
3
- "version": "0.1.155",
3
+ "version": "0.1.157",
4
4
  "description": "Create a Kudzu project",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -18,11 +18,12 @@
18
18
  },
19
19
  "files": [
20
20
  "index.mjs",
21
+ "ai.mjs",
21
22
  "README.md",
22
23
  "LICENSE"
23
24
  ],
24
25
  "scripts": {
25
- "test": "node --test ../../test/create-kudzu.test.mjs",
26
+ "test": "node --test ../../test/create-kudzu.test.mjs ../../test/create-kudzu-ai.test.mjs",
26
27
  "prepublishOnly": "npm test"
27
28
  },
28
29
  "keywords": [