@usesidekick/cli 0.1.0
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/dist/index.d.ts +2 -0
- package/dist/index.js +1485 -0
- package/dist/index.js.map +1 -0
- package/package.json +32 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1485 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import * as path4 from "path";
|
|
6
|
+
import * as fs4 from "fs/promises";
|
|
7
|
+
|
|
8
|
+
// src/analyze.ts
|
|
9
|
+
import * as ts from "typescript";
|
|
10
|
+
import * as path from "path";
|
|
11
|
+
import { glob } from "glob";
|
|
12
|
+
import * as fs from "fs/promises";
|
|
13
|
+
|
|
14
|
+
// src/types.ts
|
|
15
|
+
var ALLOWED_IMPORTS = [
|
|
16
|
+
"@usesidekick/react",
|
|
17
|
+
"react"
|
|
18
|
+
];
|
|
19
|
+
var FORBIDDEN_PATTERNS = [
|
|
20
|
+
/\bfetch\s*\(/,
|
|
21
|
+
/\beval\s*\(/,
|
|
22
|
+
/\bFunction\s*\(/,
|
|
23
|
+
/\bdocument\./,
|
|
24
|
+
/\bwindow\./,
|
|
25
|
+
/\blocalStorage\./,
|
|
26
|
+
/\bsessionStorage\./,
|
|
27
|
+
/\bXMLHttpRequest/,
|
|
28
|
+
/\bWebSocket/,
|
|
29
|
+
/\bimport\s*\(/,
|
|
30
|
+
/require\s*\(/
|
|
31
|
+
];
|
|
32
|
+
var SDK_PRIMITIVES = [
|
|
33
|
+
"ui.wrap",
|
|
34
|
+
"ui.replace",
|
|
35
|
+
"ui.addColumn",
|
|
36
|
+
"ui.renameColumn",
|
|
37
|
+
"ui.reorderColumns",
|
|
38
|
+
"ui.hideColumn",
|
|
39
|
+
"ui.filterRows",
|
|
40
|
+
"ui.addStyles",
|
|
41
|
+
"ui.setText",
|
|
42
|
+
"ui.setAttribute",
|
|
43
|
+
"ui.setStyle",
|
|
44
|
+
"ui.addClass",
|
|
45
|
+
"ui.removeClass",
|
|
46
|
+
"ui.inject",
|
|
47
|
+
"data.computed",
|
|
48
|
+
"data.addFilter",
|
|
49
|
+
"data.transform",
|
|
50
|
+
"data.intercept",
|
|
51
|
+
"behavior.addKeyboardShortcut",
|
|
52
|
+
"behavior.onDOMEvent",
|
|
53
|
+
"behavior.modifyRoute"
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
// src/analyze.ts
|
|
57
|
+
async function analyze(options) {
|
|
58
|
+
const { targetDir, outputPath } = options;
|
|
59
|
+
const files = await glob("**/*.{ts,tsx}", {
|
|
60
|
+
cwd: targetDir,
|
|
61
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/.next/**", "**/sidekick/overrides/**"],
|
|
62
|
+
absolute: true
|
|
63
|
+
});
|
|
64
|
+
const components = [];
|
|
65
|
+
const extensionPoints = [];
|
|
66
|
+
const dataModels = [];
|
|
67
|
+
const componentStyles = [];
|
|
68
|
+
for (const file of files) {
|
|
69
|
+
const content = await fs.readFile(file, "utf-8");
|
|
70
|
+
const relPath = path.relative(targetDir, file);
|
|
71
|
+
const sourceFile = ts.createSourceFile(
|
|
72
|
+
file,
|
|
73
|
+
content,
|
|
74
|
+
ts.ScriptTarget.Latest,
|
|
75
|
+
true,
|
|
76
|
+
file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS
|
|
77
|
+
);
|
|
78
|
+
const fileComponents = extractComponents(sourceFile, relPath, content);
|
|
79
|
+
components.push(...fileComponents);
|
|
80
|
+
const fileExtensionPoints = extractExtensionPoints(content, relPath);
|
|
81
|
+
extensionPoints.push(...fileExtensionPoints);
|
|
82
|
+
const fileDataModels = extractDataModels(sourceFile, relPath);
|
|
83
|
+
dataModels.push(...fileDataModels);
|
|
84
|
+
if (file.endsWith(".tsx")) {
|
|
85
|
+
const fileStyles = extractComponentStyles(sourceFile, relPath);
|
|
86
|
+
componentStyles.push(...fileStyles);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const designSystem = await extractDesignSystem(targetDir, componentStyles);
|
|
90
|
+
const schema = {
|
|
91
|
+
version: "1.0.0",
|
|
92
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
93
|
+
designSystem,
|
|
94
|
+
components,
|
|
95
|
+
extensionPoints: [...new Set(extensionPoints.map((ep) => ep.id))],
|
|
96
|
+
dataModels,
|
|
97
|
+
primitives: SDK_PRIMITIVES
|
|
98
|
+
};
|
|
99
|
+
if (outputPath) {
|
|
100
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
101
|
+
await fs.writeFile(outputPath, JSON.stringify(schema, null, 2));
|
|
102
|
+
}
|
|
103
|
+
return schema;
|
|
104
|
+
}
|
|
105
|
+
function extractComponents(sourceFile, relPath, content) {
|
|
106
|
+
const components = [];
|
|
107
|
+
function visit(node) {
|
|
108
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
109
|
+
const name = node.name.getText();
|
|
110
|
+
if (isComponentName(name)) {
|
|
111
|
+
const props = extractPropsFromFunction(node);
|
|
112
|
+
const extensible = hasExtensibleMarker(content, node.pos);
|
|
113
|
+
const renderStructure = extractRenderStructure(node) || void 0;
|
|
114
|
+
components.push({ name, file: relPath, props, extensible, renderStructure });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (ts.isVariableStatement(node)) {
|
|
118
|
+
const declarations = node.declarationList.declarations;
|
|
119
|
+
for (const decl of declarations) {
|
|
120
|
+
if (ts.isIdentifier(decl.name) && decl.initializer) {
|
|
121
|
+
const name = decl.name.getText();
|
|
122
|
+
if (isComponentName(name) && isReactComponent(decl.initializer)) {
|
|
123
|
+
const props = extractPropsFromArrowFunction(decl.initializer);
|
|
124
|
+
const extensible = hasExtensibleMarker(content, node.pos);
|
|
125
|
+
const renderStructure = extractRenderStructure(decl.initializer) || void 0;
|
|
126
|
+
components.push({ name, file: relPath, props, extensible, renderStructure });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
ts.forEachChild(node, visit);
|
|
132
|
+
}
|
|
133
|
+
visit(sourceFile);
|
|
134
|
+
return components;
|
|
135
|
+
}
|
|
136
|
+
function isComponentName(name) {
|
|
137
|
+
return /^[A-Z]/.test(name);
|
|
138
|
+
}
|
|
139
|
+
function isReactComponent(node) {
|
|
140
|
+
return ts.isArrowFunction(node) || ts.isFunctionExpression(node);
|
|
141
|
+
}
|
|
142
|
+
function extractPropsFromFunction(node) {
|
|
143
|
+
const props = [];
|
|
144
|
+
if (node.parameters.length > 0) {
|
|
145
|
+
const firstParam = node.parameters[0];
|
|
146
|
+
if (firstParam.type && ts.isTypeLiteralNode(firstParam.type)) {
|
|
147
|
+
for (const member of firstParam.type.members) {
|
|
148
|
+
if (ts.isPropertySignature(member) && member.name) {
|
|
149
|
+
props.push(member.name.getText());
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (ts.isObjectBindingPattern(firstParam.name)) {
|
|
154
|
+
for (const element of firstParam.name.elements) {
|
|
155
|
+
if (ts.isBindingElement(element) && ts.isIdentifier(element.name)) {
|
|
156
|
+
props.push(element.name.getText());
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return props;
|
|
162
|
+
}
|
|
163
|
+
function extractPropsFromArrowFunction(node) {
|
|
164
|
+
const props = [];
|
|
165
|
+
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
|
|
166
|
+
if (node.parameters.length > 0) {
|
|
167
|
+
const firstParam = node.parameters[0];
|
|
168
|
+
if (ts.isObjectBindingPattern(firstParam.name)) {
|
|
169
|
+
for (const element of firstParam.name.elements) {
|
|
170
|
+
if (ts.isBindingElement(element) && ts.isIdentifier(element.name)) {
|
|
171
|
+
props.push(element.name.getText());
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return props;
|
|
178
|
+
}
|
|
179
|
+
function hasExtensibleMarker(content, nodePos) {
|
|
180
|
+
const beforeNode = content.substring(Math.max(0, nodePos - 200), nodePos);
|
|
181
|
+
return /@sidekick:extensible/.test(beforeNode);
|
|
182
|
+
}
|
|
183
|
+
function extractRenderStructure(node) {
|
|
184
|
+
let body;
|
|
185
|
+
if (ts.isFunctionDeclaration(node) && node.body) {
|
|
186
|
+
body = node.body;
|
|
187
|
+
} else if (ts.isArrowFunction(node)) {
|
|
188
|
+
body = node.body;
|
|
189
|
+
} else if (ts.isFunctionExpression(node) && node.body) {
|
|
190
|
+
body = node.body;
|
|
191
|
+
}
|
|
192
|
+
if (!body) return null;
|
|
193
|
+
if (ts.isJsxElement(body) || ts.isJsxSelfClosingElement(body) || ts.isJsxFragment(body) || ts.isParenthesizedExpression(body)) {
|
|
194
|
+
const jsx = findJsxInExpression(body);
|
|
195
|
+
if (jsx) return summarizeJsx(jsx, 0);
|
|
196
|
+
}
|
|
197
|
+
const returnJsx = findReturnJsx(body);
|
|
198
|
+
if (returnJsx) return summarizeJsx(returnJsx, 0);
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
function findReturnJsx(node) {
|
|
202
|
+
if (ts.isReturnStatement(node) && node.expression) {
|
|
203
|
+
const jsx = findJsxInExpression(node.expression);
|
|
204
|
+
if (jsx) return jsx;
|
|
205
|
+
}
|
|
206
|
+
let result = null;
|
|
207
|
+
ts.forEachChild(node, (child) => {
|
|
208
|
+
if (!result) {
|
|
209
|
+
if (ts.isFunctionDeclaration(child) || ts.isArrowFunction(child) || ts.isFunctionExpression(child)) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
result = findReturnJsx(child);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
function findJsxInExpression(node) {
|
|
218
|
+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) {
|
|
219
|
+
return node;
|
|
220
|
+
}
|
|
221
|
+
if (ts.isParenthesizedExpression(node)) {
|
|
222
|
+
return findJsxInExpression(node.expression);
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
function summarizeJsx(node, depth) {
|
|
227
|
+
const MAX_DEPTH = 3;
|
|
228
|
+
if (depth > MAX_DEPTH) return "...";
|
|
229
|
+
if (ts.isJsxSelfClosingElement(node)) {
|
|
230
|
+
const tag = node.tagName.getText();
|
|
231
|
+
const cls = getClassNameAttr(node.attributes);
|
|
232
|
+
if (isComponentName(tag)) return `<${tag}/>`;
|
|
233
|
+
return cls ? `${tag}.${compactClass(cls)}` : tag;
|
|
234
|
+
}
|
|
235
|
+
if (ts.isJsxElement(node)) {
|
|
236
|
+
const tag = node.openingElement.tagName.getText();
|
|
237
|
+
const cls = getClassNameAttr(node.openingElement.attributes);
|
|
238
|
+
const tagStr = isComponentName(tag) ? `<${tag}>` : cls ? `${tag}.${compactClass(cls)}` : tag;
|
|
239
|
+
const childSummaries = [];
|
|
240
|
+
for (const child of node.children) {
|
|
241
|
+
if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child)) {
|
|
242
|
+
childSummaries.push(summarizeJsx(child, depth + 1));
|
|
243
|
+
} else if (ts.isJsxExpression(child) && child.expression) {
|
|
244
|
+
const expr = child.expression;
|
|
245
|
+
if (ts.isConditionalExpression(expr)) {
|
|
246
|
+
const whenTrue = findJsxInExpression(expr.whenTrue);
|
|
247
|
+
const whenFalse = findJsxInExpression(expr.whenFalse);
|
|
248
|
+
if (whenTrue) childSummaries.push(`{cond: ${summarizeJsx(whenTrue, depth + 1)}}`);
|
|
249
|
+
if (whenFalse) childSummaries.push(summarizeJsx(whenFalse, depth + 1));
|
|
250
|
+
} else if (ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
251
|
+
const jsx = findJsxInExpression(expr.right);
|
|
252
|
+
if (jsx) childSummaries.push(`{cond: ${summarizeJsx(jsx, depth + 1)}}`);
|
|
253
|
+
} else {
|
|
254
|
+
const jsx = findJsxInExpression(expr);
|
|
255
|
+
if (jsx) childSummaries.push(summarizeJsx(jsx, depth + 1));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (childSummaries.length === 0) return tagStr;
|
|
260
|
+
if (childSummaries.length === 1) return `${tagStr} > ${childSummaries[0]}`;
|
|
261
|
+
return `${tagStr} > [${childSummaries.join(", ")}]`;
|
|
262
|
+
}
|
|
263
|
+
if (ts.isJsxFragment(node)) {
|
|
264
|
+
const childSummaries = [];
|
|
265
|
+
for (const child of node.children) {
|
|
266
|
+
if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child)) {
|
|
267
|
+
childSummaries.push(summarizeJsx(child, depth + 1));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return childSummaries.length === 1 ? childSummaries[0] : `[${childSummaries.join(", ")}]`;
|
|
271
|
+
}
|
|
272
|
+
return "?";
|
|
273
|
+
}
|
|
274
|
+
function getClassNameAttr(attrs) {
|
|
275
|
+
for (const attr of attrs.properties) {
|
|
276
|
+
if (ts.isJsxAttribute(attr) && attr.name.getText() === "className" && attr.initializer) {
|
|
277
|
+
if (ts.isStringLiteral(attr.initializer)) {
|
|
278
|
+
return attr.initializer.text;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
function compactClass(cls) {
|
|
285
|
+
const parts = cls.split(/\s+/).filter((c) => !c.startsWith("dark:"));
|
|
286
|
+
const meaningful = parts.slice(0, 3);
|
|
287
|
+
return meaningful.join(".");
|
|
288
|
+
}
|
|
289
|
+
function extractExtensionPoints(content, relPath) {
|
|
290
|
+
const extensionPoints = [];
|
|
291
|
+
const commentRegex = /@sidekick:extension-point\s+([a-z0-9-]+)/g;
|
|
292
|
+
let match;
|
|
293
|
+
while ((match = commentRegex.exec(content)) !== null) {
|
|
294
|
+
const id = match[1];
|
|
295
|
+
const line = content.substring(0, match.index).split("\n").length;
|
|
296
|
+
extensionPoints.push({ id, file: relPath, line });
|
|
297
|
+
}
|
|
298
|
+
const jsxRegex = /<ExtensionPoint\s+[^>]*id=["']([a-z0-9-]+)["'][^>]*\/?>/g;
|
|
299
|
+
while ((match = jsxRegex.exec(content)) !== null) {
|
|
300
|
+
const id = match[1];
|
|
301
|
+
const line = content.substring(0, match.index).split("\n").length;
|
|
302
|
+
extensionPoints.push({ id, file: relPath, line });
|
|
303
|
+
}
|
|
304
|
+
return extensionPoints;
|
|
305
|
+
}
|
|
306
|
+
function extractDataModels(sourceFile, relPath) {
|
|
307
|
+
const dataModels = [];
|
|
308
|
+
function visit(node) {
|
|
309
|
+
if (ts.isInterfaceDeclaration(node)) {
|
|
310
|
+
const name = node.name.getText();
|
|
311
|
+
if (!name.endsWith("Props") && !name.endsWith("State")) {
|
|
312
|
+
const fields = extractFieldsFromInterface(node);
|
|
313
|
+
if (fields.length > 0) {
|
|
314
|
+
dataModels.push({ name, file: relPath, fields });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (ts.isTypeAliasDeclaration(node)) {
|
|
319
|
+
const name = node.name.getText();
|
|
320
|
+
if (!name.endsWith("Props") && !name.endsWith("State") && ts.isTypeLiteralNode(node.type)) {
|
|
321
|
+
const fields = extractFieldsFromTypeLiteral(node.type);
|
|
322
|
+
if (fields.length > 0) {
|
|
323
|
+
dataModels.push({ name, file: relPath, fields });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
ts.forEachChild(node, visit);
|
|
328
|
+
}
|
|
329
|
+
visit(sourceFile);
|
|
330
|
+
return dataModels;
|
|
331
|
+
}
|
|
332
|
+
function extractFieldsFromInterface(node) {
|
|
333
|
+
const fields = [];
|
|
334
|
+
for (const member of node.members) {
|
|
335
|
+
if (ts.isPropertySignature(member) && member.name && member.type) {
|
|
336
|
+
const name = member.name.getText();
|
|
337
|
+
const type = member.type.getText();
|
|
338
|
+
const optional = member.questionToken !== void 0;
|
|
339
|
+
fields.push({ name, type, optional: optional || void 0 });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return fields;
|
|
343
|
+
}
|
|
344
|
+
function extractFieldsFromTypeLiteral(node) {
|
|
345
|
+
const fields = [];
|
|
346
|
+
for (const member of node.members) {
|
|
347
|
+
if (ts.isPropertySignature(member) && member.name && member.type) {
|
|
348
|
+
const name = member.name.getText();
|
|
349
|
+
const type = member.type.getText();
|
|
350
|
+
const optional = member.questionToken !== void 0;
|
|
351
|
+
fields.push({ name, type, optional: optional || void 0 });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return fields;
|
|
355
|
+
}
|
|
356
|
+
function extractComponentStyles(sourceFile, relPath) {
|
|
357
|
+
const results = [];
|
|
358
|
+
function visitTopLevel(node) {
|
|
359
|
+
let componentName = null;
|
|
360
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
361
|
+
const name = node.name.getText();
|
|
362
|
+
if (isComponentName(name)) {
|
|
363
|
+
componentName = name;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (ts.isVariableStatement(node)) {
|
|
367
|
+
for (const decl of node.declarationList.declarations) {
|
|
368
|
+
if (ts.isIdentifier(decl.name) && decl.initializer) {
|
|
369
|
+
const name = decl.name.getText();
|
|
370
|
+
if (isComponentName(name) && isReactComponent(decl.initializer)) {
|
|
371
|
+
componentName = name;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (componentName) {
|
|
377
|
+
const classNames = /* @__PURE__ */ new Set();
|
|
378
|
+
collectClassNames(node, classNames, sourceFile);
|
|
379
|
+
if (classNames.size > 0) {
|
|
380
|
+
results.push({
|
|
381
|
+
component: componentName,
|
|
382
|
+
file: relPath,
|
|
383
|
+
classNames: Array.from(classNames)
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
ts.forEachChild(node, visitTopLevel);
|
|
388
|
+
}
|
|
389
|
+
visitTopLevel(sourceFile);
|
|
390
|
+
return results;
|
|
391
|
+
}
|
|
392
|
+
function collectClassNames(node, classNames, sourceFile) {
|
|
393
|
+
if (ts.isJsxAttribute(node)) {
|
|
394
|
+
const attrName = node.name.getText();
|
|
395
|
+
if (attrName === "className" && node.initializer) {
|
|
396
|
+
if (ts.isStringLiteral(node.initializer)) {
|
|
397
|
+
classNames.add(node.initializer.text);
|
|
398
|
+
}
|
|
399
|
+
if (ts.isJsxExpression(node.initializer) && node.initializer.expression) {
|
|
400
|
+
collectStringValues(node.initializer.expression, classNames);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
ts.forEachChild(node, (child) => collectClassNames(child, classNames, sourceFile));
|
|
405
|
+
}
|
|
406
|
+
function collectStringValues(node, values) {
|
|
407
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
408
|
+
const text = node.text.trim();
|
|
409
|
+
if (text && looksLikeClassNames(text)) values.add(text);
|
|
410
|
+
}
|
|
411
|
+
if (ts.isTemplateExpression(node)) {
|
|
412
|
+
const head = node.head.text.trim();
|
|
413
|
+
if (head && looksLikeClassNames(head)) values.add(head);
|
|
414
|
+
for (const span of node.templateSpans) {
|
|
415
|
+
const spanText = span.literal.text.trim();
|
|
416
|
+
if (spanText && looksLikeClassNames(spanText)) values.add(spanText);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (ts.isConditionalExpression(node)) {
|
|
420
|
+
collectStringValues(node.whenTrue, values);
|
|
421
|
+
collectStringValues(node.whenFalse, values);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
425
|
+
collectStringValues(node.left, values);
|
|
426
|
+
collectStringValues(node.right, values);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
ts.forEachChild(node, (child) => collectStringValues(child, values));
|
|
430
|
+
}
|
|
431
|
+
function looksLikeClassNames(text) {
|
|
432
|
+
if (text.length < 2) return false;
|
|
433
|
+
if (text.startsWith("http") || text.startsWith("//")) return false;
|
|
434
|
+
if (text.startsWith("/") && !text.includes(" ")) return false;
|
|
435
|
+
const words = text.split(/\s+/);
|
|
436
|
+
const classPattern = /^[a-zA-Z_\-\[\]:.\/][a-zA-Z0-9_\-\[\]:.\/%]*$/;
|
|
437
|
+
const classLikeCount = words.filter((w) => classPattern.test(w)).length;
|
|
438
|
+
return classLikeCount / words.length >= 0.5;
|
|
439
|
+
}
|
|
440
|
+
async function extractDesignSystem(targetDir, componentStyles) {
|
|
441
|
+
let framework = null;
|
|
442
|
+
let fontFamily = null;
|
|
443
|
+
let globalCss = null;
|
|
444
|
+
const dirsToCheck = [targetDir];
|
|
445
|
+
const parent1 = path.dirname(targetDir);
|
|
446
|
+
if (parent1 !== targetDir) dirsToCheck.push(parent1);
|
|
447
|
+
const parent2 = path.dirname(parent1);
|
|
448
|
+
if (parent2 !== parent1) dirsToCheck.push(parent2);
|
|
449
|
+
let tailwindConfigs = [];
|
|
450
|
+
for (const dir of dirsToCheck) {
|
|
451
|
+
tailwindConfigs = await glob("tailwind.config.{js,ts,cjs,mjs}", {
|
|
452
|
+
cwd: dir,
|
|
453
|
+
absolute: true
|
|
454
|
+
});
|
|
455
|
+
if (tailwindConfigs.length > 0) break;
|
|
456
|
+
}
|
|
457
|
+
if (tailwindConfigs.length > 0) {
|
|
458
|
+
framework = "Tailwind CSS";
|
|
459
|
+
try {
|
|
460
|
+
const configContent = await fs.readFile(tailwindConfigs[0], "utf-8");
|
|
461
|
+
const fontMatch = configContent.match(/fontFamily\s*:\s*\{[^}]*sans\s*:\s*\[([^\]]+)\]/);
|
|
462
|
+
if (fontMatch) {
|
|
463
|
+
fontFamily = fontMatch[1].split(",").map((s) => s.trim().replace(/['"]/g, "")).join(", ");
|
|
464
|
+
}
|
|
465
|
+
} catch {
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const cssFiles = await glob("**/{globals,global,styles,style,app}.css", {
|
|
469
|
+
cwd: targetDir,
|
|
470
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/.next/**"],
|
|
471
|
+
absolute: true
|
|
472
|
+
});
|
|
473
|
+
if (cssFiles.length > 0) {
|
|
474
|
+
try {
|
|
475
|
+
const contents = [];
|
|
476
|
+
for (const cssFile of cssFiles) {
|
|
477
|
+
const content = await fs.readFile(cssFile, "utf-8");
|
|
478
|
+
const stripped = content.replace(/@tailwind\s+\w+;/g, "").trim();
|
|
479
|
+
if (stripped) {
|
|
480
|
+
contents.push(stripped);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (contents.length > 0) {
|
|
484
|
+
globalCss = contents.join("\n\n");
|
|
485
|
+
}
|
|
486
|
+
} catch {
|
|
487
|
+
}
|
|
488
|
+
if (!fontFamily) {
|
|
489
|
+
for (const cssFile of cssFiles) {
|
|
490
|
+
try {
|
|
491
|
+
const content = await fs.readFile(cssFile, "utf-8");
|
|
492
|
+
const fontMatch = content.match(/font-family\s*:\s*([^;]+)/);
|
|
493
|
+
if (fontMatch) {
|
|
494
|
+
fontFamily = fontMatch[1].trim();
|
|
495
|
+
}
|
|
496
|
+
} catch {
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return {
|
|
502
|
+
framework,
|
|
503
|
+
fontFamily,
|
|
504
|
+
globalCss,
|
|
505
|
+
componentStyles
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/validate.ts
|
|
510
|
+
import * as ts2 from "typescript";
|
|
511
|
+
function validate(options) {
|
|
512
|
+
const { code, filename = "override.tsx" } = options;
|
|
513
|
+
const errors = [];
|
|
514
|
+
const warnings = [];
|
|
515
|
+
let sourceFile;
|
|
516
|
+
try {
|
|
517
|
+
sourceFile = ts2.createSourceFile(
|
|
518
|
+
filename,
|
|
519
|
+
code,
|
|
520
|
+
ts2.ScriptTarget.Latest,
|
|
521
|
+
true,
|
|
522
|
+
filename.endsWith(".tsx") ? ts2.ScriptKind.TSX : ts2.ScriptKind.TS
|
|
523
|
+
);
|
|
524
|
+
} catch (e) {
|
|
525
|
+
errors.push({
|
|
526
|
+
type: "syntax-error",
|
|
527
|
+
message: `Failed to parse code: ${e instanceof Error ? e.message : String(e)}`
|
|
528
|
+
});
|
|
529
|
+
return { valid: false, errors, warnings };
|
|
530
|
+
}
|
|
531
|
+
const parseErrors = checkParseErrors(sourceFile);
|
|
532
|
+
errors.push(...parseErrors);
|
|
533
|
+
const importErrors = checkImports(sourceFile);
|
|
534
|
+
errors.push(...importErrors);
|
|
535
|
+
const patternErrors = checkForbiddenPatterns(code);
|
|
536
|
+
errors.push(...patternErrors);
|
|
537
|
+
const primitiveWarnings = checkPrimitiveUsage(sourceFile);
|
|
538
|
+
warnings.push(...primitiveWarnings);
|
|
539
|
+
return {
|
|
540
|
+
valid: errors.length === 0,
|
|
541
|
+
errors,
|
|
542
|
+
warnings
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
function checkParseErrors(sourceFile) {
|
|
546
|
+
const errors = [];
|
|
547
|
+
const parseDiagnostics = sourceFile.parseDiagnostics;
|
|
548
|
+
if (parseDiagnostics) {
|
|
549
|
+
for (const diagnostic of parseDiagnostics) {
|
|
550
|
+
const message = ts2.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
|
|
551
|
+
const pos = diagnostic.start;
|
|
552
|
+
let line;
|
|
553
|
+
let column;
|
|
554
|
+
if (pos !== void 0) {
|
|
555
|
+
const lineAndChar = sourceFile.getLineAndCharacterOfPosition(pos);
|
|
556
|
+
line = lineAndChar.line + 1;
|
|
557
|
+
column = lineAndChar.character + 1;
|
|
558
|
+
}
|
|
559
|
+
errors.push({
|
|
560
|
+
type: "syntax-error",
|
|
561
|
+
message,
|
|
562
|
+
line,
|
|
563
|
+
column
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function visit(node) {
|
|
568
|
+
if (node.kind === ts2.SyntaxKind.Unknown) {
|
|
569
|
+
const pos = node.getStart(sourceFile);
|
|
570
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(pos);
|
|
571
|
+
errors.push({
|
|
572
|
+
type: "syntax-error",
|
|
573
|
+
message: "Unknown or invalid syntax",
|
|
574
|
+
line: line + 1,
|
|
575
|
+
column: character + 1
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
ts2.forEachChild(node, visit);
|
|
579
|
+
}
|
|
580
|
+
visit(sourceFile);
|
|
581
|
+
return errors;
|
|
582
|
+
}
|
|
583
|
+
function checkImports(sourceFile) {
|
|
584
|
+
const errors = [];
|
|
585
|
+
function visit(node) {
|
|
586
|
+
if (ts2.isImportDeclaration(node)) {
|
|
587
|
+
const moduleSpecifier = node.moduleSpecifier;
|
|
588
|
+
if (ts2.isStringLiteral(moduleSpecifier)) {
|
|
589
|
+
const moduleName = moduleSpecifier.text;
|
|
590
|
+
const isAllowed = ALLOWED_IMPORTS.some((allowed) => {
|
|
591
|
+
if (allowed === moduleName) return true;
|
|
592
|
+
if (moduleName.startsWith(allowed + "/")) return true;
|
|
593
|
+
return false;
|
|
594
|
+
});
|
|
595
|
+
if (!isAllowed) {
|
|
596
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(
|
|
597
|
+
node.getStart()
|
|
598
|
+
);
|
|
599
|
+
errors.push({
|
|
600
|
+
type: "forbidden-import",
|
|
601
|
+
message: `Import "${moduleName}" is not allowed. Only imports from ${ALLOWED_IMPORTS.join(", ")} are permitted.`,
|
|
602
|
+
line: line + 1,
|
|
603
|
+
column: character + 1
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
ts2.forEachChild(node, visit);
|
|
609
|
+
}
|
|
610
|
+
visit(sourceFile);
|
|
611
|
+
return errors;
|
|
612
|
+
}
|
|
613
|
+
function checkForbiddenPatterns(code) {
|
|
614
|
+
const errors = [];
|
|
615
|
+
const lines = code.split("\n");
|
|
616
|
+
for (let i = 0; i < lines.length; i++) {
|
|
617
|
+
const line = lines[i];
|
|
618
|
+
for (const pattern of FORBIDDEN_PATTERNS) {
|
|
619
|
+
if (pattern.test(line)) {
|
|
620
|
+
errors.push({
|
|
621
|
+
type: "forbidden-pattern",
|
|
622
|
+
message: `Forbidden pattern detected: ${pattern.source}`,
|
|
623
|
+
line: i + 1
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return errors;
|
|
629
|
+
}
|
|
630
|
+
function checkPrimitiveUsage(sourceFile) {
|
|
631
|
+
const warnings = [];
|
|
632
|
+
const code = sourceFile.getFullText();
|
|
633
|
+
const usesSDKPrimitive = /sdk\.ui\./.test(code) || /sdk\.data\./.test(code);
|
|
634
|
+
if (!usesSDKPrimitive) {
|
|
635
|
+
warnings.push({
|
|
636
|
+
type: "unused-import",
|
|
637
|
+
message: "Override does not appear to use any SDK primitives (sdk.ui.* or sdk.data.*)"
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
return warnings;
|
|
641
|
+
}
|
|
642
|
+
function validateFile(filepath, code) {
|
|
643
|
+
return validate({ code, filename: filepath });
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// src/generate.ts
|
|
647
|
+
import * as fs2 from "fs/promises";
|
|
648
|
+
import * as path2 from "path";
|
|
649
|
+
async function generate(options) {
|
|
650
|
+
const { request, schemaPath, outputDir, apiKey } = options;
|
|
651
|
+
let schema;
|
|
652
|
+
try {
|
|
653
|
+
const schemaContent = await fs2.readFile(schemaPath, "utf-8");
|
|
654
|
+
schema = JSON.parse(schemaContent);
|
|
655
|
+
} catch (e) {
|
|
656
|
+
return {
|
|
657
|
+
success: false,
|
|
658
|
+
error: `Failed to read schema: ${e instanceof Error ? e.message : String(e)}`
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
const key = apiKey || process.env.ANTHROPIC_API_KEY;
|
|
662
|
+
if (!key) {
|
|
663
|
+
return {
|
|
664
|
+
success: false,
|
|
665
|
+
error: "No API key provided. Set ANTHROPIC_API_KEY environment variable or use --api-key flag."
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
const MAX_RETRIES = 3;
|
|
669
|
+
let lastError;
|
|
670
|
+
let lastValidationErrors;
|
|
671
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
672
|
+
try {
|
|
673
|
+
const generated = await callAI(request, schema, key, lastValidationErrors);
|
|
674
|
+
const validation = validate({ code: generated.code, filename: "override.tsx" });
|
|
675
|
+
if (!validation.valid) {
|
|
676
|
+
lastValidationErrors = validation.errors.map((e) => e.message);
|
|
677
|
+
lastError = `Validation failed: ${validation.errors.map((e) => e.message).join(", ")}`;
|
|
678
|
+
if (attempt < MAX_RETRIES) {
|
|
679
|
+
console.log(`Attempt ${attempt} failed validation, retrying...`);
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
return {
|
|
683
|
+
success: false,
|
|
684
|
+
error: lastError,
|
|
685
|
+
validationErrors: lastValidationErrors
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
const overrideDir = path2.join(outputDir, generated.manifest.id);
|
|
689
|
+
await fs2.mkdir(overrideDir, { recursive: true });
|
|
690
|
+
const codePath = path2.join(overrideDir, "index.tsx");
|
|
691
|
+
await fs2.writeFile(codePath, generated.code);
|
|
692
|
+
return {
|
|
693
|
+
success: true,
|
|
694
|
+
override: generated,
|
|
695
|
+
outputPath: codePath
|
|
696
|
+
};
|
|
697
|
+
} catch (e) {
|
|
698
|
+
lastError = e instanceof Error ? e.message : String(e);
|
|
699
|
+
if (attempt === MAX_RETRIES) {
|
|
700
|
+
return {
|
|
701
|
+
success: false,
|
|
702
|
+
error: `Failed after ${MAX_RETRIES} attempts: ${lastError}`
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return {
|
|
708
|
+
success: false,
|
|
709
|
+
error: lastError || "Unknown error"
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
async function callAI(request, schema, apiKey, previousErrors) {
|
|
713
|
+
const systemPrompt = buildSystemPrompt(schema);
|
|
714
|
+
const userPrompt = buildUserPrompt(request, previousErrors);
|
|
715
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
716
|
+
method: "POST",
|
|
717
|
+
headers: {
|
|
718
|
+
"Content-Type": "application/json",
|
|
719
|
+
"x-api-key": apiKey,
|
|
720
|
+
"anthropic-version": "2023-06-01"
|
|
721
|
+
},
|
|
722
|
+
body: JSON.stringify({
|
|
723
|
+
model: "claude-sonnet-4-20250514",
|
|
724
|
+
max_tokens: 4096,
|
|
725
|
+
system: systemPrompt,
|
|
726
|
+
messages: [
|
|
727
|
+
{ role: "user", content: userPrompt }
|
|
728
|
+
]
|
|
729
|
+
})
|
|
730
|
+
});
|
|
731
|
+
if (!response.ok) {
|
|
732
|
+
const error = await response.text();
|
|
733
|
+
throw new Error(`API request failed: ${response.status} ${error}`);
|
|
734
|
+
}
|
|
735
|
+
const result = await response.json();
|
|
736
|
+
const textContent = result.content.find((c) => c.type === "text");
|
|
737
|
+
if (!textContent || !textContent.text) {
|
|
738
|
+
throw new Error("No text content in API response");
|
|
739
|
+
}
|
|
740
|
+
return parseAIResponse(textContent.text);
|
|
741
|
+
}
|
|
742
|
+
function buildSystemPrompt(schema) {
|
|
743
|
+
return `You are a code generator for Sidekick, an SDK that allows users to customize web applications.
|
|
744
|
+
Your task is to generate override modules based on user requests.
|
|
745
|
+
|
|
746
|
+
## SDK Primitives Available
|
|
747
|
+
|
|
748
|
+
The SDK provides these primitives that MUST be used:
|
|
749
|
+
|
|
750
|
+
UI Primitives:
|
|
751
|
+
- sdk.ui.inject(extensionPointId, Component) - Inject a component at an extension point
|
|
752
|
+
- sdk.ui.wrap(componentName, wrapperFn) - Wrap an existing component with a HOC
|
|
753
|
+
- sdk.ui.addColumn(tableId, config) - Add a column to a table (config: { header, accessor, render? })
|
|
754
|
+
- sdk.ui.addStyles(css) - Add CSS styles
|
|
755
|
+
- sdk.ui.replace(componentName, Component) - Replace a component entirely
|
|
756
|
+
|
|
757
|
+
Data Primitives:
|
|
758
|
+
- sdk.data.computed(fieldName, computeFn) - Add a computed field
|
|
759
|
+
- sdk.data.addFilter(name, filterFn) - Add a filter preset
|
|
760
|
+
- sdk.data.transform(dataKey, transformFn) - Transform data
|
|
761
|
+
|
|
762
|
+
## App Schema
|
|
763
|
+
|
|
764
|
+
Extension Points: ${JSON.stringify(schema.extensionPoints)}
|
|
765
|
+
|
|
766
|
+
Components: ${JSON.stringify(schema.components.map((c) => ({ name: c.name, props: c.props })))}
|
|
767
|
+
|
|
768
|
+
Data Models: ${JSON.stringify(schema.dataModels)}
|
|
769
|
+
|
|
770
|
+
## Example Overrides
|
|
771
|
+
|
|
772
|
+
### Example 1: Inject a banner
|
|
773
|
+
\`\`\`tsx
|
|
774
|
+
import { createOverride, SDK } from '@usesidekick/react';
|
|
775
|
+
|
|
776
|
+
const Banner = () => (
|
|
777
|
+
<div style={{ backgroundColor: '#10B981', color: 'white', padding: '12px', textAlign: 'center' }}>
|
|
778
|
+
Welcome Banner!
|
|
779
|
+
</div>
|
|
780
|
+
);
|
|
781
|
+
|
|
782
|
+
export default createOverride(
|
|
783
|
+
{
|
|
784
|
+
id: 'welcome-banner',
|
|
785
|
+
name: 'Welcome Banner',
|
|
786
|
+
description: 'Adds a welcome banner',
|
|
787
|
+
version: '1.0.0',
|
|
788
|
+
primitives: ['ui.inject'],
|
|
789
|
+
},
|
|
790
|
+
(sdk: SDK) => {
|
|
791
|
+
sdk.ui.inject('sidebar-header', Banner);
|
|
792
|
+
}
|
|
793
|
+
);
|
|
794
|
+
\`\`\`
|
|
795
|
+
|
|
796
|
+
### Example 2: Add a table column
|
|
797
|
+
\`\`\`tsx
|
|
798
|
+
import { createOverride, SDK } from '@usesidekick/react';
|
|
799
|
+
|
|
800
|
+
export default createOverride(
|
|
801
|
+
{
|
|
802
|
+
id: 'days-left-column',
|
|
803
|
+
name: 'Days Left Column',
|
|
804
|
+
description: 'Shows days until due date',
|
|
805
|
+
version: '1.0.0',
|
|
806
|
+
primitives: ['ui.addColumn'],
|
|
807
|
+
},
|
|
808
|
+
(sdk: SDK) => {
|
|
809
|
+
sdk.ui.addColumn('task-table', {
|
|
810
|
+
header: 'Days Left',
|
|
811
|
+
accessor: (row: Record<string, unknown>) => {
|
|
812
|
+
const dueDate = row.dueDate as string | undefined;
|
|
813
|
+
if (!dueDate) return null;
|
|
814
|
+
const days = Math.ceil((new Date(dueDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24));
|
|
815
|
+
return days;
|
|
816
|
+
},
|
|
817
|
+
render: (value: unknown) => {
|
|
818
|
+
if (value === null) return <span>-</span>;
|
|
819
|
+
const days = value as number;
|
|
820
|
+
const color = days < 0 ? 'red' : days <= 3 ? 'orange' : 'green';
|
|
821
|
+
return <span style={{ color }}>{days}d</span>;
|
|
822
|
+
},
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
);
|
|
826
|
+
\`\`\`
|
|
827
|
+
|
|
828
|
+
### Example 3: Add custom styles
|
|
829
|
+
\`\`\`tsx
|
|
830
|
+
import { createOverride, SDK } from '@usesidekick/react';
|
|
831
|
+
|
|
832
|
+
export default createOverride(
|
|
833
|
+
{
|
|
834
|
+
id: 'dark-theme',
|
|
835
|
+
name: 'Dark Theme Override',
|
|
836
|
+
description: 'Adds dark theme styles',
|
|
837
|
+
version: '1.0.0',
|
|
838
|
+
primitives: ['ui.addStyles'],
|
|
839
|
+
},
|
|
840
|
+
(sdk: SDK) => {
|
|
841
|
+
sdk.ui.addStyles(\`
|
|
842
|
+
.custom-dark { background: #1a1a1a; color: white; }
|
|
843
|
+
\`);
|
|
844
|
+
}
|
|
845
|
+
);
|
|
846
|
+
\`\`\`
|
|
847
|
+
|
|
848
|
+
## Rules
|
|
849
|
+
|
|
850
|
+
1. ONLY import from '@usesidekick/react' or 'react' - NO other imports allowed
|
|
851
|
+
2. DO NOT use fetch, eval, document.*, window.*, localStorage, etc.
|
|
852
|
+
3. Use the createOverride function from @usesidekick/react
|
|
853
|
+
4. The activate function receives an SDK object with ui and data primitives
|
|
854
|
+
5. Generate React components using JSX with inline styles
|
|
855
|
+
6. Keep code simple and focused on the user's request
|
|
856
|
+
7. Use arrow functions for components
|
|
857
|
+
|
|
858
|
+
## Output Format
|
|
859
|
+
|
|
860
|
+
Respond with a JSON object containing:
|
|
861
|
+
{
|
|
862
|
+
"manifest": {
|
|
863
|
+
"id": "override-id-kebab-case",
|
|
864
|
+
"name": "Human Readable Name",
|
|
865
|
+
"description": "What this override does",
|
|
866
|
+
"version": "1.0.0",
|
|
867
|
+
"primitives": ["list", "of", "primitives", "used"]
|
|
868
|
+
},
|
|
869
|
+
"code": "// Full TypeScript/TSX code here"
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
The code should be a complete, valid TypeScript/TSX file that exports a default override module.`;
|
|
873
|
+
}
|
|
874
|
+
function buildUserPrompt(request, previousErrors) {
|
|
875
|
+
let prompt = `Generate an override module for this request: "${request}"`;
|
|
876
|
+
if (previousErrors && previousErrors.length > 0) {
|
|
877
|
+
prompt += `
|
|
878
|
+
|
|
879
|
+
IMPORTANT: Previous generation attempt failed validation with these errors:
|
|
880
|
+
`;
|
|
881
|
+
prompt += previousErrors.map((e) => `- ${e}`).join("\n");
|
|
882
|
+
prompt += "\n\nPlease fix these issues in your response.";
|
|
883
|
+
}
|
|
884
|
+
return prompt;
|
|
885
|
+
}
|
|
886
|
+
function parseAIResponse(text) {
|
|
887
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
888
|
+
if (!jsonMatch) {
|
|
889
|
+
throw new Error("No JSON found in AI response");
|
|
890
|
+
}
|
|
891
|
+
try {
|
|
892
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
893
|
+
if (!parsed.manifest || !parsed.code) {
|
|
894
|
+
throw new Error("Invalid response structure");
|
|
895
|
+
}
|
|
896
|
+
return {
|
|
897
|
+
manifest: parsed.manifest,
|
|
898
|
+
code: parsed.code
|
|
899
|
+
};
|
|
900
|
+
} catch (e) {
|
|
901
|
+
throw new Error(`Failed to parse AI response: ${e instanceof Error ? e.message : String(e)}`);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// src/init.ts
|
|
906
|
+
import * as path3 from "path";
|
|
907
|
+
import * as fs3 from "fs/promises";
|
|
908
|
+
import { existsSync } from "fs";
|
|
909
|
+
import { execFileSync } from "child_process";
|
|
910
|
+
var NEXT_CONFIG_FILES = ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.mts"];
|
|
911
|
+
function findNextConfigFile(targetDir) {
|
|
912
|
+
for (const name of NEXT_CONFIG_FILES) {
|
|
913
|
+
if (existsSync(path3.join(targetDir, name))) return name;
|
|
914
|
+
}
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
function log(step, total, message) {
|
|
918
|
+
console.log(`[${step}/${total}] ${message}`);
|
|
919
|
+
}
|
|
920
|
+
function warn(message) {
|
|
921
|
+
console.log(` \u26A0 ${message}`);
|
|
922
|
+
}
|
|
923
|
+
async function init(targetDir) {
|
|
924
|
+
console.log("\nSidekick Setup\n");
|
|
925
|
+
const project = await detectProject(targetDir);
|
|
926
|
+
if (!project.hasNextJs) {
|
|
927
|
+
throw new Error("Next.js not found in package.json. Sidekick currently requires Next.js.");
|
|
928
|
+
}
|
|
929
|
+
const info = [
|
|
930
|
+
`Next.js ${project.nextVersion || "detected"}`,
|
|
931
|
+
project.hasTypeScript ? "TypeScript" : "JavaScript",
|
|
932
|
+
`App Router (${project.appDir}/)`
|
|
933
|
+
].join(", ");
|
|
934
|
+
console.log(` ${info}
|
|
935
|
+
`);
|
|
936
|
+
const TOTAL_STEPS = 7;
|
|
937
|
+
log(1, TOTAL_STEPS, "Installing dependencies...");
|
|
938
|
+
await installDependencies(project);
|
|
939
|
+
console.log(" done");
|
|
940
|
+
log(2, TOTAL_STEPS, "Configuring JSX runtime (.babelrc, next.config.js)...");
|
|
941
|
+
await configureBabel(project);
|
|
942
|
+
await configureNextConfig(project);
|
|
943
|
+
console.log(" done");
|
|
944
|
+
log(3, TOTAL_STEPS, "Setting up database schema...");
|
|
945
|
+
await setupDatabase(project);
|
|
946
|
+
console.log(" done");
|
|
947
|
+
log(4, TOTAL_STEPS, "Creating API route...");
|
|
948
|
+
await createApiRoute(project);
|
|
949
|
+
console.log(" done");
|
|
950
|
+
log(5, TOTAL_STEPS, "Adding Sidekick to app layout...");
|
|
951
|
+
await setupBootstrap(project);
|
|
952
|
+
console.log(" done");
|
|
953
|
+
log(6, TOTAL_STEPS, "Analyzing app components...");
|
|
954
|
+
const sidekickDir = path3.join(targetDir, project.srcDir, "sidekick");
|
|
955
|
+
const overridesDir = path3.join(sidekickDir, "overrides");
|
|
956
|
+
await fs3.mkdir(overridesDir, { recursive: true });
|
|
957
|
+
const schemaPath = path3.join(sidekickDir, "schema.json");
|
|
958
|
+
try {
|
|
959
|
+
const schema = await analyze({ targetDir, outputPath: schemaPath });
|
|
960
|
+
console.log(` ${schema.components.length} components, ${schema.extensionPoints.length} extension points`);
|
|
961
|
+
} catch {
|
|
962
|
+
warn("Analysis had issues - you can re-run with: sidekick analyze");
|
|
963
|
+
}
|
|
964
|
+
log(7, TOTAL_STEPS, "Done!\n");
|
|
965
|
+
console.log("Remaining steps:");
|
|
966
|
+
console.log(" 1. Add to .env.local:");
|
|
967
|
+
console.log(" DATABASE_URL=your_neon_database_url");
|
|
968
|
+
console.log(" ANTHROPIC_API_KEY=your_api_key");
|
|
969
|
+
console.log(" 2. Run: npx drizzle-kit push");
|
|
970
|
+
console.log(" 3. Run: npm run dev\n");
|
|
971
|
+
}
|
|
972
|
+
async function detectProject(targetDir) {
|
|
973
|
+
const pkgPath = path3.join(targetDir, "package.json");
|
|
974
|
+
if (!existsSync(pkgPath)) {
|
|
975
|
+
throw new Error("No package.json found. Run this from your project root.");
|
|
976
|
+
}
|
|
977
|
+
const pkg = JSON.parse(await fs3.readFile(pkgPath, "utf-8"));
|
|
978
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
979
|
+
let packageManager = "npm";
|
|
980
|
+
if (existsSync(path3.join(targetDir, "pnpm-lock.yaml"))) {
|
|
981
|
+
packageManager = "pnpm";
|
|
982
|
+
} else if (existsSync(path3.join(targetDir, "yarn.lock"))) {
|
|
983
|
+
packageManager = "yarn";
|
|
984
|
+
}
|
|
985
|
+
const nextVersion = deps["next"] || null;
|
|
986
|
+
const hasTypeScript = existsSync(path3.join(targetDir, "tsconfig.json"));
|
|
987
|
+
let appDir = "app";
|
|
988
|
+
let srcDir = ".";
|
|
989
|
+
if (existsSync(path3.join(targetDir, "src/app"))) {
|
|
990
|
+
appDir = "src/app";
|
|
991
|
+
srcDir = "src";
|
|
992
|
+
}
|
|
993
|
+
return {
|
|
994
|
+
targetDir,
|
|
995
|
+
packageManager,
|
|
996
|
+
hasNextJs: !!nextVersion,
|
|
997
|
+
nextVersion: nextVersion ? nextVersion.replace(/[\^~>=<]*/g, "") : null,
|
|
998
|
+
hasTypeScript,
|
|
999
|
+
appDir,
|
|
1000
|
+
srcDir,
|
|
1001
|
+
hasSidekickReact: !!deps["@usesidekick/react"],
|
|
1002
|
+
hasBabelrc: existsSync(path3.join(targetDir, ".babelrc")),
|
|
1003
|
+
hasNextConfig: !!findNextConfigFile(targetDir),
|
|
1004
|
+
nextConfigFile: findNextConfigFile(targetDir)
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
function runInstall(pm, packages, dev, cwd) {
|
|
1008
|
+
const cmd = pm === "yarn" ? "add" : "install";
|
|
1009
|
+
const flag = dev ? pm === "yarn" ? "--dev" : "-D" : "";
|
|
1010
|
+
const args = [cmd, ...flag ? [flag] : [], ...packages];
|
|
1011
|
+
execFileSync(pm, args, { stdio: "pipe", cwd, timeout: 12e4 });
|
|
1012
|
+
}
|
|
1013
|
+
async function installDependencies(project) {
|
|
1014
|
+
const { targetDir, packageManager: pm } = project;
|
|
1015
|
+
const pkg = JSON.parse(await fs3.readFile(path3.join(targetDir, "package.json"), "utf-8"));
|
|
1016
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1017
|
+
const runtimeDeps = [];
|
|
1018
|
+
if (!allDeps["@usesidekick/react"]) runtimeDeps.push("@usesidekick/react");
|
|
1019
|
+
if (!allDeps["drizzle-orm"]) runtimeDeps.push("drizzle-orm");
|
|
1020
|
+
if (!allDeps["@neondatabase/serverless"]) runtimeDeps.push("@neondatabase/serverless");
|
|
1021
|
+
const devDeps = [];
|
|
1022
|
+
if (!allDeps["babel-plugin-add-react-displayname"]) devDeps.push("babel-plugin-add-react-displayname");
|
|
1023
|
+
if (!allDeps["drizzle-kit"]) devDeps.push("drizzle-kit");
|
|
1024
|
+
try {
|
|
1025
|
+
if (runtimeDeps.length > 0) {
|
|
1026
|
+
runInstall(pm, runtimeDeps, false, targetDir);
|
|
1027
|
+
}
|
|
1028
|
+
if (devDeps.length > 0) {
|
|
1029
|
+
runInstall(pm, devDeps, true, targetDir);
|
|
1030
|
+
}
|
|
1031
|
+
} catch (e) {
|
|
1032
|
+
warn(`Dependency install failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
1033
|
+
warn("You may need to install manually: " + [...runtimeDeps, ...devDeps].join(" "));
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
async function configureBabel(project) {
|
|
1037
|
+
const babelrcPath = path3.join(project.targetDir, ".babelrc");
|
|
1038
|
+
const desiredConfig = {
|
|
1039
|
+
presets: [
|
|
1040
|
+
["next/babel", {
|
|
1041
|
+
"preset-env": {
|
|
1042
|
+
targets: { esmodules: true },
|
|
1043
|
+
exclude: ["transform-async-to-generator", "transform-regenerator"]
|
|
1044
|
+
},
|
|
1045
|
+
"preset-react": {
|
|
1046
|
+
runtime: "automatic",
|
|
1047
|
+
importSource: "@usesidekick/react"
|
|
1048
|
+
}
|
|
1049
|
+
}]
|
|
1050
|
+
],
|
|
1051
|
+
plugins: ["add-react-displayname"]
|
|
1052
|
+
};
|
|
1053
|
+
if (!project.hasBabelrc) {
|
|
1054
|
+
await fs3.writeFile(babelrcPath, JSON.stringify(desiredConfig, null, 2) + "\n");
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
try {
|
|
1058
|
+
const existing = JSON.parse(await fs3.readFile(babelrcPath, "utf-8"));
|
|
1059
|
+
const presetStr = JSON.stringify(existing.presets || []);
|
|
1060
|
+
if (presetStr.includes("@usesidekick/react")) {
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
let foundNextBabel = false;
|
|
1064
|
+
if (existing.presets) {
|
|
1065
|
+
for (let i = 0; i < existing.presets.length; i++) {
|
|
1066
|
+
const preset = existing.presets[i];
|
|
1067
|
+
const presetName = Array.isArray(preset) ? preset[0] : preset;
|
|
1068
|
+
if (presetName === "next/babel") {
|
|
1069
|
+
foundNextBabel = true;
|
|
1070
|
+
if (!Array.isArray(preset)) {
|
|
1071
|
+
existing.presets[i] = desiredConfig.presets[0];
|
|
1072
|
+
} else {
|
|
1073
|
+
const opts = preset[1] || {};
|
|
1074
|
+
opts["preset-react"] = {
|
|
1075
|
+
runtime: "automatic",
|
|
1076
|
+
importSource: "@usesidekick/react"
|
|
1077
|
+
};
|
|
1078
|
+
if (!opts["preset-env"]) {
|
|
1079
|
+
opts["preset-env"] = desiredConfig.presets[0][1]["preset-env"];
|
|
1080
|
+
}
|
|
1081
|
+
existing.presets[i] = ["next/babel", opts];
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
if (!foundNextBabel) {
|
|
1087
|
+
existing.presets = existing.presets || [];
|
|
1088
|
+
existing.presets.unshift(desiredConfig.presets[0]);
|
|
1089
|
+
}
|
|
1090
|
+
existing.plugins = existing.plugins || [];
|
|
1091
|
+
if (!existing.plugins.includes("add-react-displayname")) {
|
|
1092
|
+
existing.plugins.push("add-react-displayname");
|
|
1093
|
+
}
|
|
1094
|
+
await fs3.writeFile(babelrcPath, JSON.stringify(existing, null, 2) + "\n");
|
|
1095
|
+
} catch {
|
|
1096
|
+
warn(".babelrc exists but could not be parsed. Writing new config.");
|
|
1097
|
+
await fs3.writeFile(babelrcPath, JSON.stringify(desiredConfig, null, 2) + "\n");
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
async function configureNextConfig(project) {
|
|
1101
|
+
const newConfigTemplate = `const path = require('path');
|
|
1102
|
+
|
|
1103
|
+
/** @type {import('next').NextConfig} */
|
|
1104
|
+
const nextConfig = {
|
|
1105
|
+
webpack: (config, { isServer }) => {
|
|
1106
|
+
const sdkPath = path.dirname(require.resolve('@usesidekick/react/package.json'));
|
|
1107
|
+
config.resolve.alias = {
|
|
1108
|
+
...config.resolve.alias,
|
|
1109
|
+
'@usesidekick/react/server': path.join(sdkPath, 'dist/server/index.mjs'),
|
|
1110
|
+
'@usesidekick/react/jsx-runtime': path.join(sdkPath, 'dist/jsx-runtime.mjs'),
|
|
1111
|
+
'@usesidekick/react/jsx-dev-runtime': path.join(sdkPath, 'dist/jsx-dev-runtime.mjs'),
|
|
1112
|
+
'@usesidekick/react': path.join(sdkPath, 'dist/index.mjs'),
|
|
1113
|
+
};
|
|
1114
|
+
return config;
|
|
1115
|
+
},
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
module.exports = nextConfig;
|
|
1119
|
+
`;
|
|
1120
|
+
if (!project.hasNextConfig) {
|
|
1121
|
+
await fs3.writeFile(path3.join(project.targetDir, "next.config.js"), newConfigTemplate);
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
const configFile = project.nextConfigFile;
|
|
1125
|
+
const configPath = path3.join(project.targetDir, configFile);
|
|
1126
|
+
if (configFile !== "next.config.js") {
|
|
1127
|
+
try {
|
|
1128
|
+
const existing = await fs3.readFile(configPath, "utf-8");
|
|
1129
|
+
if (existing.includes("@usesidekick/react")) return;
|
|
1130
|
+
} catch {
|
|
1131
|
+
}
|
|
1132
|
+
warn(`Found ${configFile} \u2014 cannot auto-modify. Please add webpack aliases manually:`);
|
|
1133
|
+
console.log(" Add to your webpack config:");
|
|
1134
|
+
console.log(" const path = require('path'); // or import path from 'path'");
|
|
1135
|
+
console.log(" const sdkPath = path.dirname(require.resolve('@usesidekick/react/package.json'));");
|
|
1136
|
+
console.log(" config.resolve.alias['@usesidekick/react/server'] = path.join(sdkPath, 'dist/server/index.mjs');");
|
|
1137
|
+
console.log(" config.resolve.alias['@usesidekick/react/jsx-runtime'] = path.join(sdkPath, 'dist/jsx-runtime.mjs');");
|
|
1138
|
+
console.log(" config.resolve.alias['@usesidekick/react/jsx-dev-runtime'] = path.join(sdkPath, 'dist/jsx-dev-runtime.mjs');");
|
|
1139
|
+
console.log(" config.resolve.alias['@usesidekick/react'] = path.join(sdkPath, 'dist/index.mjs');");
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
try {
|
|
1143
|
+
const existing = await fs3.readFile(configPath, "utf-8");
|
|
1144
|
+
if (existing.includes("@usesidekick/react")) return;
|
|
1145
|
+
const backupPath = configPath + ".sidekick-backup";
|
|
1146
|
+
await fs3.writeFile(backupPath, existing);
|
|
1147
|
+
warn(`Backed up existing next.config.js to ${path3.basename(backupPath)}`);
|
|
1148
|
+
if (existing.includes("webpack")) {
|
|
1149
|
+
warn("next.config.js already has a webpack config. Please add the following aliases manually:");
|
|
1150
|
+
console.log(" const sdkPath = path.dirname(require.resolve('@usesidekick/react/package.json'));");
|
|
1151
|
+
console.log(" config.resolve.alias['@usesidekick/react/server'] = path.join(sdkPath, 'dist/server/index.mjs');");
|
|
1152
|
+
console.log(" config.resolve.alias['@usesidekick/react/jsx-runtime'] = path.join(sdkPath, 'dist/jsx-runtime.mjs');");
|
|
1153
|
+
console.log(" config.resolve.alias['@usesidekick/react/jsx-dev-runtime'] = path.join(sdkPath, 'dist/jsx-dev-runtime.mjs');");
|
|
1154
|
+
console.log(" config.resolve.alias['@usesidekick/react'] = path.join(sdkPath, 'dist/index.mjs');");
|
|
1155
|
+
} else {
|
|
1156
|
+
const modifiedConfig = existing.replace(
|
|
1157
|
+
/(const\s+nextConfig\s*=\s*\{)/,
|
|
1158
|
+
`$1
|
|
1159
|
+
webpack: (config, { isServer }) => {
|
|
1160
|
+
const sdkPath = path.dirname(require.resolve('@usesidekick/react/package.json'));
|
|
1161
|
+
config.resolve.alias = {
|
|
1162
|
+
...config.resolve.alias,
|
|
1163
|
+
'@usesidekick/react/server': path.join(sdkPath, 'dist/server/index.mjs'),
|
|
1164
|
+
'@usesidekick/react/jsx-runtime': path.join(sdkPath, 'dist/jsx-runtime.mjs'),
|
|
1165
|
+
'@usesidekick/react/jsx-dev-runtime': path.join(sdkPath, 'dist/jsx-dev-runtime.mjs'),
|
|
1166
|
+
'@usesidekick/react': path.join(sdkPath, 'dist/index.mjs'),
|
|
1167
|
+
};
|
|
1168
|
+
return config;
|
|
1169
|
+
},`
|
|
1170
|
+
);
|
|
1171
|
+
let finalConfig = modifiedConfig;
|
|
1172
|
+
if (!modifiedConfig.includes("require('path')") && !modifiedConfig.includes('require("path")')) {
|
|
1173
|
+
finalConfig = "const path = require('path');\n" + finalConfig;
|
|
1174
|
+
}
|
|
1175
|
+
await fs3.writeFile(configPath, finalConfig);
|
|
1176
|
+
}
|
|
1177
|
+
} catch {
|
|
1178
|
+
warn("Could not modify next.config.js. Writing new config.");
|
|
1179
|
+
await fs3.writeFile(configPath, newConfigTemplate);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
async function setupDatabase(project) {
|
|
1183
|
+
const { targetDir, srcDir } = project;
|
|
1184
|
+
const dbDir = path3.join(targetDir, srcDir, "lib", "db");
|
|
1185
|
+
await fs3.mkdir(dbDir, { recursive: true });
|
|
1186
|
+
const schemaPath = path3.join(dbDir, "schema.ts");
|
|
1187
|
+
if (!existsSync(schemaPath)) {
|
|
1188
|
+
await fs3.writeFile(schemaPath, `import { pgTable, text, timestamp, boolean } from 'drizzle-orm/pg-core';
|
|
1189
|
+
|
|
1190
|
+
// Sidekick overrides table
|
|
1191
|
+
export const overrides = pgTable('overrides', {
|
|
1192
|
+
id: text('id').primaryKey(),
|
|
1193
|
+
name: text('name').notNull(),
|
|
1194
|
+
description: text('description').notNull(),
|
|
1195
|
+
version: text('version').notNull().default('1.0.0'),
|
|
1196
|
+
primitives: text('primitives').notNull(),
|
|
1197
|
+
code: text('code').notNull(),
|
|
1198
|
+
enabled: boolean('enabled').notNull().default(true),
|
|
1199
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
1200
|
+
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
export type Override = typeof overrides.$inferSelect;
|
|
1204
|
+
export type NewOverride = typeof overrides.$inferInsert;
|
|
1205
|
+
`);
|
|
1206
|
+
} else {
|
|
1207
|
+
const existing = await fs3.readFile(schemaPath, "utf-8");
|
|
1208
|
+
if (!existing.includes("overrides") || !existing.includes("pgTable('overrides'")) {
|
|
1209
|
+
warn("schema.ts exists but does not contain an overrides table. Please add it manually.");
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
const dbIndexPath = path3.join(dbDir, "index.ts");
|
|
1213
|
+
if (!existsSync(dbIndexPath)) {
|
|
1214
|
+
await fs3.writeFile(dbIndexPath, `import { neon } from '@neondatabase/serverless';
|
|
1215
|
+
import { drizzle } from 'drizzle-orm/neon-http';
|
|
1216
|
+
import * as schema from './schema';
|
|
1217
|
+
|
|
1218
|
+
let _db: ReturnType<typeof drizzle> | null = null;
|
|
1219
|
+
|
|
1220
|
+
export function getDb() {
|
|
1221
|
+
if (!_db) {
|
|
1222
|
+
const url = process.env.DATABASE_URL;
|
|
1223
|
+
if (!url) {
|
|
1224
|
+
throw new Error('DATABASE_URL environment variable is not set');
|
|
1225
|
+
}
|
|
1226
|
+
const sql = neon(url);
|
|
1227
|
+
_db = drizzle(sql, { schema });
|
|
1228
|
+
}
|
|
1229
|
+
return _db;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
export const db = new Proxy({} as ReturnType<typeof drizzle>, {
|
|
1233
|
+
get(_, prop) {
|
|
1234
|
+
return getDb()[prop as keyof ReturnType<typeof drizzle>];
|
|
1235
|
+
},
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
export * from './schema';
|
|
1239
|
+
`);
|
|
1240
|
+
}
|
|
1241
|
+
const drizzleConfigPath = path3.join(targetDir, "drizzle.config.ts");
|
|
1242
|
+
if (!existsSync(drizzleConfigPath)) {
|
|
1243
|
+
const schemaRelPath = srcDir === "." ? "./lib/db/schema.ts" : `./${srcDir}/lib/db/schema.ts`;
|
|
1244
|
+
await fs3.writeFile(drizzleConfigPath, `import { config } from 'dotenv';
|
|
1245
|
+
import { defineConfig } from 'drizzle-kit';
|
|
1246
|
+
|
|
1247
|
+
config({ path: '.env.local' });
|
|
1248
|
+
|
|
1249
|
+
if (!process.env.DATABASE_URL) {
|
|
1250
|
+
throw new Error('DATABASE_URL is not set in .env.local');
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
export default defineConfig({
|
|
1254
|
+
schema: '${schemaRelPath}',
|
|
1255
|
+
out: './drizzle',
|
|
1256
|
+
dialect: 'postgresql',
|
|
1257
|
+
dbCredentials: {
|
|
1258
|
+
url: process.env.DATABASE_URL,
|
|
1259
|
+
},
|
|
1260
|
+
});
|
|
1261
|
+
`);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
async function createApiRoute(project) {
|
|
1265
|
+
const { targetDir, appDir } = project;
|
|
1266
|
+
const routeDir = path3.join(targetDir, appDir, "api", "sidekick", "[...action]");
|
|
1267
|
+
const sidekickApiDir = path3.join(targetDir, appDir, "api", "sidekick");
|
|
1268
|
+
if (existsSync(sidekickApiDir)) {
|
|
1269
|
+
try {
|
|
1270
|
+
const entries = await fs3.readdir(sidekickApiDir);
|
|
1271
|
+
const existingRoutes = entries.filter((e) => e !== "[...action]");
|
|
1272
|
+
if (existingRoutes.length > 0) {
|
|
1273
|
+
warn("Existing sidekick API routes found: " + existingRoutes.join(", "));
|
|
1274
|
+
warn("Skipping API route creation to avoid conflicts. Please migrate manually.");
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
} catch {
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
await fs3.mkdir(routeDir, { recursive: true });
|
|
1281
|
+
const routePath = path3.join(routeDir, "route.ts");
|
|
1282
|
+
await fs3.writeFile(routePath, `import { createSidekickHandler, createDrizzleStorage } from '@usesidekick/react/server';
|
|
1283
|
+
import { db } from '@/lib/db';
|
|
1284
|
+
import { overrides } from '@/lib/db/schema';
|
|
1285
|
+
|
|
1286
|
+
export const dynamic = 'force-dynamic';
|
|
1287
|
+
|
|
1288
|
+
const handler = createSidekickHandler({
|
|
1289
|
+
storage: createDrizzleStorage(db, overrides),
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
export const GET = handler.GET;
|
|
1293
|
+
export const POST = handler.POST;
|
|
1294
|
+
`);
|
|
1295
|
+
}
|
|
1296
|
+
async function setupBootstrap(project) {
|
|
1297
|
+
const { targetDir, srcDir, appDir } = project;
|
|
1298
|
+
const componentsDir = path3.join(targetDir, srcDir, "components");
|
|
1299
|
+
await fs3.mkdir(componentsDir, { recursive: true });
|
|
1300
|
+
const bootstrapPath = path3.join(componentsDir, "SidekickBootstrap.tsx");
|
|
1301
|
+
if (!existsSync(bootstrapPath)) {
|
|
1302
|
+
await fs3.writeFile(bootstrapPath, `'use client';
|
|
1303
|
+
|
|
1304
|
+
import { useEffect, useState, ReactNode } from 'react';
|
|
1305
|
+
import { SidekickProvider, SidekickPanel } from '@usesidekick/react';
|
|
1306
|
+
|
|
1307
|
+
interface SidekickBootstrapProps {
|
|
1308
|
+
children: ReactNode;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
export function SidekickBootstrap({ children }: SidekickBootstrapProps) {
|
|
1312
|
+
const [mounted, setMounted] = useState(false);
|
|
1313
|
+
|
|
1314
|
+
useEffect(() => {
|
|
1315
|
+
setMounted(true);
|
|
1316
|
+
}, []);
|
|
1317
|
+
|
|
1318
|
+
if (!mounted) {
|
|
1319
|
+
return <>{children}</>;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
return (
|
|
1323
|
+
<SidekickProvider overridesEndpoint="/api/sidekick/overrides">
|
|
1324
|
+
{children}
|
|
1325
|
+
<SidekickPanel
|
|
1326
|
+
apiEndpoint="/api/sidekick/generate"
|
|
1327
|
+
toggleEndpoint="/api/sidekick/toggle"
|
|
1328
|
+
/>
|
|
1329
|
+
</SidekickProvider>
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
`);
|
|
1333
|
+
}
|
|
1334
|
+
const layoutExts = ["layout.tsx", "layout.jsx", "layout.ts", "layout.js"];
|
|
1335
|
+
let layoutPath = null;
|
|
1336
|
+
for (const ext of layoutExts) {
|
|
1337
|
+
const candidate = path3.join(targetDir, appDir, ext);
|
|
1338
|
+
if (existsSync(candidate)) {
|
|
1339
|
+
layoutPath = candidate;
|
|
1340
|
+
break;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
if (!layoutPath) {
|
|
1344
|
+
warn("layout.tsx not found. Please add SidekickBootstrap to your layout manually.");
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
const layout = await fs3.readFile(layoutPath, "utf-8");
|
|
1348
|
+
if (layout.includes("SidekickBootstrap")) {
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
const backupPath = layoutPath + ".sidekick-backup";
|
|
1352
|
+
await fs3.writeFile(backupPath, layout);
|
|
1353
|
+
try {
|
|
1354
|
+
let modified = layout;
|
|
1355
|
+
const importLine = "import { SidekickBootstrap } from '@/components/SidekickBootstrap';";
|
|
1356
|
+
const importRegex = /^import\s+(?:(?:\{[^}]*\}|[^;'"]*)\s+from\s+)?['"][^'"]+['"];?/gm;
|
|
1357
|
+
let lastImportEnd = 0;
|
|
1358
|
+
let match;
|
|
1359
|
+
while ((match = importRegex.exec(modified)) !== null) {
|
|
1360
|
+
lastImportEnd = match.index + match[0].length;
|
|
1361
|
+
}
|
|
1362
|
+
if (lastImportEnd > 0) {
|
|
1363
|
+
modified = modified.slice(0, lastImportEnd) + "\n" + importLine + modified.slice(lastImportEnd);
|
|
1364
|
+
} else {
|
|
1365
|
+
modified = importLine + "\n" + modified;
|
|
1366
|
+
}
|
|
1367
|
+
const jsxChildrenRegex = /(>[\s]*)\{children\}([\s]*<)/;
|
|
1368
|
+
if (jsxChildrenRegex.test(modified)) {
|
|
1369
|
+
modified = modified.replace(
|
|
1370
|
+
jsxChildrenRegex,
|
|
1371
|
+
"$1<SidekickBootstrap>{children}</SidekickBootstrap>$2"
|
|
1372
|
+
);
|
|
1373
|
+
} else if (modified.includes("{children}")) {
|
|
1374
|
+
const indentedChildrenRegex = /^(\s{4,})\{children\}/m;
|
|
1375
|
+
if (indentedChildrenRegex.test(modified)) {
|
|
1376
|
+
modified = modified.replace(
|
|
1377
|
+
indentedChildrenRegex,
|
|
1378
|
+
"$1<SidekickBootstrap>{children}</SidekickBootstrap>"
|
|
1379
|
+
);
|
|
1380
|
+
} else {
|
|
1381
|
+
warn("Could not find {children} in JSX context in layout. Please wrap your content with <SidekickBootstrap> manually.");
|
|
1382
|
+
}
|
|
1383
|
+
} else {
|
|
1384
|
+
warn("Could not find {children} in layout. Please wrap your content with <SidekickBootstrap> manually.");
|
|
1385
|
+
}
|
|
1386
|
+
await fs3.writeFile(layoutPath, modified);
|
|
1387
|
+
} catch {
|
|
1388
|
+
warn("Could not modify layout.tsx. Please add SidekickBootstrap manually:");
|
|
1389
|
+
console.log(" 1. Import: import { SidekickBootstrap } from '@/components/SidekickBootstrap';");
|
|
1390
|
+
console.log(" 2. Wrap {children} with <SidekickBootstrap>{children}</SidekickBootstrap>");
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// src/index.ts
|
|
1395
|
+
var program = new Command();
|
|
1396
|
+
program.name("sidekick").description("Sidekick CLI - Generate and manage override modules").version("0.1.0");
|
|
1397
|
+
program.command("analyze").description("Analyze an app and generate schema.json").option("-d, --dir <path>", "Target directory to analyze", ".").option("-o, --output <path>", "Output path for schema.json", "./sidekick/schema.json").action(async (options) => {
|
|
1398
|
+
const targetDir = path4.resolve(options.dir);
|
|
1399
|
+
const outputPath = path4.resolve(options.output);
|
|
1400
|
+
console.log(`Analyzing ${targetDir}...`);
|
|
1401
|
+
try {
|
|
1402
|
+
const schema = await analyze({ targetDir, outputPath });
|
|
1403
|
+
console.log("\nAnalysis complete!");
|
|
1404
|
+
console.log(` Components: ${schema.components.length}`);
|
|
1405
|
+
console.log(` Extension points: ${schema.extensionPoints.length}`);
|
|
1406
|
+
console.log(` Data models: ${schema.dataModels.length}`);
|
|
1407
|
+
console.log(` Design system: ${schema.designSystem.framework || "plain CSS"}, ${schema.designSystem.componentStyles.length} component style samples`);
|
|
1408
|
+
console.log(`
|
|
1409
|
+
Schema written to: ${outputPath}`);
|
|
1410
|
+
} catch (error) {
|
|
1411
|
+
console.error("Analysis failed:", error instanceof Error ? error.message : error);
|
|
1412
|
+
process.exit(1);
|
|
1413
|
+
}
|
|
1414
|
+
});
|
|
1415
|
+
program.command("generate").description("Generate an override module from a natural language request").argument("<request>", "Natural language description of the customization").option("-s, --schema <path>", "Path to schema.json", "./sidekick/schema.json").option("-o, --output <path>", "Output directory for overrides", "./sidekick/overrides").option("-k, --api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY)").action(async (request, options) => {
|
|
1416
|
+
const schemaPath = path4.resolve(options.schema);
|
|
1417
|
+
const outputDir = path4.resolve(options.output);
|
|
1418
|
+
console.log(`Generating override for: "${request}"`);
|
|
1419
|
+
console.log(`Using schema: ${schemaPath}`);
|
|
1420
|
+
try {
|
|
1421
|
+
const result = await generate({
|
|
1422
|
+
request,
|
|
1423
|
+
schemaPath,
|
|
1424
|
+
outputDir,
|
|
1425
|
+
apiKey: options.apiKey
|
|
1426
|
+
});
|
|
1427
|
+
if (result.success) {
|
|
1428
|
+
console.log("\nGeneration successful!");
|
|
1429
|
+
console.log(` Override ID: ${result.override?.manifest.id}`);
|
|
1430
|
+
console.log(` Name: ${result.override?.manifest.name}`);
|
|
1431
|
+
console.log(` Output: ${result.outputPath}`);
|
|
1432
|
+
console.log("\nTo enable this override, add it to your flags.json or use the Sidekick panel.");
|
|
1433
|
+
} else {
|
|
1434
|
+
console.error("\nGeneration failed:", result.error);
|
|
1435
|
+
if (result.validationErrors) {
|
|
1436
|
+
console.error("Validation errors:");
|
|
1437
|
+
result.validationErrors.forEach((e) => console.error(` - ${e}`));
|
|
1438
|
+
}
|
|
1439
|
+
process.exit(1);
|
|
1440
|
+
}
|
|
1441
|
+
} catch (error) {
|
|
1442
|
+
console.error("Generation failed:", error instanceof Error ? error.message : error);
|
|
1443
|
+
process.exit(1);
|
|
1444
|
+
}
|
|
1445
|
+
});
|
|
1446
|
+
program.command("validate").description("Validate an override module").argument("<file>", "Path to the override file to validate").action(async (file) => {
|
|
1447
|
+
const filepath = path4.resolve(file);
|
|
1448
|
+
console.log(`Validating ${filepath}...`);
|
|
1449
|
+
try {
|
|
1450
|
+
const code = await fs4.readFile(filepath, "utf-8");
|
|
1451
|
+
const result = validateFile(filepath, code);
|
|
1452
|
+
if (result.valid) {
|
|
1453
|
+
console.log("\nValidation passed!");
|
|
1454
|
+
if (result.warnings.length > 0) {
|
|
1455
|
+
console.log("\nWarnings:");
|
|
1456
|
+
result.warnings.forEach((w) => {
|
|
1457
|
+
const location = w.line ? ` (line ${w.line})` : "";
|
|
1458
|
+
console.log(` - ${w.type}${location}: ${w.message}`);
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
} else {
|
|
1462
|
+
console.error("\nValidation failed!");
|
|
1463
|
+
console.error("\nErrors:");
|
|
1464
|
+
result.errors.forEach((e) => {
|
|
1465
|
+
const location = e.line ? ` (line ${e.line}${e.column ? `:${e.column}` : ""})` : "";
|
|
1466
|
+
console.error(` - ${e.type}${location}: ${e.message}`);
|
|
1467
|
+
});
|
|
1468
|
+
process.exit(1);
|
|
1469
|
+
}
|
|
1470
|
+
} catch (error) {
|
|
1471
|
+
console.error("Validation failed:", error instanceof Error ? error.message : error);
|
|
1472
|
+
process.exit(1);
|
|
1473
|
+
}
|
|
1474
|
+
});
|
|
1475
|
+
program.command("init").description("Initialize Sidekick in the current project (installs deps, configures build, creates API routes, modifies layout)").option("-d, --dir <path>", "Target directory", ".").action(async (options) => {
|
|
1476
|
+
const targetDir = path4.resolve(options.dir);
|
|
1477
|
+
try {
|
|
1478
|
+
await init(targetDir);
|
|
1479
|
+
} catch (error) {
|
|
1480
|
+
console.error("Initialization failed:", error instanceof Error ? error.message : error);
|
|
1481
|
+
process.exit(1);
|
|
1482
|
+
}
|
|
1483
|
+
});
|
|
1484
|
+
program.parse();
|
|
1485
|
+
//# sourceMappingURL=index.js.map
|