@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<$2");
|
|
229
|
+
out = out.replace(/>([^<\n]*)<([^/a-zA-Z!{>\n])/g, ">$1<$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 (!
|
|
446
|
+
if (!existsSync2(planPath)) return null;
|
|
226
447
|
try {
|
|
227
|
-
const raw = JSON.parse(
|
|
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
|
|
324
|
-
const
|
|
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:
|
|
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,
|