create-kudzu 0.1.155 → 0.1.156
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 +72 -1
- package/ai.mjs +112 -0
- package/index.mjs +29 -2
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -10,6 +10,77 @@ 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.
|
|
13
|
+
The generated project is a working showcase on `@kudzujs/core@^0.16.34` 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.156` adds optional local AI developer tools:
|
|
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
|
+
- Full stdout/stderr are retained in a unique `.kudzu-ai/check-*/output.log`.
|
|
57
|
+
Large logs show the first/last 2,048 bytes with an omission marker; an error may
|
|
58
|
+
be in the omitted middle. Read the full log when needed. Old logs are not deleted
|
|
59
|
+
automatically; remove `.kudzu-ai/` when no longer needed.
|
|
60
|
+
- The tool does not cache checks or certify browser behavior. Later edits require
|
|
61
|
+
a new check. Scripts that intentionally detach background jobs or custom
|
|
62
|
+
deployment rules need their own lifecycle/asset handling. Use
|
|
63
|
+
`npm run --silent ai -- check` to suppress npm's outer lifecycle banner.
|
|
64
|
+
|
|
65
|
+
Start a new [OpenCode](https://opencode.ai/docs/rules/) or
|
|
66
|
+
[Codex](https://developers.openai.com/codex/agent-configuration/agents-md) session
|
|
67
|
+
from the app root to use their documented project-instruction discovery. Other
|
|
68
|
+
agents need support for `AGENTS.md` or an explicit request to read it. Editor
|
|
69
|
+
installation alone does not load instructions. Overrides, disabled discovery,
|
|
70
|
+
context limits, and headless modes can change loading; verify the effective
|
|
71
|
+
instructions in the agent you actually use.
|
|
72
|
+
|
|
73
|
+
AGENTS.md supplies guidance; kudzu-ai.mjs performs real document lookup and check
|
|
74
|
+
execution. The host's existing shell tool can invoke it without an MCP connection
|
|
75
|
+
or plugin. It does not intercept every agent action or force use of these commands.
|
|
76
|
+
There is no additional dependency, agent installation, or model call for these
|
|
77
|
+
tools. Both files stay outside `src` and `public` and are excluded by the default
|
|
78
|
+
Kudzu build. Custom asset-copy/deployment settings can still publish root files.
|
|
79
|
+
Instructions and tool responses add input context; lower total AI cost is unmeasured.
|
|
80
|
+
|
|
81
|
+
For an existing app, generate a disposable `--ai --no-install` project, copy
|
|
82
|
+
`kudzu-ai.mjs`, add `"ai": "node kudzu-ai.mjs"` to your scripts and `.kudzu-ai/` to
|
|
83
|
+
your ignore rules, and merge the applicable instructions into your existing rules.
|
|
84
|
+
Keep your real `check` script; do not make it invoke `ai check` recursively.
|
|
85
|
+
The generator rejects nonempty targets. Generated tools/rules are snapshots, not
|
|
86
|
+
auto-updated when Kudzu is upgraded; review them with your existing app setup.
|
package/ai.mjs
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
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
|
+
try {
|
|
45
|
+
const result = await new Promise(resolveRun => {
|
|
46
|
+
const child = spawn(process.platform === "win32" ? "npm.cmd" : "npm", ["run", "check"], {
|
|
47
|
+
cwd: root, detached: process.platform !== "win32", shell: process.platform === "win32",
|
|
48
|
+
stdio: ["ignore", log.fd, log.fd], env: { ...process.env, FORCE_COLOR: "0", KUDZU_AI_CHECK_ROOT: root },
|
|
49
|
+
})
|
|
50
|
+
let timedOut = false, interrupted = null, error = null
|
|
51
|
+
const terminate = () => {
|
|
52
|
+
if (!child.pid) return
|
|
53
|
+
if (process.platform === "win32") {
|
|
54
|
+
const killed = spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { encoding: "utf8" })
|
|
55
|
+
if (killed.status !== 0) error = killed.error?.message ?? killed.stderr ?? "Could not terminate check tree"
|
|
56
|
+
} else {
|
|
57
|
+
try { process.kill(-child.pid, "SIGKILL") } catch (failure) { if (failure.code !== "ESRCH") error = failure.message }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const onInterrupt = signal => { interrupted = signal; terminate() }
|
|
61
|
+
const onSigint = () => onInterrupt("SIGINT"), onSigterm = () => onInterrupt("SIGTERM")
|
|
62
|
+
process.once("SIGINT", onSigint)
|
|
63
|
+
process.once("SIGTERM", onSigterm)
|
|
64
|
+
const timer = setTimeout(() => { timedOut = true; terminate() }, timeoutMs)
|
|
65
|
+
child.once("error", failure => { error = failure.message })
|
|
66
|
+
child.once("close", (exitCode, signal) => {
|
|
67
|
+
clearTimeout(timer)
|
|
68
|
+
process.removeListener("SIGINT", onSigint)
|
|
69
|
+
process.removeListener("SIGTERM", onSigterm)
|
|
70
|
+
resolveRun({ exitCode, signal, timedOut, interrupted, error })
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
const { size } = await log.stat()
|
|
74
|
+
const buffer = Buffer.alloc(Math.min(size, 4096))
|
|
75
|
+
// ponytail: bounded head/tail is an excerpt, never a substitute for the retained full log.
|
|
76
|
+
let excerpt
|
|
77
|
+
if (size <= 4096) {
|
|
78
|
+
const { bytesRead } = await log.read(buffer, 0, buffer.length, 0)
|
|
79
|
+
excerpt = buffer.subarray(0, bytesRead).toString("utf8")
|
|
80
|
+
} else {
|
|
81
|
+
await log.read(buffer, 0, 2048, 0)
|
|
82
|
+
await log.read(buffer, 2048, 2048, size - 2048)
|
|
83
|
+
excerpt = buffer.subarray(0, 2048).toString("utf8") + "\n[... omitted; read full output.log ...]\n" + buffer.subarray(2048).toString("utf8")
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
command: "npm run check", passed: result.exitCode === 0 && !result.timedOut && !result.interrupted && !result.error,
|
|
87
|
+
...result, elapsedMs: Math.round(performance.now() - start),
|
|
88
|
+
log: { path, bytes: size, excerpt, truncated: size > 4096 },
|
|
89
|
+
browserVerified: false, note: "Fresh check result only; later edits invalidate it. Compiler success is not browser or accessibility proof.",
|
|
90
|
+
}
|
|
91
|
+
} finally {
|
|
92
|
+
await log.close()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (process.argv[1] && await realpath(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
97
|
+
try {
|
|
98
|
+
const { positionals, values } = parseArgs({ allowPositionals: true, strict: true, options: { "timeout-ms": { type: "string" } } })
|
|
99
|
+
const root = dirname(fileURLToPath(import.meta.url)), [command, heading] = positionals
|
|
100
|
+
let result
|
|
101
|
+
if (command === "docs" && positionals.length <= 2 && values["timeout-ms"] === undefined) result = await docs(root, heading)
|
|
102
|
+
else if (command === "check" && positionals.length === 1) {
|
|
103
|
+
if (values["timeout-ms"] !== undefined && !/^\d+$/.test(values["timeout-ms"])) throw new Error("timeout-ms must be a decimal integer")
|
|
104
|
+
result = await check(root, values["timeout-ms"] === undefined ? undefined : Number(values["timeout-ms"]))
|
|
105
|
+
if (!result.passed) process.exitCode = result.exitCode || 1
|
|
106
|
+
} else throw new Error("Use: npm run ai -- docs [heading] | npm run ai -- check [--timeout-ms 300000]")
|
|
107
|
+
process.stdout.write(JSON.stringify(result) + "\n")
|
|
108
|
+
} catch (error) {
|
|
109
|
+
process.stderr.write(JSON.stringify({ error: error.message }) + "\n")
|
|
110
|
+
process.exitCode = 1
|
|
111
|
+
}
|
|
112
|
+
}
|
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.
|
|
37
|
+
"@kudzujs/core": "^0.16.34"
|
|
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.
|
|
3
|
+
"version": "0.1.156",
|
|
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": [
|