create-kudzu 0.1.156 → 0.1.158

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 +11 -5
  2. package/ai.mjs +45 -11
  3. package/index.mjs +1 -1
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -10,13 +10,13 @@ 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.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.
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
16
 
17
17
  ## Optional AI authoring guidance
18
18
 
19
- `create-kudzu@0.1.156` adds optional local AI developer tools:
19
+ `create-kudzu@0.1.158` provides optional local AI developer tools with managed check cancellation:
20
20
 
21
21
  ```bash
22
22
  npm create kudzu@latest my-app -- --ai
@@ -53,10 +53,16 @@ npm run ai -- check --timeout-ms 600000
53
53
  log excerpt. Failure stays nonzero. The default timeout is five minutes; choose
54
54
  an integer from 1 to 1,200,000 ms. Timeout/cancellation terminates the spawned
55
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.
56
61
  - 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.
62
+ Long successful checks return only the last 512 bytes. Failed checks return
63
+ bounded head/tail excerpts and the first recognized diagnostic in the first
64
+ MiB when present. Read the full log if the excerpt is insufficient. Old logs
65
+ are not deleted automatically; remove `.kudzu-ai/` when no longer needed.
60
66
  - The tool does not cache checks or certify browser behavior. Later edits require
61
67
  a new check. Scripts that intentionally detach background jobs or custom
62
68
  deployment rules need their own lifecycle/asset handling. Use
package/ai.mjs CHANGED
@@ -41,20 +41,34 @@ export async function check(root, timeoutMs = 300000) {
41
41
  const directory = await mkdtemp(join(logs, "check-")), path = join(directory, "output.log")
42
42
  const log = await open(path, "wx+")
43
43
  const start = performance.now()
44
+ const managed = process.env.KUDZU_AI_DELIVERY_GROUP === "1"
44
45
  try {
45
46
  const result = await new Promise(resolveRun => {
46
47
  const child = spawn(process.platform === "win32" ? "npm.cmd" : "npm", ["run", "check"], {
47
- cwd: root, detached: process.platform !== "win32", shell: process.platform === "win32",
48
+ cwd: root, detached: process.platform !== "win32" && !managed, shell: process.platform === "win32",
48
49
  stdio: ["ignore", log.fd, log.fd], env: { ...process.env, FORCE_COLOR: "0", KUDZU_AI_CHECK_ROOT: root },
49
50
  })
50
51
  let timedOut = false, interrupted = null, error = null
51
52
  const terminate = () => {
52
- if (!child.pid) return
53
+ if (!child.pid || child.exitCode !== null || child.signalCode !== null) return
53
54
  if (process.platform === "win32") {
54
55
  const killed = spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { encoding: "utf8" })
55
56
  if (killed.status !== 0) error = killed.error?.message ?? killed.stderr ?? "Could not terminate check tree"
56
57
  } else {
57
- try { process.kill(-child.pid, "SIGKILL") } catch (failure) { if (failure.code !== "ESRCH") error = failure.message }
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 }
58
72
  }
59
73
  }
60
74
  const onInterrupt = signal => { interrupted = signal; terminate() }
@@ -71,21 +85,41 @@ export async function check(root, timeoutMs = 300000) {
71
85
  })
72
86
  })
73
87
  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.
88
+ const passed = result.exitCode === 0 && !result.timedOut && !result.interrupted && !result.error
89
+ const limit = passed ? 512 : 4096
90
+ const buffer = Buffer.alloc(Math.min(size, limit))
91
+ // ponytail: a bounded excerpt never substitutes for the retained full log.
76
92
  let excerpt
77
- if (size <= 4096) {
93
+ if (size <= limit) {
78
94
  const { bytesRead } = await log.read(buffer, 0, buffer.length, 0)
79
95
  excerpt = buffer.subarray(0, bytesRead).toString("utf8")
96
+ } else if (passed) {
97
+ await log.read(buffer, 0, limit, size - limit)
98
+ excerpt = buffer.toString("utf8")
80
99
  } 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")
100
+ const middle = Buffer.alloc(8192)
101
+ let diagnostic = ""
102
+ // ponytail: scan at most the first MiB for a useful error; the full log stays on disk.
103
+ for (let offset = 2048; offset < Math.min(size - 2048, 1 << 20); offset += middle.length - 128) {
104
+ const length = Math.min(middle.length, size - 2048 - offset)
105
+ await log.read(middle, 0, length, offset)
106
+ const text = middle.subarray(0, length).toString("utf8")
107
+ const match = /(?:\berror(?: TS\d{4})?|\bfailed)\s*:|\bTS\d{4}\s*:|✖/i.exec(text)
108
+ if (!match) continue
109
+ const start = Math.max(text.lastIndexOf("\n", match.index) + 1, match.index - 120)
110
+ const end = text.indexOf("\n", match.index)
111
+ diagnostic = text.slice(start, end < 0 ? match.index + 240 : Math.min(end, match.index + 240)).trim()
112
+ break
113
+ }
114
+ const edge = diagnostic ? 1536 : 2048
115
+ await log.read(buffer, 0, edge, 0)
116
+ await log.read(buffer, edge, edge, size - edge)
117
+ excerpt = buffer.subarray(0, edge).toString("utf8") + "\n[... omitted; read full output.log ...]\n" + (diagnostic ? diagnostic.slice(0, 400) + "\n[... omitted ...]\n" : "") + buffer.subarray(edge, edge * 2).toString("utf8")
84
118
  }
85
119
  return {
86
- command: "npm run check", passed: result.exitCode === 0 && !result.timedOut && !result.interrupted && !result.error,
120
+ command: "npm run check", passed,
87
121
  ...result, elapsedMs: Math.round(performance.now() - start),
88
- log: { path, bytes: size, excerpt, truncated: size > 4096 },
122
+ log: { path, bytes: size, excerpt, truncated: size > limit },
89
123
  browserVerified: false, note: "Fresh check result only; later edits invalidate it. Compiler success is not browser or accessibility proof.",
90
124
  }
91
125
  } finally {
package/index.mjs CHANGED
@@ -34,7 +34,7 @@ const files = {
34
34
  check: "tsc --noEmit && kudzu build"
35
35
  },
36
36
  dependencies: {
37
- "@kudzujs/core": "^0.16.34"
37
+ "@kudzujs/core": "^0.16.36"
38
38
  },
39
39
  devDependencies: {
40
40
  typescript: "^5.9.2"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-kudzu",
3
- "version": "0.1.156",
3
+ "version": "0.1.158",
4
4
  "description": "Create a Kudzu project",
5
5
  "type": "module",
6
6
  "license": "MIT",