southpaw 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -0
- package/api/client.ts +83 -0
- package/api/helpers/fs.ts +98 -0
- package/api/helpers/stream.ts +6 -0
- package/api/index.ts +50 -0
- package/api/server.ts +566 -0
- package/constants/fonts.ts +1583 -0
- package/constants/icons.ts +2124 -0
- package/constants/langs.ts +2592 -0
- package/constants/ssr-loading.tsx +17 -0
- package/contexts/dev.tsx +124 -0
- package/hooks/useDebounceCb.ts +21 -0
- package/modules/app.tsx +102 -0
- package/modules/behaviors.ts +161 -0
- package/modules/component.tsx +483 -0
- package/modules/config/index.ts +8 -0
- package/modules/database.ts +25 -0
- package/modules/document.tsx +23 -0
- package/modules/error.tsx +16 -0
- package/modules/page/autozoom.tsx +92 -0
- package/modules/page/browser.tsx +166 -0
- package/modules/page/index.tsx +327 -0
- package/modules/page/insert-modal.tsx +225 -0
- package/modules/page/inspect/index.tsx +237 -0
- package/modules/page/inspect/wrapper.tsx +88 -0
- package/modules/page/knobs/action.tsx +58 -0
- package/modules/page/knobs/color.tsx +78 -0
- package/modules/page/knobs/helpers.ts +71 -0
- package/modules/page/knobs/icon.tsx +85 -0
- package/modules/page/knobs/linker.tsx +188 -0
- package/modules/page/knobs/list-json.tsx +149 -0
- package/modules/page/knobs/list-text.tsx +129 -0
- package/modules/page/knobs/number.tsx +84 -0
- package/modules/page/knobs/segmented.tsx +81 -0
- package/modules/page/knobs/select.tsx +89 -0
- package/modules/page/knobs/size.tsx +106 -0
- package/modules/page/knobs/switch.tsx +75 -0
- package/modules/page/knobs/text.tsx +93 -0
- package/modules/page/left-tabs.tsx +36 -0
- package/modules/page/right-tabs.tsx +31 -0
- package/modules/page/seo.tsx +69 -0
- package/modules/page/tree.tsx +860 -0
- package/modules/state.ts +787 -0
- package/modules/store.ts +37 -0
- package/modules/theme.ts +105 -0
- package/modules/translations.ts +15 -0
- package/package.json +68 -0
- package/scripts/bin.js +607 -0
- package/scripts/test.js +145 -0
- package/themes/base.tsx +8962 -0
- package/themes/index.ts +4 -0
- package/themes/m2.ts +39 -0
- package/themes/newspaper.ts +39 -0
- package/types/api.ts +57 -0
- package/types/fiber_node.ts +69 -0
- package/types/react_element.ts +14 -0
- package/ui/accordion.tsx +70 -0
- package/ui/actionicon.tsx +41 -0
- package/ui/affix.tsx +14 -0
- package/ui/alert.tsx +37 -0
- package/ui/anchor.tsx +52 -0
- package/ui/arrow.tsx +116 -0
- package/ui/aspectratio.tsx +21 -0
- package/ui/autocomplete.tsx +34 -0
- package/ui/avatar.tsx +36 -0
- package/ui/backgroundimage.tsx +16 -0
- package/ui/badge.tsx +28 -0
- package/ui/blockquote.tsx +15 -0
- package/ui/blocks/faq.tsx +145 -0
- package/ui/blocks/features.tsx +167 -0
- package/ui/blocks/footer.tsx +182 -0
- package/ui/blocks/herobanner.tsx +391 -0
- package/ui/blocks/index.ts +44 -0
- package/ui/blocks/leftnav.tsx +141 -0
- package/ui/blocks/pricing.tsx +210 -0
- package/ui/blocks/testimonials.tsx +149 -0
- package/ui/blocks/uidocs.tsx +1429 -0
- package/ui/box.tsx +27 -0
- package/ui/breadcrumbs.tsx +33 -0
- package/ui/burger.tsx +23 -0
- package/ui/button.tsx +85 -0
- package/ui/card.tsx +28 -0
- package/ui/center.tsx +26 -0
- package/ui/checkbox.tsx +35 -0
- package/ui/chip.tsx +32 -0
- package/ui/closebutton.tsx +15 -0
- package/ui/code.tsx +104 -0
- package/ui/collapse.tsx +16 -0
- package/ui/colorinput.tsx +32 -0
- package/ui/colorpicker.tsx +26 -0
- package/ui/colorswatch.tsx +17 -0
- package/ui/combobox.tsx +80 -0
- package/ui/container.tsx +27 -0
- package/ui/dialog.tsx +15 -0
- package/ui/divider.tsx +58 -0
- package/ui/drawer.tsx +28 -0
- package/ui/fieldset.tsx +15 -0
- package/ui/filebutton.tsx +23 -0
- package/ui/fileinput.tsx +36 -0
- package/ui/flex.tsx +139 -0
- package/ui/focustrap.tsx +15 -0
- package/ui/form.tsx +26 -0
- package/ui/grid.tsx +69 -0
- package/ui/highlight.tsx +15 -0
- package/ui/hoverable.tsx +66 -0
- package/ui/hovercard.tsx +45 -0
- package/ui/icon.tsx +76 -0
- package/ui/image.tsx +32 -0
- package/ui/index.ts +1355 -0
- package/ui/indicator.tsx +42 -0
- package/ui/infobox.tsx +200 -0
- package/ui/input.tsx +18 -0
- package/ui/inviewport.tsx +23 -0
- package/ui/jsoninput.tsx +35 -0
- package/ui/kbd.tsx +20 -0
- package/ui/list.tsx +33 -0
- package/ui/loader.tsx +26 -0
- package/ui/loadingoverlay.tsx +22 -0
- package/ui/mark.tsx +15 -0
- package/ui/menu.tsx +89 -0
- package/ui/modal.tsx +27 -0
- package/ui/multiselect.tsx +42 -0
- package/ui/nativeselect.tsx +17 -0
- package/ui/navlink.tsx +40 -0
- package/ui/notification.tsx +25 -0
- package/ui/numberformatter.tsx +15 -0
- package/ui/numberinput.tsx +37 -0
- package/ui/overlay.tsx +15 -0
- package/ui/pagination.tsx +21 -0
- package/ui/paper.tsx +33 -0
- package/ui/passwordinput.tsx +36 -0
- package/ui/pill.tsx +17 -0
- package/ui/pillsinput.tsx +37 -0
- package/ui/pininput.tsx +30 -0
- package/ui/popover.tsx +43 -0
- package/ui/portal.tsx +15 -0
- package/ui/progress.tsx +49 -0
- package/ui/props.ts +91 -0
- package/ui/propsDef.ts +38 -0
- package/ui/radio.tsx +30 -0
- package/ui/rating.tsx +25 -0
- package/ui/ringprogress.tsx +17 -0
- package/ui/scrollarea.tsx +27 -0
- package/ui/segmentedcontrol.tsx +38 -0
- package/ui/select.tsx +43 -0
- package/ui/simplegrid.tsx +23 -0
- package/ui/skeleton.tsx +15 -0
- package/ui/slider.tsx +31 -0
- package/ui/space.tsx +19 -0
- package/ui/spoiler.tsx +23 -0
- package/ui/stepper.tsx +63 -0
- package/ui/switch.tsx +29 -0
- package/ui/table.tsx +93 -0
- package/ui/tabs.tsx +80 -0
- package/ui/tagsinput.tsx +19 -0
- package/ui/terminal.tsx +44 -0
- package/ui/text.tsx +168 -0
- package/ui/textarea.tsx +36 -0
- package/ui/textinput.tsx +34 -0
- package/ui/timeline.tsx +30 -0
- package/ui/title.tsx +42 -0
- package/ui/tooltip.tsx +32 -0
- package/ui/transition.tsx +21 -0
- package/ui/tree.tsx +42 -0
- package/ui/typographystylesprovider.tsx +20 -0
- package/ui/unstyledbutton.tsx +18 -0
- package/ui/video.tsx +16 -0
- package/ui/visuallyhidden.tsx +15 -0
- package/utils/index.ts +76 -0
package/scripts/bin.js
ADDED
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { input, select, confirm, checkbox } from "@inquirer/prompts";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import shell from "shelljs";
|
|
6
|
+
import { testRoutes } from "./test.js";
|
|
7
|
+
|
|
8
|
+
const model = "phi4";
|
|
9
|
+
const createProject = async () => {
|
|
10
|
+
const name = await input({
|
|
11
|
+
message: "Enter your project name",
|
|
12
|
+
required: true,
|
|
13
|
+
validate: (input) => {
|
|
14
|
+
if (input.length < 3) {
|
|
15
|
+
return "Project name must be at least 3 characters long";
|
|
16
|
+
}
|
|
17
|
+
if (!/^[a-z0-9_-]+$/i.test(input)) {
|
|
18
|
+
return "Project name must only contain letters, numbers, underscores, or hyphens";
|
|
19
|
+
}
|
|
20
|
+
if (input !== input.toLowerCase()) {
|
|
21
|
+
return "Project name must be lowercase";
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const template = await select({
|
|
28
|
+
message: "Select a template",
|
|
29
|
+
choices: [
|
|
30
|
+
{
|
|
31
|
+
name: "Blank",
|
|
32
|
+
value: "blank",
|
|
33
|
+
description: "This is a blank template",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "Portfolio",
|
|
37
|
+
value: "portfolio",
|
|
38
|
+
description: "This is a portfolio template",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "Todolist",
|
|
42
|
+
value: "todolist",
|
|
43
|
+
description: "This is a todolist template",
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const ok = await confirm({ message: "Confirm?" });
|
|
49
|
+
if (ok) {
|
|
50
|
+
console.log(template);
|
|
51
|
+
shell.exec(
|
|
52
|
+
`git clone https://github.com/solidsnail/southpaw-templates-${template} ${name}`
|
|
53
|
+
);
|
|
54
|
+
console.log("✔ Project created successfully!");
|
|
55
|
+
console.log(`1. cd to ${name}`);
|
|
56
|
+
console.log(`2. npm i`);
|
|
57
|
+
console.log(`3. npm run dev`);
|
|
58
|
+
} else {
|
|
59
|
+
console.log("✘ Project creation cancelled!");
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const generateBlock = async () => {
|
|
65
|
+
const name = await input({
|
|
66
|
+
message: "Enter your block name",
|
|
67
|
+
required: true,
|
|
68
|
+
validate: (input) => {
|
|
69
|
+
if (input.length < 3) {
|
|
70
|
+
return "Block name must be at least 3 characters long";
|
|
71
|
+
}
|
|
72
|
+
if (!/^[a-z0-9_-]+$/i.test(input)) {
|
|
73
|
+
return "Block name must only contain letters, numbers, underscores, or hyphens";
|
|
74
|
+
}
|
|
75
|
+
// Check if block already exists
|
|
76
|
+
if (fs.existsSync(`.southpaw/ui/blocks/${input}.tsx`)) {
|
|
77
|
+
return `Block '${input}' already exists. Choose a different name.`;
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const description = await input({
|
|
84
|
+
message: "Enter your block description",
|
|
85
|
+
required: true,
|
|
86
|
+
validate: (input) => {
|
|
87
|
+
if (input.length < 10) {
|
|
88
|
+
return "Description must be at least 10 characters long for better generation quality";
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const blockType = await select({
|
|
95
|
+
message: "What type of block are you creating?",
|
|
96
|
+
choices: [
|
|
97
|
+
{
|
|
98
|
+
name: "UI Component",
|
|
99
|
+
value: "ui",
|
|
100
|
+
description: "Interactive UI element (buttons, forms, cards)",
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "Layout",
|
|
104
|
+
value: "layout",
|
|
105
|
+
description: "Layout component (navigation, hero, footer)",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "Data Display",
|
|
109
|
+
value: "data",
|
|
110
|
+
description: "Data visualization or list component",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "Form",
|
|
114
|
+
value: "form",
|
|
115
|
+
description: "Form or input-related component",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "Content",
|
|
119
|
+
value: "content",
|
|
120
|
+
description: "Content display component",
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const features = await checkbox({
|
|
126
|
+
message: "What features should this block have?",
|
|
127
|
+
choices: [
|
|
128
|
+
{ name: "Interactive state", value: "state" },
|
|
129
|
+
{ name: "Props configuration", value: "props" },
|
|
130
|
+
{ name: "Click handlers", value: "handlers" },
|
|
131
|
+
{ name: "Form inputs", value: "forms" },
|
|
132
|
+
{ name: "Styling options", value: "styling" },
|
|
133
|
+
{ name: "Animation/transitions", value: "animations" },
|
|
134
|
+
{ name: "Responsive design", value: "responsive" },
|
|
135
|
+
],
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const complexity = await select({
|
|
139
|
+
message: "Component complexity level?",
|
|
140
|
+
choices: [
|
|
141
|
+
{
|
|
142
|
+
name: "Simple",
|
|
143
|
+
value: "simple",
|
|
144
|
+
description: "Basic component with minimal features",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "Medium",
|
|
148
|
+
value: "medium",
|
|
149
|
+
description: "Moderate complexity with some interactivity",
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: "Complex",
|
|
153
|
+
value: "complex",
|
|
154
|
+
description: "Advanced component with rich features",
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
console.log("\n📋 Generation Summary:");
|
|
160
|
+
console.log(` Name: ${name}`);
|
|
161
|
+
console.log(` Type: ${blockType}`);
|
|
162
|
+
console.log(` Features: ${features.join(", ") || "None"}`);
|
|
163
|
+
console.log(` Complexity: ${complexity}`);
|
|
164
|
+
console.log(` Description: ${description}\n`);
|
|
165
|
+
|
|
166
|
+
const ok = await confirm({ message: "Generate this block?" });
|
|
167
|
+
|
|
168
|
+
if (ok) {
|
|
169
|
+
console.log("🤖 Generating block with AI...\n");
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
// Read example files more safely
|
|
173
|
+
const exampleFiles = [
|
|
174
|
+
"herobanner1.tsx",
|
|
175
|
+
"leftnav.tsx",
|
|
176
|
+
"uidocs.tsx",
|
|
177
|
+
"counter.tsx",
|
|
178
|
+
"todolist.tsx",
|
|
179
|
+
].filter((file) => fs.existsSync(`.southpaw/ui/blocks/${file}`));
|
|
180
|
+
|
|
181
|
+
if (exampleFiles.length === 0) {
|
|
182
|
+
console.error("❌ No example blocks found in .southpaw/ui/blocks/");
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Build examples string
|
|
187
|
+
let examplesContent = "";
|
|
188
|
+
for (const file of exampleFiles) {
|
|
189
|
+
try {
|
|
190
|
+
const content = fs.readFileSync(
|
|
191
|
+
`.southpaw/ui/blocks/${file}`,
|
|
192
|
+
"utf-8"
|
|
193
|
+
);
|
|
194
|
+
examplesContent += `//.southpaw/ui/blocks/${file}\n${content}\n\n`;
|
|
195
|
+
} catch {
|
|
196
|
+
console.warn(`⚠️ Could not read ${file}, skipping...`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Read supporting files
|
|
201
|
+
let uiIndexContent = "";
|
|
202
|
+
let componentModuleContent = "";
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
uiIndexContent = fs.readFileSync(".southpaw/ui/index.ts", "utf-8");
|
|
206
|
+
} catch {
|
|
207
|
+
console.warn("⚠️ Could not read .southpaw/ui/index.ts");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
componentModuleContent = fs.readFileSync(
|
|
212
|
+
".southpaw/modules/component.tsx",
|
|
213
|
+
"utf-8"
|
|
214
|
+
);
|
|
215
|
+
} catch {
|
|
216
|
+
console.warn("⚠️ Could not read .southpaw/modules/component.tsx");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const SYSTEM = `You are an expert Southpaw framework developer (a superset of Next.js). Your task is to generate high-quality, functional blocks based on user requirements.
|
|
220
|
+
|
|
221
|
+
FRAMEWORK KNOWLEDGE:
|
|
222
|
+
${examplesContent}
|
|
223
|
+
|
|
224
|
+
//.southpaw/ui/index.ts
|
|
225
|
+
${uiIndexContent}
|
|
226
|
+
|
|
227
|
+
//.southpaw/modules/component.tsx
|
|
228
|
+
${componentModuleContent}
|
|
229
|
+
|
|
230
|
+
GENERATION RULES:
|
|
231
|
+
1. ONLY return the TypeScript/TSX code for the component
|
|
232
|
+
2. DO NOT wrap the code in backticks, markdown fences, or any code blocks
|
|
233
|
+
3. DO NOT include any explanatory text before or after the code
|
|
234
|
+
4. Start directly with imports and end with the export default statement
|
|
235
|
+
5. Follow the exact pattern from the examples
|
|
236
|
+
6. Use create_component() function with proper structure
|
|
237
|
+
7. Include proper TypeScript types
|
|
238
|
+
8. Use UI components from the UI object
|
|
239
|
+
9. Implement proper props with propsDef for editor integration
|
|
240
|
+
10. Add appropriate state management if needed
|
|
241
|
+
11. Include proper methods for interactivity
|
|
242
|
+
12. Follow React/Next.js best practices
|
|
243
|
+
13. Ensure the component is self-contained and functional
|
|
244
|
+
|
|
245
|
+
CRITICAL: Your response should start with "import" and end with "export default Component;" with NO additional text or formatting.
|
|
246
|
+
|
|
247
|
+
COMPONENT REQUIREMENTS:
|
|
248
|
+
- Block Type: ${blockType}
|
|
249
|
+
- Features: ${features.join(", ") || "basic functionality"}
|
|
250
|
+
- Complexity: ${complexity}
|
|
251
|
+
- Must be production-ready and follow existing patterns
|
|
252
|
+
- Include proper prop definitions for the visual editor
|
|
253
|
+
- Use semantic and accessible HTML structure
|
|
254
|
+
- Follow the naming convention from examples
|
|
255
|
+
|
|
256
|
+
QUALITY CHECKLIST:
|
|
257
|
+
✓ Proper TypeScript types
|
|
258
|
+
✓ Complete propsDef object for editor
|
|
259
|
+
✓ Functional render method
|
|
260
|
+
✓ Appropriate default props
|
|
261
|
+
✓ Clean, readable code structure
|
|
262
|
+
✓ Follows Southpaw patterns exactly`;
|
|
263
|
+
|
|
264
|
+
const userPrompt = `Generate: //.southpaw/ui/blocks/${name}.tsx
|
|
265
|
+
|
|
266
|
+
Requirements:
|
|
267
|
+
- Component name: ${name}
|
|
268
|
+
- Description: ${description}
|
|
269
|
+
- Type: ${blockType}
|
|
270
|
+
- Features needed: ${features.join(", ") || "basic"}
|
|
271
|
+
- Complexity level: ${complexity}
|
|
272
|
+
|
|
273
|
+
Create a complete, functional Southpaw block component following the established patterns.`;
|
|
274
|
+
|
|
275
|
+
const messages = [
|
|
276
|
+
{ role: "system", content: SYSTEM },
|
|
277
|
+
{ role: "user", content: userPrompt },
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
let generatedCode = "";
|
|
281
|
+
|
|
282
|
+
const { default: ollama } = await import("ollama");
|
|
283
|
+
const response = await ollama.chat({
|
|
284
|
+
model,
|
|
285
|
+
messages,
|
|
286
|
+
stream: true,
|
|
287
|
+
think: false,
|
|
288
|
+
options: {
|
|
289
|
+
temperature: 0.05,
|
|
290
|
+
num_ctx: 32000,
|
|
291
|
+
top_p: 0.9,
|
|
292
|
+
repeat_penalty: 1.1,
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
process.stdout.write("📝 ");
|
|
297
|
+
for await (const part of response) {
|
|
298
|
+
const content = part.message.content;
|
|
299
|
+
process.stdout.write(content);
|
|
300
|
+
generatedCode += content;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
console.log("\n");
|
|
304
|
+
|
|
305
|
+
// Clean the generated code
|
|
306
|
+
generatedCode = cleanGeneratedCode(generatedCode);
|
|
307
|
+
|
|
308
|
+
// Validate the generated code
|
|
309
|
+
if (!validateGeneratedCode(generatedCode)) {
|
|
310
|
+
console.error("❌ Generated code validation failed. Please try again.");
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Save the generated block
|
|
315
|
+
const blockPath = `.southpaw/ui/blocks/${name}.tsx`;
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
// Ensure directory exists
|
|
319
|
+
const dir = path.dirname(blockPath);
|
|
320
|
+
if (!fs.existsSync(dir)) {
|
|
321
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Write the file
|
|
325
|
+
fs.writeFileSync(blockPath, generatedCode.trim(), "utf-8");
|
|
326
|
+
console.log(`✅ Block saved to: ${blockPath}`);
|
|
327
|
+
|
|
328
|
+
// Update the blocks index file
|
|
329
|
+
await updateBlocksIndex(name);
|
|
330
|
+
|
|
331
|
+
console.log("🎉 Block generation completed successfully!");
|
|
332
|
+
console.log("\nNext steps:");
|
|
333
|
+
console.log(`1. Review the generated code in ${blockPath}`);
|
|
334
|
+
console.log(`2. Test the component in your application`);
|
|
335
|
+
console.log(`3. Customize props and styling as needed`);
|
|
336
|
+
} catch (writeError) {
|
|
337
|
+
console.error("❌ Error saving block:", writeError.message);
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
} catch (error) {
|
|
341
|
+
console.error("❌ Error generating block:", error.message);
|
|
342
|
+
|
|
343
|
+
if (error.message.includes("model")) {
|
|
344
|
+
console.log("\n💡 Troubleshooting:");
|
|
345
|
+
console.log("• Make sure Ollama is running: `ollama serve`");
|
|
346
|
+
console.log(`• Check if ${model} model is installed: \`ollama list\``);
|
|
347
|
+
console.log(`• Install ${model} if needed: \`ollama pull ${model}\``);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
process.exit(1);
|
|
351
|
+
}
|
|
352
|
+
} else {
|
|
353
|
+
console.log("❌ Block generation cancelled!");
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const cleanGeneratedCode = (code) => {
|
|
359
|
+
// Remove markdown code fences
|
|
360
|
+
let cleaned = code.replace(/^```(?:tsx?|typescript|javascript)?\s*/gim, "");
|
|
361
|
+
cleaned = cleaned.replace(/```\s*$/gim, "");
|
|
362
|
+
|
|
363
|
+
// Remove any leading/trailing whitespace
|
|
364
|
+
cleaned = cleaned.trim();
|
|
365
|
+
|
|
366
|
+
// Remove any extra explanatory text that might be at the beginning
|
|
367
|
+
const lines = cleaned.split("\n");
|
|
368
|
+
let startIndex = 0;
|
|
369
|
+
|
|
370
|
+
// Find the first line that looks like an import or the component start
|
|
371
|
+
for (let i = 0; i < lines.length; i++) {
|
|
372
|
+
const line = lines[i].trim();
|
|
373
|
+
if (
|
|
374
|
+
line.startsWith("import ") ||
|
|
375
|
+
line.startsWith("type ") ||
|
|
376
|
+
line.startsWith("interface ") ||
|
|
377
|
+
line.includes("create_component")
|
|
378
|
+
) {
|
|
379
|
+
startIndex = i;
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Remove any trailing explanatory text after the last export
|
|
385
|
+
let endIndex = lines.length - 1;
|
|
386
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
387
|
+
const line = lines[i].trim();
|
|
388
|
+
if (line.startsWith("export default") || line === "});") {
|
|
389
|
+
endIndex = i;
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
cleaned = lines.slice(startIndex, endIndex + 1).join("\n");
|
|
395
|
+
|
|
396
|
+
// Ensure proper formatting
|
|
397
|
+
cleaned = cleaned.trim();
|
|
398
|
+
|
|
399
|
+
return cleaned;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const validateGeneratedCode = (code) => {
|
|
403
|
+
// Basic validation checks
|
|
404
|
+
const checks = [
|
|
405
|
+
{
|
|
406
|
+
test: () => code.includes("create_component"),
|
|
407
|
+
message: "Missing create_component function call",
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
test: () => code.includes("export default"),
|
|
411
|
+
message: "Missing export default statement",
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
test: () => code.includes("render()"),
|
|
415
|
+
message: "Missing render method",
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
test: () => code.includes("import") && code.includes("from"),
|
|
419
|
+
message: "Missing proper imports",
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
test: () => code.length > 100,
|
|
423
|
+
message: "Generated code is too short",
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
test: () => !code.includes("```"),
|
|
427
|
+
message: "Code still contains markdown fences",
|
|
428
|
+
},
|
|
429
|
+
];
|
|
430
|
+
|
|
431
|
+
for (const check of checks) {
|
|
432
|
+
if (!check.test()) {
|
|
433
|
+
console.error(`❌ Validation failed: ${check.message}`);
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
console.log("✅ Generated code validation passed");
|
|
439
|
+
return true;
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const updateBlocksIndex = async (blockName) => {
|
|
443
|
+
const indexPath = ".southpaw/ui/blocks/index.ts";
|
|
444
|
+
|
|
445
|
+
try {
|
|
446
|
+
let indexContent = "";
|
|
447
|
+
|
|
448
|
+
// Read existing content if file exists
|
|
449
|
+
if (fs.existsSync(indexPath)) {
|
|
450
|
+
indexContent = fs.readFileSync(indexPath, "utf-8");
|
|
451
|
+
} else {
|
|
452
|
+
// Create initial structure
|
|
453
|
+
indexContent = `import dynamic from "next/dynamic";\n\nexport const Blocks = {\n};\n`;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Check if block is already in the index
|
|
457
|
+
if (indexContent.includes(`${blockName}:`)) {
|
|
458
|
+
console.log(`ℹ️ Block '${blockName}' already exists in index`);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Add the new block to the Blocks object
|
|
463
|
+
const blockEntry = ` ${blockName}: (() => {
|
|
464
|
+
const Comp = dynamic(() => import("./${blockName}"));
|
|
465
|
+
Comp.displayName = "Blocks.${blockName}";
|
|
466
|
+
return Comp;
|
|
467
|
+
})(),`;
|
|
468
|
+
|
|
469
|
+
// Insert before the closing brace
|
|
470
|
+
const updatedContent = indexContent.replace(
|
|
471
|
+
/(\s*)(};?\s*)$/,
|
|
472
|
+
`$1${blockEntry}\n$1$2`
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
fs.writeFileSync(indexPath, updatedContent, "utf-8");
|
|
476
|
+
console.log(`✅ Updated blocks index: ${indexPath}`);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
console.warn(`⚠️ Could not update blocks index: ${error.message}`);
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const generateBehavior = async () => {
|
|
483
|
+
const name = await input({
|
|
484
|
+
message: "Enter behavior name (e.g. count, todoList)",
|
|
485
|
+
required: true,
|
|
486
|
+
validate: (value) =>
|
|
487
|
+
/^[a-zA-Z0-9_-]+$/.test(value) ||
|
|
488
|
+
"Name must contain only letters, numbers, dashes and underscores",
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
const ok = await confirm({ message: `Create src/behaviors/${name}.ts?` });
|
|
492
|
+
if (!ok) {
|
|
493
|
+
console.log("❌ Behavior generation cancelled!");
|
|
494
|
+
process.exit(1);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const filePath = path.join("src", "behaviors", `${name}.ts`);
|
|
498
|
+
if (fs.existsSync(filePath)) {
|
|
499
|
+
console.log(`❌ ${filePath} already exists`);
|
|
500
|
+
process.exit(1);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const template = `import { create_behaviors } from "southpaw/modules/behaviors";
|
|
504
|
+
|
|
505
|
+
export default create_behaviors(
|
|
506
|
+
{
|
|
507
|
+
state: {
|
|
508
|
+
// TODO: define initial state
|
|
509
|
+
},
|
|
510
|
+
actions: {
|
|
511
|
+
// TODO: define actions that mutate state and return Promise.resolve()
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
when: {
|
|
516
|
+
// "Do something"(arg: string) { return this.store.actions.doSomething(arg); },
|
|
517
|
+
},
|
|
518
|
+
then: {
|
|
519
|
+
// "Some derived value"() { return this.store.state.value; },
|
|
520
|
+
},
|
|
521
|
+
given: {
|
|
522
|
+
// "Some precondition"() { this.store.state = { ... }; return Promise.resolve(); },
|
|
523
|
+
},
|
|
524
|
+
}
|
|
525
|
+
);
|
|
526
|
+
`;
|
|
527
|
+
|
|
528
|
+
fs.writeFileSync(filePath, template, "utf-8");
|
|
529
|
+
console.log(`✅ Behavior created at ${filePath}`);
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
const generatePage = async () => {
|
|
533
|
+
const name = await input({
|
|
534
|
+
message: "Enter page name (without extension)",
|
|
535
|
+
required: true,
|
|
536
|
+
validate: (value) =>
|
|
537
|
+
/^[a-zA-Z0-9_-]+$/.test(value) ||
|
|
538
|
+
"Name must contain only letters, numbers, dashes and underscores",
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
const routePath = await input({
|
|
542
|
+
message: "Enter route path (e.g. /demo, /dashboard)",
|
|
543
|
+
required: true,
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
const ok = await confirm({
|
|
547
|
+
message: `Create src/pages/${name}.tsx for route ${routePath}?`,
|
|
548
|
+
});
|
|
549
|
+
if (!ok) {
|
|
550
|
+
console.log("❌ Page generation cancelled!");
|
|
551
|
+
process.exit(1);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const filePath = path.join("src", "pages", `${name}.tsx`);
|
|
555
|
+
if (fs.existsSync(filePath)) {
|
|
556
|
+
console.log(`❌ ${filePath} already exists`);
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const template = `import { create_page } from "southpaw/modules/page";
|
|
561
|
+
import { UI } from "southpaw/ui";
|
|
562
|
+
|
|
563
|
+
export default create_page({
|
|
564
|
+
name: "${name}",
|
|
565
|
+
description: "${name} page",
|
|
566
|
+
seo: {
|
|
567
|
+
title: "${name}",
|
|
568
|
+
description: "${name} page",
|
|
569
|
+
},
|
|
570
|
+
render() {
|
|
571
|
+
return (
|
|
572
|
+
<UI.flex direction="column" h="100%">
|
|
573
|
+
<Components.topbar page={this} />
|
|
574
|
+
<UI.divider />
|
|
575
|
+
<UI.container py="lg" size="lg">
|
|
576
|
+
<UI.title order={1} text="${name}" />
|
|
577
|
+
<UI.text c="dimmed" text="${routePath}" />
|
|
578
|
+
</UI.container>
|
|
579
|
+
</UI.flex>
|
|
580
|
+
);
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
`;
|
|
584
|
+
|
|
585
|
+
fs.writeFileSync(filePath, template, "utf-8");
|
|
586
|
+
console.log(`✅ Page created at ${filePath}`);
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
const [, , mode] = process.argv;
|
|
590
|
+
|
|
591
|
+
switch (mode) {
|
|
592
|
+
case "test":
|
|
593
|
+
await testRoutes();
|
|
594
|
+
break;
|
|
595
|
+
case "generate-block":
|
|
596
|
+
await generateBlock();
|
|
597
|
+
break;
|
|
598
|
+
case "generate-behavior":
|
|
599
|
+
await generateBehavior();
|
|
600
|
+
break;
|
|
601
|
+
case "generate-page":
|
|
602
|
+
await generatePage();
|
|
603
|
+
break;
|
|
604
|
+
default:
|
|
605
|
+
await createProject();
|
|
606
|
+
break;
|
|
607
|
+
}
|