@getcoherent/cli 0.6.25 → 0.6.26

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.
@@ -3,13 +3,234 @@ import {
3
3
  getDesignQualityForType,
4
4
  inferPageTypeFromRoute
5
5
  } from "./chunk-5AHG4NNX.js";
6
+ import {
7
+ autoFixCode
8
+ } from "./chunk-H644LLXJ.js";
6
9
 
7
10
  // src/commands/chat/plan-generator.ts
8
11
  import { z } from "zod";
9
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
12
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
10
13
  import { resolve } from "path";
11
14
  import chalk from "chalk";
12
15
  import { generateSharedComponent } from "@getcoherent/core";
16
+
17
+ // src/utils/self-heal.ts
18
+ import { existsSync, readFileSync } from "fs";
19
+ import { join } from "path";
20
+ import { readdir, readFile } from "fs/promises";
21
+ import { execSync } from "child_process";
22
+ var COHERENT_REQUIRED_PACKAGES = [
23
+ "lucide-react",
24
+ "class-variance-authority",
25
+ "clsx",
26
+ "tailwind-merge",
27
+ "@radix-ui/react-slot"
28
+ ];
29
+ var IMPORT_FROM_REGEX = /from\s+['"]([^'"]+)['"]/g;
30
+ var NODE_BUILTINS = /* @__PURE__ */ new Set([
31
+ "assert",
32
+ "async_hooks",
33
+ "buffer",
34
+ "child_process",
35
+ "cluster",
36
+ "console",
37
+ "constants",
38
+ "crypto",
39
+ "dgram",
40
+ "diagnostics_channel",
41
+ "dns",
42
+ "domain",
43
+ "events",
44
+ "fs",
45
+ "http",
46
+ "http2",
47
+ "https",
48
+ "inspector",
49
+ "module",
50
+ "net",
51
+ "os",
52
+ "path",
53
+ "perf_hooks",
54
+ "process",
55
+ "punycode",
56
+ "querystring",
57
+ "readline",
58
+ "repl",
59
+ "stream",
60
+ "string_decoder",
61
+ "sys",
62
+ "test",
63
+ "timers",
64
+ "tls",
65
+ "trace_events",
66
+ "tty",
67
+ "url",
68
+ "util",
69
+ "v8",
70
+ "vm",
71
+ "wasi",
72
+ "worker_threads",
73
+ "zlib",
74
+ "fs/promises",
75
+ "path/posix",
76
+ "path/win32",
77
+ "stream/promises",
78
+ "stream/web",
79
+ "timers/promises",
80
+ "util/types"
81
+ ]);
82
+ function extractNpmPackagesFromCode(code) {
83
+ if (typeof code !== "string") return /* @__PURE__ */ new Set();
84
+ const pkgs = /* @__PURE__ */ new Set();
85
+ let m;
86
+ IMPORT_FROM_REGEX.lastIndex = 0;
87
+ while ((m = IMPORT_FROM_REGEX.exec(code)) !== null) {
88
+ const spec = m[1];
89
+ if (spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("@/") || spec === "next") continue;
90
+ if (spec.startsWith("node:") || NODE_BUILTINS.has(spec)) continue;
91
+ const pkg = spec.startsWith("@") ? spec.split("/").slice(0, 2).join("/") : spec.split("/")[0];
92
+ if (pkg && !NODE_BUILTINS.has(pkg)) pkgs.add(pkg);
93
+ }
94
+ return pkgs;
95
+ }
96
+ function getInstalledPackages(projectRoot) {
97
+ const pkgPath = join(projectRoot, "package.json");
98
+ if (!existsSync(pkgPath)) return /* @__PURE__ */ new Set();
99
+ try {
100
+ const json = JSON.parse(readFileSync(pkgPath, "utf-8"));
101
+ const deps = { ...json.dependencies ?? {}, ...json.devDependencies ?? {} };
102
+ return new Set(Object.keys(deps));
103
+ } catch (e) {
104
+ if (process.env.COHERENT_DEBUG === "1") console.error("Failed to read package.json:", e);
105
+ return /* @__PURE__ */ new Set();
106
+ }
107
+ }
108
+ async function collectImportedPackages(dir, extensions) {
109
+ const packages = /* @__PURE__ */ new Set();
110
+ if (!existsSync(dir)) return packages;
111
+ async function walk(d) {
112
+ let entries;
113
+ try {
114
+ entries = await readdir(d, { withFileTypes: true });
115
+ } catch {
116
+ return;
117
+ }
118
+ for (const e of entries) {
119
+ const full = join(d, e.name);
120
+ if (e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules") {
121
+ await walk(full);
122
+ continue;
123
+ }
124
+ if (!e.isFile()) continue;
125
+ const ext = e.name.replace(/^.*\./, "");
126
+ if (!extensions.has(ext)) continue;
127
+ const content = await readFile(full, "utf-8").catch(() => "");
128
+ extractNpmPackagesFromCode(content).forEach((p) => packages.add(p));
129
+ }
130
+ }
131
+ await walk(dir);
132
+ return packages;
133
+ }
134
+ async function findMissingPackages(projectRoot, dirs = ["app", "components"]) {
135
+ const installed = getInstalledPackages(projectRoot);
136
+ const required = new Set(COHERENT_REQUIRED_PACKAGES);
137
+ const imported = /* @__PURE__ */ new Set();
138
+ const extensions = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx"]);
139
+ for (const d of dirs) {
140
+ const abs = join(projectRoot, d);
141
+ const pkgs = await collectImportedPackages(abs, extensions);
142
+ pkgs.forEach((p) => imported.add(p));
143
+ }
144
+ const needed = /* @__PURE__ */ new Set([...required, ...imported]);
145
+ return [...needed].filter((p) => !installed.has(p)).sort();
146
+ }
147
+ function findMissingPackagesInCode(code, projectRoot) {
148
+ const installed = getInstalledPackages(projectRoot);
149
+ const required = new Set(COHERENT_REQUIRED_PACKAGES);
150
+ const fromCode = extractNpmPackagesFromCode(code);
151
+ const needed = /* @__PURE__ */ new Set([...required, ...fromCode]);
152
+ return [...needed].filter((p) => !installed.has(p)).sort();
153
+ }
154
+ var SAFE_PKG_NAME = /^(@[a-z0-9._-]+\/)?[a-z0-9._-]+$/;
155
+ function installPackages(projectRoot, packages) {
156
+ if (packages.length === 0) return Promise.resolve(true);
157
+ const safe = packages.filter((p) => SAFE_PKG_NAME.test(p));
158
+ if (safe.length === 0) return Promise.resolve(true);
159
+ return new Promise((resolve2) => {
160
+ try {
161
+ const hasPnpm = existsSync(join(projectRoot, "pnpm-lock.yaml"));
162
+ if (hasPnpm) {
163
+ execSync(`pnpm add ${safe.join(" ")}`, { cwd: projectRoot, stdio: "pipe" });
164
+ } else {
165
+ execSync(`npm install --legacy-peer-deps ${safe.join(" ")}`, {
166
+ cwd: projectRoot,
167
+ stdio: "pipe"
168
+ });
169
+ }
170
+ resolve2(true);
171
+ } catch (e) {
172
+ if (process.env.COHERENT_DEBUG === "1") console.error("Failed to install packages:", e);
173
+ resolve2(false);
174
+ }
175
+ });
176
+ }
177
+ var CLIENT_HOOKS = /\b(useState|useEffect|useRef|useContext|useReducer|useCallback|useMemo|useId|useTransition|useDeferredValue)\s*\(/;
178
+ var CLIENT_EVENTS = /\b(onClick|onChange|onSubmit|onBlur|onFocus|onKeyDown|onKeyUp|onMouseEnter|onMouseLeave|onScroll|onInput)\s*[={]/;
179
+ function stripMetadataFromCode(code) {
180
+ const match = code.match(/\bexport\s+const\s+metadata\s*:\s*Metadata\s*=\s*\{/);
181
+ if (!match) return code;
182
+ const start = code.indexOf(match[0]);
183
+ const open = code.indexOf("{", start);
184
+ if (open === -1) return code;
185
+ let depth = 1;
186
+ let i = open + 1;
187
+ while (i < code.length && depth > 0) {
188
+ const c = code[i];
189
+ if (c === "{") depth++;
190
+ else if (c === "}") depth--;
191
+ i++;
192
+ }
193
+ const end = i;
194
+ const tail = code.slice(end);
195
+ const semicolon = tail.match(/^\s*;/);
196
+ const removeEnd = semicolon ? end + (semicolon.index + semicolon[0].length) : end;
197
+ return (code.slice(0, start) + code.slice(removeEnd)).replace(/\n{3,}/g, "\n\n").trim();
198
+ }
199
+ function ensureUseClientIfNeeded(code) {
200
+ const trimmed = code.trimStart();
201
+ const hasUseClient = trimmed.startsWith("'use client'") || trimmed.startsWith('"use client"');
202
+ const needsUseClient = CLIENT_HOOKS.test(code) || CLIENT_EVENTS.test(code);
203
+ let out = code;
204
+ if (hasUseClient || needsUseClient) {
205
+ out = stripMetadataFromCode(out);
206
+ if (needsUseClient && !hasUseClient) out = `'use client'
207
+
208
+ ${out}`;
209
+ }
210
+ return out;
211
+ }
212
+ function sanitizeMetadataStrings(code) {
213
+ let out = code.replace(/\\'(\s*[}\],])/g, "'$1");
214
+ out = out.replace(/(:\s*'.+)\\'(\s*)$/gm, "$1'$2");
215
+ for (const key of ["description", "title"]) {
216
+ const re = new RegExp(`\\b${key}:\\s*'((?:[^'\\\\]|'(?![,}]))*)'`, "gs");
217
+ out = out.replace(re, (_, inner) => `${key}: '${inner.replace(/'/g, "\\'")}'`);
218
+ }
219
+ return out;
220
+ }
221
+ function fixEscapedClosingQuotes(code) {
222
+ let out = code.replace(/\\'(\s*[}\],])/g, "'$1");
223
+ out = out.replace(/(:\s*'.+)\\'(\s*)$/gm, "$1'$2");
224
+ return out;
225
+ }
226
+ function fixUnescapedLtInJsx(code) {
227
+ let out = code;
228
+ out = out.replace(/>([^<\n]*)<(\d)/g, ">$1&lt;$2");
229
+ out = out.replace(/>([^<\n]*)<([^/a-zA-Z!{>\n])/g, ">$1&lt;$2");
230
+ return out;
231
+ }
232
+
233
+ // src/commands/chat/plan-generator.ts
13
234
  var LAYOUT_SYNONYMS = {
14
235
  horizontal: "header",
15
236
  top: "header",
@@ -222,9 +443,9 @@ function savePlan(projectRoot, plan) {
222
443
  function loadPlan(projectRoot) {
223
444
  const planPath = resolve(projectRoot, ".coherent", "plan.json");
224
445
  if (cachedPlan?.path === planPath) return cachedPlan.plan;
225
- if (!existsSync(planPath)) return null;
446
+ if (!existsSync2(planPath)) return null;
226
447
  try {
227
- const raw = JSON.parse(readFileSync(planPath, "utf-8"));
448
+ const raw = JSON.parse(readFileSync2(planPath, "utf-8"));
228
449
  const parsed = ArchitecturePlanSchema.safeParse(raw);
229
450
  if (!parsed.success) return null;
230
451
  cachedPlan = { path: planPath, plan: parsed.data };
@@ -320,12 +541,15 @@ Return JSON with { requests: [{ type: "add-page", changes: { name: "ComponentNam
320
541
  }
321
542
  for (const comp of results) {
322
543
  const planned = plan.sharedComponents.find((c) => c.name === comp.name);
323
- const propsInterface = extractPropsInterface(comp.code, comp.name);
324
- const usageExample = extractUsageExample(comp.code, comp.name);
544
+ const { code: fixedCode } = await autoFixCode(comp.code);
545
+ const missing = findMissingPackagesInCode(fixedCode, projectRoot);
546
+ if (missing.length > 0) await installPackages(projectRoot, missing);
547
+ const propsInterface = extractPropsInterface(fixedCode, comp.name);
548
+ const usageExample = extractUsageExample(fixedCode, comp.name);
325
549
  await generateSharedComponent(projectRoot, {
326
550
  name: comp.name,
327
551
  type: planned?.type ?? "section",
328
- code: comp.code,
552
+ code: fixedCode,
329
553
  description: planned?.description,
330
554
  usedIn: planned?.usedBy ?? [],
331
555
  source: "generated",
@@ -338,6 +562,16 @@ Return JSON with { requests: [{ type: "add-page", changes: { name: "ComponentNam
338
562
  }
339
563
 
340
564
  export {
565
+ COHERENT_REQUIRED_PACKAGES,
566
+ extractNpmPackagesFromCode,
567
+ getInstalledPackages,
568
+ findMissingPackages,
569
+ findMissingPackagesInCode,
570
+ installPackages,
571
+ ensureUseClientIfNeeded,
572
+ sanitizeMetadataStrings,
573
+ fixEscapedClosingQuotes,
574
+ fixUnescapedLtInJsx,
341
575
  RouteGroupSchema,
342
576
  PlannedComponentSchema,
343
577
  PageNoteSchema,