mycontext-cli 0.2.38 → 0.4.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/agents/implementations/CodeGenSubAgent.d.ts +2 -0
- package/dist/agents/implementations/CodeGenSubAgent.d.ts.map +1 -1
- package/dist/agents/implementations/CodeGenSubAgent.js +344 -24
- package/dist/agents/implementations/CodeGenSubAgent.js.map +1 -1
- package/dist/agents/implementations/DocsSubAgent.js +1 -1
- package/dist/agents/implementations/DocsSubAgent.js.map +1 -1
- package/dist/agents/implementations/EnhancementAgent.d.ts.map +1 -1
- package/dist/agents/implementations/EnhancementAgent.js +31 -44
- package/dist/agents/implementations/EnhancementAgent.js.map +1 -1
- package/dist/agents/implementations/QASubAgent.js +1 -1
- package/dist/agents/orchestrator/SubAgentOrchestrator.js +1 -1
- package/dist/agents/orchestrator/SubAgentOrchestrator.js.map +1 -1
- package/dist/cli.js +38 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/auth.d.ts +8 -1
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +63 -22
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/core.d.ts +12 -0
- package/dist/commands/core.d.ts.map +1 -0
- package/dist/commands/core.js +133 -0
- package/dist/commands/core.js.map +1 -0
- package/dist/commands/enhance.d.ts +1 -1
- package/dist/commands/enhance.d.ts.map +1 -1
- package/dist/commands/enhance.js +54 -59
- package/dist/commands/enhance.js.map +1 -1
- package/dist/commands/generate-components.d.ts +22 -0
- package/dist/commands/generate-components.d.ts.map +1 -1
- package/dist/commands/generate-components.js +449 -69
- package/dist/commands/generate-components.js.map +1 -1
- package/dist/commands/generate.d.ts +5 -0
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +277 -35
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/init.js +1 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +25 -9
- package/dist/commands/list.js.map +1 -1
- package/dist/config/ai-providers.json +25 -0
- package/dist/utils/apiKeyManager.d.ts +1 -1
- package/dist/utils/apiKeyManager.js +1 -1
- package/dist/utils/errorHandler.d.ts +4 -0
- package/dist/utils/errorHandler.d.ts.map +1 -1
- package/dist/utils/errorHandler.js +4 -0
- package/dist/utils/errorHandler.js.map +1 -1
- package/dist/utils/fileSystem.d.ts.map +1 -1
- package/dist/utils/fileSystem.js +5 -7
- package/dist/utils/fileSystem.js.map +1 -1
- package/dist/utils/hybridAIClient.d.ts +1 -0
- package/dist/utils/hybridAIClient.d.ts.map +1 -1
- package/dist/utils/hybridAIClient.js +13 -0
- package/dist/utils/hybridAIClient.js.map +1 -1
- package/dist/utils/xaiClient.d.ts +19 -0
- package/dist/utils/xaiClient.d.ts.map +1 -0
- package/dist/utils/xaiClient.js +108 -0
- package/dist/utils/xaiClient.js.map +1 -0
- package/package.json +8 -2
|
@@ -40,40 +40,10 @@ exports.GenerateComponentsCommand = void 0;
|
|
|
40
40
|
const fileSystem_1 = require("../utils/fileSystem");
|
|
41
41
|
const spinner_1 = require("../utils/spinner");
|
|
42
42
|
const chalk_1 = __importDefault(require("chalk"));
|
|
43
|
+
const prompts_1 = __importDefault(require("prompts"));
|
|
43
44
|
const fs = __importStar(require("fs-extra"));
|
|
44
45
|
const path = __importStar(require("path"));
|
|
45
46
|
const child_process_1 = require("child_process");
|
|
46
|
-
// Code Generation Sub-Agent
|
|
47
|
-
class CodeGenSubAgent {
|
|
48
|
-
constructor() {
|
|
49
|
-
this.name = "CodeGenSubAgent";
|
|
50
|
-
}
|
|
51
|
-
async run({ component, group, options }) {
|
|
52
|
-
// Use the existing generateComponentCode logic
|
|
53
|
-
return new GenerateComponentsCommand().generateComponentCode(component, group, options);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
// QA Sub-Agent (stub for demonstration)
|
|
57
|
-
class QASubAgent {
|
|
58
|
-
constructor() {
|
|
59
|
-
this.name = "QASubAgent";
|
|
60
|
-
}
|
|
61
|
-
async run({ code, component }) {
|
|
62
|
-
// Placeholder: In real implementation, run static analysis, lint, or type checks
|
|
63
|
-
// For now, always return true (pass)
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
// Docs Writer Sub-Agent (stub for demonstration)
|
|
68
|
-
class DocsSubAgent {
|
|
69
|
-
constructor() {
|
|
70
|
-
this.name = "DocsSubAgent";
|
|
71
|
-
}
|
|
72
|
-
async run({ component, group }) {
|
|
73
|
-
// Placeholder: Generate markdown or JSDoc for the component
|
|
74
|
-
return `# ${component.name}\n\n${component.description}\n`;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
47
|
// --- Orchestration in GenerateComponentsCommand ---
|
|
78
48
|
class GenerateComponentsCommand {
|
|
79
49
|
constructor() {
|
|
@@ -117,6 +87,8 @@ class GenerateComponentsCommand {
|
|
|
117
87
|
async execute(target, options) {
|
|
118
88
|
const spinner = new spinner_1.EnhancedSpinner("Initializing component generation...");
|
|
119
89
|
try {
|
|
90
|
+
// System reminder: high-level plan
|
|
91
|
+
console.log(chalk_1.default.gray("\n[mycontext] Plan: plan → generate → QA → docs → preview (→ checks)\n"));
|
|
120
92
|
// Check authentication unless local mode is enabled
|
|
121
93
|
let userInfo = null;
|
|
122
94
|
if (!options.local) {
|
|
@@ -146,6 +118,36 @@ class GenerateComponentsCommand {
|
|
|
146
118
|
// Determine if we're generating a specific group or all components
|
|
147
119
|
const isAll = target === "all" || options.all;
|
|
148
120
|
const groupName = isAll ? undefined : target;
|
|
121
|
+
// Core-first flow: generate a single BrandComp then exit
|
|
122
|
+
if (isAll && options.coreFirst) {
|
|
123
|
+
await this.generateCoreBrandComp(options, spinner);
|
|
124
|
+
// Update preview registry and ensure /preview route
|
|
125
|
+
if (options.updatePreview !== false) {
|
|
126
|
+
const componentsDir = options.output || path.join("components", ".mycontext");
|
|
127
|
+
await this.updatePreviewRegistry(componentsDir);
|
|
128
|
+
await this.ensurePreviewRoute();
|
|
129
|
+
}
|
|
130
|
+
// Guidance for refinement step
|
|
131
|
+
console.log(chalk_1.default.blue("\n🧩 Core-first step complete: edit 'components/.mycontext/core/BrandComp.tsx' until you're happy."));
|
|
132
|
+
console.log(chalk_1.default.gray(" You can also run: mycontext enhance components/.mycontext/core/BrandComp.tsx --prompt 'Tweak spacing/colors'\n"));
|
|
133
|
+
// If non-interactive, stop here
|
|
134
|
+
if (options.yes) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// Ask whether to proceed with remaining components now
|
|
138
|
+
const answer = await (0, prompts_1.default)({
|
|
139
|
+
type: "toggle",
|
|
140
|
+
name: "proceed",
|
|
141
|
+
message: "Proceed to generate the remaining components now? (You can rerun later)",
|
|
142
|
+
initial: false,
|
|
143
|
+
active: "yes",
|
|
144
|
+
inactive: "no",
|
|
145
|
+
});
|
|
146
|
+
if (answer?.proceed) {
|
|
147
|
+
await this.generateAllComponents(options, spinner, userInfo.userId);
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
149
151
|
if (isAll) {
|
|
150
152
|
await this.generateAllComponents(options, spinner, userInfo.userId);
|
|
151
153
|
}
|
|
@@ -239,7 +241,9 @@ class GenerateComponentsCommand {
|
|
|
239
241
|
const list = this.contextArtifacts.compList;
|
|
240
242
|
if (!list || !Array.isArray(list.groups))
|
|
241
243
|
return null;
|
|
242
|
-
const norm = (s) => String(s || "")
|
|
244
|
+
const norm = (s) => String(s || "")
|
|
245
|
+
.toLowerCase()
|
|
246
|
+
.replace(/\s+/g, " ");
|
|
243
247
|
const g = list.groups.find((x) => norm(x?.name) === norm(groupName));
|
|
244
248
|
if (!g || !Array.isArray(g.components))
|
|
245
249
|
return null;
|
|
@@ -270,6 +274,8 @@ class GenerateComponentsCommand {
|
|
|
270
274
|
}
|
|
271
275
|
// Ensure shadcn/ui components are available before generation
|
|
272
276
|
await this.ensureShadcnComponentsInstalled(groups, spinner);
|
|
277
|
+
// Ensure form dependencies (zod, react-hook-form) exist
|
|
278
|
+
await this.ensureFormDeps(spinner);
|
|
273
279
|
// Ensure test scaffold exists (optional)
|
|
274
280
|
if (options.withTests) {
|
|
275
281
|
await this.ensureTestsScaffold(spinner);
|
|
@@ -281,6 +287,7 @@ class GenerateComponentsCommand {
|
|
|
281
287
|
let generatedGroups = 0;
|
|
282
288
|
for (const group of groups) {
|
|
283
289
|
spinner.updateText(`Generating ${group.name} components...`);
|
|
290
|
+
console.log(chalk_1.default.gray(`\n[mycontext] Reminder: generate → QA → docs for group '${group.name}'`));
|
|
284
291
|
const groupDir = path.join(componentsDir, this.toKebabCase(group.name));
|
|
285
292
|
await this.fs.ensureDir(groupDir);
|
|
286
293
|
const components = group.components || [];
|
|
@@ -302,6 +309,8 @@ class GenerateComponentsCommand {
|
|
|
302
309
|
await this.updatePreviewRegistry(componentsDir);
|
|
303
310
|
await this.ensurePreviewRoute();
|
|
304
311
|
}
|
|
312
|
+
// Post-generation: scan actual imports and ensure missing shadcn primitives are installed
|
|
313
|
+
await this.scanAndInstallShadcnFromComponents(componentsDir, spinner);
|
|
305
314
|
spinner.success({
|
|
306
315
|
text: `Generated ${totalComponents} components across ${generatedGroups} groups!`,
|
|
307
316
|
});
|
|
@@ -318,6 +327,21 @@ class GenerateComponentsCommand {
|
|
|
318
327
|
});
|
|
319
328
|
// Post-run hints
|
|
320
329
|
this.printNextStepsAfterComponents(options);
|
|
330
|
+
// Optionally open preview in browser
|
|
331
|
+
if (options.openPreview !== false) {
|
|
332
|
+
try {
|
|
333
|
+
const url = "http://localhost:3000/preview";
|
|
334
|
+
console.log(chalk_1.default.blue(`\n🌐 Opening preview: ${url}`));
|
|
335
|
+
// best-effort open using the OS default opener
|
|
336
|
+
const opener = process.platform === "darwin"
|
|
337
|
+
? "open"
|
|
338
|
+
: process.platform === "win32"
|
|
339
|
+
? "start"
|
|
340
|
+
: "xdg-open";
|
|
341
|
+
(0, child_process_1.execSync)(`${opener} ${url}`, { stdio: "ignore" });
|
|
342
|
+
}
|
|
343
|
+
catch { }
|
|
344
|
+
}
|
|
321
345
|
}
|
|
322
346
|
async generateComponentGroup(groupName, options, spinner, userId) {
|
|
323
347
|
spinner.updateText(`Generating ${groupName} components...`);
|
|
@@ -340,6 +364,8 @@ class GenerateComponentsCommand {
|
|
|
340
364
|
}
|
|
341
365
|
// Ensure shadcn/ui components are available before generation
|
|
342
366
|
await this.ensureShadcnComponentsInstalled([group], spinner);
|
|
367
|
+
// Ensure form dependencies (zod, react-hook-form) exist
|
|
368
|
+
await this.ensureFormDeps(spinner);
|
|
343
369
|
// Ensure test scaffold exists (optional)
|
|
344
370
|
if (options.withTests) {
|
|
345
371
|
await this.ensureTestsScaffold(spinner);
|
|
@@ -364,6 +390,8 @@ class GenerateComponentsCommand {
|
|
|
364
390
|
await this.updatePreviewRegistry(compBaseDir);
|
|
365
391
|
await this.ensurePreviewRoute();
|
|
366
392
|
}
|
|
393
|
+
// Post-generation: scan actual imports and ensure missing shadcn primitives are installed
|
|
394
|
+
await this.scanAndInstallShadcnFromComponents(compBaseDir, spinner);
|
|
367
395
|
spinner.success({
|
|
368
396
|
text: `Generated ${components.length} components in ${group.name}!`,
|
|
369
397
|
});
|
|
@@ -374,6 +402,204 @@ class GenerateComponentsCommand {
|
|
|
374
402
|
});
|
|
375
403
|
console.log(chalk_1.default.gray(` • index.ts`));
|
|
376
404
|
console.log(chalk_1.default.gray(` • page.tsx`));
|
|
405
|
+
// Optionally open preview in browser
|
|
406
|
+
if (options.openPreview !== false) {
|
|
407
|
+
try {
|
|
408
|
+
const url = "http://localhost:3000/preview";
|
|
409
|
+
console.log(chalk_1.default.blue(`\n🌐 Opening preview: ${url}`));
|
|
410
|
+
const opener = process.platform === "darwin"
|
|
411
|
+
? "open"
|
|
412
|
+
: process.platform === "win32"
|
|
413
|
+
? "start"
|
|
414
|
+
: "xdg-open";
|
|
415
|
+
(0, child_process_1.execSync)(`${opener} ${url}`, { stdio: "ignore" });
|
|
416
|
+
}
|
|
417
|
+
catch { }
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Generate a single core BrandComp using PRD/types/brand context to act as the design anchor.
|
|
422
|
+
*/
|
|
423
|
+
async generateCoreBrandComp(options, spinner) {
|
|
424
|
+
spinner.updateText("Generating core BrandComp...");
|
|
425
|
+
const componentsDir = options.output || path.join("components", ".mycontext");
|
|
426
|
+
const groupDir = path.join(componentsDir, "core");
|
|
427
|
+
await this.fs.ensureDir(groupDir);
|
|
428
|
+
// Ensure shadcn initialized
|
|
429
|
+
await this.ensureShadcnComponentsInstalled([
|
|
430
|
+
{
|
|
431
|
+
name: "Core",
|
|
432
|
+
components: [
|
|
433
|
+
{
|
|
434
|
+
name: "BrandComp",
|
|
435
|
+
type: "layout",
|
|
436
|
+
description: "Core brand canvas",
|
|
437
|
+
tags: ["layout", "canvas"],
|
|
438
|
+
},
|
|
439
|
+
],
|
|
440
|
+
},
|
|
441
|
+
], spinner);
|
|
442
|
+
// Best-effort brand token application
|
|
443
|
+
await this.applyBrandTokens();
|
|
444
|
+
const brandComp = {
|
|
445
|
+
name: "BrandComp",
|
|
446
|
+
type: "layout",
|
|
447
|
+
description: "A core brand canvas showcasing typography, primary/secondary colors, interactive states, and spacing. Acts as a reference for other components.",
|
|
448
|
+
userStories: [
|
|
449
|
+
"As a designer, I can see brand colors and typography applied consistently.",
|
|
450
|
+
"As a developer, I can reference spacing, radius, and interactive states.",
|
|
451
|
+
],
|
|
452
|
+
actionFunctions: [],
|
|
453
|
+
dependencies: ["react"],
|
|
454
|
+
tags: ["brand", "layout", "canvas"],
|
|
455
|
+
};
|
|
456
|
+
// Try sub-agent generation first
|
|
457
|
+
try {
|
|
458
|
+
const { orchestrator } = await Promise.resolve().then(() => __importStar(require("../agents/orchestrator/SubAgentOrchestrator")));
|
|
459
|
+
const codeResult = (await orchestrator.executeAgent("CodeGenSubAgent", {
|
|
460
|
+
component: brandComp,
|
|
461
|
+
group: { name: "Core" },
|
|
462
|
+
options: {
|
|
463
|
+
...options,
|
|
464
|
+
context: {
|
|
465
|
+
prd: this.contextArtifacts.prd,
|
|
466
|
+
types: this.contextArtifacts.types,
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
}));
|
|
470
|
+
await this.fs.writeFile(path.join(groupDir, `BrandComp.tsx`), codeResult.code);
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
const code = this.generateComponentCode(brandComp, { name: "Core" }, {
|
|
474
|
+
...options,
|
|
475
|
+
context: {
|
|
476
|
+
prd: this.contextArtifacts.prd,
|
|
477
|
+
types: this.contextArtifacts.types,
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
await this.fs.writeFile(path.join(groupDir, `BrandComp.tsx`), code);
|
|
481
|
+
}
|
|
482
|
+
// Group index and preview
|
|
483
|
+
await this.generateGroupIndex({
|
|
484
|
+
name: "Core",
|
|
485
|
+
description: "Core brand canvas",
|
|
486
|
+
components: [brandComp],
|
|
487
|
+
}, groupDir);
|
|
488
|
+
await this.generatePreviewPage({
|
|
489
|
+
name: "Core",
|
|
490
|
+
description: "Core brand canvas",
|
|
491
|
+
components: [brandComp],
|
|
492
|
+
}, groupDir);
|
|
493
|
+
spinner.success({ text: "Generated core BrandComp." });
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Scan generated TSX files for imports from '@/components/ui/*' and ensure those shadcn primitives are installed.
|
|
497
|
+
*/
|
|
498
|
+
async scanAndInstallShadcnFromComponents(componentsBaseDir, spinner) {
|
|
499
|
+
try {
|
|
500
|
+
const projectRoot = process.cwd();
|
|
501
|
+
const pkgJsonPath = path.join(projectRoot, "package.json");
|
|
502
|
+
if (!(await fs.pathExists(pkgJsonPath)))
|
|
503
|
+
return;
|
|
504
|
+
const needed = new Set();
|
|
505
|
+
const walk = async (dir) => {
|
|
506
|
+
const entries = await fs.readdir(dir);
|
|
507
|
+
for (const entry of entries) {
|
|
508
|
+
const full = path.join(dir, entry);
|
|
509
|
+
const stat = await fs.stat(full);
|
|
510
|
+
if (stat.isDirectory())
|
|
511
|
+
await walk(full);
|
|
512
|
+
else if (entry.endsWith(".tsx")) {
|
|
513
|
+
const src = await fs.readFile(full, "utf8");
|
|
514
|
+
const re = /from\s+["']@\/components\/ui\/([^"']+)["']/g;
|
|
515
|
+
let m;
|
|
516
|
+
while ((m = re.exec(src))) {
|
|
517
|
+
const mod = m[1];
|
|
518
|
+
if (mod)
|
|
519
|
+
needed.add(mod);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
await walk(componentsBaseDir);
|
|
525
|
+
if (needed.size === 0)
|
|
526
|
+
return;
|
|
527
|
+
// Filter out already-present ui files
|
|
528
|
+
const uiDir = path.join(projectRoot, "components", "ui");
|
|
529
|
+
const missing = [];
|
|
530
|
+
for (const name of needed) {
|
|
531
|
+
const p = path.join(uiDir, `${name}.tsx`);
|
|
532
|
+
if (!(await fs.pathExists(p)))
|
|
533
|
+
missing.push(name);
|
|
534
|
+
}
|
|
535
|
+
if (missing.length === 0)
|
|
536
|
+
return;
|
|
537
|
+
spinner.updateText(`Installing missing shadcn primitives from imports (${missing.length})...`);
|
|
538
|
+
const pm = await this.detectPackageManager(projectRoot);
|
|
539
|
+
try {
|
|
540
|
+
if (pm === "pnpm") {
|
|
541
|
+
(0, child_process_1.execSync)(`pnpm dlx shadcn@latest add ${missing.join(" ")}`.trim(), {
|
|
542
|
+
cwd: projectRoot,
|
|
543
|
+
stdio: "inherit",
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
(0, child_process_1.execSync)(`npx shadcn@latest add ${missing.join(" ")}`.trim(), {
|
|
548
|
+
cwd: projectRoot,
|
|
549
|
+
stdio: "inherit",
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
console.log(chalk_1.default.yellow(" ⚠️ shadcn add (post-scan) failed; you can add components manually."));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
catch (error) {
|
|
558
|
+
console.log(chalk_1.default.yellow(` ⚠️ shadcn post-scan encountered an issue: ${error instanceof Error ? error.message : String(error)}`));
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Apply brand tokens to globals.css (:root variables) if branding exists.
|
|
563
|
+
*/
|
|
564
|
+
async applyBrandTokens() {
|
|
565
|
+
try {
|
|
566
|
+
const projectRoot = process.cwd();
|
|
567
|
+
const appDir = (await fs.pathExists(path.join(projectRoot, "src", "app")))
|
|
568
|
+
? path.join(projectRoot, "src", "app")
|
|
569
|
+
: path.join(projectRoot, "app");
|
|
570
|
+
const globalsPath = path.join(appDir, "globals.css");
|
|
571
|
+
if (!(await fs.pathExists(globalsPath)))
|
|
572
|
+
return;
|
|
573
|
+
const brandPath = path.join(projectRoot, ".mycontext", "03-branding.md");
|
|
574
|
+
if (!(await fs.pathExists(brandPath)))
|
|
575
|
+
return;
|
|
576
|
+
const brand = await fs.readFile(brandPath, "utf8");
|
|
577
|
+
const pick = (label) => {
|
|
578
|
+
const m = brand.match(new RegExp(label + ".*?(#[0-9a-fA-F]{6})"));
|
|
579
|
+
return m ? m[1] : null;
|
|
580
|
+
};
|
|
581
|
+
const primary = pick("primary") || pick("Primary") || null;
|
|
582
|
+
const secondary = pick("secondary") || pick("Secondary") || null;
|
|
583
|
+
const accent = pick("accent") || pick("Accent") || null;
|
|
584
|
+
if (!primary && !secondary && !accent)
|
|
585
|
+
return;
|
|
586
|
+
let css = await fs.readFile(globalsPath, "utf8");
|
|
587
|
+
const ensureVar = (name, value) => {
|
|
588
|
+
const re = new RegExp(`(--${name}:\s*)([^;]+)(;)`);
|
|
589
|
+
if (re.test(css))
|
|
590
|
+
css = css.replace(re, `$1${value}$3`);
|
|
591
|
+
else
|
|
592
|
+
css = css.replace(/:root\s*\{/, (m) => `${m}\n --${name}: ${value};`);
|
|
593
|
+
};
|
|
594
|
+
if (primary)
|
|
595
|
+
ensureVar("primary", primary);
|
|
596
|
+
if (secondary)
|
|
597
|
+
ensureVar("secondary", secondary);
|
|
598
|
+
if (accent)
|
|
599
|
+
ensureVar("accent", accent);
|
|
600
|
+
await fs.writeFile(globalsPath, css);
|
|
601
|
+
}
|
|
602
|
+
catch { }
|
|
377
603
|
}
|
|
378
604
|
async generateComponent(component, group, groupDir, options, userId) {
|
|
379
605
|
try {
|
|
@@ -435,15 +661,23 @@ class GenerateComponentsCommand {
|
|
|
435
661
|
.replace(new RegExp(`${safeName} Default`, "g"), `${safeName}Default`);
|
|
436
662
|
await this.fs.writeFile(componentPath, fixedCode);
|
|
437
663
|
// Execute QA and docs in parallel
|
|
664
|
+
const componentWithContext = {
|
|
665
|
+
...component,
|
|
666
|
+
_context: {
|
|
667
|
+
group: group?.name,
|
|
668
|
+
prd: this.contextArtifacts.prd,
|
|
669
|
+
types: this.contextArtifacts.types,
|
|
670
|
+
},
|
|
671
|
+
};
|
|
438
672
|
const [qaResult, docsResult] = (await Promise.all([
|
|
439
673
|
orchestrator.executeAgent("QASubAgent", {
|
|
440
674
|
code: codeResult.code,
|
|
441
|
-
component,
|
|
675
|
+
component: componentWithContext,
|
|
442
676
|
standards: ["typescript", "react", "accessibility"],
|
|
443
677
|
}),
|
|
444
678
|
orchestrator.executeAgent("DocsSubAgent", {
|
|
445
679
|
code: codeResult.code,
|
|
446
|
-
component,
|
|
680
|
+
component: componentWithContext,
|
|
447
681
|
format: "readme",
|
|
448
682
|
}),
|
|
449
683
|
]));
|
|
@@ -515,6 +749,11 @@ class GenerateComponentsCommand {
|
|
|
515
749
|
async ensureShadcnComponentsInstalled(groups, spinner) {
|
|
516
750
|
try {
|
|
517
751
|
const projectRoot = process.cwd();
|
|
752
|
+
const pkgJsonPath = path.join(projectRoot, "package.json");
|
|
753
|
+
if (!(await fs.pathExists(pkgJsonPath))) {
|
|
754
|
+
console.log(chalk_1.default.gray(" Skipping shadcn setup: no package.json in current directory. Run inside an existing Next.js project."));
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
518
757
|
const componentsJsonPath = path.join(projectRoot, "components.json");
|
|
519
758
|
// Initialize shadcn if components.json is missing
|
|
520
759
|
if (!(await fs.pathExists(componentsJsonPath))) {
|
|
@@ -551,6 +790,10 @@ class GenerateComponentsCommand {
|
|
|
551
790
|
const names = Array.from(needed);
|
|
552
791
|
spinner.updateText(`Installing shadcn components (${names.length})...`);
|
|
553
792
|
try {
|
|
793
|
+
if (!(await fs.pathExists(pkgJsonPath))) {
|
|
794
|
+
console.log(chalk_1.default.gray(" Skipping 'shadcn add' because no package.json was found."));
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
554
797
|
if (pkgManager === "pnpm") {
|
|
555
798
|
(0, child_process_1.execSync)(`pnpm dlx shadcn@latest add ${names.join(" ")}`, {
|
|
556
799
|
cwd: projectRoot,
|
|
@@ -572,6 +815,50 @@ class GenerateComponentsCommand {
|
|
|
572
815
|
console.log(chalk_1.default.yellow(` ⚠️ shadcn setup encountered an issue: ${error instanceof Error ? error.message : String(error)}`));
|
|
573
816
|
}
|
|
574
817
|
}
|
|
818
|
+
async ensureFormDeps(spinner) {
|
|
819
|
+
try {
|
|
820
|
+
const projectRoot = process.cwd();
|
|
821
|
+
const pkgPath = path.join(projectRoot, "package.json");
|
|
822
|
+
const pkg = (await fs.pathExists(pkgPath))
|
|
823
|
+
? await fs.readJson(pkgPath)
|
|
824
|
+
: {};
|
|
825
|
+
const deps = {
|
|
826
|
+
...(pkg.dependencies || {}),
|
|
827
|
+
...(pkg.devDependencies || {}),
|
|
828
|
+
};
|
|
829
|
+
const need = [];
|
|
830
|
+
if (!deps["zod"])
|
|
831
|
+
need.push("zod");
|
|
832
|
+
if (!deps["react-hook-form"])
|
|
833
|
+
need.push("react-hook-form");
|
|
834
|
+
if (!deps["@hookform/resolvers"])
|
|
835
|
+
need.push("@hookform/resolvers");
|
|
836
|
+
if (need.length === 0)
|
|
837
|
+
return;
|
|
838
|
+
spinner.updateText(`Ensuring form dependencies: ${need.join(", ")}`);
|
|
839
|
+
const pm = await this.detectPackageManager(projectRoot);
|
|
840
|
+
try {
|
|
841
|
+
if (pm === "pnpm") {
|
|
842
|
+
(0, child_process_1.execSync)(`pnpm add ${need.join(" ")}`, {
|
|
843
|
+
cwd: projectRoot,
|
|
844
|
+
stdio: "inherit",
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
else {
|
|
848
|
+
(0, child_process_1.execSync)(`npm i ${need.join(" ")}`, {
|
|
849
|
+
cwd: projectRoot,
|
|
850
|
+
stdio: "inherit",
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
catch {
|
|
855
|
+
console.log(chalk_1.default.yellow(" ⚠️ Failed to install form deps automatically. You can install them manually."));
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
catch (error) {
|
|
859
|
+
console.log(chalk_1.default.yellow(` ⚠️ Form deps step encountered an issue: ${error instanceof Error ? error.message : String(error)}`));
|
|
860
|
+
}
|
|
861
|
+
}
|
|
575
862
|
inferShadcnForComponent(component, out) {
|
|
576
863
|
const type = (component.type || "").toLowerCase();
|
|
577
864
|
const name = String(component.name || "").toLowerCase();
|
|
@@ -608,7 +895,7 @@ class GenerateComponentsCommand {
|
|
|
608
895
|
/alert|dialog|toast|progress|skeleton/.test(name)) {
|
|
609
896
|
add(["alert", "alert-dialog", "dialog", "progress", "skeleton"]);
|
|
610
897
|
}
|
|
611
|
-
if (type === "data" || /table|command|combobox/.test(name)) {
|
|
898
|
+
if (type === "data" || /table|command|combobox|list/.test(name)) {
|
|
612
899
|
add(["table", "command"]);
|
|
613
900
|
}
|
|
614
901
|
if (type === "overlay" || /popover|tooltip|sheet|drawer/.test(name)) {
|
|
@@ -1008,7 +1295,6 @@ export default ${name};
|
|
|
1008
1295
|
e.preventDefault();
|
|
1009
1296
|
setIsSubmitting(true);
|
|
1010
1297
|
try {
|
|
1011
|
-
// TODO: Implement form submission logic
|
|
1012
1298
|
onSubmit?.(formData);
|
|
1013
1299
|
} catch (error) {
|
|
1014
1300
|
console.error("Form submission error:", error);
|
|
@@ -1050,42 +1336,19 @@ export default ${name};
|
|
|
1050
1336
|
switch (normalizedType) {
|
|
1051
1337
|
case "form":
|
|
1052
1338
|
return `
|
|
1339
|
+
{/* Example domain-agnostic form scaffold. Replace fields per component plan. */}
|
|
1053
1340
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
1054
1341
|
<div className="space-y-2">
|
|
1055
|
-
<label htmlFor="
|
|
1056
|
-
|
|
1057
|
-
</label>
|
|
1058
|
-
<input
|
|
1059
|
-
id="email"
|
|
1060
|
-
type="email"
|
|
1061
|
-
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
1062
|
-
placeholder="Enter your email"
|
|
1063
|
-
required
|
|
1064
|
-
/>
|
|
1342
|
+
<label htmlFor="field1" className="text-sm font-medium">Field 1</label>
|
|
1343
|
+
<input id="field1" className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" placeholder="Type here" />
|
|
1065
1344
|
</div>
|
|
1066
|
-
<div className="
|
|
1067
|
-
<label htmlFor="password" className="text-sm font-medium">
|
|
1068
|
-
Password
|
|
1069
|
-
</label>
|
|
1070
|
-
<input
|
|
1071
|
-
id="password"
|
|
1072
|
-
type="password"
|
|
1073
|
-
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
1074
|
-
placeholder="Enter your password"
|
|
1075
|
-
required
|
|
1076
|
-
/>
|
|
1077
|
-
</div>
|
|
1078
|
-
{error && (
|
|
1079
|
-
<div className="text-sm text-destructive">
|
|
1080
|
-
{error}
|
|
1081
|
-
</div>
|
|
1082
|
-
)}
|
|
1345
|
+
{error ? <div className="text-sm text-destructive">{error}</div> : null}
|
|
1083
1346
|
<button
|
|
1084
1347
|
type="submit"
|
|
1085
1348
|
disabled={loading || isSubmitting}
|
|
1086
1349
|
className="inline-flex h-10 items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground ring-offset-background transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
|
1087
1350
|
>
|
|
1088
|
-
{loading || isSubmitting ? "
|
|
1351
|
+
{loading || isSubmitting ? "Submitting..." : "Submit"}
|
|
1089
1352
|
</button>
|
|
1090
1353
|
</form>`;
|
|
1091
1354
|
case "layout":
|
|
@@ -1146,14 +1409,14 @@ export default ${name};
|
|
|
1146
1409
|
case "handleLogin":
|
|
1147
1410
|
return `
|
|
1148
1411
|
export async function handleLogin(email: string, password: string) {
|
|
1149
|
-
//
|
|
1412
|
+
// Implement login logic in application layer
|
|
1150
1413
|
console.log("Logging in with:", { email, password });
|
|
1151
1414
|
return { success: true };
|
|
1152
1415
|
}`;
|
|
1153
1416
|
case "handleSignup":
|
|
1154
1417
|
return `
|
|
1155
1418
|
export async function handleSignup(email: string, password: string, name: string) {
|
|
1156
|
-
//
|
|
1419
|
+
// Implement signup logic in application layer
|
|
1157
1420
|
console.log("Signing up with:", { email, password, name });
|
|
1158
1421
|
return { success: true };
|
|
1159
1422
|
}`;
|
|
@@ -1166,14 +1429,14 @@ export function validateEmail(email: string): boolean {
|
|
|
1166
1429
|
case "checkUsername":
|
|
1167
1430
|
return `
|
|
1168
1431
|
export async function checkUsername(username: string): Promise<boolean> {
|
|
1169
|
-
//
|
|
1432
|
+
// Implement username availability check in application layer
|
|
1170
1433
|
console.log("Checking username:", username);
|
|
1171
1434
|
return true;
|
|
1172
1435
|
}`;
|
|
1173
1436
|
default:
|
|
1174
1437
|
return `
|
|
1175
1438
|
export function ${func}() {
|
|
1176
|
-
//
|
|
1439
|
+
// Implement ${func} logic in application layer
|
|
1177
1440
|
console.log("${func} called");
|
|
1178
1441
|
}`;
|
|
1179
1442
|
}
|
|
@@ -1264,6 +1527,8 @@ ${components
|
|
|
1264
1527
|
.join(" ");
|
|
1265
1528
|
groups.push({ dir: item, name: title, files });
|
|
1266
1529
|
}
|
|
1530
|
+
// Build preview props based on component prop interfaces and types
|
|
1531
|
+
await this.buildPreviewProps(componentsDir);
|
|
1267
1532
|
const registryPath = path.join(componentsDir, "registry.tsx");
|
|
1268
1533
|
const entries = [];
|
|
1269
1534
|
for (const g of groups) {
|
|
@@ -1276,6 +1541,7 @@ ${components
|
|
|
1276
1541
|
const content = `"use client";
|
|
1277
1542
|
import React from 'react';
|
|
1278
1543
|
import dynamic from 'next/dynamic';
|
|
1544
|
+
import { previewProps } from './preview-props';
|
|
1279
1545
|
|
|
1280
1546
|
export type PreviewRegistryItem = {
|
|
1281
1547
|
group: string;
|
|
@@ -1305,7 +1571,8 @@ export const previewItems = items.map((item) => ({
|
|
|
1305
1571
|
if (!comp) {
|
|
1306
1572
|
console.warn('[mycontext/preview] Missing export ' + item.name + ' in ' + item.path);
|
|
1307
1573
|
}
|
|
1308
|
-
|
|
1574
|
+
const C = (comp || ((props: any) => <MissingComponent name={item.name} {...props} />)) as React.ComponentType<any>;
|
|
1575
|
+
return (props: any) => <C {...(previewProps[item.name] || {})} {...props} />;
|
|
1309
1576
|
} catch (e) {
|
|
1310
1577
|
console.warn('[mycontext/preview] Failed to load ' + item.path + ':', e);
|
|
1311
1578
|
return ((props: any) => <MissingComponent name={item.name} {...props} />) as React.ComponentType<any>;
|
|
@@ -1368,6 +1635,119 @@ function groupBy<T, K extends string | number>(
|
|
|
1368
1635
|
console.log(chalk_1.default.yellow(` ⚠️ Failed to update preview registry: ${error instanceof Error ? error.message : String(error)}`));
|
|
1369
1636
|
}
|
|
1370
1637
|
}
|
|
1638
|
+
/**
|
|
1639
|
+
* Build preview-props.ts by parsing component prop interfaces and .mycontext/02-types.ts
|
|
1640
|
+
* Generates sample values for primitives and known interfaces; skips unknowns.
|
|
1641
|
+
*/
|
|
1642
|
+
async buildPreviewProps(componentsDir) {
|
|
1643
|
+
try {
|
|
1644
|
+
const projectRoot = process.cwd();
|
|
1645
|
+
const typesPathCandidates = [
|
|
1646
|
+
path.join(projectRoot, ".mycontext", "02-types.ts"),
|
|
1647
|
+
path.join(projectRoot, ".mycontext", "types.ts"),
|
|
1648
|
+
path.join(projectRoot, "context", "types.ts"),
|
|
1649
|
+
];
|
|
1650
|
+
let typesSource = "";
|
|
1651
|
+
for (const p of typesPathCandidates) {
|
|
1652
|
+
if (await fs.pathExists(p)) {
|
|
1653
|
+
typesSource = await fs.readFile(p, "utf8");
|
|
1654
|
+
break;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
// Parse interfaces from types file
|
|
1658
|
+
const interfaceRegex = /interface\s+(\w+)\s*\{([\s\S]*?)\}/g;
|
|
1659
|
+
const interfaces = {};
|
|
1660
|
+
let match;
|
|
1661
|
+
while ((match = interfaceRegex.exec(typesSource))) {
|
|
1662
|
+
const name = match[1];
|
|
1663
|
+
const body = match[2];
|
|
1664
|
+
const fields = {};
|
|
1665
|
+
body
|
|
1666
|
+
.split("\n")
|
|
1667
|
+
.map((l) => l.trim())
|
|
1668
|
+
.filter(Boolean)
|
|
1669
|
+
.forEach((line) => {
|
|
1670
|
+
const m = line.match(/^(\w+)\??:\s*([^;]+);/);
|
|
1671
|
+
if (m)
|
|
1672
|
+
fields[m[1]] = m[2].trim();
|
|
1673
|
+
});
|
|
1674
|
+
interfaces[name] = fields;
|
|
1675
|
+
}
|
|
1676
|
+
// Discover components and parse their Props interfaces
|
|
1677
|
+
const propsMap = {};
|
|
1678
|
+
const groups = await fs.readdir(componentsDir);
|
|
1679
|
+
for (const g of groups) {
|
|
1680
|
+
const groupPath = path.join(componentsDir, g);
|
|
1681
|
+
const stat = await fs.stat(groupPath);
|
|
1682
|
+
if (!stat.isDirectory())
|
|
1683
|
+
continue;
|
|
1684
|
+
const files = (await fs.readdir(groupPath)).filter((f) => f.endsWith(".tsx"));
|
|
1685
|
+
for (const file of files) {
|
|
1686
|
+
if (file === "page.tsx")
|
|
1687
|
+
continue;
|
|
1688
|
+
const base = file.replace(/\.tsx$/, "");
|
|
1689
|
+
const full = path.join(groupPath, file);
|
|
1690
|
+
const src = await fs.readFile(full, "utf8");
|
|
1691
|
+
const propsInterfaceMatch = src.match(/interface\s+(\w+)Props\s*\{([\s\S]*?)\}/);
|
|
1692
|
+
if (!propsInterfaceMatch)
|
|
1693
|
+
continue;
|
|
1694
|
+
const propsBody = propsInterfaceMatch[2];
|
|
1695
|
+
const entries = propsBody
|
|
1696
|
+
.split("\n")
|
|
1697
|
+
.map((l) => l.trim())
|
|
1698
|
+
.filter((l) => /:\s*/.test(l));
|
|
1699
|
+
const propsObj = {};
|
|
1700
|
+
for (const line of entries) {
|
|
1701
|
+
const m = line.match(/^(\w+)\??:\s*([^;]+);/);
|
|
1702
|
+
if (!m)
|
|
1703
|
+
continue;
|
|
1704
|
+
const propName = m[1];
|
|
1705
|
+
const typeStr = m[2].trim();
|
|
1706
|
+
const value = this.generateSampleValue(typeStr, interfaces, 0);
|
|
1707
|
+
if (value !== undefined)
|
|
1708
|
+
propsObj[propName] = value;
|
|
1709
|
+
}
|
|
1710
|
+
if (Object.keys(propsObj).length > 0)
|
|
1711
|
+
propsMap[base] = propsObj;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
const out = `export const previewProps: Record<string, any> = ${JSON.stringify(propsMap, null, 2)};\n`;
|
|
1715
|
+
await fs.writeFile(path.join(componentsDir, "preview-props.ts"), out);
|
|
1716
|
+
}
|
|
1717
|
+
catch (error) {
|
|
1718
|
+
console.log(chalk_1.default.yellow(` ⚠️ Failed to build preview props: ${error instanceof Error ? error.message : String(error)}`));
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
generateSampleValue(typeStr, interfaces, depth) {
|
|
1722
|
+
if (depth > 2)
|
|
1723
|
+
return undefined;
|
|
1724
|
+
const t = typeStr.replace(/\s+/g, "");
|
|
1725
|
+
if (t.endsWith("[]")) {
|
|
1726
|
+
const inner = t.slice(0, -2);
|
|
1727
|
+
const v = this.generateSampleValue(inner, interfaces, depth + 1);
|
|
1728
|
+
return v === undefined ? undefined : [v];
|
|
1729
|
+
}
|
|
1730
|
+
if (/^string\b/.test(t))
|
|
1731
|
+
return "Sample";
|
|
1732
|
+
if (/^number\b/.test(t))
|
|
1733
|
+
return 1;
|
|
1734
|
+
if (/^boolean\b/.test(t))
|
|
1735
|
+
return true;
|
|
1736
|
+
if (/^Date\b/.test(t))
|
|
1737
|
+
return new Date().toISOString();
|
|
1738
|
+
if (interfaces[t]) {
|
|
1739
|
+
const fields = interfaces[t];
|
|
1740
|
+
const obj = {};
|
|
1741
|
+
for (const [k, vt] of Object.entries(fields)) {
|
|
1742
|
+
const v = this.generateSampleValue(vt, interfaces, depth + 1);
|
|
1743
|
+
if (v !== undefined)
|
|
1744
|
+
obj[k] = v;
|
|
1745
|
+
}
|
|
1746
|
+
return obj;
|
|
1747
|
+
}
|
|
1748
|
+
// Unknown type: skip
|
|
1749
|
+
return undefined;
|
|
1750
|
+
}
|
|
1371
1751
|
async ensurePreviewRoute() {
|
|
1372
1752
|
try {
|
|
1373
1753
|
const projectRoot = process.cwd();
|