lynxprompt 0.1.0 → 0.2.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/README.md +194 -45
- package/dist/index.js +2829 -230
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
5
|
+
import chalk16 from "chalk";
|
|
6
6
|
|
|
7
7
|
// src/commands/login.ts
|
|
8
8
|
import chalk from "chalk";
|
|
@@ -135,6 +135,19 @@ var ApiClient = class {
|
|
|
135
135
|
});
|
|
136
136
|
return this.request(`/api/blueprints?${params}`);
|
|
137
137
|
}
|
|
138
|
+
async createBlueprint(data) {
|
|
139
|
+
return this.request("/api/v1/blueprints", {
|
|
140
|
+
method: "POST",
|
|
141
|
+
body: JSON.stringify(data)
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
async updateBlueprint(id, data) {
|
|
145
|
+
const apiId = id.startsWith("bp_") ? id : `bp_${id}`;
|
|
146
|
+
return this.request(`/api/v1/blueprints/${apiId}`, {
|
|
147
|
+
method: "PUT",
|
|
148
|
+
body: JSON.stringify(data)
|
|
149
|
+
});
|
|
150
|
+
}
|
|
138
151
|
};
|
|
139
152
|
var ApiRequestError = class extends Error {
|
|
140
153
|
constructor(message, statusCode, response) {
|
|
@@ -408,67 +421,319 @@ function handleApiError(error) {
|
|
|
408
421
|
import chalk5 from "chalk";
|
|
409
422
|
import ora4 from "ora";
|
|
410
423
|
import prompts from "prompts";
|
|
411
|
-
import { writeFile
|
|
424
|
+
import { writeFile as writeFile2, mkdir as mkdir2, readFile as readFile2 } from "fs/promises";
|
|
425
|
+
import { join as join2, dirname as dirname2 } from "path";
|
|
426
|
+
import { existsSync } from "fs";
|
|
427
|
+
|
|
428
|
+
// src/utils/blueprint-tracker.ts
|
|
429
|
+
import { readFile, writeFile, mkdir, access } from "fs/promises";
|
|
412
430
|
import { join, dirname } from "path";
|
|
431
|
+
import { createHash } from "crypto";
|
|
432
|
+
import * as yaml from "yaml";
|
|
433
|
+
var BLUEPRINTS_FILE = ".lynxprompt/blueprints.yml";
|
|
434
|
+
function calculateChecksum(content) {
|
|
435
|
+
return createHash("sha256").update(content).digest("hex").substring(0, 16);
|
|
436
|
+
}
|
|
437
|
+
async function loadBlueprints(cwd) {
|
|
438
|
+
const filePath = join(cwd, BLUEPRINTS_FILE);
|
|
439
|
+
try {
|
|
440
|
+
await access(filePath);
|
|
441
|
+
const content = await readFile(filePath, "utf-8");
|
|
442
|
+
const config2 = yaml.parse(content);
|
|
443
|
+
return config2 || { version: "1", blueprints: [] };
|
|
444
|
+
} catch {
|
|
445
|
+
return { version: "1", blueprints: [] };
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
async function saveBlueprints(cwd, config2) {
|
|
449
|
+
const filePath = join(cwd, BLUEPRINTS_FILE);
|
|
450
|
+
const dir = dirname(filePath);
|
|
451
|
+
await mkdir(dir, { recursive: true });
|
|
452
|
+
const content = yaml.stringify(config2, {
|
|
453
|
+
lineWidth: 0,
|
|
454
|
+
singleQuote: false
|
|
455
|
+
});
|
|
456
|
+
await writeFile(filePath, content, "utf-8");
|
|
457
|
+
}
|
|
458
|
+
async function trackBlueprint(cwd, blueprint) {
|
|
459
|
+
const config2 = await loadBlueprints(cwd);
|
|
460
|
+
config2.blueprints = config2.blueprints.filter((b) => b.file !== blueprint.file);
|
|
461
|
+
const editable = blueprint.source !== "marketplace";
|
|
462
|
+
const canPull = true;
|
|
463
|
+
config2.blueprints.push({
|
|
464
|
+
id: blueprint.id,
|
|
465
|
+
source: blueprint.source,
|
|
466
|
+
file: blueprint.file,
|
|
467
|
+
name: blueprint.name,
|
|
468
|
+
pulledAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
469
|
+
checksum: calculateChecksum(blueprint.content),
|
|
470
|
+
version: blueprint.version,
|
|
471
|
+
editable,
|
|
472
|
+
canPull
|
|
473
|
+
});
|
|
474
|
+
await saveBlueprints(cwd, config2);
|
|
475
|
+
}
|
|
476
|
+
async function findBlueprintByFile(cwd, file) {
|
|
477
|
+
const config2 = await loadBlueprints(cwd);
|
|
478
|
+
return config2.blueprints.find((b) => b.file === file) || null;
|
|
479
|
+
}
|
|
480
|
+
async function hasLocalChanges(cwd, tracked) {
|
|
481
|
+
try {
|
|
482
|
+
const filePath = join(cwd, tracked.file);
|
|
483
|
+
const content = await readFile(filePath, "utf-8");
|
|
484
|
+
const currentChecksum = calculateChecksum(content);
|
|
485
|
+
return currentChecksum !== tracked.checksum;
|
|
486
|
+
} catch {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
async function updateChecksum(cwd, file, content) {
|
|
491
|
+
const config2 = await loadBlueprints(cwd);
|
|
492
|
+
const blueprint = config2.blueprints.find((b) => b.file === file);
|
|
493
|
+
if (blueprint) {
|
|
494
|
+
blueprint.checksum = calculateChecksum(content);
|
|
495
|
+
blueprint.pulledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
496
|
+
await saveBlueprints(cwd, config2);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
async function untrackBlueprint(cwd, file) {
|
|
500
|
+
const config2 = await loadBlueprints(cwd);
|
|
501
|
+
const initialCount = config2.blueprints.length;
|
|
502
|
+
config2.blueprints = config2.blueprints.filter((b) => b.file !== file);
|
|
503
|
+
if (config2.blueprints.length < initialCount) {
|
|
504
|
+
await saveBlueprints(cwd, config2);
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
async function linkBlueprint(cwd, file, blueprintId, blueprintName, source) {
|
|
510
|
+
try {
|
|
511
|
+
const filePath = join(cwd, file);
|
|
512
|
+
const content = await readFile(filePath, "utf-8");
|
|
513
|
+
await trackBlueprint(cwd, {
|
|
514
|
+
id: blueprintId,
|
|
515
|
+
name: blueprintName,
|
|
516
|
+
file,
|
|
517
|
+
content,
|
|
518
|
+
source
|
|
519
|
+
});
|
|
520
|
+
} catch (error) {
|
|
521
|
+
throw new Error(`Could not read file ${file} to link`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
async function checkSyncStatus(cwd) {
|
|
525
|
+
const config2 = await loadBlueprints(cwd);
|
|
526
|
+
const results = [];
|
|
527
|
+
for (const blueprint of config2.blueprints) {
|
|
528
|
+
const filePath = join(cwd, blueprint.file);
|
|
529
|
+
let fileExists2 = false;
|
|
530
|
+
let localModified = false;
|
|
531
|
+
try {
|
|
532
|
+
await access(filePath);
|
|
533
|
+
fileExists2 = true;
|
|
534
|
+
localModified = await hasLocalChanges(cwd, blueprint);
|
|
535
|
+
} catch {
|
|
536
|
+
fileExists2 = false;
|
|
537
|
+
}
|
|
538
|
+
results.push({ blueprint, localModified, fileExists: fileExists2 });
|
|
539
|
+
}
|
|
540
|
+
return results;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/commands/pull.ts
|
|
413
544
|
var TYPE_TO_FILENAME = {
|
|
414
545
|
AGENTS_MD: "AGENTS.md",
|
|
415
|
-
CURSOR_RULES: ".
|
|
546
|
+
CURSOR_RULES: ".cursor/rules/project.mdc",
|
|
416
547
|
COPILOT_INSTRUCTIONS: ".github/copilot-instructions.md",
|
|
417
548
|
WINDSURF_RULES: ".windsurfrules",
|
|
418
549
|
ZED_INSTRUCTIONS: ".zed/instructions.md",
|
|
419
550
|
CLAUDE_MD: "CLAUDE.md",
|
|
420
551
|
GENERIC: "ai-config.md"
|
|
421
552
|
};
|
|
553
|
+
function getSourceFromVisibility(visibility) {
|
|
554
|
+
switch (visibility) {
|
|
555
|
+
case "PUBLIC":
|
|
556
|
+
return "marketplace";
|
|
557
|
+
case "TEAM":
|
|
558
|
+
return "team";
|
|
559
|
+
case "PRIVATE":
|
|
560
|
+
return "private";
|
|
561
|
+
default:
|
|
562
|
+
return "marketplace";
|
|
563
|
+
}
|
|
564
|
+
}
|
|
422
565
|
async function pullCommand(id, options) {
|
|
423
566
|
if (!isAuthenticated()) {
|
|
424
567
|
console.log(
|
|
425
|
-
chalk5.yellow("Not logged in. Run '
|
|
568
|
+
chalk5.yellow("Not logged in. Run 'lynxp login' to authenticate.")
|
|
426
569
|
);
|
|
427
570
|
process.exit(1);
|
|
428
571
|
}
|
|
572
|
+
const cwd = process.cwd();
|
|
429
573
|
const spinner = ora4(`Fetching blueprint ${chalk5.cyan(id)}...`).start();
|
|
430
574
|
try {
|
|
431
575
|
const { blueprint } = await api.getBlueprint(id);
|
|
432
576
|
spinner.stop();
|
|
433
577
|
if (!blueprint.content) {
|
|
434
|
-
console.error(chalk5.red("Blueprint has no content."));
|
|
578
|
+
console.error(chalk5.red("\u2717 Blueprint has no content."));
|
|
435
579
|
process.exit(1);
|
|
436
580
|
}
|
|
581
|
+
const source = getSourceFromVisibility(blueprint.visibility);
|
|
582
|
+
const isMarketplace = source === "marketplace";
|
|
583
|
+
console.log();
|
|
584
|
+
console.log(chalk5.cyan(`\u{1F431} Blueprint: ${chalk5.bold(blueprint.name)}`));
|
|
585
|
+
if (blueprint.description) {
|
|
586
|
+
console.log(chalk5.gray(` ${blueprint.description}`));
|
|
587
|
+
}
|
|
588
|
+
console.log(chalk5.gray(` Type: ${blueprint.type} \u2022 Tier: ${blueprint.tier} \u2022 Visibility: ${blueprint.visibility}`));
|
|
589
|
+
if (isMarketplace) {
|
|
590
|
+
console.log(chalk5.yellow(` \u{1F4E6} Marketplace blueprint (read-only - changes won't sync back)`));
|
|
591
|
+
} else if (source === "team") {
|
|
592
|
+
console.log(chalk5.blue(` \u{1F465} Team blueprint (can sync changes)`));
|
|
593
|
+
} else if (source === "private") {
|
|
594
|
+
console.log(chalk5.green(` \u{1F512} Private blueprint (can sync changes)`));
|
|
595
|
+
}
|
|
596
|
+
console.log();
|
|
597
|
+
if (options.preview) {
|
|
598
|
+
console.log(chalk5.gray("\u2500".repeat(60)));
|
|
599
|
+
console.log();
|
|
600
|
+
const lines = blueprint.content.split("\n");
|
|
601
|
+
const previewLines = lines.slice(0, 50);
|
|
602
|
+
for (const line of previewLines) {
|
|
603
|
+
if (line.startsWith("#")) {
|
|
604
|
+
console.log(chalk5.cyan(line));
|
|
605
|
+
} else if (line.startsWith(">")) {
|
|
606
|
+
console.log(chalk5.gray(line));
|
|
607
|
+
} else if (line.startsWith("- ") || line.startsWith("* ")) {
|
|
608
|
+
console.log(chalk5.white(line));
|
|
609
|
+
} else if (line.startsWith("```")) {
|
|
610
|
+
console.log(chalk5.yellow(line));
|
|
611
|
+
} else {
|
|
612
|
+
console.log(line);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (lines.length > 50) {
|
|
616
|
+
console.log();
|
|
617
|
+
console.log(chalk5.gray(`... ${lines.length - 50} more lines`));
|
|
618
|
+
}
|
|
619
|
+
console.log();
|
|
620
|
+
console.log(chalk5.gray("\u2500".repeat(60)));
|
|
621
|
+
console.log();
|
|
622
|
+
console.log(chalk5.gray("Run without --preview to download this blueprint."));
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
437
625
|
const filename = TYPE_TO_FILENAME[blueprint.type] || "ai-config.md";
|
|
438
|
-
const outputPath =
|
|
439
|
-
let
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
626
|
+
const outputPath = join2(options.output, filename);
|
|
627
|
+
let localContent = null;
|
|
628
|
+
if (existsSync(outputPath)) {
|
|
629
|
+
try {
|
|
630
|
+
localContent = await readFile2(outputPath, "utf-8");
|
|
631
|
+
} catch {
|
|
632
|
+
}
|
|
444
633
|
}
|
|
445
|
-
|
|
634
|
+
const existingTracked = await findBlueprintByFile(cwd, filename);
|
|
635
|
+
if (existingTracked && existingTracked.id !== id) {
|
|
636
|
+
console.log(chalk5.yellow(`\u26A0 This file is already linked to a different blueprint: ${existingTracked.id}`));
|
|
637
|
+
if (!options.yes) {
|
|
638
|
+
const { proceed } = await prompts({
|
|
639
|
+
type: "confirm",
|
|
640
|
+
name: "proceed",
|
|
641
|
+
message: "Replace the link with the new blueprint?",
|
|
642
|
+
initial: false
|
|
643
|
+
});
|
|
644
|
+
if (!proceed) {
|
|
645
|
+
console.log(chalk5.gray("Cancelled."));
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (localContent && !options.yes) {
|
|
651
|
+
const localLines = localContent.split("\n").length;
|
|
652
|
+
const remoteLines = blueprint.content.split("\n").length;
|
|
653
|
+
console.log(chalk5.yellow(`\u26A0 File exists: ${outputPath}`));
|
|
654
|
+
console.log(chalk5.gray(` Local: ${localLines} lines`));
|
|
655
|
+
console.log(chalk5.gray(` Remote: ${remoteLines} lines`));
|
|
656
|
+
console.log();
|
|
446
657
|
const response = await prompts({
|
|
447
|
-
type: "
|
|
448
|
-
name: "
|
|
449
|
-
message:
|
|
450
|
-
|
|
658
|
+
type: "select",
|
|
659
|
+
name: "action",
|
|
660
|
+
message: "What would you like to do?",
|
|
661
|
+
choices: [
|
|
662
|
+
{ title: "Overwrite with remote version", value: "overwrite" },
|
|
663
|
+
{ title: "Preview remote content first", value: "preview" },
|
|
664
|
+
{ title: "Cancel", value: "cancel" }
|
|
665
|
+
]
|
|
451
666
|
});
|
|
452
|
-
if (!response.
|
|
453
|
-
console.log(chalk5.
|
|
667
|
+
if (response.action === "cancel" || !response.action) {
|
|
668
|
+
console.log(chalk5.gray("Cancelled."));
|
|
454
669
|
return;
|
|
455
670
|
}
|
|
671
|
+
if (response.action === "preview") {
|
|
672
|
+
console.log();
|
|
673
|
+
console.log(chalk5.gray("\u2500".repeat(60)));
|
|
674
|
+
console.log();
|
|
675
|
+
const lines = blueprint.content.split("\n").slice(0, 30);
|
|
676
|
+
for (const line of lines) {
|
|
677
|
+
if (line.startsWith("#")) {
|
|
678
|
+
console.log(chalk5.cyan(line));
|
|
679
|
+
} else {
|
|
680
|
+
console.log(line);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
if (blueprint.content.split("\n").length > 30) {
|
|
684
|
+
console.log(chalk5.gray(`... ${blueprint.content.split("\n").length - 30} more lines`));
|
|
685
|
+
}
|
|
686
|
+
console.log();
|
|
687
|
+
console.log(chalk5.gray("\u2500".repeat(60)));
|
|
688
|
+
console.log();
|
|
689
|
+
const confirmResponse = await prompts({
|
|
690
|
+
type: "confirm",
|
|
691
|
+
name: "confirm",
|
|
692
|
+
message: "Download and overwrite local file?",
|
|
693
|
+
initial: false
|
|
694
|
+
});
|
|
695
|
+
if (!confirmResponse.confirm) {
|
|
696
|
+
console.log(chalk5.gray("Cancelled."));
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
456
700
|
}
|
|
457
|
-
const dir =
|
|
701
|
+
const dir = dirname2(outputPath);
|
|
458
702
|
if (dir !== ".") {
|
|
459
|
-
await
|
|
703
|
+
await mkdir2(dir, { recursive: true });
|
|
704
|
+
}
|
|
705
|
+
await writeFile2(outputPath, blueprint.content, "utf-8");
|
|
706
|
+
if (options.track !== false) {
|
|
707
|
+
await trackBlueprint(cwd, {
|
|
708
|
+
id: blueprint.id,
|
|
709
|
+
name: blueprint.name,
|
|
710
|
+
file: filename,
|
|
711
|
+
content: blueprint.content,
|
|
712
|
+
source
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
console.log(chalk5.green(`\u2705 Downloaded: ${chalk5.bold(outputPath)}`));
|
|
716
|
+
if (options.track !== false) {
|
|
717
|
+
console.log(chalk5.gray(` Linked to: ${blueprint.id}`));
|
|
718
|
+
if (isMarketplace) {
|
|
719
|
+
console.log(chalk5.gray(` Updates: Run 'lynxp pull ${id}' to sync updates`));
|
|
720
|
+
} else {
|
|
721
|
+
console.log(chalk5.gray(` Sync: Run 'lynxp push ${filename}' to push changes`));
|
|
722
|
+
}
|
|
460
723
|
}
|
|
461
|
-
await writeFile(outputPath, blueprint.content, "utf-8");
|
|
462
|
-
console.log();
|
|
463
|
-
console.log(chalk5.green(`\u2705 Downloaded ${chalk5.bold(blueprint.name)}`));
|
|
464
|
-
console.log(` ${chalk5.gray("File:")} ${chalk5.cyan(outputPath)}`);
|
|
465
|
-
console.log(` ${chalk5.gray("Type:")} ${blueprint.type}`);
|
|
466
|
-
console.log(` ${chalk5.gray("Tier:")} ${blueprint.tier}`);
|
|
467
724
|
console.log();
|
|
468
725
|
const editorHint = getEditorHint(blueprint.type);
|
|
469
726
|
if (editorHint) {
|
|
470
727
|
console.log(chalk5.gray(`\u{1F4A1} ${editorHint}`));
|
|
728
|
+
console.log();
|
|
471
729
|
}
|
|
730
|
+
console.log(chalk5.gray("Tips:"));
|
|
731
|
+
console.log(chalk5.gray(` \u2022 Run 'lynxp status' to see tracked blueprints`));
|
|
732
|
+
console.log(chalk5.gray(` \u2022 Run 'lynxp diff ${id}' to see changes between local and remote`));
|
|
733
|
+
if (isMarketplace) {
|
|
734
|
+
console.log(chalk5.gray(` \u2022 Run 'lynxp unlink ${filename}' to disconnect and make editable`));
|
|
735
|
+
}
|
|
736
|
+
console.log();
|
|
472
737
|
} catch (error) {
|
|
473
738
|
spinner.fail("Failed to pull blueprint");
|
|
474
739
|
handleApiError2(error);
|
|
@@ -496,175 +761,1208 @@ function handleApiError2(error) {
|
|
|
496
761
|
if (error instanceof ApiRequestError) {
|
|
497
762
|
if (error.statusCode === 401) {
|
|
498
763
|
console.error(
|
|
499
|
-
chalk5.red("Your session has expired. Please run '
|
|
764
|
+
chalk5.red("\u2717 Your session has expired. Please run 'lynxp login' again.")
|
|
500
765
|
);
|
|
501
766
|
} else if (error.statusCode === 403) {
|
|
502
767
|
console.error(
|
|
503
|
-
chalk5.red("You don't have access to this blueprint.")
|
|
768
|
+
chalk5.red("\u2717 You don't have access to this blueprint.")
|
|
504
769
|
);
|
|
505
770
|
console.error(
|
|
506
771
|
chalk5.gray(
|
|
507
|
-
"This might be a private blueprint or require a higher subscription tier."
|
|
772
|
+
" This might be a private blueprint or require a higher subscription tier."
|
|
508
773
|
)
|
|
509
774
|
);
|
|
510
775
|
} else if (error.statusCode === 404) {
|
|
511
|
-
console.error(chalk5.red("Blueprint not found."));
|
|
776
|
+
console.error(chalk5.red("\u2717 Blueprint not found."));
|
|
512
777
|
console.error(
|
|
513
778
|
chalk5.gray(
|
|
514
|
-
"Make sure you have the correct blueprint ID. Use '
|
|
779
|
+
" Make sure you have the correct blueprint ID. Use 'lynxp list' to see your blueprints."
|
|
515
780
|
)
|
|
516
781
|
);
|
|
517
782
|
} else {
|
|
518
|
-
console.error(chalk5.red(
|
|
783
|
+
console.error(chalk5.red(`\u2717 Error: ${error.message}`));
|
|
519
784
|
}
|
|
520
785
|
} else {
|
|
521
|
-
console.error(chalk5.red("An unexpected error occurred."));
|
|
786
|
+
console.error(chalk5.red("\u2717 An unexpected error occurred."));
|
|
522
787
|
}
|
|
523
788
|
process.exit(1);
|
|
524
789
|
}
|
|
525
790
|
|
|
526
|
-
// src/commands/
|
|
791
|
+
// src/commands/push.ts
|
|
527
792
|
import chalk6 from "chalk";
|
|
528
|
-
import prompts2 from "prompts";
|
|
529
793
|
import ora5 from "ora";
|
|
530
|
-
import
|
|
531
|
-
import
|
|
794
|
+
import fs from "fs";
|
|
795
|
+
import path from "path";
|
|
796
|
+
import prompts2 from "prompts";
|
|
797
|
+
async function pushCommand(fileArg, options) {
|
|
798
|
+
const cwd = process.cwd();
|
|
799
|
+
if (!isAuthenticated()) {
|
|
800
|
+
console.log(chalk6.yellow("You need to be logged in to push blueprints."));
|
|
801
|
+
console.log(chalk6.gray("Run 'lynxp login' to authenticate."));
|
|
802
|
+
process.exit(1);
|
|
803
|
+
}
|
|
804
|
+
const file = fileArg || findDefaultFile();
|
|
805
|
+
if (!file) {
|
|
806
|
+
console.log(chalk6.red("No AI configuration file found."));
|
|
807
|
+
console.log(
|
|
808
|
+
chalk6.gray("Specify a file or run in a directory with AGENTS.md, CLAUDE.md, etc.")
|
|
809
|
+
);
|
|
810
|
+
process.exit(1);
|
|
811
|
+
}
|
|
812
|
+
if (!fs.existsSync(file)) {
|
|
813
|
+
console.log(chalk6.red(`File not found: ${file}`));
|
|
814
|
+
process.exit(1);
|
|
815
|
+
}
|
|
816
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
817
|
+
const filename = path.basename(file);
|
|
818
|
+
const linked = await findBlueprintByFile(cwd, file);
|
|
819
|
+
if (linked) {
|
|
820
|
+
await updateBlueprint(cwd, file, linked.id, content, options);
|
|
821
|
+
} else {
|
|
822
|
+
await createOrLinkBlueprint(cwd, file, filename, content, options);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
async function updateBlueprint(cwd, file, blueprintId, content, options) {
|
|
826
|
+
console.log(chalk6.cyan(`
|
|
827
|
+
\u{1F4E4} Updating blueprint ${chalk6.bold(blueprintId)}...`));
|
|
828
|
+
console.log(chalk6.gray(` File: ${file}`));
|
|
829
|
+
if (!options.yes) {
|
|
830
|
+
const confirm = await prompts2({
|
|
831
|
+
type: "confirm",
|
|
832
|
+
name: "value",
|
|
833
|
+
message: `Push changes to ${blueprintId}?`,
|
|
834
|
+
initial: true
|
|
835
|
+
});
|
|
836
|
+
if (!confirm.value) {
|
|
837
|
+
console.log(chalk6.yellow("Push cancelled."));
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
const spinner = ora5("Pushing changes...").start();
|
|
842
|
+
try {
|
|
843
|
+
const result = await api.updateBlueprint(blueprintId, { content });
|
|
844
|
+
spinner.succeed("Blueprint updated!");
|
|
845
|
+
await updateChecksum(cwd, file, content);
|
|
846
|
+
console.log();
|
|
847
|
+
console.log(chalk6.green(`\u2705 Successfully updated ${chalk6.bold(result.blueprint.name)}`));
|
|
848
|
+
console.log(chalk6.gray(` ID: ${blueprintId}`));
|
|
849
|
+
console.log(chalk6.gray(` View: https://lynxprompt.com/templates/${blueprintId.replace("bp_", "")}`));
|
|
850
|
+
} catch (error) {
|
|
851
|
+
spinner.fail("Failed to update blueprint");
|
|
852
|
+
handleError(error);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
async function createOrLinkBlueprint(cwd, file, filename, content, options) {
|
|
856
|
+
console.log(chalk6.cyan("\n\u{1F4E4} Push new blueprint"));
|
|
857
|
+
console.log(chalk6.gray(` File: ${file}`));
|
|
858
|
+
let name = options.name;
|
|
859
|
+
let description = options.description;
|
|
860
|
+
let visibility = options.visibility || "PRIVATE";
|
|
861
|
+
let tags = options.tags ? options.tags.split(",").map((t) => t.trim()) : [];
|
|
862
|
+
if (!options.yes) {
|
|
863
|
+
const responses = await prompts2([
|
|
864
|
+
{
|
|
865
|
+
type: name ? null : "text",
|
|
866
|
+
name: "name",
|
|
867
|
+
message: "Blueprint name:",
|
|
868
|
+
initial: filename.replace(/\.(md|mdc|json|yml|yaml)$/, ""),
|
|
869
|
+
validate: (v) => v.length > 0 || "Name is required"
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
type: description ? null : "text",
|
|
873
|
+
name: "description",
|
|
874
|
+
message: "Description:",
|
|
875
|
+
initial: ""
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
type: "select",
|
|
879
|
+
name: "visibility",
|
|
880
|
+
message: "Visibility:",
|
|
881
|
+
choices: [
|
|
882
|
+
{ title: "Private (only you)", value: "PRIVATE" },
|
|
883
|
+
{ title: "Team (your team members)", value: "TEAM" },
|
|
884
|
+
{ title: "Public (visible to everyone)", value: "PUBLIC" }
|
|
885
|
+
],
|
|
886
|
+
initial: 0
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
type: "text",
|
|
890
|
+
name: "tags",
|
|
891
|
+
message: "Tags (comma-separated):",
|
|
892
|
+
initial: ""
|
|
893
|
+
}
|
|
894
|
+
]);
|
|
895
|
+
if (!responses.name && !name) {
|
|
896
|
+
console.log(chalk6.yellow("Push cancelled."));
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
name = name || responses.name;
|
|
900
|
+
description = description || responses.description || "";
|
|
901
|
+
visibility = responses.visibility || visibility;
|
|
902
|
+
tags = responses.tags ? responses.tags.split(",").map((t) => t.trim()).filter(Boolean) : tags;
|
|
903
|
+
}
|
|
904
|
+
if (!name) {
|
|
905
|
+
name = filename.replace(/\.(md|mdc|json|yml|yaml)$/, "");
|
|
906
|
+
}
|
|
907
|
+
const spinner = ora5("Creating blueprint...").start();
|
|
908
|
+
try {
|
|
909
|
+
const result = await api.createBlueprint({
|
|
910
|
+
name,
|
|
911
|
+
description: description || "",
|
|
912
|
+
content,
|
|
913
|
+
visibility,
|
|
914
|
+
tags
|
|
915
|
+
});
|
|
916
|
+
spinner.succeed("Blueprint created!");
|
|
917
|
+
await trackBlueprint(cwd, {
|
|
918
|
+
id: result.blueprint.id,
|
|
919
|
+
name: result.blueprint.name,
|
|
920
|
+
file,
|
|
921
|
+
content,
|
|
922
|
+
source: "private"
|
|
923
|
+
});
|
|
924
|
+
console.log();
|
|
925
|
+
console.log(chalk6.green(`\u2705 Created blueprint ${chalk6.bold(result.blueprint.name)}`));
|
|
926
|
+
console.log(chalk6.gray(` ID: ${result.blueprint.id}`));
|
|
927
|
+
console.log(chalk6.gray(` Visibility: ${visibility}`));
|
|
928
|
+
if (visibility === "PUBLIC") {
|
|
929
|
+
console.log(chalk6.gray(` View: https://lynxprompt.com/templates/${result.blueprint.id.replace("bp_", "")}`));
|
|
930
|
+
}
|
|
931
|
+
console.log();
|
|
932
|
+
console.log(chalk6.cyan("The file is now linked. Future 'lynxp push' will update this blueprint."));
|
|
933
|
+
} catch (error) {
|
|
934
|
+
spinner.fail("Failed to create blueprint");
|
|
935
|
+
handleError(error);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
function findDefaultFile() {
|
|
939
|
+
const candidates = [
|
|
940
|
+
"AGENTS.md",
|
|
941
|
+
"CLAUDE.md",
|
|
942
|
+
".cursor/rules/project.mdc",
|
|
943
|
+
".github/copilot-instructions.md",
|
|
944
|
+
".windsurfrules",
|
|
945
|
+
"AIDER.md",
|
|
946
|
+
"GEMINI.md",
|
|
947
|
+
".clinerules"
|
|
948
|
+
];
|
|
949
|
+
for (const candidate of candidates) {
|
|
950
|
+
if (fs.existsSync(candidate)) {
|
|
951
|
+
return candidate;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
956
|
+
function handleError(error) {
|
|
957
|
+
if (error instanceof ApiRequestError) {
|
|
958
|
+
console.error(chalk6.red(`Error: ${error.message}`));
|
|
959
|
+
if (error.statusCode === 401) {
|
|
960
|
+
console.error(chalk6.gray("Your session may have expired. Run 'lynxp login' to re-authenticate."));
|
|
961
|
+
} else if (error.statusCode === 403) {
|
|
962
|
+
console.error(chalk6.gray("You don't have permission to modify this blueprint."));
|
|
963
|
+
} else if (error.statusCode === 404) {
|
|
964
|
+
console.error(chalk6.gray("Blueprint not found. It may have been deleted."));
|
|
965
|
+
}
|
|
966
|
+
} else {
|
|
967
|
+
console.error(chalk6.red("An unexpected error occurred."));
|
|
968
|
+
}
|
|
969
|
+
process.exit(1);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// src/commands/init.ts
|
|
973
|
+
import chalk7 from "chalk";
|
|
974
|
+
import prompts3 from "prompts";
|
|
975
|
+
import ora6 from "ora";
|
|
976
|
+
import { writeFile as writeFile3, mkdir as mkdir3, readFile as readFile4 } from "fs/promises";
|
|
977
|
+
import { join as join5, dirname as dirname3, basename } from "path";
|
|
978
|
+
import { existsSync as existsSync3 } from "fs";
|
|
979
|
+
import * as yaml2 from "yaml";
|
|
980
|
+
|
|
981
|
+
// src/utils/agent-detector.ts
|
|
982
|
+
import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "fs";
|
|
983
|
+
import { join as join3 } from "path";
|
|
984
|
+
|
|
985
|
+
// src/utils/agents.ts
|
|
986
|
+
var AGENTS = [
|
|
987
|
+
// === POPULAR AGENTS ===
|
|
988
|
+
{
|
|
989
|
+
id: "cursor",
|
|
990
|
+
name: "Cursor",
|
|
991
|
+
description: "AI-powered code editor with .cursor/rules/ support",
|
|
992
|
+
patterns: [".cursor/rules/"],
|
|
993
|
+
output: ".cursor/rules/",
|
|
994
|
+
format: "mdc",
|
|
995
|
+
category: "popular",
|
|
996
|
+
popular: true
|
|
997
|
+
},
|
|
998
|
+
{
|
|
999
|
+
id: "agents",
|
|
1000
|
+
name: "AGENTS.md",
|
|
1001
|
+
description: "Universal format for Claude Code, GitHub Copilot, Aider, and others",
|
|
1002
|
+
patterns: ["AGENTS.md"],
|
|
1003
|
+
output: "AGENTS.md",
|
|
1004
|
+
format: "markdown",
|
|
1005
|
+
category: "popular",
|
|
1006
|
+
popular: true
|
|
1007
|
+
},
|
|
1008
|
+
{
|
|
1009
|
+
id: "claude",
|
|
1010
|
+
name: "Claude Code",
|
|
1011
|
+
description: "Anthropic's Claude with CLAUDE.md support",
|
|
1012
|
+
patterns: ["CLAUDE.md"],
|
|
1013
|
+
output: "CLAUDE.md",
|
|
1014
|
+
format: "markdown",
|
|
1015
|
+
category: "popular",
|
|
1016
|
+
popular: true
|
|
1017
|
+
},
|
|
1018
|
+
{
|
|
1019
|
+
id: "copilot",
|
|
1020
|
+
name: "GitHub Copilot",
|
|
1021
|
+
description: "GitHub's AI pair programmer",
|
|
1022
|
+
patterns: [".github/copilot-instructions.md"],
|
|
1023
|
+
output: ".github/copilot-instructions.md",
|
|
1024
|
+
format: "markdown",
|
|
1025
|
+
category: "popular",
|
|
1026
|
+
popular: true
|
|
1027
|
+
},
|
|
1028
|
+
{
|
|
1029
|
+
id: "windsurf",
|
|
1030
|
+
name: "Windsurf",
|
|
1031
|
+
description: "Codeium's AI IDE with .windsurfrules support",
|
|
1032
|
+
patterns: [".windsurfrules", ".windsurf/rules/"],
|
|
1033
|
+
output: ".windsurfrules",
|
|
1034
|
+
format: "text",
|
|
1035
|
+
category: "popular",
|
|
1036
|
+
popular: true
|
|
1037
|
+
},
|
|
1038
|
+
// === MARKDOWN FORMAT AGENTS ===
|
|
1039
|
+
{
|
|
1040
|
+
id: "gemini",
|
|
1041
|
+
name: "Gemini",
|
|
1042
|
+
description: "Google's Gemini AI assistant",
|
|
1043
|
+
patterns: ["GEMINI.md"],
|
|
1044
|
+
output: "GEMINI.md",
|
|
1045
|
+
format: "markdown",
|
|
1046
|
+
category: "markdown"
|
|
1047
|
+
},
|
|
1048
|
+
{
|
|
1049
|
+
id: "warp",
|
|
1050
|
+
name: "Warp AI",
|
|
1051
|
+
description: "Warp terminal's AI assistant",
|
|
1052
|
+
patterns: ["WARP.md"],
|
|
1053
|
+
output: "WARP.md",
|
|
1054
|
+
format: "markdown",
|
|
1055
|
+
category: "markdown"
|
|
1056
|
+
},
|
|
1057
|
+
{
|
|
1058
|
+
id: "zed",
|
|
1059
|
+
name: "Zed",
|
|
1060
|
+
description: "High-performance code editor with AI features",
|
|
1061
|
+
patterns: [".zed/instructions.md", "ZED.md"],
|
|
1062
|
+
output: ".zed/instructions.md",
|
|
1063
|
+
format: "markdown",
|
|
1064
|
+
category: "markdown"
|
|
1065
|
+
},
|
|
1066
|
+
{
|
|
1067
|
+
id: "crush",
|
|
1068
|
+
name: "Crush",
|
|
1069
|
+
description: "AI coding assistant",
|
|
1070
|
+
patterns: ["CRUSH.md"],
|
|
1071
|
+
output: "CRUSH.md",
|
|
1072
|
+
format: "markdown",
|
|
1073
|
+
category: "markdown"
|
|
1074
|
+
},
|
|
1075
|
+
{
|
|
1076
|
+
id: "junie",
|
|
1077
|
+
name: "Junie",
|
|
1078
|
+
description: "JetBrains' AI coding assistant",
|
|
1079
|
+
patterns: [".junie/guidelines.md"],
|
|
1080
|
+
output: ".junie/guidelines.md",
|
|
1081
|
+
format: "markdown",
|
|
1082
|
+
category: "markdown"
|
|
1083
|
+
},
|
|
1084
|
+
{
|
|
1085
|
+
id: "openhands",
|
|
1086
|
+
name: "OpenHands",
|
|
1087
|
+
description: "Open-source AI coding agent",
|
|
1088
|
+
patterns: [".openhands/microagents/repo.md"],
|
|
1089
|
+
output: ".openhands/microagents/repo.md",
|
|
1090
|
+
format: "markdown",
|
|
1091
|
+
category: "markdown"
|
|
1092
|
+
},
|
|
1093
|
+
// === PLAIN TEXT FORMAT ===
|
|
1094
|
+
{
|
|
1095
|
+
id: "cline",
|
|
1096
|
+
name: "Cline",
|
|
1097
|
+
description: "VS Code AI assistant extension",
|
|
1098
|
+
patterns: [".clinerules"],
|
|
1099
|
+
output: ".clinerules",
|
|
1100
|
+
format: "text",
|
|
1101
|
+
category: "config"
|
|
1102
|
+
},
|
|
1103
|
+
{
|
|
1104
|
+
id: "goose",
|
|
1105
|
+
name: "Goose",
|
|
1106
|
+
description: "Block's AI coding assistant",
|
|
1107
|
+
patterns: [".goosehints"],
|
|
1108
|
+
output: ".goosehints",
|
|
1109
|
+
format: "text",
|
|
1110
|
+
category: "config"
|
|
1111
|
+
},
|
|
1112
|
+
{
|
|
1113
|
+
id: "aider",
|
|
1114
|
+
name: "Aider",
|
|
1115
|
+
description: "AI pair programming in your terminal",
|
|
1116
|
+
patterns: [".aider.conf.yml", "AIDER.md"],
|
|
1117
|
+
output: "AIDER.md",
|
|
1118
|
+
format: "markdown",
|
|
1119
|
+
category: "config"
|
|
1120
|
+
},
|
|
1121
|
+
// === DIRECTORY-BASED AGENTS ===
|
|
1122
|
+
{
|
|
1123
|
+
id: "amazonq",
|
|
1124
|
+
name: "Amazon Q",
|
|
1125
|
+
description: "AWS's AI coding assistant",
|
|
1126
|
+
patterns: [".amazonq/rules/"],
|
|
1127
|
+
output: ".amazonq/rules/",
|
|
1128
|
+
format: "mdc",
|
|
1129
|
+
category: "directory"
|
|
1130
|
+
},
|
|
1131
|
+
{
|
|
1132
|
+
id: "augmentcode",
|
|
1133
|
+
name: "Augment Code",
|
|
1134
|
+
description: "AI code augmentation tool",
|
|
1135
|
+
patterns: [".augment/rules/"],
|
|
1136
|
+
output: ".augment/rules/",
|
|
1137
|
+
format: "mdc",
|
|
1138
|
+
category: "directory"
|
|
1139
|
+
},
|
|
1140
|
+
{
|
|
1141
|
+
id: "kilocode",
|
|
1142
|
+
name: "Kilocode",
|
|
1143
|
+
description: "AI-powered code generation",
|
|
1144
|
+
patterns: [".kilocode/rules/"],
|
|
1145
|
+
output: ".kilocode/rules/",
|
|
1146
|
+
format: "mdc",
|
|
1147
|
+
category: "directory"
|
|
1148
|
+
},
|
|
1149
|
+
{
|
|
1150
|
+
id: "kiro",
|
|
1151
|
+
name: "Kiro",
|
|
1152
|
+
description: "AWS's spec-driven AI coding agent",
|
|
1153
|
+
patterns: [".kiro/steering/"],
|
|
1154
|
+
output: ".kiro/steering/",
|
|
1155
|
+
format: "mdc",
|
|
1156
|
+
category: "directory"
|
|
1157
|
+
},
|
|
1158
|
+
{
|
|
1159
|
+
id: "trae-ai",
|
|
1160
|
+
name: "Trae AI",
|
|
1161
|
+
description: "ByteDance's AI coding assistant",
|
|
1162
|
+
patterns: [".trae/rules/"],
|
|
1163
|
+
output: ".trae/rules/",
|
|
1164
|
+
format: "mdc",
|
|
1165
|
+
category: "directory"
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
id: "firebase-studio",
|
|
1169
|
+
name: "Firebase Studio",
|
|
1170
|
+
description: "Google's Firebase development environment",
|
|
1171
|
+
patterns: [".idx/"],
|
|
1172
|
+
output: ".idx/",
|
|
1173
|
+
format: "mdc",
|
|
1174
|
+
category: "directory"
|
|
1175
|
+
},
|
|
1176
|
+
{
|
|
1177
|
+
id: "roocode",
|
|
1178
|
+
name: "Roo Code",
|
|
1179
|
+
description: "AI coding assistant for VS Code",
|
|
1180
|
+
patterns: [".roo/rules/"],
|
|
1181
|
+
output: ".roo/rules/",
|
|
1182
|
+
format: "mdc",
|
|
1183
|
+
category: "directory"
|
|
1184
|
+
},
|
|
1185
|
+
// === JSON/CONFIG FORMAT ===
|
|
1186
|
+
{
|
|
1187
|
+
id: "firebender",
|
|
1188
|
+
name: "Firebender",
|
|
1189
|
+
description: "AI code transformation tool",
|
|
1190
|
+
patterns: ["firebender.json"],
|
|
1191
|
+
output: "firebender.json",
|
|
1192
|
+
format: "json",
|
|
1193
|
+
category: "config"
|
|
1194
|
+
},
|
|
1195
|
+
{
|
|
1196
|
+
id: "opencode",
|
|
1197
|
+
name: "Open Code",
|
|
1198
|
+
description: "Open-source AI coding tool",
|
|
1199
|
+
patterns: ["opencode.json"],
|
|
1200
|
+
output: "opencode.json",
|
|
1201
|
+
format: "json",
|
|
1202
|
+
category: "config"
|
|
1203
|
+
},
|
|
1204
|
+
// === MCP (Model Context Protocol) AGENTS ===
|
|
1205
|
+
{
|
|
1206
|
+
id: "vscode-mcp",
|
|
1207
|
+
name: "VS Code MCP",
|
|
1208
|
+
description: "VS Code with Model Context Protocol",
|
|
1209
|
+
patterns: [".vscode/mcp.json"],
|
|
1210
|
+
output: ".vscode/mcp.json",
|
|
1211
|
+
format: "json",
|
|
1212
|
+
category: "mcp"
|
|
1213
|
+
},
|
|
1214
|
+
{
|
|
1215
|
+
id: "cursor-mcp",
|
|
1216
|
+
name: "Cursor MCP",
|
|
1217
|
+
description: "Cursor with Model Context Protocol",
|
|
1218
|
+
patterns: [".cursor/mcp.json"],
|
|
1219
|
+
output: ".cursor/mcp.json",
|
|
1220
|
+
format: "json",
|
|
1221
|
+
category: "mcp"
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
id: "root-mcp",
|
|
1225
|
+
name: "Root MCP",
|
|
1226
|
+
description: "Root-level MCP config for Claude Code, Aider",
|
|
1227
|
+
patterns: [".mcp.json"],
|
|
1228
|
+
output: ".mcp.json",
|
|
1229
|
+
format: "json",
|
|
1230
|
+
category: "mcp"
|
|
1231
|
+
},
|
|
1232
|
+
{
|
|
1233
|
+
id: "windsurf-mcp",
|
|
1234
|
+
name: "Windsurf MCP",
|
|
1235
|
+
description: "Windsurf with Model Context Protocol",
|
|
1236
|
+
patterns: [".windsurf/mcp_config.json"],
|
|
1237
|
+
output: ".windsurf/mcp_config.json",
|
|
1238
|
+
format: "json",
|
|
1239
|
+
category: "mcp"
|
|
1240
|
+
}
|
|
1241
|
+
];
|
|
1242
|
+
function getAgent(id) {
|
|
1243
|
+
return AGENTS.find((a) => a.id === id);
|
|
1244
|
+
}
|
|
1245
|
+
function getPopularAgents() {
|
|
1246
|
+
return AGENTS.filter((a) => a.popular);
|
|
1247
|
+
}
|
|
1248
|
+
function getAgentDisplayName(id) {
|
|
1249
|
+
const agent = getAgent(id);
|
|
1250
|
+
return agent?.name ?? id;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// src/utils/agent-detector.ts
|
|
1254
|
+
function detectAgents(cwd = process.cwd()) {
|
|
1255
|
+
const detected = [];
|
|
1256
|
+
for (const agent of AGENTS) {
|
|
1257
|
+
const result = detectAgent(cwd, agent);
|
|
1258
|
+
if (result) {
|
|
1259
|
+
detected.push(result);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
const popularDetected = detected.filter((d) => d.agent.popular);
|
|
1263
|
+
const importable = detected.filter((d) => d.hasContent);
|
|
1264
|
+
let summary;
|
|
1265
|
+
if (detected.length === 0) {
|
|
1266
|
+
summary = "No AI agent configuration files detected";
|
|
1267
|
+
} else if (detected.length === 1) {
|
|
1268
|
+
summary = `Found ${getAgentDisplayName(detected[0].agent.id)} configuration`;
|
|
1269
|
+
} else {
|
|
1270
|
+
const names = detected.slice(0, 3).map((d) => d.agent.name);
|
|
1271
|
+
const more = detected.length > 3 ? ` +${detected.length - 3} more` : "";
|
|
1272
|
+
summary = `Found ${detected.length} agents: ${names.join(", ")}${more}`;
|
|
1273
|
+
}
|
|
1274
|
+
return { detected, popularDetected, importable, summary };
|
|
1275
|
+
}
|
|
1276
|
+
function detectAgent(cwd, agent) {
|
|
1277
|
+
const files = [];
|
|
1278
|
+
let hasContent = false;
|
|
1279
|
+
let ruleCount = 0;
|
|
1280
|
+
for (const pattern of agent.patterns) {
|
|
1281
|
+
const fullPath = join3(cwd, pattern);
|
|
1282
|
+
if (!existsSync2(fullPath)) {
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
const stats = statSync(fullPath);
|
|
1286
|
+
if (stats.isDirectory()) {
|
|
1287
|
+
const dirFiles = scanDirectory(fullPath, agent.format);
|
|
1288
|
+
if (dirFiles.length > 0) {
|
|
1289
|
+
files.push(pattern);
|
|
1290
|
+
hasContent = true;
|
|
1291
|
+
ruleCount += dirFiles.reduce((sum, f) => sum + f.sections, 0);
|
|
1292
|
+
}
|
|
1293
|
+
} else if (stats.isFile()) {
|
|
1294
|
+
files.push(pattern);
|
|
1295
|
+
const content = safeReadFile(fullPath);
|
|
1296
|
+
if (content && content.trim().length > 0) {
|
|
1297
|
+
hasContent = true;
|
|
1298
|
+
ruleCount += countSections(content);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
if (files.length === 0) {
|
|
1303
|
+
return null;
|
|
1304
|
+
}
|
|
1305
|
+
return { agent, files, hasContent, ruleCount };
|
|
1306
|
+
}
|
|
1307
|
+
function scanDirectory(dirPath, format) {
|
|
1308
|
+
const results = [];
|
|
1309
|
+
try {
|
|
1310
|
+
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
1311
|
+
for (const entry of entries) {
|
|
1312
|
+
if (!entry.isFile()) continue;
|
|
1313
|
+
const name = entry.name.toLowerCase();
|
|
1314
|
+
const isRuleFile = format === "mdc" && name.endsWith(".mdc") || format === "markdown" && name.endsWith(".md") || format === "json" && name.endsWith(".json") || format === "yaml" && (name.endsWith(".yml") || name.endsWith(".yaml"));
|
|
1315
|
+
if (!isRuleFile) continue;
|
|
1316
|
+
const filePath = join3(dirPath, entry.name);
|
|
1317
|
+
const content = safeReadFile(filePath);
|
|
1318
|
+
if (content && content.trim().length > 0) {
|
|
1319
|
+
results.push({
|
|
1320
|
+
path: filePath,
|
|
1321
|
+
sections: countSections(content)
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
} catch {
|
|
1326
|
+
}
|
|
1327
|
+
return results;
|
|
1328
|
+
}
|
|
1329
|
+
function countSections(content) {
|
|
1330
|
+
const headings = content.match(/^#{1,6}\s+.+$/gm);
|
|
1331
|
+
return headings ? headings.length : content.trim().length > 0 ? 1 : 0;
|
|
1332
|
+
}
|
|
1333
|
+
function safeReadFile(path2) {
|
|
1334
|
+
try {
|
|
1335
|
+
return readFileSync(path2, "utf-8");
|
|
1336
|
+
} catch {
|
|
1337
|
+
return null;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
function formatDetectionResults(result) {
|
|
1341
|
+
if (result.detected.length === 0) {
|
|
1342
|
+
return "No AI agent configuration files found.\n\nRun 'lynxp wizard' to create your first configuration.";
|
|
1343
|
+
}
|
|
1344
|
+
const lines = [
|
|
1345
|
+
`Found ${result.detected.length} AI agent${result.detected.length === 1 ? "" : "s"}:`,
|
|
1346
|
+
""
|
|
1347
|
+
];
|
|
1348
|
+
for (const detected of result.detected) {
|
|
1349
|
+
const icon = detected.hasContent ? "\u2713" : "\u25CB";
|
|
1350
|
+
const rules = detected.ruleCount > 0 ? ` (${detected.ruleCount} sections)` : "";
|
|
1351
|
+
lines.push(` ${icon} ${detected.agent.name}${rules}`);
|
|
1352
|
+
for (const file of detected.files) {
|
|
1353
|
+
lines.push(` \u2514\u2500 ${file}`);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
if (result.importable.length > 0) {
|
|
1357
|
+
lines.push("");
|
|
1358
|
+
lines.push(`${result.importable.length} can be imported into LynxPrompt.`);
|
|
1359
|
+
}
|
|
1360
|
+
return lines.join("\n");
|
|
1361
|
+
}
|
|
532
1362
|
|
|
533
1363
|
// src/utils/detect.ts
|
|
534
|
-
import { readFile, access as
|
|
535
|
-
import { join as
|
|
1364
|
+
import { readFile as readFile3, access as access3 } from "fs/promises";
|
|
1365
|
+
import { join as join4 } from "path";
|
|
1366
|
+
var JS_FRAMEWORK_PATTERNS = {
|
|
1367
|
+
nextjs: ["next"],
|
|
1368
|
+
react: ["react", "react-dom"],
|
|
1369
|
+
vue: ["vue"],
|
|
1370
|
+
angular: ["@angular/core"],
|
|
1371
|
+
svelte: ["svelte", "@sveltejs/kit"],
|
|
1372
|
+
solid: ["solid-js"],
|
|
1373
|
+
remix: ["@remix-run/react"],
|
|
1374
|
+
astro: ["astro"],
|
|
1375
|
+
nuxt: ["nuxt"],
|
|
1376
|
+
gatsby: ["gatsby"]
|
|
1377
|
+
};
|
|
1378
|
+
var JS_TOOL_PATTERNS = {
|
|
1379
|
+
typescript: ["typescript"],
|
|
1380
|
+
tailwind: ["tailwindcss"],
|
|
1381
|
+
prisma: ["prisma", "@prisma/client"],
|
|
1382
|
+
drizzle: ["drizzle-orm"],
|
|
1383
|
+
express: ["express"],
|
|
1384
|
+
fastify: ["fastify"],
|
|
1385
|
+
hono: ["hono"],
|
|
1386
|
+
elysia: ["elysia"],
|
|
1387
|
+
trpc: ["@trpc/server"],
|
|
1388
|
+
graphql: ["graphql", "@apollo/server"],
|
|
1389
|
+
jest: ["jest"],
|
|
1390
|
+
vitest: ["vitest"],
|
|
1391
|
+
playwright: ["@playwright/test"],
|
|
1392
|
+
cypress: ["cypress"],
|
|
1393
|
+
eslint: ["eslint"],
|
|
1394
|
+
biome: ["@biomejs/biome"],
|
|
1395
|
+
prettier: ["prettier"],
|
|
1396
|
+
vite: ["vite"],
|
|
1397
|
+
webpack: ["webpack"],
|
|
1398
|
+
turbo: ["turbo"]
|
|
1399
|
+
};
|
|
536
1400
|
async function detectProject(cwd) {
|
|
537
1401
|
const detected = {
|
|
538
1402
|
name: null,
|
|
539
1403
|
stack: [],
|
|
540
1404
|
commands: {},
|
|
541
|
-
packageManager: null
|
|
1405
|
+
packageManager: null,
|
|
1406
|
+
type: "unknown"
|
|
542
1407
|
};
|
|
543
|
-
const packageJsonPath =
|
|
1408
|
+
const packageJsonPath = join4(cwd, "package.json");
|
|
544
1409
|
if (await fileExists(packageJsonPath)) {
|
|
545
1410
|
try {
|
|
546
|
-
const content = await
|
|
1411
|
+
const content = await readFile3(packageJsonPath, "utf-8");
|
|
547
1412
|
const pkg = JSON.parse(content);
|
|
548
1413
|
detected.name = pkg.name || null;
|
|
1414
|
+
detected.description = pkg.description;
|
|
1415
|
+
if (pkg.workspaces || await fileExists(join4(cwd, "pnpm-workspace.yaml"))) {
|
|
1416
|
+
detected.type = "monorepo";
|
|
1417
|
+
} else if (pkg.main || pkg.exports) {
|
|
1418
|
+
detected.type = "library";
|
|
1419
|
+
} else {
|
|
1420
|
+
detected.type = "application";
|
|
1421
|
+
}
|
|
549
1422
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
1423
|
+
for (const [framework, deps] of Object.entries(JS_FRAMEWORK_PATTERNS)) {
|
|
1424
|
+
if (deps.some((dep) => allDeps[dep])) {
|
|
1425
|
+
detected.stack.push(framework);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
for (const [tool, deps] of Object.entries(JS_TOOL_PATTERNS)) {
|
|
1429
|
+
if (deps.some((dep) => allDeps[dep])) {
|
|
1430
|
+
detected.stack.push(tool);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
if (detected.stack.length === 0 || detected.stack.length === 1 && detected.stack[0] === "typescript") {
|
|
1434
|
+
detected.stack.unshift("javascript");
|
|
1435
|
+
}
|
|
560
1436
|
if (pkg.scripts) {
|
|
561
1437
|
detected.commands.build = pkg.scripts.build;
|
|
562
1438
|
detected.commands.test = pkg.scripts.test;
|
|
563
|
-
detected.commands.lint = pkg.scripts.lint;
|
|
564
|
-
detected.commands.dev = pkg.scripts.dev || pkg.scripts.start;
|
|
1439
|
+
detected.commands.lint = pkg.scripts.lint || pkg.scripts["lint:check"];
|
|
1440
|
+
detected.commands.dev = pkg.scripts.dev || pkg.scripts.start || pkg.scripts.serve;
|
|
1441
|
+
detected.commands.format = pkg.scripts.format || pkg.scripts.prettier;
|
|
565
1442
|
}
|
|
566
|
-
if (await fileExists(
|
|
1443
|
+
if (await fileExists(join4(cwd, "pnpm-lock.yaml"))) {
|
|
567
1444
|
detected.packageManager = "pnpm";
|
|
568
|
-
} else if (await fileExists(
|
|
1445
|
+
} else if (await fileExists(join4(cwd, "yarn.lock"))) {
|
|
569
1446
|
detected.packageManager = "yarn";
|
|
570
|
-
} else if (await fileExists(
|
|
1447
|
+
} else if (await fileExists(join4(cwd, "bun.lockb"))) {
|
|
571
1448
|
detected.packageManager = "bun";
|
|
572
|
-
} else if (await fileExists(
|
|
1449
|
+
} else if (await fileExists(join4(cwd, "package-lock.json"))) {
|
|
573
1450
|
detected.packageManager = "npm";
|
|
574
1451
|
}
|
|
1452
|
+
if (detected.packageManager && detected.packageManager !== "npm") {
|
|
1453
|
+
const pm = detected.packageManager;
|
|
1454
|
+
for (const [key, value] of Object.entries(detected.commands)) {
|
|
1455
|
+
if (value && !value.startsWith(pm) && !value.startsWith("npx")) {
|
|
1456
|
+
detected.commands[key] = `${pm} run ${value}`;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
} else if (detected.commands) {
|
|
1460
|
+
for (const [key, value] of Object.entries(detected.commands)) {
|
|
1461
|
+
if (value && !value.startsWith("npm") && !value.startsWith("npx")) {
|
|
1462
|
+
detected.commands[key] = `npm run ${value}`;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
if (pkg.scripts) {
|
|
1467
|
+
detected.commands.build = pkg.scripts.build ? "build" : void 0;
|
|
1468
|
+
detected.commands.test = pkg.scripts.test ? "test" : void 0;
|
|
1469
|
+
detected.commands.lint = pkg.scripts.lint ? "lint" : pkg.scripts["lint:check"] ? "lint:check" : void 0;
|
|
1470
|
+
detected.commands.dev = pkg.scripts.dev ? "dev" : pkg.scripts.start ? "start" : pkg.scripts.serve ? "serve" : void 0;
|
|
1471
|
+
}
|
|
575
1472
|
return detected;
|
|
576
1473
|
} catch {
|
|
577
1474
|
}
|
|
578
1475
|
}
|
|
579
|
-
const pyprojectPath =
|
|
1476
|
+
const pyprojectPath = join4(cwd, "pyproject.toml");
|
|
580
1477
|
if (await fileExists(pyprojectPath)) {
|
|
581
1478
|
try {
|
|
582
|
-
const content = await
|
|
1479
|
+
const content = await readFile3(pyprojectPath, "utf-8");
|
|
583
1480
|
detected.stack.push("python");
|
|
1481
|
+
detected.type = "application";
|
|
584
1482
|
const nameMatch = content.match(/name\s*=\s*"([^"]+)"/);
|
|
585
1483
|
if (nameMatch) detected.name = nameMatch[1];
|
|
586
1484
|
if (content.includes("fastapi")) detected.stack.push("fastapi");
|
|
587
1485
|
if (content.includes("django")) detected.stack.push("django");
|
|
588
1486
|
if (content.includes("flask")) detected.stack.push("flask");
|
|
1487
|
+
if (content.includes("pydantic")) detected.stack.push("pydantic");
|
|
1488
|
+
if (content.includes("sqlalchemy")) detected.stack.push("sqlalchemy");
|
|
1489
|
+
if (content.includes("pytest")) detected.stack.push("pytest");
|
|
1490
|
+
if (content.includes("ruff")) detected.stack.push("ruff");
|
|
1491
|
+
if (content.includes("mypy")) detected.stack.push("mypy");
|
|
589
1492
|
detected.commands.test = "pytest";
|
|
590
|
-
detected.commands.lint = "ruff check";
|
|
1493
|
+
detected.commands.lint = "ruff check .";
|
|
1494
|
+
if (content.includes("[tool.poetry]")) {
|
|
1495
|
+
detected.packageManager = "yarn";
|
|
1496
|
+
detected.commands.dev = "poetry run python -m uvicorn main:app --reload";
|
|
1497
|
+
} else if (await fileExists(join4(cwd, "uv.lock"))) {
|
|
1498
|
+
detected.commands.dev = "uv run python main.py";
|
|
1499
|
+
}
|
|
591
1500
|
return detected;
|
|
592
1501
|
} catch {
|
|
593
1502
|
}
|
|
594
1503
|
}
|
|
595
|
-
const requirementsPath =
|
|
1504
|
+
const requirementsPath = join4(cwd, "requirements.txt");
|
|
596
1505
|
if (await fileExists(requirementsPath)) {
|
|
597
1506
|
try {
|
|
598
|
-
const content = await
|
|
1507
|
+
const content = await readFile3(requirementsPath, "utf-8");
|
|
599
1508
|
detected.stack.push("python");
|
|
600
|
-
|
|
601
|
-
if (content.includes("
|
|
602
|
-
if (content.includes("
|
|
1509
|
+
detected.type = "application";
|
|
1510
|
+
if (content.toLowerCase().includes("fastapi")) detected.stack.push("fastapi");
|
|
1511
|
+
if (content.toLowerCase().includes("django")) detected.stack.push("django");
|
|
1512
|
+
if (content.toLowerCase().includes("flask")) detected.stack.push("flask");
|
|
603
1513
|
detected.commands.test = "pytest";
|
|
604
|
-
detected.commands.lint = "ruff check";
|
|
1514
|
+
detected.commands.lint = "ruff check .";
|
|
605
1515
|
return detected;
|
|
606
1516
|
} catch {
|
|
607
1517
|
}
|
|
608
1518
|
}
|
|
609
|
-
const cargoPath =
|
|
1519
|
+
const cargoPath = join4(cwd, "Cargo.toml");
|
|
610
1520
|
if (await fileExists(cargoPath)) {
|
|
611
1521
|
try {
|
|
612
|
-
const content = await
|
|
1522
|
+
const content = await readFile3(cargoPath, "utf-8");
|
|
613
1523
|
detected.stack.push("rust");
|
|
1524
|
+
detected.type = "application";
|
|
614
1525
|
const nameMatch = content.match(/name\s*=\s*"([^"]+)"/);
|
|
615
1526
|
if (nameMatch) detected.name = nameMatch[1];
|
|
1527
|
+
if (content.includes("actix-web")) detected.stack.push("actix");
|
|
1528
|
+
if (content.includes("axum")) detected.stack.push("axum");
|
|
1529
|
+
if (content.includes("tokio")) detected.stack.push("tokio");
|
|
1530
|
+
if (content.includes("serde")) detected.stack.push("serde");
|
|
1531
|
+
if (content.includes("sqlx")) detected.stack.push("sqlx");
|
|
616
1532
|
detected.commands.build = "cargo build";
|
|
617
1533
|
detected.commands.test = "cargo test";
|
|
618
1534
|
detected.commands.lint = "cargo clippy";
|
|
1535
|
+
detected.commands.dev = "cargo run";
|
|
619
1536
|
return detected;
|
|
620
1537
|
} catch {
|
|
621
1538
|
}
|
|
622
1539
|
}
|
|
623
|
-
const goModPath =
|
|
1540
|
+
const goModPath = join4(cwd, "go.mod");
|
|
624
1541
|
if (await fileExists(goModPath)) {
|
|
625
1542
|
try {
|
|
626
|
-
const content = await
|
|
1543
|
+
const content = await readFile3(goModPath, "utf-8");
|
|
627
1544
|
detected.stack.push("go");
|
|
1545
|
+
detected.type = "application";
|
|
628
1546
|
const moduleMatch = content.match(/module\s+(\S+)/);
|
|
629
1547
|
if (moduleMatch) {
|
|
630
1548
|
const parts = moduleMatch[1].split("/");
|
|
631
1549
|
detected.name = parts[parts.length - 1];
|
|
632
1550
|
}
|
|
1551
|
+
if (content.includes("gin-gonic/gin")) detected.stack.push("gin");
|
|
1552
|
+
if (content.includes("gofiber/fiber")) detected.stack.push("fiber");
|
|
1553
|
+
if (content.includes("labstack/echo")) detected.stack.push("echo");
|
|
1554
|
+
if (content.includes("gorm.io/gorm")) detected.stack.push("gorm");
|
|
633
1555
|
detected.commands.build = "go build";
|
|
634
1556
|
detected.commands.test = "go test ./...";
|
|
635
1557
|
detected.commands.lint = "golangci-lint run";
|
|
1558
|
+
detected.commands.dev = "go run .";
|
|
636
1559
|
return detected;
|
|
637
1560
|
} catch {
|
|
638
1561
|
}
|
|
639
1562
|
}
|
|
640
|
-
const makefilePath =
|
|
1563
|
+
const makefilePath = join4(cwd, "Makefile");
|
|
641
1564
|
if (await fileExists(makefilePath)) {
|
|
642
1565
|
try {
|
|
643
|
-
const content = await
|
|
1566
|
+
const content = await readFile3(makefilePath, "utf-8");
|
|
644
1567
|
if (content.includes("build:")) detected.commands.build = "make build";
|
|
645
1568
|
if (content.includes("test:")) detected.commands.test = "make test";
|
|
646
1569
|
if (content.includes("lint:")) detected.commands.lint = "make lint";
|
|
1570
|
+
if (content.includes("dev:")) detected.commands.dev = "make dev";
|
|
1571
|
+
if (content.includes("run:")) detected.commands.dev = detected.commands.dev || "make run";
|
|
647
1572
|
if (Object.keys(detected.commands).length > 0) {
|
|
1573
|
+
detected.type = "application";
|
|
648
1574
|
return detected;
|
|
649
1575
|
}
|
|
650
1576
|
} catch {
|
|
651
1577
|
}
|
|
652
1578
|
}
|
|
1579
|
+
if (await fileExists(join4(cwd, "Dockerfile")) || await fileExists(join4(cwd, "docker-compose.yml"))) {
|
|
1580
|
+
detected.stack.push("docker");
|
|
1581
|
+
detected.type = "application";
|
|
1582
|
+
}
|
|
653
1583
|
return detected.stack.length > 0 || detected.name ? detected : null;
|
|
654
1584
|
}
|
|
655
|
-
async function fileExists(
|
|
1585
|
+
async function fileExists(path2) {
|
|
656
1586
|
try {
|
|
657
|
-
await
|
|
1587
|
+
await access3(path2);
|
|
658
1588
|
return true;
|
|
659
1589
|
} catch {
|
|
660
1590
|
return false;
|
|
661
1591
|
}
|
|
662
1592
|
}
|
|
663
1593
|
|
|
1594
|
+
// src/commands/init.ts
|
|
1595
|
+
var LYNXPROMPT_DIR = ".lynxprompt";
|
|
1596
|
+
var LYNXPROMPT_CONFIG = ".lynxprompt/conf.yml";
|
|
1597
|
+
var LYNXPROMPT_RULES = ".lynxprompt/rules";
|
|
1598
|
+
var AGENT_FILES = [
|
|
1599
|
+
{ name: "AGENTS.md", agent: "Universal (AGENTS.md)" },
|
|
1600
|
+
{ name: "CLAUDE.md", agent: "Claude Code" },
|
|
1601
|
+
{ name: ".windsurfrules", agent: "Windsurf" },
|
|
1602
|
+
{ name: ".clinerules", agent: "Cline" },
|
|
1603
|
+
{ name: ".goosehints", agent: "Goose" },
|
|
1604
|
+
{ name: "AIDER.md", agent: "Aider" },
|
|
1605
|
+
{ name: ".github/copilot-instructions.md", agent: "GitHub Copilot" },
|
|
1606
|
+
{ name: ".zed/instructions.md", agent: "Zed" }
|
|
1607
|
+
];
|
|
1608
|
+
var AGENT_DIRS = [
|
|
1609
|
+
{ path: ".cursor/rules", agent: "Cursor" },
|
|
1610
|
+
{ path: ".amazonq/rules", agent: "Amazon Q" },
|
|
1611
|
+
{ path: ".augment/rules", agent: "Augment Code" }
|
|
1612
|
+
];
|
|
1613
|
+
async function scanForExistingFiles(cwd) {
|
|
1614
|
+
const detected = [];
|
|
1615
|
+
for (const file of AGENT_FILES) {
|
|
1616
|
+
const filePath = join5(cwd, file.name);
|
|
1617
|
+
if (existsSync3(filePath)) {
|
|
1618
|
+
try {
|
|
1619
|
+
const content = await readFile4(filePath, "utf-8");
|
|
1620
|
+
detected.push({ path: file.name, agent: file.agent, content });
|
|
1621
|
+
} catch {
|
|
1622
|
+
detected.push({ path: file.name, agent: file.agent });
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
for (const dir of AGENT_DIRS) {
|
|
1627
|
+
const dirPath = join5(cwd, dir.path);
|
|
1628
|
+
if (existsSync3(dirPath)) {
|
|
1629
|
+
detected.push({ path: dir.path, agent: dir.agent });
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
return detected;
|
|
1633
|
+
}
|
|
1634
|
+
function createStarterAgentsMd(projectName) {
|
|
1635
|
+
return `# ${projectName} - AI Agent Instructions
|
|
1636
|
+
|
|
1637
|
+
> Edit this file to customize how AI agents work with your codebase.
|
|
1638
|
+
> This is the source of truth for all AI assistants in this project.
|
|
1639
|
+
|
|
1640
|
+
## Project Overview
|
|
1641
|
+
|
|
1642
|
+
Describe your project here. What does it do? What are its main features?
|
|
1643
|
+
|
|
1644
|
+
## Tech Stack
|
|
1645
|
+
|
|
1646
|
+
List the technologies used in this project:
|
|
1647
|
+
|
|
1648
|
+
- Language: (e.g., TypeScript, Python, Go)
|
|
1649
|
+
- Framework: (e.g., Next.js, FastAPI, Rails)
|
|
1650
|
+
- Database: (e.g., PostgreSQL, MongoDB)
|
|
1651
|
+
- Other tools: (e.g., Docker, Kubernetes)
|
|
1652
|
+
|
|
1653
|
+
## Code Style
|
|
1654
|
+
|
|
1655
|
+
Follow these conventions:
|
|
1656
|
+
|
|
1657
|
+
- Write clean, readable code
|
|
1658
|
+
- Use descriptive variable and function names
|
|
1659
|
+
- Keep functions focused and testable
|
|
1660
|
+
- Add comments for complex logic only
|
|
1661
|
+
|
|
1662
|
+
## Commands
|
|
1663
|
+
|
|
1664
|
+
\`\`\`bash
|
|
1665
|
+
# Build
|
|
1666
|
+
npm run build
|
|
1667
|
+
|
|
1668
|
+
# Test
|
|
1669
|
+
npm test
|
|
1670
|
+
|
|
1671
|
+
# Lint
|
|
1672
|
+
npm run lint
|
|
1673
|
+
|
|
1674
|
+
# Dev server
|
|
1675
|
+
npm run dev
|
|
1676
|
+
\`\`\`
|
|
1677
|
+
|
|
1678
|
+
## Boundaries
|
|
1679
|
+
|
|
1680
|
+
### \u2705 Always (do without asking)
|
|
1681
|
+
|
|
1682
|
+
- Read any file in the project
|
|
1683
|
+
- Modify files in src/ or lib/
|
|
1684
|
+
- Run build, test, and lint commands
|
|
1685
|
+
- Create test files
|
|
1686
|
+
|
|
1687
|
+
### \u26A0\uFE0F Ask First
|
|
1688
|
+
|
|
1689
|
+
- Add new dependencies
|
|
1690
|
+
- Modify configuration files
|
|
1691
|
+
- Create new modules or directories
|
|
1692
|
+
|
|
1693
|
+
### \u{1F6AB} Never
|
|
1694
|
+
|
|
1695
|
+
- Modify .env files or secrets
|
|
1696
|
+
- Delete critical files without backup
|
|
1697
|
+
- Force push to git
|
|
1698
|
+
- Expose sensitive information
|
|
1699
|
+
|
|
1700
|
+
---
|
|
1701
|
+
|
|
1702
|
+
*Managed by [LynxPrompt](https://lynxprompt.com)*
|
|
1703
|
+
`;
|
|
1704
|
+
}
|
|
1705
|
+
function createDefaultConfig(exporters = ["agents"]) {
|
|
1706
|
+
const config2 = {
|
|
1707
|
+
version: "1",
|
|
1708
|
+
exporters,
|
|
1709
|
+
sources: [
|
|
1710
|
+
{
|
|
1711
|
+
type: "local",
|
|
1712
|
+
path: ".lynxprompt/rules"
|
|
1713
|
+
}
|
|
1714
|
+
]
|
|
1715
|
+
};
|
|
1716
|
+
return yaml2.stringify(config2);
|
|
1717
|
+
}
|
|
1718
|
+
function createLynxpromptReadme() {
|
|
1719
|
+
return `# .lynxprompt
|
|
1720
|
+
|
|
1721
|
+
This directory contains your LynxPrompt configuration and rules.
|
|
1722
|
+
|
|
1723
|
+
> **Note**: This is an advanced setup for managing rules across multiple AI editors.
|
|
1724
|
+
> Most users should use \`lynxp wizard\` instead for simple, direct file generation.
|
|
1725
|
+
|
|
1726
|
+
## Directory structure
|
|
1727
|
+
|
|
1728
|
+
- **\`rules/\`** - Your AI rules. Edit files here, then sync to agents.
|
|
1729
|
+
- **\`conf.yml\`** - Configuration file (exporters, sources, options)
|
|
1730
|
+
|
|
1731
|
+
## Editing rules
|
|
1732
|
+
|
|
1733
|
+
Add markdown files to \`rules/\`:
|
|
1734
|
+
|
|
1735
|
+
\`\`\`markdown
|
|
1736
|
+
# My Rule
|
|
1737
|
+
|
|
1738
|
+
Description of what this rule does...
|
|
1739
|
+
\`\`\`
|
|
1740
|
+
|
|
1741
|
+
## Syncing
|
|
1742
|
+
|
|
1743
|
+
After editing rules, run:
|
|
1744
|
+
|
|
1745
|
+
\`\`\`bash
|
|
1746
|
+
lynxp sync
|
|
1747
|
+
\`\`\`
|
|
1748
|
+
|
|
1749
|
+
This exports your rules to the configured agent formats (AGENTS.md, .cursor/rules/, etc.)
|
|
1750
|
+
|
|
1751
|
+
## More information
|
|
1752
|
+
|
|
1753
|
+
- Docs: https://lynxprompt.com/docs/cli
|
|
1754
|
+
- Support: https://lynxprompt.com/support
|
|
1755
|
+
`;
|
|
1756
|
+
}
|
|
1757
|
+
async function initCommand(options) {
|
|
1758
|
+
console.log();
|
|
1759
|
+
console.log(chalk7.cyan("\u{1F431} LynxPrompt Init"));
|
|
1760
|
+
console.log(chalk7.gray("Advanced mode: Multi-editor rule management"));
|
|
1761
|
+
console.log();
|
|
1762
|
+
if (!options.yes && !options.force) {
|
|
1763
|
+
console.log(chalk7.yellow("\u{1F4A1} Tip: Most users should use 'lynxp wizard' instead."));
|
|
1764
|
+
console.log(chalk7.gray(" The wizard generates files directly without the .lynxprompt/ folder."));
|
|
1765
|
+
console.log();
|
|
1766
|
+
const { proceed } = await prompts3({
|
|
1767
|
+
type: "confirm",
|
|
1768
|
+
name: "proceed",
|
|
1769
|
+
message: "Continue with advanced setup?",
|
|
1770
|
+
initial: false
|
|
1771
|
+
});
|
|
1772
|
+
if (!proceed) {
|
|
1773
|
+
console.log();
|
|
1774
|
+
console.log(chalk7.gray("Run 'lynxp wizard' for simple file generation."));
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
console.log();
|
|
1778
|
+
}
|
|
1779
|
+
const cwd = process.cwd();
|
|
1780
|
+
const projectName = basename(cwd);
|
|
1781
|
+
const lynxpromptDir = join5(cwd, LYNXPROMPT_DIR);
|
|
1782
|
+
const configPath = join5(cwd, LYNXPROMPT_CONFIG);
|
|
1783
|
+
const rulesDir = join5(cwd, LYNXPROMPT_RULES);
|
|
1784
|
+
if (existsSync3(configPath) && !options.force) {
|
|
1785
|
+
console.log(chalk7.yellow("LynxPrompt is already initialized in this project."));
|
|
1786
|
+
console.log(chalk7.gray(`Config: ${LYNXPROMPT_CONFIG}`));
|
|
1787
|
+
console.log(chalk7.gray(`Rules: ${LYNXPROMPT_RULES}/`));
|
|
1788
|
+
console.log();
|
|
1789
|
+
console.log(chalk7.gray("Run 'lynxp sync' to export rules to your agents."));
|
|
1790
|
+
console.log(chalk7.gray("Run 'lynxp wizard' to generate new configurations."));
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
const spinner = ora6("Scanning project...").start();
|
|
1794
|
+
const [projectInfo, agentDetection] = await Promise.all([
|
|
1795
|
+
detectProject(cwd),
|
|
1796
|
+
Promise.resolve(detectAgents(cwd))
|
|
1797
|
+
]);
|
|
1798
|
+
const existingFiles = await scanForExistingFiles(cwd);
|
|
1799
|
+
spinner.stop();
|
|
1800
|
+
if (projectInfo) {
|
|
1801
|
+
console.log(chalk7.green("\u2713 Detected project:"));
|
|
1802
|
+
if (projectInfo.name) console.log(chalk7.gray(` Name: ${projectInfo.name}`));
|
|
1803
|
+
if (projectInfo.stack.length > 0) console.log(chalk7.gray(` Stack: ${projectInfo.stack.join(", ")}`));
|
|
1804
|
+
if (projectInfo.packageManager) console.log(chalk7.gray(` Package manager: ${projectInfo.packageManager}`));
|
|
1805
|
+
console.log();
|
|
1806
|
+
}
|
|
1807
|
+
if (agentDetection.detected.length > 0) {
|
|
1808
|
+
console.log(chalk7.green(`\u2713 Detected ${agentDetection.detected.length} AI agent${agentDetection.detected.length === 1 ? "" : "s"}:`));
|
|
1809
|
+
for (const detected of agentDetection.detected) {
|
|
1810
|
+
const rules = detected.ruleCount > 0 ? chalk7.gray(` (${detected.ruleCount} sections)`) : "";
|
|
1811
|
+
console.log(` ${chalk7.cyan("\u2022")} ${detected.agent.name}${rules}`);
|
|
1812
|
+
}
|
|
1813
|
+
console.log();
|
|
1814
|
+
}
|
|
1815
|
+
if (existingFiles.length > 0) {
|
|
1816
|
+
console.log(chalk7.green("\u2713 Found existing AI configuration files:"));
|
|
1817
|
+
for (const file of existingFiles) {
|
|
1818
|
+
console.log(` ${chalk7.cyan(file.path)} ${chalk7.gray(`(${file.agent})`)}`);
|
|
1819
|
+
}
|
|
1820
|
+
console.log();
|
|
1821
|
+
if (!options.yes) {
|
|
1822
|
+
const { action } = await prompts3({
|
|
1823
|
+
type: "select",
|
|
1824
|
+
name: "action",
|
|
1825
|
+
message: "What would you like to do?",
|
|
1826
|
+
choices: [
|
|
1827
|
+
{ title: "Import existing files to .lynxprompt/rules/", value: "import" },
|
|
1828
|
+
{ title: "Start fresh (keep existing files, create new rules)", value: "fresh" },
|
|
1829
|
+
{ title: "Cancel", value: "cancel" }
|
|
1830
|
+
]
|
|
1831
|
+
});
|
|
1832
|
+
if (action === "cancel" || !action) {
|
|
1833
|
+
console.log(chalk7.gray("Cancelled."));
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
if (action === "import") {
|
|
1837
|
+
await mkdir3(rulesDir, { recursive: true });
|
|
1838
|
+
let importedCount = 0;
|
|
1839
|
+
for (const file of existingFiles) {
|
|
1840
|
+
if (file.content) {
|
|
1841
|
+
const ruleName = file.path.replace(/^\./, "").replace(/\//g, "-").replace(/\.md$/, "") + ".md";
|
|
1842
|
+
const rulePath = join5(rulesDir, ruleName);
|
|
1843
|
+
await writeFile3(rulePath, file.content, "utf-8");
|
|
1844
|
+
console.log(chalk7.gray(` Imported: ${file.path} \u2192 .lynxprompt/rules/${ruleName}`));
|
|
1845
|
+
importedCount++;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
if (importedCount === 0) {
|
|
1849
|
+
const starterPath = join5(rulesDir, "agents.md");
|
|
1850
|
+
await writeFile3(starterPath, createStarterAgentsMd(projectName), "utf-8");
|
|
1851
|
+
console.log(chalk7.gray(" Created starter: .lynxprompt/rules/agents.md"));
|
|
1852
|
+
}
|
|
1853
|
+
} else {
|
|
1854
|
+
await mkdir3(rulesDir, { recursive: true });
|
|
1855
|
+
const starterPath = join5(rulesDir, "agents.md");
|
|
1856
|
+
await writeFile3(starterPath, createStarterAgentsMd(projectName), "utf-8");
|
|
1857
|
+
console.log(chalk7.gray("Created starter: .lynxprompt/rules/agents.md"));
|
|
1858
|
+
}
|
|
1859
|
+
} else {
|
|
1860
|
+
await mkdir3(rulesDir, { recursive: true });
|
|
1861
|
+
for (const file of existingFiles) {
|
|
1862
|
+
if (file.content) {
|
|
1863
|
+
const ruleName = file.path.replace(/^\./, "").replace(/\//g, "-").replace(/\.md$/, "") + ".md";
|
|
1864
|
+
const rulePath = join5(rulesDir, ruleName);
|
|
1865
|
+
await writeFile3(rulePath, file.content, "utf-8");
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
} else {
|
|
1870
|
+
console.log(chalk7.gray("No existing AI configuration files found."));
|
|
1871
|
+
console.log();
|
|
1872
|
+
if (!options.yes) {
|
|
1873
|
+
const { create } = await prompts3({
|
|
1874
|
+
type: "confirm",
|
|
1875
|
+
name: "create",
|
|
1876
|
+
message: "Create a starter template?",
|
|
1877
|
+
initial: true
|
|
1878
|
+
});
|
|
1879
|
+
if (!create) {
|
|
1880
|
+
console.log(chalk7.gray("Cancelled."));
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
await mkdir3(rulesDir, { recursive: true });
|
|
1885
|
+
const starterPath = join5(rulesDir, "agents.md");
|
|
1886
|
+
await writeFile3(starterPath, createStarterAgentsMd(projectName), "utf-8");
|
|
1887
|
+
console.log(chalk7.gray("Created: .lynxprompt/rules/agents.md"));
|
|
1888
|
+
}
|
|
1889
|
+
let exporters = [];
|
|
1890
|
+
if (agentDetection.detected.length > 0) {
|
|
1891
|
+
exporters = agentDetection.detected.map((d) => d.agent.id);
|
|
1892
|
+
if (agentDetection.detected.length > 3 && !options.yes) {
|
|
1893
|
+
const { selected } = await prompts3({
|
|
1894
|
+
type: "multiselect",
|
|
1895
|
+
name: "selected",
|
|
1896
|
+
message: "Select agents to enable:",
|
|
1897
|
+
choices: agentDetection.detected.map((d) => ({
|
|
1898
|
+
title: d.agent.name,
|
|
1899
|
+
value: d.agent.id,
|
|
1900
|
+
selected: true
|
|
1901
|
+
})),
|
|
1902
|
+
hint: "- Space to toggle, Enter to confirm"
|
|
1903
|
+
});
|
|
1904
|
+
if (selected && selected.length > 0) {
|
|
1905
|
+
exporters = selected;
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
} else {
|
|
1909
|
+
exporters = ["agents"];
|
|
1910
|
+
if (!options.yes) {
|
|
1911
|
+
const popular = getPopularAgents();
|
|
1912
|
+
const { selected } = await prompts3({
|
|
1913
|
+
type: "multiselect",
|
|
1914
|
+
name: "selected",
|
|
1915
|
+
message: "Select AI agents to sync to:",
|
|
1916
|
+
choices: popular.map((a) => ({
|
|
1917
|
+
title: `${a.name} - ${a.description}`,
|
|
1918
|
+
value: a.id,
|
|
1919
|
+
selected: a.id === "agents"
|
|
1920
|
+
// Default select AGENTS.md
|
|
1921
|
+
})),
|
|
1922
|
+
hint: "- Space to toggle, Enter to confirm"
|
|
1923
|
+
});
|
|
1924
|
+
if (selected && selected.length > 0) {
|
|
1925
|
+
exporters = selected;
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
console.log(chalk7.gray(`Enabling ${exporters.length} exporter${exporters.length === 1 ? "" : "s"}: ${exporters.join(", ")}`));
|
|
1930
|
+
await mkdir3(dirname3(configPath), { recursive: true });
|
|
1931
|
+
await writeFile3(configPath, createDefaultConfig(exporters), "utf-8");
|
|
1932
|
+
const readmePath = join5(lynxpromptDir, "README.md");
|
|
1933
|
+
await writeFile3(readmePath, createLynxpromptReadme(), "utf-8");
|
|
1934
|
+
const gitignorePath = join5(lynxpromptDir, ".gitignore");
|
|
1935
|
+
const gitignoreContent = `# Local state files
|
|
1936
|
+
.cache/
|
|
1937
|
+
.backups/
|
|
1938
|
+
`;
|
|
1939
|
+
await writeFile3(gitignorePath, gitignoreContent, "utf-8");
|
|
1940
|
+
console.log();
|
|
1941
|
+
console.log(chalk7.green("\u2705 LynxPrompt initialized!"));
|
|
1942
|
+
console.log();
|
|
1943
|
+
console.log(chalk7.gray("Created:"));
|
|
1944
|
+
console.log(chalk7.gray(` ${LYNXPROMPT_CONFIG} - Configuration`));
|
|
1945
|
+
console.log(chalk7.gray(` ${LYNXPROMPT_RULES}/ - Your rules (edit here)`));
|
|
1946
|
+
console.log();
|
|
1947
|
+
console.log(chalk7.cyan("Next steps:"));
|
|
1948
|
+
console.log(chalk7.gray(" 1. Edit your rules in .lynxprompt/rules/"));
|
|
1949
|
+
console.log(chalk7.gray(" 2. Run 'lynxp sync' to export to your AI agents"));
|
|
1950
|
+
console.log(chalk7.gray(" 3. Or run 'lynxp agents' to manage which agents to sync to"));
|
|
1951
|
+
console.log();
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
// src/commands/wizard.ts
|
|
1955
|
+
import chalk8 from "chalk";
|
|
1956
|
+
import prompts4 from "prompts";
|
|
1957
|
+
import ora7 from "ora";
|
|
1958
|
+
import { writeFile as writeFile4, mkdir as mkdir4, access as access5 } from "fs/promises";
|
|
1959
|
+
import { join as join6, dirname as dirname4 } from "path";
|
|
1960
|
+
|
|
664
1961
|
// src/utils/generator.ts
|
|
665
1962
|
var PLATFORM_FILES = {
|
|
666
|
-
|
|
667
|
-
|
|
1963
|
+
agents: "AGENTS.md",
|
|
1964
|
+
cursor: ".cursor/rules/project.mdc",
|
|
1965
|
+
claude: "CLAUDE.md",
|
|
668
1966
|
copilot: ".github/copilot-instructions.md",
|
|
669
1967
|
windsurf: ".windsurfrules",
|
|
670
1968
|
zed: ".zed/instructions.md"
|
|
@@ -774,13 +2072,25 @@ function generateConfig(options) {
|
|
|
774
2072
|
}
|
|
775
2073
|
function generateFileContent(options, platform) {
|
|
776
2074
|
const sections = [];
|
|
777
|
-
const
|
|
2075
|
+
const isMdc = platform === "cursor";
|
|
2076
|
+
const isPlainText = platform === "windsurf";
|
|
2077
|
+
const isMarkdown = !isMdc && !isPlainText;
|
|
2078
|
+
if (isMdc) {
|
|
2079
|
+
sections.push("---");
|
|
2080
|
+
sections.push(`description: "${options.name} - AI coding rules"`);
|
|
2081
|
+
sections.push('globs: ["**/*"]');
|
|
2082
|
+
sections.push("alwaysApply: true");
|
|
2083
|
+
sections.push("---");
|
|
2084
|
+
sections.push("");
|
|
2085
|
+
sections.push(`# ${options.name} - AI Assistant Configuration`);
|
|
2086
|
+
sections.push("");
|
|
2087
|
+
}
|
|
778
2088
|
if (isMarkdown) {
|
|
779
2089
|
sections.push(`# ${options.name} - AI Assistant Configuration`);
|
|
780
2090
|
sections.push("");
|
|
781
2091
|
}
|
|
782
2092
|
const personaDesc = PERSONA_DESCRIPTIONS[options.persona] || options.persona;
|
|
783
|
-
if (isMarkdown) {
|
|
2093
|
+
if (isMarkdown || isMdc) {
|
|
784
2094
|
sections.push("## Persona");
|
|
785
2095
|
sections.push("");
|
|
786
2096
|
sections.push(`You are ${personaDesc}. You assist developers working on ${options.name}.`);
|
|
@@ -793,14 +2103,14 @@ function generateFileContent(options, platform) {
|
|
|
793
2103
|
}
|
|
794
2104
|
sections.push("");
|
|
795
2105
|
if (options.stack.length > 0) {
|
|
796
|
-
if (isMarkdown) {
|
|
2106
|
+
if (isMarkdown || isMdc) {
|
|
797
2107
|
sections.push("## Tech Stack");
|
|
798
2108
|
sections.push("");
|
|
799
2109
|
} else {
|
|
800
2110
|
sections.push("Tech Stack:");
|
|
801
2111
|
}
|
|
802
2112
|
const stackList = options.stack.map((s) => STACK_NAMES[s] || s);
|
|
803
|
-
if (isMarkdown) {
|
|
2113
|
+
if (isMarkdown || isMdc) {
|
|
804
2114
|
for (const tech of stackList) {
|
|
805
2115
|
sections.push(`- ${tech}`);
|
|
806
2116
|
}
|
|
@@ -811,7 +2121,7 @@ function generateFileContent(options, platform) {
|
|
|
811
2121
|
}
|
|
812
2122
|
const hasCommands = Object.values(options.commands).some(Boolean);
|
|
813
2123
|
if (hasCommands) {
|
|
814
|
-
if (isMarkdown) {
|
|
2124
|
+
if (isMarkdown || isMdc) {
|
|
815
2125
|
sections.push("## Commands");
|
|
816
2126
|
sections.push("");
|
|
817
2127
|
sections.push("Use these commands for common tasks:");
|
|
@@ -821,25 +2131,25 @@ function generateFileContent(options, platform) {
|
|
|
821
2131
|
sections.push("Commands:");
|
|
822
2132
|
}
|
|
823
2133
|
if (options.commands.build) {
|
|
824
|
-
sections.push(isMarkdown ? `# Build: ${options.commands.build}` : `- Build: ${options.commands.build}`);
|
|
2134
|
+
sections.push(isMarkdown || isMdc ? `# Build: ${options.commands.build}` : `- Build: ${options.commands.build}`);
|
|
825
2135
|
}
|
|
826
2136
|
if (options.commands.test) {
|
|
827
|
-
sections.push(isMarkdown ? `# Test: ${options.commands.test}` : `- Test: ${options.commands.test}`);
|
|
2137
|
+
sections.push(isMarkdown || isMdc ? `# Test: ${options.commands.test}` : `- Test: ${options.commands.test}`);
|
|
828
2138
|
}
|
|
829
2139
|
if (options.commands.lint) {
|
|
830
|
-
sections.push(isMarkdown ? `# Lint: ${options.commands.lint}` : `- Lint: ${options.commands.lint}`);
|
|
2140
|
+
sections.push(isMarkdown || isMdc ? `# Lint: ${options.commands.lint}` : `- Lint: ${options.commands.lint}`);
|
|
831
2141
|
}
|
|
832
2142
|
if (options.commands.dev) {
|
|
833
|
-
sections.push(isMarkdown ? `# Dev: ${options.commands.dev}` : `- Dev: ${options.commands.dev}`);
|
|
2143
|
+
sections.push(isMarkdown || isMdc ? `# Dev: ${options.commands.dev}` : `- Dev: ${options.commands.dev}`);
|
|
834
2144
|
}
|
|
835
|
-
if (isMarkdown) {
|
|
2145
|
+
if (isMarkdown || isMdc) {
|
|
836
2146
|
sections.push("```");
|
|
837
2147
|
}
|
|
838
2148
|
sections.push("");
|
|
839
2149
|
}
|
|
840
2150
|
const boundaries = BOUNDARIES[options.boundaries];
|
|
841
2151
|
if (boundaries) {
|
|
842
|
-
if (isMarkdown) {
|
|
2152
|
+
if (isMarkdown || isMdc) {
|
|
843
2153
|
sections.push("## Boundaries");
|
|
844
2154
|
sections.push("");
|
|
845
2155
|
sections.push("### \u2705 Always (do without asking)");
|
|
@@ -879,7 +2189,7 @@ function generateFileContent(options, platform) {
|
|
|
879
2189
|
}
|
|
880
2190
|
sections.push("");
|
|
881
2191
|
}
|
|
882
|
-
if (isMarkdown) {
|
|
2192
|
+
if (isMarkdown || isMdc) {
|
|
883
2193
|
sections.push("## Code Style");
|
|
884
2194
|
sections.push("");
|
|
885
2195
|
sections.push("Follow these conventions:");
|
|
@@ -915,7 +2225,7 @@ function generateFileContent(options, platform) {
|
|
|
915
2225
|
sections.push("- Keep functions focused and testable");
|
|
916
2226
|
sections.push("");
|
|
917
2227
|
}
|
|
918
|
-
if (isMarkdown) {
|
|
2228
|
+
if (isMarkdown || isMdc) {
|
|
919
2229
|
sections.push("---");
|
|
920
2230
|
sections.push("");
|
|
921
2231
|
sections.push(`*Generated by [LynxPrompt](https://lynxprompt.com) CLI*`);
|
|
@@ -923,7 +2233,25 @@ function generateFileContent(options, platform) {
|
|
|
923
2233
|
return sections.join("\n");
|
|
924
2234
|
}
|
|
925
2235
|
|
|
926
|
-
// src/commands/
|
|
2236
|
+
// src/commands/wizard.ts
|
|
2237
|
+
var OUTPUT_FORMATS = [
|
|
2238
|
+
{
|
|
2239
|
+
title: "AGENTS.md (Universal)",
|
|
2240
|
+
value: "agents",
|
|
2241
|
+
description: "Works with Claude Code, GitHub Copilot, Aider, and most AI editors",
|
|
2242
|
+
recommended: true
|
|
2243
|
+
},
|
|
2244
|
+
{
|
|
2245
|
+
title: "Cursor (.cursor/rules/)",
|
|
2246
|
+
value: "cursor",
|
|
2247
|
+
description: "Cursor IDE with MDC format"
|
|
2248
|
+
},
|
|
2249
|
+
{
|
|
2250
|
+
title: "Multiple formats",
|
|
2251
|
+
value: "multiple",
|
|
2252
|
+
description: "Select multiple AI editors to generate for"
|
|
2253
|
+
}
|
|
2254
|
+
];
|
|
927
2255
|
var TECH_STACKS = [
|
|
928
2256
|
{ title: "TypeScript", value: "typescript" },
|
|
929
2257
|
{ title: "JavaScript", value: "javascript" },
|
|
@@ -951,22 +2279,30 @@ var FRAMEWORKS = [
|
|
|
951
2279
|
{ title: "Laravel", value: "laravel" }
|
|
952
2280
|
];
|
|
953
2281
|
var PLATFORMS = [
|
|
954
|
-
{ title: "
|
|
955
|
-
{ title: "
|
|
2282
|
+
{ title: "AGENTS.md (Universal)", value: "agents", filename: "AGENTS.md" },
|
|
2283
|
+
{ title: "Cursor (.cursor/rules/)", value: "cursor", filename: ".cursor/rules/project.mdc" },
|
|
2284
|
+
{ title: "Claude Code (CLAUDE.md)", value: "claude", filename: "CLAUDE.md" },
|
|
956
2285
|
{ title: "GitHub Copilot", value: "copilot", filename: ".github/copilot-instructions.md" },
|
|
957
2286
|
{ title: "Windsurf (.windsurfrules)", value: "windsurf", filename: ".windsurfrules" },
|
|
958
2287
|
{ title: "Zed", value: "zed", filename: ".zed/instructions.md" }
|
|
959
2288
|
];
|
|
960
2289
|
var PERSONAS = [
|
|
2290
|
+
{ title: "Full-Stack Developer - Complete application setups", value: "fullstack" },
|
|
961
2291
|
{ title: "Backend Developer - APIs, databases, microservices", value: "backend" },
|
|
962
2292
|
{ title: "Frontend Developer - UI, components, styling", value: "frontend" },
|
|
963
|
-
{ title: "Full-Stack Developer - Complete application setups", value: "fullstack" },
|
|
964
2293
|
{ title: "DevOps Engineer - Infrastructure, CI/CD, containers", value: "devops" },
|
|
965
2294
|
{ title: "Data Engineer - Pipelines, ETL, databases", value: "data" },
|
|
966
2295
|
{ title: "Security Engineer - Secure code, vulnerabilities", value: "security" },
|
|
967
2296
|
{ title: "Custom...", value: "custom" }
|
|
968
2297
|
];
|
|
969
2298
|
var BOUNDARY_PRESETS = [
|
|
2299
|
+
{
|
|
2300
|
+
title: "Standard - Balance of freedom and safety (recommended)",
|
|
2301
|
+
value: "standard",
|
|
2302
|
+
always: ["Read any file", "Modify files in src/", "Run build/test/lint", "Create test files"],
|
|
2303
|
+
askFirst: ["Add new dependencies", "Modify config files", "Create new modules"],
|
|
2304
|
+
never: ["Delete production data", "Modify .env secrets", "Force push"]
|
|
2305
|
+
},
|
|
970
2306
|
{
|
|
971
2307
|
title: "Conservative - Ask before most changes",
|
|
972
2308
|
value: "conservative",
|
|
@@ -974,13 +2310,6 @@ var BOUNDARY_PRESETS = [
|
|
|
974
2310
|
askFirst: ["Modify any file", "Add dependencies", "Create files", "Run tests"],
|
|
975
2311
|
never: ["Delete files", "Modify .env", "Push to git"]
|
|
976
2312
|
},
|
|
977
|
-
{
|
|
978
|
-
title: "Standard - Balance of freedom and safety",
|
|
979
|
-
value: "standard",
|
|
980
|
-
always: ["Read any file", "Modify files in src/", "Run build/test/lint", "Create test files"],
|
|
981
|
-
askFirst: ["Add new dependencies", "Modify config files", "Create new modules"],
|
|
982
|
-
never: ["Delete production data", "Modify .env secrets", "Force push"]
|
|
983
|
-
},
|
|
984
2313
|
{
|
|
985
2314
|
title: "Permissive - AI can modify freely within src/",
|
|
986
2315
|
value: "permissive",
|
|
@@ -989,26 +2318,36 @@ var BOUNDARY_PRESETS = [
|
|
|
989
2318
|
never: ["Modify .env", "Access external APIs without confirmation"]
|
|
990
2319
|
}
|
|
991
2320
|
];
|
|
992
|
-
async function
|
|
2321
|
+
async function wizardCommand(options) {
|
|
993
2322
|
console.log();
|
|
994
|
-
console.log(
|
|
2323
|
+
console.log(chalk8.cyan("\u{1F431} LynxPrompt Wizard"));
|
|
2324
|
+
console.log(chalk8.gray("Generate AI IDE configuration in seconds"));
|
|
995
2325
|
console.log();
|
|
996
2326
|
const detected = await detectProject(process.cwd());
|
|
997
2327
|
if (detected) {
|
|
998
|
-
console.log(
|
|
999
|
-
if (detected.name) console.log(
|
|
1000
|
-
if (detected.stack.length > 0) console.log(
|
|
1001
|
-
if (detected.
|
|
1002
|
-
if (detected.commands.
|
|
2328
|
+
console.log(chalk8.green("\u2713 Detected project:"));
|
|
2329
|
+
if (detected.name) console.log(chalk8.gray(` Name: ${detected.name}`));
|
|
2330
|
+
if (detected.stack.length > 0) console.log(chalk8.gray(` Stack: ${detected.stack.join(", ")}`));
|
|
2331
|
+
if (detected.packageManager) console.log(chalk8.gray(` Package manager: ${detected.packageManager}`));
|
|
2332
|
+
if (detected.commands.build) console.log(chalk8.gray(` Build: ${detected.commands.build}`));
|
|
2333
|
+
if (detected.commands.test) console.log(chalk8.gray(` Test: ${detected.commands.test}`));
|
|
1003
2334
|
console.log();
|
|
1004
2335
|
}
|
|
1005
2336
|
let config2;
|
|
1006
2337
|
if (options.yes) {
|
|
2338
|
+
let platforms;
|
|
2339
|
+
if (options.format) {
|
|
2340
|
+
platforms = options.format.split(",").map((f) => f.trim());
|
|
2341
|
+
} else if (options.platforms) {
|
|
2342
|
+
platforms = options.platforms.split(",").map((p) => p.trim());
|
|
2343
|
+
} else {
|
|
2344
|
+
platforms = ["agents"];
|
|
2345
|
+
}
|
|
1007
2346
|
config2 = {
|
|
1008
2347
|
name: options.name || detected?.name || "my-project",
|
|
1009
2348
|
description: options.description || "",
|
|
1010
2349
|
stack: options.stack?.split(",").map((s) => s.trim()) || detected?.stack || [],
|
|
1011
|
-
platforms
|
|
2350
|
+
platforms,
|
|
1012
2351
|
persona: options.persona || "fullstack",
|
|
1013
2352
|
boundaries: options.boundaries || "standard",
|
|
1014
2353
|
commands: detected?.commands || {}
|
|
@@ -1016,123 +2355,164 @@ async function initCommand(options) {
|
|
|
1016
2355
|
} else {
|
|
1017
2356
|
config2 = await runInteractiveWizard(options, detected);
|
|
1018
2357
|
}
|
|
1019
|
-
const spinner =
|
|
2358
|
+
const spinner = ora7("Generating configuration...").start();
|
|
1020
2359
|
try {
|
|
1021
2360
|
const files = generateConfig(config2);
|
|
1022
2361
|
spinner.stop();
|
|
1023
2362
|
console.log();
|
|
1024
|
-
console.log(
|
|
2363
|
+
console.log(chalk8.green("\u2705 Generated:"));
|
|
1025
2364
|
for (const [filename, content] of Object.entries(files)) {
|
|
1026
|
-
const outputPath =
|
|
2365
|
+
const outputPath = join6(process.cwd(), filename);
|
|
1027
2366
|
let exists = false;
|
|
1028
2367
|
try {
|
|
1029
|
-
await
|
|
2368
|
+
await access5(outputPath);
|
|
1030
2369
|
exists = true;
|
|
1031
2370
|
} catch {
|
|
1032
2371
|
}
|
|
1033
2372
|
if (exists && !options.yes) {
|
|
1034
|
-
const response = await
|
|
2373
|
+
const response = await prompts4({
|
|
1035
2374
|
type: "confirm",
|
|
1036
2375
|
name: "overwrite",
|
|
1037
2376
|
message: `${filename} already exists. Overwrite?`,
|
|
1038
2377
|
initial: false
|
|
1039
2378
|
});
|
|
1040
2379
|
if (!response.overwrite) {
|
|
1041
|
-
console.log(
|
|
2380
|
+
console.log(chalk8.yellow(` Skipped: ${filename}`));
|
|
1042
2381
|
continue;
|
|
1043
2382
|
}
|
|
1044
2383
|
}
|
|
1045
|
-
const dir =
|
|
2384
|
+
const dir = dirname4(outputPath);
|
|
1046
2385
|
if (dir !== ".") {
|
|
1047
|
-
await
|
|
2386
|
+
await mkdir4(dir, { recursive: true });
|
|
1048
2387
|
}
|
|
1049
|
-
await
|
|
1050
|
-
console.log(` ${
|
|
2388
|
+
await writeFile4(outputPath, content, "utf-8");
|
|
2389
|
+
console.log(` ${chalk8.cyan(filename)}`);
|
|
1051
2390
|
}
|
|
1052
2391
|
console.log();
|
|
1053
|
-
console.log(
|
|
1054
|
-
console.log(
|
|
2392
|
+
console.log(chalk8.gray("Your AI assistant will now follow these instructions."));
|
|
2393
|
+
console.log();
|
|
2394
|
+
console.log(chalk8.gray("Tips:"));
|
|
2395
|
+
console.log(chalk8.gray(" \u2022 Edit the generated file anytime to customize rules"));
|
|
2396
|
+
console.log(chalk8.gray(" \u2022 Run 'lynxp wizard' again to regenerate"));
|
|
2397
|
+
console.log(chalk8.gray(" \u2022 Run 'lynxp check' to validate your configuration"));
|
|
1055
2398
|
console.log();
|
|
1056
2399
|
} catch (error) {
|
|
1057
2400
|
spinner.fail("Failed to generate files");
|
|
1058
|
-
console.error(
|
|
2401
|
+
console.error(chalk8.red("\n\u2717 An error occurred while generating configuration files."));
|
|
1059
2402
|
if (error instanceof Error) {
|
|
1060
|
-
console.error(
|
|
2403
|
+
console.error(chalk8.gray(` ${error.message}`));
|
|
1061
2404
|
}
|
|
2405
|
+
console.error(chalk8.gray("\nTry running with --yes flag for default settings."));
|
|
1062
2406
|
process.exit(1);
|
|
1063
2407
|
}
|
|
1064
2408
|
}
|
|
1065
2409
|
async function runInteractiveWizard(options, detected) {
|
|
1066
2410
|
const answers = {};
|
|
1067
|
-
|
|
2411
|
+
let platforms;
|
|
2412
|
+
if (options.format) {
|
|
2413
|
+
platforms = options.format.split(",").map((f) => f.trim());
|
|
2414
|
+
} else {
|
|
2415
|
+
const formatResponse = await prompts4({
|
|
2416
|
+
type: "select",
|
|
2417
|
+
name: "format",
|
|
2418
|
+
message: "Select output format:",
|
|
2419
|
+
choices: OUTPUT_FORMATS.map((f) => ({
|
|
2420
|
+
title: f.recommended ? `${f.title} ${chalk8.green("(recommended)")}` : f.title,
|
|
2421
|
+
value: f.value,
|
|
2422
|
+
description: f.description
|
|
2423
|
+
})),
|
|
2424
|
+
initial: 0
|
|
2425
|
+
// AGENTS.md is default
|
|
2426
|
+
});
|
|
2427
|
+
if (formatResponse.format === "multiple") {
|
|
2428
|
+
const platformResponse = await prompts4({
|
|
2429
|
+
type: "multiselect",
|
|
2430
|
+
name: "platforms",
|
|
2431
|
+
message: "Select AI editors:",
|
|
2432
|
+
choices: PLATFORMS.map((p) => ({ title: p.title, value: p.value })),
|
|
2433
|
+
hint: "- Space to select, Enter to confirm",
|
|
2434
|
+
min: 1
|
|
2435
|
+
});
|
|
2436
|
+
platforms = platformResponse.platforms || ["agents"];
|
|
2437
|
+
} else {
|
|
2438
|
+
platforms = [formatResponse.format || "agents"];
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
answers.platforms = platforms;
|
|
2442
|
+
const nameResponse = await prompts4({
|
|
1068
2443
|
type: "text",
|
|
1069
2444
|
name: "name",
|
|
1070
|
-
message: "
|
|
2445
|
+
message: "Project name:",
|
|
1071
2446
|
initial: options.name || detected?.name || "my-project"
|
|
1072
2447
|
});
|
|
1073
|
-
answers.name = nameResponse.name;
|
|
1074
|
-
const descResponse = await
|
|
2448
|
+
answers.name = nameResponse.name || "my-project";
|
|
2449
|
+
const descResponse = await prompts4({
|
|
1075
2450
|
type: "text",
|
|
1076
2451
|
name: "description",
|
|
1077
|
-
message: "
|
|
2452
|
+
message: "Brief description (optional):",
|
|
1078
2453
|
initial: options.description || ""
|
|
1079
2454
|
});
|
|
1080
|
-
answers.description = descResponse.description;
|
|
2455
|
+
answers.description = descResponse.description || "";
|
|
1081
2456
|
const allStackOptions = [...TECH_STACKS, ...FRAMEWORKS];
|
|
1082
|
-
const
|
|
2457
|
+
const detectedStackSet = new Set(detected?.stack || []);
|
|
2458
|
+
const stackResponse = await prompts4({
|
|
1083
2459
|
type: "multiselect",
|
|
1084
2460
|
name: "stack",
|
|
1085
|
-
message: "
|
|
1086
|
-
choices: allStackOptions
|
|
2461
|
+
message: "Tech stack:",
|
|
2462
|
+
choices: allStackOptions.map((s) => ({
|
|
2463
|
+
title: s.title,
|
|
2464
|
+
value: s.value,
|
|
2465
|
+
selected: detectedStackSet.has(s.value)
|
|
2466
|
+
})),
|
|
1087
2467
|
hint: "- Space to select, Enter to confirm"
|
|
1088
2468
|
});
|
|
1089
2469
|
answers.stack = stackResponse.stack || [];
|
|
1090
|
-
const
|
|
1091
|
-
type: "multiselect",
|
|
1092
|
-
name: "platforms",
|
|
1093
|
-
message: "Which AI IDEs do you use?",
|
|
1094
|
-
choices: PLATFORMS,
|
|
1095
|
-
hint: "- Space to select, Enter to confirm",
|
|
1096
|
-
min: 1
|
|
1097
|
-
});
|
|
1098
|
-
answers.platforms = platformResponse.platforms || ["cursor"];
|
|
1099
|
-
const personaResponse = await prompts2({
|
|
2470
|
+
const personaResponse = await prompts4({
|
|
1100
2471
|
type: "select",
|
|
1101
2472
|
name: "persona",
|
|
1102
|
-
message: "
|
|
2473
|
+
message: "AI persona:",
|
|
1103
2474
|
choices: PERSONAS,
|
|
1104
|
-
initial:
|
|
2475
|
+
initial: 0
|
|
1105
2476
|
// Full-stack by default
|
|
1106
2477
|
});
|
|
1107
2478
|
if (personaResponse.persona === "custom") {
|
|
1108
|
-
const customPersona = await
|
|
2479
|
+
const customPersona = await prompts4({
|
|
1109
2480
|
type: "text",
|
|
1110
2481
|
name: "value",
|
|
1111
2482
|
message: "Describe the custom persona:"
|
|
1112
2483
|
});
|
|
1113
2484
|
answers.persona = customPersona.value || "fullstack";
|
|
1114
2485
|
} else {
|
|
1115
|
-
answers.persona = personaResponse.persona;
|
|
2486
|
+
answers.persona = personaResponse.persona || "fullstack";
|
|
1116
2487
|
}
|
|
2488
|
+
const boundaryResponse = await prompts4({
|
|
2489
|
+
type: "select",
|
|
2490
|
+
name: "boundaries",
|
|
2491
|
+
message: "AI boundaries:",
|
|
2492
|
+
choices: BOUNDARY_PRESETS.map((b) => ({ title: b.title, value: b.value })),
|
|
2493
|
+
initial: 0
|
|
2494
|
+
// Standard by default
|
|
2495
|
+
});
|
|
2496
|
+
answers.boundaries = boundaryResponse.boundaries || "standard";
|
|
1117
2497
|
if (detected?.commands && Object.keys(detected.commands).length > 0) {
|
|
1118
2498
|
console.log();
|
|
1119
|
-
console.log(
|
|
1120
|
-
if (detected.commands.build) console.log(
|
|
1121
|
-
if (detected.commands.test) console.log(
|
|
1122
|
-
if (detected.commands.lint) console.log(
|
|
1123
|
-
if (detected.commands.dev) console.log(
|
|
1124
|
-
const editCommands = await
|
|
2499
|
+
console.log(chalk8.gray("Auto-detected commands:"));
|
|
2500
|
+
if (detected.commands.build) console.log(chalk8.gray(` Build: ${detected.commands.build}`));
|
|
2501
|
+
if (detected.commands.test) console.log(chalk8.gray(` Test: ${detected.commands.test}`));
|
|
2502
|
+
if (detected.commands.lint) console.log(chalk8.gray(` Lint: ${detected.commands.lint}`));
|
|
2503
|
+
if (detected.commands.dev) console.log(chalk8.gray(` Dev: ${detected.commands.dev}`));
|
|
2504
|
+
const editCommands = await prompts4({
|
|
1125
2505
|
type: "confirm",
|
|
1126
2506
|
name: "edit",
|
|
1127
|
-
message: "Edit
|
|
2507
|
+
message: "Edit commands?",
|
|
1128
2508
|
initial: false
|
|
1129
2509
|
});
|
|
1130
2510
|
if (editCommands.edit) {
|
|
1131
|
-
const commandsResponse = await
|
|
1132
|
-
{ type: "text", name: "build", message: "Build
|
|
1133
|
-
{ type: "text", name: "test", message: "Test
|
|
1134
|
-
{ type: "text", name: "lint", message: "Lint
|
|
1135
|
-
{ type: "text", name: "dev", message: "Dev
|
|
2511
|
+
const commandsResponse = await prompts4([
|
|
2512
|
+
{ type: "text", name: "build", message: "Build:", initial: detected.commands.build },
|
|
2513
|
+
{ type: "text", name: "test", message: "Test:", initial: detected.commands.test },
|
|
2514
|
+
{ type: "text", name: "lint", message: "Lint:", initial: detected.commands.lint },
|
|
2515
|
+
{ type: "text", name: "dev", message: "Dev:", initial: detected.commands.dev }
|
|
1136
2516
|
]);
|
|
1137
2517
|
answers.commands = commandsResponse;
|
|
1138
2518
|
} else {
|
|
@@ -1141,15 +2521,6 @@ async function runInteractiveWizard(options, detected) {
|
|
|
1141
2521
|
} else {
|
|
1142
2522
|
answers.commands = {};
|
|
1143
2523
|
}
|
|
1144
|
-
const boundaryResponse = await prompts2({
|
|
1145
|
-
type: "select",
|
|
1146
|
-
name: "boundaries",
|
|
1147
|
-
message: "Select boundary preset:",
|
|
1148
|
-
choices: BOUNDARY_PRESETS.map((b) => ({ title: b.title, value: b.value })),
|
|
1149
|
-
initial: 1
|
|
1150
|
-
// Standard by default
|
|
1151
|
-
});
|
|
1152
|
-
answers.boundaries = boundaryResponse.boundaries || "standard";
|
|
1153
2524
|
return {
|
|
1154
2525
|
name: answers.name,
|
|
1155
2526
|
description: answers.description,
|
|
@@ -1162,52 +2533,52 @@ async function runInteractiveWizard(options, detected) {
|
|
|
1162
2533
|
}
|
|
1163
2534
|
|
|
1164
2535
|
// src/commands/search.ts
|
|
1165
|
-
import
|
|
1166
|
-
import
|
|
2536
|
+
import chalk9 from "chalk";
|
|
2537
|
+
import ora8 from "ora";
|
|
1167
2538
|
async function searchCommand(query, options) {
|
|
1168
|
-
const spinner =
|
|
2539
|
+
const spinner = ora8(`Searching for "${query}"...`).start();
|
|
1169
2540
|
try {
|
|
1170
2541
|
const limit = parseInt(options.limit, 10) || 20;
|
|
1171
2542
|
const { templates, total, hasMore } = await api.searchBlueprints(query, limit);
|
|
1172
2543
|
spinner.stop();
|
|
1173
2544
|
if (templates.length === 0) {
|
|
1174
2545
|
console.log();
|
|
1175
|
-
console.log(
|
|
1176
|
-
console.log(
|
|
2546
|
+
console.log(chalk9.yellow(`No blueprints found for "${query}".`));
|
|
2547
|
+
console.log(chalk9.gray("Try different keywords or browse at https://lynxprompt.com/blueprints"));
|
|
1177
2548
|
return;
|
|
1178
2549
|
}
|
|
1179
2550
|
console.log();
|
|
1180
|
-
console.log(
|
|
2551
|
+
console.log(chalk9.cyan(`\u{1F50D} Search Results for "${query}" (${total} found)`));
|
|
1181
2552
|
console.log();
|
|
1182
2553
|
for (const result of templates) {
|
|
1183
2554
|
printSearchResult(result);
|
|
1184
2555
|
}
|
|
1185
2556
|
if (hasMore) {
|
|
1186
|
-
console.log(
|
|
2557
|
+
console.log(chalk9.gray(`Showing ${templates.length} of ${total}. Use --limit to see more.`));
|
|
1187
2558
|
}
|
|
1188
2559
|
console.log();
|
|
1189
|
-
console.log(
|
|
2560
|
+
console.log(chalk9.gray("Use 'lynxprompt pull <id>' to download a blueprint."));
|
|
1190
2561
|
} catch (error) {
|
|
1191
2562
|
spinner.fail("Search failed");
|
|
1192
2563
|
handleApiError3(error);
|
|
1193
2564
|
}
|
|
1194
2565
|
}
|
|
1195
2566
|
function printSearchResult(result) {
|
|
1196
|
-
const priceInfo = result.price ?
|
|
1197
|
-
const officialBadge = result.isOfficial ?
|
|
1198
|
-
console.log(` ${
|
|
1199
|
-
console.log(` ${
|
|
2567
|
+
const priceInfo = result.price ? chalk9.yellow(`\u20AC${(result.price / 100).toFixed(2)}`) : chalk9.green("Free");
|
|
2568
|
+
const officialBadge = result.isOfficial ? chalk9.magenta(" \u2605 Official") : "";
|
|
2569
|
+
console.log(` ${chalk9.bold(result.name)}${officialBadge}`);
|
|
2570
|
+
console.log(` ${chalk9.cyan(result.id)} \u2022 ${priceInfo}`);
|
|
1200
2571
|
if (result.description) {
|
|
1201
|
-
console.log(` ${
|
|
2572
|
+
console.log(` ${chalk9.gray(truncate2(result.description, 60))}`);
|
|
1202
2573
|
}
|
|
1203
|
-
console.log(` ${
|
|
2574
|
+
console.log(` ${chalk9.gray(`by ${result.author}`)} \u2022 ${chalk9.gray(`\u2193${result.downloads}`)} ${chalk9.gray(`\u2665${result.likes}`)}`);
|
|
1204
2575
|
if (result.tags && result.tags.length > 0) {
|
|
1205
2576
|
console.log(` ${formatTags2(result.tags)}`);
|
|
1206
2577
|
}
|
|
1207
2578
|
console.log();
|
|
1208
2579
|
}
|
|
1209
2580
|
function formatTags2(tags) {
|
|
1210
|
-
return tags.slice(0, 4).map((t) =>
|
|
2581
|
+
return tags.slice(0, 4).map((t) => chalk9.gray(`#${t}`)).join(" ");
|
|
1211
2582
|
}
|
|
1212
2583
|
function truncate2(str, maxLength) {
|
|
1213
2584
|
if (str.length <= maxLength) return str;
|
|
@@ -1215,58 +2586,141 @@ function truncate2(str, maxLength) {
|
|
|
1215
2586
|
}
|
|
1216
2587
|
function handleApiError3(error) {
|
|
1217
2588
|
if (error instanceof ApiRequestError) {
|
|
1218
|
-
console.error(
|
|
2589
|
+
console.error(chalk9.red(`Error: ${error.message}`));
|
|
1219
2590
|
} else {
|
|
1220
|
-
console.error(
|
|
2591
|
+
console.error(chalk9.red("An unexpected error occurred."));
|
|
1221
2592
|
}
|
|
1222
2593
|
process.exit(1);
|
|
1223
2594
|
}
|
|
1224
2595
|
|
|
1225
2596
|
// src/commands/status.ts
|
|
1226
|
-
import
|
|
1227
|
-
import { access as
|
|
1228
|
-
import { join as
|
|
2597
|
+
import chalk10 from "chalk";
|
|
2598
|
+
import { access as access6, readFile as readFile6, readdir as readdir3 } from "fs/promises";
|
|
2599
|
+
import { join as join7 } from "path";
|
|
2600
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1229
2601
|
var CONFIG_FILES = [
|
|
1230
2602
|
{ path: "AGENTS.md", name: "AGENTS.md", platform: "Claude Code, Cursor, AI Agents" },
|
|
1231
2603
|
{ path: "CLAUDE.md", name: "CLAUDE.md", platform: "Claude Code" },
|
|
1232
|
-
{ path: ".cursorrules", name: ".cursorrules", platform: "Cursor" },
|
|
1233
2604
|
{ path: ".github/copilot-instructions.md", name: "Copilot Instructions", platform: "GitHub Copilot" },
|
|
1234
2605
|
{ path: ".windsurfrules", name: ".windsurfrules", platform: "Windsurf" },
|
|
1235
|
-
{ path: ".zed/instructions.md", name: "Zed Instructions", platform: "Zed" }
|
|
2606
|
+
{ path: ".zed/instructions.md", name: "Zed Instructions", platform: "Zed" },
|
|
2607
|
+
{ path: ".clinerules", name: ".clinerules", platform: "Cline" },
|
|
2608
|
+
{ path: ".goosehints", name: ".goosehints", platform: "Goose" },
|
|
2609
|
+
{ path: "AIDER.md", name: "AIDER.md", platform: "Aider" }
|
|
2610
|
+
];
|
|
2611
|
+
var CONFIG_DIRS = [
|
|
2612
|
+
{ path: ".cursor/rules", name: ".cursor/rules/", platform: "Cursor" },
|
|
2613
|
+
{ path: ".amazonq/rules", name: ".amazonq/rules/", platform: "Amazon Q" },
|
|
2614
|
+
{ path: ".augment/rules", name: ".augment/rules/", platform: "Augment Code" }
|
|
1236
2615
|
];
|
|
1237
2616
|
async function statusCommand() {
|
|
1238
2617
|
const cwd = process.cwd();
|
|
1239
2618
|
console.log();
|
|
1240
|
-
console.log(
|
|
1241
|
-
console.log(
|
|
2619
|
+
console.log(chalk10.cyan("\u{1F431} LynxPrompt Status"));
|
|
2620
|
+
console.log(chalk10.gray(` Directory: ${cwd}`));
|
|
2621
|
+
console.log();
|
|
2622
|
+
const lynxpromptExists = existsSync4(join7(cwd, ".lynxprompt"));
|
|
2623
|
+
if (lynxpromptExists) {
|
|
2624
|
+
console.log(chalk10.green("\u2713 LynxPrompt initialized"));
|
|
2625
|
+
const configPath = join7(cwd, ".lynxprompt/conf.yml");
|
|
2626
|
+
if (existsSync4(configPath)) {
|
|
2627
|
+
try {
|
|
2628
|
+
const content = await readFile6(configPath, "utf-8");
|
|
2629
|
+
const { parse: parse5 } = await import("yaml");
|
|
2630
|
+
const config2 = parse5(content);
|
|
2631
|
+
if (config2?.exporters?.length > 0) {
|
|
2632
|
+
console.log(chalk10.gray(` Exporters: ${config2.exporters.join(", ")}`));
|
|
2633
|
+
}
|
|
2634
|
+
} catch {
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
console.log();
|
|
2638
|
+
}
|
|
2639
|
+
const trackedStatus = await checkSyncStatus(cwd);
|
|
2640
|
+
if (trackedStatus.length > 0) {
|
|
2641
|
+
console.log(chalk10.cyan("\u{1F4E6} Tracked Blueprints"));
|
|
2642
|
+
console.log();
|
|
2643
|
+
for (const { blueprint, localModified, fileExists: fileExists2 } of trackedStatus) {
|
|
2644
|
+
const statusIcon = !fileExists2 ? chalk10.red("\u2717") : localModified ? chalk10.yellow("\u25CF") : chalk10.green("\u2713");
|
|
2645
|
+
const sourceLabel = {
|
|
2646
|
+
marketplace: chalk10.gray("[marketplace]"),
|
|
2647
|
+
team: chalk10.blue("[team]"),
|
|
2648
|
+
private: chalk10.green("[private]"),
|
|
2649
|
+
local: chalk10.gray("[local]")
|
|
2650
|
+
}[blueprint.source];
|
|
2651
|
+
console.log(` ${statusIcon} ${chalk10.bold(blueprint.file)} ${sourceLabel}`);
|
|
2652
|
+
console.log(` ${chalk10.gray(`ID: ${blueprint.id} \u2022 ${blueprint.name}`)}`);
|
|
2653
|
+
if (!fileExists2) {
|
|
2654
|
+
console.log(chalk10.red(` \u26A0 File missing - run 'lynxp pull ${blueprint.id}' to restore`));
|
|
2655
|
+
} else if (localModified) {
|
|
2656
|
+
if (blueprint.source === "marketplace") {
|
|
2657
|
+
console.log(chalk10.yellow(` \u26A0 Local changes (marketplace = read-only, won't sync back)`));
|
|
2658
|
+
} else {
|
|
2659
|
+
console.log(chalk10.yellow(` \u26A0 Local changes - run 'lynxp push ${blueprint.file}' to sync`));
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
console.log();
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
console.log(chalk10.cyan("\u{1F4C4} AI Config Files"));
|
|
1242
2666
|
console.log();
|
|
1243
2667
|
let foundAny = false;
|
|
1244
2668
|
for (const config2 of CONFIG_FILES) {
|
|
1245
|
-
const filePath =
|
|
2669
|
+
const filePath = join7(cwd, config2.path);
|
|
1246
2670
|
try {
|
|
1247
|
-
await
|
|
1248
|
-
const content = await
|
|
2671
|
+
await access6(filePath);
|
|
2672
|
+
const content = await readFile6(filePath, "utf-8");
|
|
1249
2673
|
const lines = content.split("\n").length;
|
|
1250
2674
|
const size = formatBytes(content.length);
|
|
1251
2675
|
foundAny = true;
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
console.log(`
|
|
2676
|
+
const tracked = trackedStatus.find((t) => t.blueprint.file === config2.path);
|
|
2677
|
+
const trackedLabel = tracked ? chalk10.cyan(" (tracked)") : "";
|
|
2678
|
+
console.log(` ${chalk10.green("\u2713")} ${chalk10.bold(config2.name)}${trackedLabel}`);
|
|
2679
|
+
console.log(` ${chalk10.gray(`Platform: ${config2.platform}`)}`);
|
|
2680
|
+
console.log(` ${chalk10.gray(`Size: ${size} (${lines} lines)`)}`);
|
|
1255
2681
|
const preview = getPreview(content);
|
|
1256
2682
|
if (preview) {
|
|
1257
|
-
console.log(` ${
|
|
2683
|
+
console.log(` ${chalk10.gray(`Preview: ${preview}`)}`);
|
|
1258
2684
|
}
|
|
1259
2685
|
console.log();
|
|
1260
2686
|
} catch {
|
|
1261
2687
|
}
|
|
1262
2688
|
}
|
|
2689
|
+
for (const config2 of CONFIG_DIRS) {
|
|
2690
|
+
const dirPath = join7(cwd, config2.path);
|
|
2691
|
+
if (existsSync4(dirPath)) {
|
|
2692
|
+
try {
|
|
2693
|
+
const files = await readdir3(dirPath);
|
|
2694
|
+
const ruleFiles = files.filter((f) => f.endsWith(".md") || f.endsWith(".mdc"));
|
|
2695
|
+
if (ruleFiles.length > 0) {
|
|
2696
|
+
foundAny = true;
|
|
2697
|
+
console.log(` ${chalk10.green("\u2713")} ${chalk10.bold(config2.name)}`);
|
|
2698
|
+
console.log(` ${chalk10.gray(`Platform: ${config2.platform}`)}`);
|
|
2699
|
+
console.log(` ${chalk10.gray(`Rules: ${ruleFiles.length} file${ruleFiles.length === 1 ? "" : "s"}`)}`);
|
|
2700
|
+
for (const file of ruleFiles.slice(0, 3)) {
|
|
2701
|
+
console.log(` ${chalk10.gray(` \u2022 ${file}`)}`);
|
|
2702
|
+
}
|
|
2703
|
+
if (ruleFiles.length > 3) {
|
|
2704
|
+
console.log(` ${chalk10.gray(` ... and ${ruleFiles.length - 3} more`)}`);
|
|
2705
|
+
}
|
|
2706
|
+
console.log();
|
|
2707
|
+
}
|
|
2708
|
+
} catch {
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
1263
2712
|
if (!foundAny) {
|
|
1264
|
-
console.log(
|
|
2713
|
+
console.log(chalk10.yellow(" No AI configuration files found."));
|
|
1265
2714
|
console.log();
|
|
1266
|
-
console.log(
|
|
1267
|
-
console.log(
|
|
2715
|
+
console.log(chalk10.gray(" Get started:"));
|
|
2716
|
+
console.log(chalk10.gray(" lynxp wizard Generate a configuration"));
|
|
2717
|
+
console.log(chalk10.gray(" lynxp pull <id> Download from marketplace"));
|
|
2718
|
+
console.log(chalk10.gray(" lynxp search <query> Search for blueprints"));
|
|
1268
2719
|
} else {
|
|
1269
|
-
console.log(
|
|
2720
|
+
console.log(chalk10.gray("Commands:"));
|
|
2721
|
+
console.log(chalk10.gray(" lynxp wizard Regenerate configuration"));
|
|
2722
|
+
console.log(chalk10.gray(" lynxp check Validate files"));
|
|
2723
|
+
console.log(chalk10.gray(" lynxp link --list Show tracked blueprints"));
|
|
1270
2724
|
}
|
|
1271
2725
|
console.log();
|
|
1272
2726
|
}
|
|
@@ -1274,7 +2728,7 @@ function getPreview(content) {
|
|
|
1274
2728
|
const lines = content.split("\n");
|
|
1275
2729
|
for (const line of lines) {
|
|
1276
2730
|
const trimmed = line.trim();
|
|
1277
|
-
if (trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("//") && !trimmed.startsWith("<!--")) {
|
|
2731
|
+
if (trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("//") && !trimmed.startsWith("<!--") && !trimmed.startsWith("---") && !trimmed.startsWith(">")) {
|
|
1278
2732
|
return truncate3(trimmed, 50);
|
|
1279
2733
|
}
|
|
1280
2734
|
}
|
|
@@ -1290,34 +2744,1179 @@ function formatBytes(bytes) {
|
|
|
1290
2744
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1291
2745
|
}
|
|
1292
2746
|
|
|
1293
|
-
// src/
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
"
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
);
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
2747
|
+
// src/commands/sync.ts
|
|
2748
|
+
import chalk11 from "chalk";
|
|
2749
|
+
import ora9 from "ora";
|
|
2750
|
+
import prompts5 from "prompts";
|
|
2751
|
+
import { readFile as readFile7, writeFile as writeFile5, mkdir as mkdir5, readdir as readdir4 } from "fs/promises";
|
|
2752
|
+
import { join as join8, dirname as dirname5 } from "path";
|
|
2753
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2754
|
+
import * as yaml3 from "yaml";
|
|
2755
|
+
var CONFIG_FILE = ".lynxprompt/conf.yml";
|
|
2756
|
+
var RULES_DIR = ".lynxprompt/rules";
|
|
2757
|
+
async function syncCommand(options = {}) {
|
|
2758
|
+
console.log();
|
|
2759
|
+
console.log(chalk11.cyan("\u{1F431} LynxPrompt Sync"));
|
|
2760
|
+
console.log();
|
|
2761
|
+
const cwd = process.cwd();
|
|
2762
|
+
const configPath = join8(cwd, CONFIG_FILE);
|
|
2763
|
+
if (!existsSync5(configPath)) {
|
|
2764
|
+
console.log(chalk11.yellow("LynxPrompt is not initialized in this project."));
|
|
2765
|
+
console.log();
|
|
2766
|
+
console.log(chalk11.gray("Run 'lynxp init' first to set up LynxPrompt."));
|
|
2767
|
+
return;
|
|
2768
|
+
}
|
|
2769
|
+
const spinner = ora9("Loading configuration...").start();
|
|
2770
|
+
let config2;
|
|
2771
|
+
try {
|
|
2772
|
+
const configContent = await readFile7(configPath, "utf-8");
|
|
2773
|
+
config2 = yaml3.parse(configContent);
|
|
2774
|
+
spinner.succeed("Configuration loaded");
|
|
2775
|
+
} catch (error) {
|
|
2776
|
+
spinner.fail("Failed to load configuration");
|
|
2777
|
+
console.log(chalk11.red("Could not parse .lynxprompt/conf.yml"));
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
if (!config2.exporters || config2.exporters.length === 0) {
|
|
2781
|
+
console.log(chalk11.yellow("No exporters configured."));
|
|
2782
|
+
console.log();
|
|
2783
|
+
console.log(chalk11.gray("Add exporters to .lynxprompt/conf.yml or run 'lynxp agents enable <agent>'"));
|
|
2784
|
+
return;
|
|
2785
|
+
}
|
|
2786
|
+
const validExporters = [];
|
|
2787
|
+
const invalidExporters = [];
|
|
2788
|
+
for (const exporterId of config2.exporters) {
|
|
2789
|
+
const agent = getAgent(exporterId);
|
|
2790
|
+
if (agent) {
|
|
2791
|
+
validExporters.push(agent);
|
|
2792
|
+
} else {
|
|
2793
|
+
invalidExporters.push(exporterId);
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
if (invalidExporters.length > 0) {
|
|
2797
|
+
console.log(chalk11.yellow(`Unknown exporters: ${invalidExporters.join(", ")}`));
|
|
2798
|
+
}
|
|
2799
|
+
if (validExporters.length === 0) {
|
|
2800
|
+
console.log(chalk11.red("No valid exporters configured."));
|
|
2801
|
+
return;
|
|
2802
|
+
}
|
|
2803
|
+
console.log(chalk11.gray(`Exporters: ${validExporters.map((e) => e.name).join(", ")}`));
|
|
2804
|
+
console.log();
|
|
2805
|
+
const rulesPath = join8(cwd, RULES_DIR);
|
|
2806
|
+
if (!existsSync5(rulesPath)) {
|
|
2807
|
+
console.log(chalk11.yellow("No rules found."));
|
|
2808
|
+
console.log(chalk11.gray(`Create rules in ${RULES_DIR}/ to sync them.`));
|
|
2809
|
+
return;
|
|
2810
|
+
}
|
|
2811
|
+
const rulesContent = await loadRules(rulesPath);
|
|
2812
|
+
if (!rulesContent) {
|
|
2813
|
+
console.log(chalk11.yellow("No rule files found in .lynxprompt/rules/"));
|
|
2814
|
+
return;
|
|
2815
|
+
}
|
|
2816
|
+
console.log(chalk11.gray(`Loaded ${rulesContent.fileCount} rule file${rulesContent.fileCount === 1 ? "" : "s"}`));
|
|
2817
|
+
console.log();
|
|
2818
|
+
if (options.dryRun) {
|
|
2819
|
+
console.log(chalk11.cyan("Dry run - no files will be written"));
|
|
2820
|
+
console.log();
|
|
2821
|
+
console.log("Would write:");
|
|
2822
|
+
for (const exporter of validExporters) {
|
|
2823
|
+
console.log(chalk11.gray(` ${exporter.output}`));
|
|
2824
|
+
}
|
|
2825
|
+
console.log();
|
|
2826
|
+
return;
|
|
2827
|
+
}
|
|
2828
|
+
if (!options.force) {
|
|
2829
|
+
const { confirm } = await prompts5({
|
|
2830
|
+
type: "confirm",
|
|
2831
|
+
name: "confirm",
|
|
2832
|
+
message: `Sync to ${validExporters.length} agent${validExporters.length === 1 ? "" : "s"}?`,
|
|
2833
|
+
initial: true
|
|
2834
|
+
});
|
|
2835
|
+
if (!confirm) {
|
|
2836
|
+
console.log(chalk11.gray("Cancelled."));
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
const result = { written: [], skipped: [], errors: [] };
|
|
2841
|
+
const syncSpinner = ora9("Syncing rules...").start();
|
|
2842
|
+
for (const exporter of validExporters) {
|
|
2843
|
+
try {
|
|
2844
|
+
await syncToAgent(cwd, exporter, rulesContent.combined);
|
|
2845
|
+
result.written.push(exporter.output);
|
|
2846
|
+
} catch (error) {
|
|
2847
|
+
result.errors.push(`${exporter.id}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
syncSpinner.stop();
|
|
2851
|
+
if (result.written.length > 0) {
|
|
2852
|
+
console.log(chalk11.green(`\u2713 Synced to ${result.written.length} agent${result.written.length === 1 ? "" : "s"}:`));
|
|
2853
|
+
for (const file of result.written) {
|
|
2854
|
+
console.log(chalk11.gray(` ${file}`));
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
if (result.errors.length > 0) {
|
|
2858
|
+
console.log();
|
|
2859
|
+
console.log(chalk11.red("Errors:"));
|
|
2860
|
+
for (const error of result.errors) {
|
|
2861
|
+
console.log(chalk11.red(` ${error}`));
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
console.log();
|
|
2865
|
+
}
|
|
2866
|
+
async function loadRules(rulesPath) {
|
|
2867
|
+
const files = [];
|
|
2868
|
+
try {
|
|
2869
|
+
const entries = await readdir4(rulesPath, { withFileTypes: true });
|
|
2870
|
+
for (const entry of entries) {
|
|
2871
|
+
if (!entry.isFile()) continue;
|
|
2872
|
+
if (!entry.name.endsWith(".md")) continue;
|
|
2873
|
+
const filePath = join8(rulesPath, entry.name);
|
|
2874
|
+
const content = await readFile7(filePath, "utf-8");
|
|
2875
|
+
if (content.trim()) {
|
|
2876
|
+
files.push({ name: entry.name, content: content.trim() });
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
} catch {
|
|
2880
|
+
return null;
|
|
2881
|
+
}
|
|
2882
|
+
if (files.length === 0) {
|
|
2883
|
+
return null;
|
|
2884
|
+
}
|
|
2885
|
+
const combined = files.map((f) => f.content).join("\n\n---\n\n");
|
|
2886
|
+
return { combined, files, fileCount: files.length };
|
|
2887
|
+
}
|
|
2888
|
+
async function syncToAgent(cwd, agent, content) {
|
|
2889
|
+
const outputPath = join8(cwd, agent.output);
|
|
2890
|
+
if (agent.output.endsWith("/")) {
|
|
2891
|
+
await syncToDirectory(cwd, agent, content);
|
|
2892
|
+
return;
|
|
2893
|
+
}
|
|
2894
|
+
const formatted = formatForAgent(agent, content);
|
|
2895
|
+
const dir = dirname5(outputPath);
|
|
2896
|
+
if (dir !== ".") {
|
|
2897
|
+
await mkdir5(dir, { recursive: true });
|
|
2898
|
+
}
|
|
2899
|
+
await writeFile5(outputPath, formatted, "utf-8");
|
|
2900
|
+
}
|
|
2901
|
+
async function syncToDirectory(cwd, agent, content) {
|
|
2902
|
+
const outputDir = join8(cwd, agent.output);
|
|
2903
|
+
await mkdir5(outputDir, { recursive: true });
|
|
2904
|
+
const extension = agent.format === "mdc" ? ".mdc" : ".md";
|
|
2905
|
+
const filename = `lynxprompt-rules${extension}`;
|
|
2906
|
+
const outputPath = join8(outputDir, filename);
|
|
2907
|
+
const formatted = formatForAgent(agent, content);
|
|
2908
|
+
await writeFile5(outputPath, formatted, "utf-8");
|
|
2909
|
+
}
|
|
2910
|
+
function formatForAgent(agent, content) {
|
|
2911
|
+
switch (agent.format) {
|
|
2912
|
+
case "mdc":
|
|
2913
|
+
return formatAsMdc(content, agent);
|
|
2914
|
+
case "markdown":
|
|
2915
|
+
return formatAsMarkdown(content, agent);
|
|
2916
|
+
case "json":
|
|
2917
|
+
return formatAsJson(content, agent);
|
|
2918
|
+
case "text":
|
|
2919
|
+
return content;
|
|
2920
|
+
default:
|
|
2921
|
+
return content;
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
function formatAsMdc(content, agent) {
|
|
2925
|
+
const frontmatter = yaml3.stringify({
|
|
2926
|
+
description: "LynxPrompt rules - AI coding guidelines",
|
|
2927
|
+
globs: ["**/*"],
|
|
2928
|
+
alwaysApply: true
|
|
2929
|
+
});
|
|
2930
|
+
return `---
|
|
2931
|
+
${frontmatter}---
|
|
2932
|
+
|
|
2933
|
+
${content}`;
|
|
2934
|
+
}
|
|
2935
|
+
function formatAsMarkdown(content, agent) {
|
|
2936
|
+
const header = `# AI Coding Rules
|
|
2937
|
+
|
|
2938
|
+
> Generated by [LynxPrompt](https://lynxprompt.com)
|
|
2939
|
+
|
|
2940
|
+
`;
|
|
2941
|
+
return header + content;
|
|
2942
|
+
}
|
|
2943
|
+
function formatAsJson(content, agent) {
|
|
2944
|
+
return JSON.stringify(
|
|
2945
|
+
{
|
|
2946
|
+
$schema: "https://lynxprompt.com/schemas/rules.json",
|
|
2947
|
+
version: "1.0",
|
|
2948
|
+
rules: content,
|
|
2949
|
+
meta: {
|
|
2950
|
+
generator: "lynxprompt",
|
|
2951
|
+
url: "https://lynxprompt.com"
|
|
2952
|
+
}
|
|
2953
|
+
},
|
|
2954
|
+
null,
|
|
2955
|
+
2
|
|
2956
|
+
);
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
// src/commands/agents.ts
|
|
2960
|
+
import chalk12 from "chalk";
|
|
2961
|
+
import prompts6 from "prompts";
|
|
2962
|
+
import { readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
|
|
2963
|
+
import { join as join9 } from "path";
|
|
2964
|
+
import { existsSync as existsSync6 } from "fs";
|
|
2965
|
+
import * as yaml4 from "yaml";
|
|
2966
|
+
var CONFIG_FILE2 = ".lynxprompt/conf.yml";
|
|
2967
|
+
async function agentsCommand(action, agentId, options = {}) {
|
|
2968
|
+
console.log();
|
|
2969
|
+
switch (action) {
|
|
2970
|
+
case "enable":
|
|
2971
|
+
await enableAgent(agentId, options);
|
|
2972
|
+
break;
|
|
2973
|
+
case "disable":
|
|
2974
|
+
await disableAgent(agentId);
|
|
2975
|
+
break;
|
|
2976
|
+
case "detect":
|
|
2977
|
+
await detectAgentsInProject();
|
|
2978
|
+
break;
|
|
2979
|
+
case "list":
|
|
2980
|
+
default:
|
|
2981
|
+
await listAgents();
|
|
2982
|
+
break;
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
async function listAgents() {
|
|
2986
|
+
console.log(chalk12.cyan("\u{1F431} LynxPrompt Agents"));
|
|
2987
|
+
console.log();
|
|
2988
|
+
const config2 = await loadConfig();
|
|
2989
|
+
const enabledSet = new Set(config2?.exporters ?? []);
|
|
2990
|
+
const detection = detectAgents();
|
|
2991
|
+
if (enabledSet.size > 0) {
|
|
2992
|
+
console.log(chalk12.green("Enabled:"));
|
|
2993
|
+
for (const id of enabledSet) {
|
|
2994
|
+
const agent = getAgent(id);
|
|
2995
|
+
const detected = detection.detected.find((d) => d.agent.id === id);
|
|
2996
|
+
const status = detected ? chalk12.gray("(detected)") : "";
|
|
2997
|
+
console.log(` ${chalk12.green("\u2713")} ${agent?.name ?? id} ${status}`);
|
|
2998
|
+
}
|
|
2999
|
+
console.log();
|
|
3000
|
+
}
|
|
3001
|
+
const detectedNotEnabled = detection.detected.filter(
|
|
3002
|
+
(d) => !enabledSet.has(d.agent.id)
|
|
3003
|
+
);
|
|
3004
|
+
if (detectedNotEnabled.length > 0) {
|
|
3005
|
+
console.log(chalk12.yellow("Detected (not enabled):"));
|
|
3006
|
+
for (const detected of detectedNotEnabled) {
|
|
3007
|
+
const rules = detected.ruleCount > 0 ? chalk12.gray(` (${detected.ruleCount} rules)`) : "";
|
|
3008
|
+
console.log(` ${chalk12.yellow("\u25CB")} ${detected.agent.name}${rules}`);
|
|
3009
|
+
}
|
|
3010
|
+
console.log();
|
|
3011
|
+
}
|
|
3012
|
+
const popular = getPopularAgents().filter(
|
|
3013
|
+
(a) => !enabledSet.has(a.id) && !detectedNotEnabled.some((d) => d.agent.id === a.id)
|
|
3014
|
+
);
|
|
3015
|
+
if (popular.length > 0) {
|
|
3016
|
+
console.log(chalk12.gray("Popular (available):"));
|
|
3017
|
+
for (const agent of popular) {
|
|
3018
|
+
console.log(` ${chalk12.gray("-")} ${agent.name} - ${agent.description}`);
|
|
3019
|
+
}
|
|
3020
|
+
console.log();
|
|
3021
|
+
}
|
|
3022
|
+
console.log(chalk12.gray(`Total: ${AGENTS.length} agents supported`));
|
|
3023
|
+
console.log();
|
|
3024
|
+
console.log(chalk12.gray("Commands:"));
|
|
3025
|
+
console.log(chalk12.gray(" lynxp agents enable <agent> Enable an agent"));
|
|
3026
|
+
console.log(chalk12.gray(" lynxp agents disable <agent> Disable an agent"));
|
|
3027
|
+
console.log(chalk12.gray(" lynxp agents detect Auto-detect agents"));
|
|
3028
|
+
console.log();
|
|
3029
|
+
}
|
|
3030
|
+
async function enableAgent(agentId, options = {}) {
|
|
3031
|
+
const cwd = process.cwd();
|
|
3032
|
+
const configPath = join9(cwd, CONFIG_FILE2);
|
|
3033
|
+
if (!existsSync6(configPath)) {
|
|
3034
|
+
console.log(chalk12.yellow("LynxPrompt not initialized. Run 'lynxp init' first."));
|
|
3035
|
+
return;
|
|
3036
|
+
}
|
|
3037
|
+
let config2 = await loadConfig();
|
|
3038
|
+
if (!config2) {
|
|
3039
|
+
console.log(chalk12.red("Could not load configuration."));
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
if (!agentId || options.interactive) {
|
|
3043
|
+
const enabledSet = new Set(config2.exporters ?? []);
|
|
3044
|
+
const choices = AGENTS.map((agent2) => ({
|
|
3045
|
+
title: `${agent2.name} - ${agent2.description}`,
|
|
3046
|
+
value: agent2.id,
|
|
3047
|
+
selected: enabledSet.has(agent2.id)
|
|
3048
|
+
}));
|
|
3049
|
+
const { selected } = await prompts6({
|
|
3050
|
+
type: "multiselect",
|
|
3051
|
+
name: "selected",
|
|
3052
|
+
message: "Select agents to enable:",
|
|
3053
|
+
choices,
|
|
3054
|
+
hint: "- Space to select, Enter to confirm"
|
|
3055
|
+
});
|
|
3056
|
+
if (!selected || selected.length === 0) {
|
|
3057
|
+
console.log(chalk12.yellow("No agents selected."));
|
|
3058
|
+
return;
|
|
3059
|
+
}
|
|
3060
|
+
config2.exporters = selected;
|
|
3061
|
+
await saveConfig(config2);
|
|
3062
|
+
console.log(chalk12.green(`\u2713 Enabled ${selected.length} agent${selected.length === 1 ? "" : "s"}`));
|
|
3063
|
+
console.log();
|
|
3064
|
+
console.log(chalk12.gray("Run 'lynxp sync' to sync your rules."));
|
|
3065
|
+
return;
|
|
3066
|
+
}
|
|
3067
|
+
const agent = getAgent(agentId);
|
|
3068
|
+
if (!agent) {
|
|
3069
|
+
const similar = AGENTS.filter(
|
|
3070
|
+
(a) => a.id.includes(agentId.toLowerCase()) || a.name.toLowerCase().includes(agentId.toLowerCase())
|
|
3071
|
+
);
|
|
3072
|
+
console.log(chalk12.red(`Unknown agent: ${agentId}`));
|
|
3073
|
+
if (similar.length > 0) {
|
|
3074
|
+
console.log();
|
|
3075
|
+
console.log(chalk12.gray("Did you mean:"));
|
|
3076
|
+
for (const a of similar.slice(0, 5)) {
|
|
3077
|
+
console.log(chalk12.gray(` ${a.id} - ${a.name}`));
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
return;
|
|
3081
|
+
}
|
|
3082
|
+
if (!config2.exporters) {
|
|
3083
|
+
config2.exporters = [];
|
|
3084
|
+
}
|
|
3085
|
+
if (config2.exporters.includes(agent.id)) {
|
|
3086
|
+
console.log(chalk12.yellow(`${agent.name} is already enabled.`));
|
|
3087
|
+
return;
|
|
3088
|
+
}
|
|
3089
|
+
config2.exporters.push(agent.id);
|
|
3090
|
+
await saveConfig(config2);
|
|
3091
|
+
console.log(chalk12.green(`\u2713 Enabled ${agent.name}`));
|
|
3092
|
+
console.log();
|
|
3093
|
+
console.log(chalk12.gray(`Output: ${agent.output}`));
|
|
3094
|
+
console.log(chalk12.gray("Run 'lynxp sync' to sync your rules."));
|
|
3095
|
+
}
|
|
3096
|
+
async function disableAgent(agentId) {
|
|
3097
|
+
if (!agentId) {
|
|
3098
|
+
console.log(chalk12.yellow("Usage: lynxp agents disable <agent>"));
|
|
3099
|
+
return;
|
|
3100
|
+
}
|
|
3101
|
+
const cwd = process.cwd();
|
|
3102
|
+
const configPath = join9(cwd, CONFIG_FILE2);
|
|
3103
|
+
if (!existsSync6(configPath)) {
|
|
3104
|
+
console.log(chalk12.yellow("LynxPrompt not initialized. Run 'lynxp init' first."));
|
|
3105
|
+
return;
|
|
3106
|
+
}
|
|
3107
|
+
const config2 = await loadConfig();
|
|
3108
|
+
if (!config2) {
|
|
3109
|
+
console.log(chalk12.red("Could not load configuration."));
|
|
3110
|
+
return;
|
|
3111
|
+
}
|
|
3112
|
+
if (!config2.exporters || !config2.exporters.includes(agentId)) {
|
|
3113
|
+
const agent2 = getAgent(agentId);
|
|
3114
|
+
console.log(chalk12.yellow(`${agent2?.name ?? agentId} is not enabled.`));
|
|
3115
|
+
return;
|
|
3116
|
+
}
|
|
3117
|
+
if (config2.exporters.length === 1) {
|
|
3118
|
+
console.log(chalk12.yellow("Cannot disable the last agent. At least one must be enabled."));
|
|
3119
|
+
return;
|
|
3120
|
+
}
|
|
3121
|
+
config2.exporters = config2.exporters.filter((e) => e !== agentId);
|
|
3122
|
+
await saveConfig(config2);
|
|
3123
|
+
const agent = getAgent(agentId);
|
|
3124
|
+
console.log(chalk12.green(`\u2713 Disabled ${agent?.name ?? agentId}`));
|
|
3125
|
+
}
|
|
3126
|
+
async function detectAgentsInProject() {
|
|
3127
|
+
console.log(chalk12.cyan("\u{1F50D} Detecting AI agents..."));
|
|
3128
|
+
console.log();
|
|
3129
|
+
const detection = detectAgents();
|
|
3130
|
+
console.log(formatDetectionResults(detection));
|
|
3131
|
+
console.log();
|
|
3132
|
+
if (detection.detected.length === 0) {
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
3135
|
+
const config2 = await loadConfig();
|
|
3136
|
+
const enabledSet = new Set(config2?.exporters ?? []);
|
|
3137
|
+
const newAgents = detection.detected.filter((d) => !enabledSet.has(d.agent.id));
|
|
3138
|
+
if (newAgents.length === 0) {
|
|
3139
|
+
console.log(chalk12.gray("All detected agents are already enabled."));
|
|
3140
|
+
return;
|
|
3141
|
+
}
|
|
3142
|
+
const { enable } = await prompts6({
|
|
3143
|
+
type: "confirm",
|
|
3144
|
+
name: "enable",
|
|
3145
|
+
message: `Enable ${newAgents.length} detected agent${newAgents.length === 1 ? "" : "s"}?`,
|
|
3146
|
+
initial: true
|
|
3147
|
+
});
|
|
3148
|
+
if (enable && config2) {
|
|
3149
|
+
config2.exporters = [
|
|
3150
|
+
...config2.exporters ?? [],
|
|
3151
|
+
...newAgents.map((d) => d.agent.id)
|
|
3152
|
+
];
|
|
3153
|
+
await saveConfig(config2);
|
|
3154
|
+
console.log(chalk12.green(`\u2713 Enabled ${newAgents.length} agent${newAgents.length === 1 ? "" : "s"}`));
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
async function loadConfig() {
|
|
3158
|
+
const cwd = process.cwd();
|
|
3159
|
+
const configPath = join9(cwd, CONFIG_FILE2);
|
|
3160
|
+
if (!existsSync6(configPath)) {
|
|
3161
|
+
return null;
|
|
3162
|
+
}
|
|
3163
|
+
try {
|
|
3164
|
+
const content = await readFile8(configPath, "utf-8");
|
|
3165
|
+
return yaml4.parse(content);
|
|
3166
|
+
} catch {
|
|
3167
|
+
return null;
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
async function saveConfig(config2) {
|
|
3171
|
+
const cwd = process.cwd();
|
|
3172
|
+
const configPath = join9(cwd, CONFIG_FILE2);
|
|
3173
|
+
const content = yaml4.stringify(config2);
|
|
3174
|
+
await writeFile6(configPath, content, "utf-8");
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
// src/commands/check.ts
|
|
3178
|
+
import chalk13 from "chalk";
|
|
3179
|
+
import ora10 from "ora";
|
|
3180
|
+
import { readFile as readFile9, readdir as readdir5, stat as stat2 } from "fs/promises";
|
|
3181
|
+
import { join as join10 } from "path";
|
|
3182
|
+
import { existsSync as existsSync7 } from "fs";
|
|
3183
|
+
import * as yaml5 from "yaml";
|
|
3184
|
+
var CONFIG_FILES2 = [
|
|
3185
|
+
{ path: "AGENTS.md", name: "AGENTS.md" },
|
|
3186
|
+
{ path: "CLAUDE.md", name: "CLAUDE.md" },
|
|
3187
|
+
{ path: ".github/copilot-instructions.md", name: "GitHub Copilot" },
|
|
3188
|
+
{ path: ".windsurfrules", name: "Windsurf" },
|
|
3189
|
+
{ path: ".clinerules", name: "Cline" },
|
|
3190
|
+
{ path: ".goosehints", name: "Goose" },
|
|
3191
|
+
{ path: ".zed/instructions.md", name: "Zed" }
|
|
3192
|
+
];
|
|
3193
|
+
var CONFIG_DIRS2 = [
|
|
3194
|
+
{ path: ".cursor/rules", name: "Cursor" },
|
|
3195
|
+
{ path: ".lynxprompt", name: "LynxPrompt" }
|
|
3196
|
+
];
|
|
3197
|
+
function validateMarkdown(content, filename) {
|
|
3198
|
+
const errors = [];
|
|
3199
|
+
const warnings = [];
|
|
3200
|
+
if (!content.trim()) {
|
|
3201
|
+
errors.push(`${filename}: File is empty`);
|
|
3202
|
+
return { errors, warnings };
|
|
3203
|
+
}
|
|
3204
|
+
if (content.trim().length < 50) {
|
|
3205
|
+
warnings.push(`${filename}: Content seems too short (< 50 chars)`);
|
|
3206
|
+
}
|
|
3207
|
+
if (!content.includes("#")) {
|
|
3208
|
+
warnings.push(`${filename}: No markdown headers found`);
|
|
3209
|
+
}
|
|
3210
|
+
const placeholders = [
|
|
3211
|
+
"TODO",
|
|
3212
|
+
"FIXME",
|
|
3213
|
+
"YOUR_",
|
|
3214
|
+
"REPLACE_",
|
|
3215
|
+
"[INSERT",
|
|
3216
|
+
"example.com"
|
|
3217
|
+
];
|
|
3218
|
+
for (const placeholder of placeholders) {
|
|
3219
|
+
if (content.includes(placeholder)) {
|
|
3220
|
+
warnings.push(`${filename}: Contains placeholder text "${placeholder}"`);
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
const secretPatterns = [
|
|
3224
|
+
/sk[_-][a-zA-Z0-9]{20,}/,
|
|
3225
|
+
// Stripe-like keys
|
|
3226
|
+
/ghp_[a-zA-Z0-9]{36}/,
|
|
3227
|
+
// GitHub tokens
|
|
3228
|
+
/api[_-]?key[_-]?=\s*[a-zA-Z0-9]{20,}/i
|
|
3229
|
+
];
|
|
3230
|
+
for (const pattern of secretPatterns) {
|
|
3231
|
+
if (pattern.test(content)) {
|
|
3232
|
+
errors.push(`${filename}: Potential secret/API key detected - DO NOT commit secrets!`);
|
|
3233
|
+
break;
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
return { errors, warnings };
|
|
3237
|
+
}
|
|
3238
|
+
async function validateLynxPromptConfig(cwd) {
|
|
3239
|
+
const errors = [];
|
|
3240
|
+
const warnings = [];
|
|
3241
|
+
const configPath = join10(cwd, ".lynxprompt/conf.yml");
|
|
3242
|
+
if (!existsSync7(configPath)) {
|
|
3243
|
+
return { errors, warnings };
|
|
3244
|
+
}
|
|
3245
|
+
try {
|
|
3246
|
+
const content = await readFile9(configPath, "utf-8");
|
|
3247
|
+
const config2 = yaml5.parse(content);
|
|
3248
|
+
if (!config2.version) {
|
|
3249
|
+
warnings.push(".lynxprompt/conf.yml: Missing 'version' field");
|
|
3250
|
+
}
|
|
3251
|
+
if (!config2.exporters || !Array.isArray(config2.exporters)) {
|
|
3252
|
+
errors.push(".lynxprompt/conf.yml: Missing or invalid 'exporters' field");
|
|
3253
|
+
} else if (config2.exporters.length === 0) {
|
|
3254
|
+
warnings.push(".lynxprompt/conf.yml: No exporters configured");
|
|
3255
|
+
}
|
|
3256
|
+
if (!config2.sources || !Array.isArray(config2.sources)) {
|
|
3257
|
+
errors.push(".lynxprompt/conf.yml: Missing or invalid 'sources' field");
|
|
3258
|
+
} else {
|
|
3259
|
+
for (const source of config2.sources) {
|
|
3260
|
+
if (source.type === "local" && source.path) {
|
|
3261
|
+
const sourcePath = join10(cwd, source.path);
|
|
3262
|
+
if (!existsSync7(sourcePath)) {
|
|
3263
|
+
errors.push(`.lynxprompt/conf.yml: Source path not found: ${source.path}`);
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
} catch (error) {
|
|
3269
|
+
errors.push(`.lynxprompt/conf.yml: Invalid YAML syntax - ${error instanceof Error ? error.message : "parse error"}`);
|
|
3270
|
+
}
|
|
3271
|
+
return { errors, warnings };
|
|
3272
|
+
}
|
|
3273
|
+
function validateMdc(content, filename) {
|
|
3274
|
+
const errors = [];
|
|
3275
|
+
const warnings = [];
|
|
3276
|
+
if (!content.startsWith("---")) {
|
|
3277
|
+
warnings.push(`${filename}: Missing YAML frontmatter`);
|
|
3278
|
+
} else {
|
|
3279
|
+
const frontmatterEnd = content.indexOf("---", 3);
|
|
3280
|
+
if (frontmatterEnd === -1) {
|
|
3281
|
+
errors.push(`${filename}: Unclosed YAML frontmatter`);
|
|
3282
|
+
} else {
|
|
3283
|
+
const frontmatter = content.substring(3, frontmatterEnd).trim();
|
|
3284
|
+
try {
|
|
3285
|
+
yaml5.parse(frontmatter);
|
|
3286
|
+
} catch {
|
|
3287
|
+
errors.push(`${filename}: Invalid YAML frontmatter`);
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
const bodyStart = content.indexOf("---", 3);
|
|
3292
|
+
if (bodyStart !== -1) {
|
|
3293
|
+
const body = content.substring(bodyStart + 3).trim();
|
|
3294
|
+
const mdResult = validateMarkdown(body, filename);
|
|
3295
|
+
warnings.push(...mdResult.warnings);
|
|
3296
|
+
}
|
|
3297
|
+
return { errors, warnings };
|
|
3298
|
+
}
|
|
3299
|
+
async function checkCommand(options = {}) {
|
|
3300
|
+
const isCi = options.ci;
|
|
3301
|
+
const cwd = process.cwd();
|
|
3302
|
+
if (!isCi) {
|
|
3303
|
+
console.log();
|
|
3304
|
+
console.log(chalk13.cyan("\u{1F431} LynxPrompt Check"));
|
|
3305
|
+
console.log();
|
|
3306
|
+
}
|
|
3307
|
+
const result = {
|
|
3308
|
+
valid: true,
|
|
3309
|
+
errors: [],
|
|
3310
|
+
warnings: [],
|
|
3311
|
+
files: []
|
|
3312
|
+
};
|
|
3313
|
+
const spinner = !isCi ? ora10("Scanning for configuration files...").start() : null;
|
|
3314
|
+
for (const file of CONFIG_FILES2) {
|
|
3315
|
+
const filePath = join10(cwd, file.path);
|
|
3316
|
+
if (existsSync7(filePath)) {
|
|
3317
|
+
result.files.push(file.path);
|
|
3318
|
+
try {
|
|
3319
|
+
const content = await readFile9(filePath, "utf-8");
|
|
3320
|
+
const validation = validateMarkdown(content, file.path);
|
|
3321
|
+
result.errors.push(...validation.errors);
|
|
3322
|
+
result.warnings.push(...validation.warnings);
|
|
3323
|
+
} catch (error) {
|
|
3324
|
+
result.errors.push(`${file.path}: Could not read file`);
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
for (const dir of CONFIG_DIRS2) {
|
|
3329
|
+
const dirPath = join10(cwd, dir.path);
|
|
3330
|
+
if (existsSync7(dirPath)) {
|
|
3331
|
+
try {
|
|
3332
|
+
const files = await readdir5(dirPath);
|
|
3333
|
+
for (const file of files) {
|
|
3334
|
+
const filePath = join10(dirPath, file);
|
|
3335
|
+
const fileStat = await stat2(filePath);
|
|
3336
|
+
if (fileStat.isFile()) {
|
|
3337
|
+
result.files.push(`${dir.path}/${file}`);
|
|
3338
|
+
const content = await readFile9(filePath, "utf-8");
|
|
3339
|
+
if (file.endsWith(".mdc")) {
|
|
3340
|
+
const validation = validateMdc(content, `${dir.path}/${file}`);
|
|
3341
|
+
result.errors.push(...validation.errors);
|
|
3342
|
+
result.warnings.push(...validation.warnings);
|
|
3343
|
+
} else if (file.endsWith(".md")) {
|
|
3344
|
+
const validation = validateMarkdown(content, `${dir.path}/${file}`);
|
|
3345
|
+
result.errors.push(...validation.errors);
|
|
3346
|
+
result.warnings.push(...validation.warnings);
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
} catch {
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
const lynxpromptValidation = await validateLynxPromptConfig(cwd);
|
|
3355
|
+
result.errors.push(...lynxpromptValidation.errors);
|
|
3356
|
+
result.warnings.push(...lynxpromptValidation.warnings);
|
|
3357
|
+
spinner?.stop();
|
|
3358
|
+
result.valid = result.errors.length === 0;
|
|
3359
|
+
if (isCi) {
|
|
3360
|
+
if (!result.valid) {
|
|
3361
|
+
console.error("\u2717 Validation failed");
|
|
3362
|
+
for (const error of result.errors) {
|
|
3363
|
+
console.error(` ${error}`);
|
|
3364
|
+
}
|
|
3365
|
+
process.exit(1);
|
|
3366
|
+
} else if (result.files.length === 0) {
|
|
3367
|
+
console.error("\u2717 No configuration files found");
|
|
3368
|
+
process.exit(1);
|
|
3369
|
+
} else {
|
|
3370
|
+
console.log("\u2713 Validation passed");
|
|
3371
|
+
if (result.warnings.length > 0) {
|
|
3372
|
+
console.log(` (${result.warnings.length} warning${result.warnings.length === 1 ? "" : "s"})`);
|
|
3373
|
+
}
|
|
3374
|
+
process.exit(0);
|
|
3375
|
+
}
|
|
3376
|
+
} else {
|
|
3377
|
+
if (result.files.length === 0) {
|
|
3378
|
+
console.log(chalk13.yellow("\u26A0 No AI configuration files found."));
|
|
3379
|
+
console.log();
|
|
3380
|
+
console.log(chalk13.gray("Run 'lynxp wizard' to create a configuration."));
|
|
3381
|
+
return;
|
|
3382
|
+
}
|
|
3383
|
+
console.log(chalk13.green(`\u2713 Found ${result.files.length} configuration file${result.files.length === 1 ? "" : "s"}:`));
|
|
3384
|
+
for (const file of result.files) {
|
|
3385
|
+
console.log(chalk13.gray(` ${file}`));
|
|
3386
|
+
}
|
|
3387
|
+
console.log();
|
|
3388
|
+
if (result.errors.length > 0) {
|
|
3389
|
+
console.log(chalk13.red(`\u2717 ${result.errors.length} error${result.errors.length === 1 ? "" : "s"}:`));
|
|
3390
|
+
for (const error of result.errors) {
|
|
3391
|
+
console.log(chalk13.red(` ${error}`));
|
|
3392
|
+
}
|
|
3393
|
+
console.log();
|
|
3394
|
+
}
|
|
3395
|
+
if (result.warnings.length > 0) {
|
|
3396
|
+
console.log(chalk13.yellow(`\u26A0 ${result.warnings.length} warning${result.warnings.length === 1 ? "" : "s"}:`));
|
|
3397
|
+
for (const warning of result.warnings) {
|
|
3398
|
+
console.log(chalk13.yellow(` ${warning}`));
|
|
3399
|
+
}
|
|
3400
|
+
console.log();
|
|
3401
|
+
}
|
|
3402
|
+
if (result.valid) {
|
|
3403
|
+
console.log(chalk13.green("\u2705 Validation passed!"));
|
|
3404
|
+
} else {
|
|
3405
|
+
console.log(chalk13.red("\u274C Validation failed. Fix the errors above."));
|
|
3406
|
+
}
|
|
3407
|
+
console.log();
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3411
|
+
// src/commands/diff.ts
|
|
3412
|
+
import chalk14 from "chalk";
|
|
3413
|
+
import ora11 from "ora";
|
|
3414
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
3415
|
+
import { join as join11 } from "path";
|
|
3416
|
+
import { existsSync as existsSync8 } from "fs";
|
|
3417
|
+
function computeDiff(oldText, newText) {
|
|
3418
|
+
const oldLines = oldText.split("\n");
|
|
3419
|
+
const newLines = newText.split("\n");
|
|
3420
|
+
const result = [];
|
|
3421
|
+
const lcs = longestCommonSubsequence(oldLines, newLines);
|
|
3422
|
+
let oldIndex = 0;
|
|
3423
|
+
let newIndex = 0;
|
|
3424
|
+
let lcsIndex = 0;
|
|
3425
|
+
while (oldIndex < oldLines.length || newIndex < newLines.length) {
|
|
3426
|
+
if (lcsIndex < lcs.length && oldIndex < oldLines.length && oldLines[oldIndex] === lcs[lcsIndex]) {
|
|
3427
|
+
if (newIndex < newLines.length && newLines[newIndex] === lcs[lcsIndex]) {
|
|
3428
|
+
result.push({ type: "same", line: oldLines[oldIndex] });
|
|
3429
|
+
oldIndex++;
|
|
3430
|
+
newIndex++;
|
|
3431
|
+
lcsIndex++;
|
|
3432
|
+
} else {
|
|
3433
|
+
result.push({ type: "add", line: newLines[newIndex] });
|
|
3434
|
+
newIndex++;
|
|
3435
|
+
}
|
|
3436
|
+
} else if (oldIndex < oldLines.length && (lcsIndex >= lcs.length || oldLines[oldIndex] !== lcs[lcsIndex])) {
|
|
3437
|
+
result.push({ type: "remove", line: oldLines[oldIndex] });
|
|
3438
|
+
oldIndex++;
|
|
3439
|
+
} else if (newIndex < newLines.length) {
|
|
3440
|
+
result.push({ type: "add", line: newLines[newIndex] });
|
|
3441
|
+
newIndex++;
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
return result;
|
|
3445
|
+
}
|
|
3446
|
+
function longestCommonSubsequence(a, b) {
|
|
3447
|
+
const m = a.length;
|
|
3448
|
+
const n = b.length;
|
|
3449
|
+
const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
|
|
3450
|
+
for (let i2 = 1; i2 <= m; i2++) {
|
|
3451
|
+
for (let j2 = 1; j2 <= n; j2++) {
|
|
3452
|
+
if (a[i2 - 1] === b[j2 - 1]) {
|
|
3453
|
+
dp[i2][j2] = dp[i2 - 1][j2 - 1] + 1;
|
|
3454
|
+
} else {
|
|
3455
|
+
dp[i2][j2] = Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
const lcs = [];
|
|
3460
|
+
let i = m;
|
|
3461
|
+
let j = n;
|
|
3462
|
+
while (i > 0 && j > 0) {
|
|
3463
|
+
if (a[i - 1] === b[j - 1]) {
|
|
3464
|
+
lcs.unshift(a[i - 1]);
|
|
3465
|
+
i--;
|
|
3466
|
+
j--;
|
|
3467
|
+
} else if (dp[i - 1][j] > dp[i][j - 1]) {
|
|
3468
|
+
i--;
|
|
3469
|
+
} else {
|
|
3470
|
+
j--;
|
|
3471
|
+
}
|
|
3472
|
+
}
|
|
3473
|
+
return lcs;
|
|
3474
|
+
}
|
|
3475
|
+
function formatDiff(diff, contextLines = 3) {
|
|
3476
|
+
const output = [];
|
|
3477
|
+
let lastPrintedIndex = -1;
|
|
3478
|
+
let inHunk = false;
|
|
3479
|
+
const changeIndices = diff.map((d, i) => d.type !== "same" ? i : -1).filter((i) => i !== -1);
|
|
3480
|
+
if (changeIndices.length === 0) {
|
|
3481
|
+
return chalk14.gray(" (no changes)");
|
|
3482
|
+
}
|
|
3483
|
+
for (let i = 0; i < diff.length; i++) {
|
|
3484
|
+
const item = diff[i];
|
|
3485
|
+
const nearChange = changeIndices.some((ci) => Math.abs(ci - i) <= contextLines);
|
|
3486
|
+
if (nearChange) {
|
|
3487
|
+
if (lastPrintedIndex !== -1 && i - lastPrintedIndex > 1) {
|
|
3488
|
+
output.push(chalk14.gray(" ..."));
|
|
3489
|
+
}
|
|
3490
|
+
if (item.type === "add") {
|
|
3491
|
+
output.push(chalk14.green(`+ ${item.line}`));
|
|
3492
|
+
} else if (item.type === "remove") {
|
|
3493
|
+
output.push(chalk14.red(`- ${item.line}`));
|
|
3494
|
+
} else {
|
|
3495
|
+
output.push(chalk14.gray(` ${item.line}`));
|
|
3496
|
+
}
|
|
3497
|
+
lastPrintedIndex = i;
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
return output.join("\n");
|
|
3501
|
+
}
|
|
3502
|
+
function getDiffStats(diff) {
|
|
3503
|
+
return {
|
|
3504
|
+
added: diff.filter((d) => d.type === "add").length,
|
|
3505
|
+
removed: diff.filter((d) => d.type === "remove").length,
|
|
3506
|
+
unchanged: diff.filter((d) => d.type === "same").length
|
|
3507
|
+
};
|
|
3508
|
+
}
|
|
3509
|
+
async function diffCommand(blueprintId, options = {}) {
|
|
3510
|
+
console.log();
|
|
3511
|
+
console.log(chalk14.cyan("\u{1F431} LynxPrompt Diff"));
|
|
3512
|
+
console.log();
|
|
3513
|
+
const cwd = process.cwd();
|
|
3514
|
+
if (options.local) {
|
|
3515
|
+
await diffLocal(cwd);
|
|
3516
|
+
return;
|
|
3517
|
+
}
|
|
3518
|
+
if (!blueprintId) {
|
|
3519
|
+
console.log(chalk14.red("\u2717 Please provide a blueprint ID to compare with."));
|
|
3520
|
+
console.log();
|
|
3521
|
+
console.log(chalk14.gray("Usage:"));
|
|
3522
|
+
console.log(chalk14.gray(" lynxp diff <blueprint-id> Compare local with remote blueprint"));
|
|
3523
|
+
console.log(chalk14.gray(" lynxp diff --local Compare .lynxprompt/rules/ with exports"));
|
|
3524
|
+
return;
|
|
3525
|
+
}
|
|
3526
|
+
if (!isAuthenticated()) {
|
|
3527
|
+
console.log(chalk14.yellow("\u26A0 Not logged in. Some blueprints may not be accessible."));
|
|
3528
|
+
console.log(chalk14.gray("Run 'lynxp login' to authenticate."));
|
|
3529
|
+
console.log();
|
|
3530
|
+
}
|
|
3531
|
+
const spinner = ora11("Fetching blueprint...").start();
|
|
3532
|
+
try {
|
|
3533
|
+
const { blueprint } = await api.getBlueprint(blueprintId);
|
|
3534
|
+
spinner.stop();
|
|
3535
|
+
if (!blueprint || !blueprint.content) {
|
|
3536
|
+
console.log(chalk14.red(`\u2717 Blueprint not found or has no content: ${blueprintId}`));
|
|
3537
|
+
return;
|
|
3538
|
+
}
|
|
3539
|
+
console.log(chalk14.green(`\u2713 Blueprint: ${blueprint.name || blueprintId}`));
|
|
3540
|
+
if (blueprint.description) {
|
|
3541
|
+
console.log(chalk14.gray(` ${blueprint.description}`));
|
|
3542
|
+
}
|
|
3543
|
+
console.log();
|
|
3544
|
+
const localPaths = [
|
|
3545
|
+
"AGENTS.md",
|
|
3546
|
+
"CLAUDE.md",
|
|
3547
|
+
".cursor/rules/project.mdc",
|
|
3548
|
+
".github/copilot-instructions.md",
|
|
3549
|
+
".windsurfrules"
|
|
3550
|
+
];
|
|
3551
|
+
let localContent = null;
|
|
3552
|
+
let localPath = null;
|
|
3553
|
+
for (const path2 of localPaths) {
|
|
3554
|
+
const fullPath = join11(cwd, path2);
|
|
3555
|
+
if (existsSync8(fullPath)) {
|
|
3556
|
+
try {
|
|
3557
|
+
localContent = await readFile10(fullPath, "utf-8");
|
|
3558
|
+
localPath = path2;
|
|
3559
|
+
break;
|
|
3560
|
+
} catch {
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
if (!localContent) {
|
|
3565
|
+
console.log(chalk14.yellow("\u26A0 No local AI configuration file found."));
|
|
3566
|
+
console.log(chalk14.gray("Run 'lynxp wizard' to create one, or 'lynxp pull' to download the blueprint."));
|
|
3567
|
+
return;
|
|
3568
|
+
}
|
|
3569
|
+
console.log(chalk14.gray(`Comparing with: ${localPath}`));
|
|
3570
|
+
console.log();
|
|
3571
|
+
const diff = computeDiff(blueprint.content, localContent);
|
|
3572
|
+
const stats = getDiffStats(diff);
|
|
3573
|
+
if (stats.added === 0 && stats.removed === 0) {
|
|
3574
|
+
console.log(chalk14.green("\u2713 Files are identical!"));
|
|
3575
|
+
} else {
|
|
3576
|
+
console.log(chalk14.gray("Changes (remote \u2192 local):"));
|
|
3577
|
+
console.log();
|
|
3578
|
+
console.log(formatDiff(diff));
|
|
3579
|
+
console.log();
|
|
3580
|
+
console.log(chalk14.gray(`Summary: ${chalk14.green(`+${stats.added}`)} ${chalk14.red(`-${stats.removed}`)} lines changed`));
|
|
3581
|
+
}
|
|
3582
|
+
console.log();
|
|
3583
|
+
} catch (error) {
|
|
3584
|
+
spinner.stop();
|
|
3585
|
+
if (error instanceof ApiRequestError) {
|
|
3586
|
+
if (error.statusCode === 401) {
|
|
3587
|
+
console.log(chalk14.red("\u2717 Authentication required. Run 'lynxp login' first."));
|
|
3588
|
+
} else if (error.statusCode === 404) {
|
|
3589
|
+
console.log(chalk14.red(`\u2717 Blueprint not found: ${blueprintId}`));
|
|
3590
|
+
} else if (error.statusCode === 403) {
|
|
3591
|
+
console.log(chalk14.red("\u2717 Access denied to this blueprint."));
|
|
3592
|
+
} else {
|
|
3593
|
+
console.log(chalk14.red(`\u2717 API error: ${error.message}`));
|
|
3594
|
+
}
|
|
3595
|
+
} else {
|
|
3596
|
+
console.log(chalk14.red("\u2717 Failed to fetch blueprint"));
|
|
3597
|
+
if (error instanceof Error) {
|
|
3598
|
+
console.log(chalk14.gray(` ${error.message}`));
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
async function diffLocal(cwd) {
|
|
3604
|
+
const rulesDir = join11(cwd, ".lynxprompt/rules");
|
|
3605
|
+
if (!existsSync8(rulesDir)) {
|
|
3606
|
+
console.log(chalk14.yellow("\u26A0 No .lynxprompt/rules/ directory found."));
|
|
3607
|
+
console.log(chalk14.gray("Run 'lynxp init' to set up the advanced workflow, or 'lynxp wizard' for simple file generation."));
|
|
3608
|
+
return;
|
|
3609
|
+
}
|
|
3610
|
+
console.log(chalk14.gray("Comparing .lynxprompt/rules/ with exported files..."));
|
|
3611
|
+
console.log();
|
|
3612
|
+
const rulesPath = join11(rulesDir, "agents.md");
|
|
3613
|
+
if (!existsSync8(rulesPath)) {
|
|
3614
|
+
console.log(chalk14.yellow("\u26A0 No rules files found in .lynxprompt/rules/"));
|
|
3615
|
+
return;
|
|
3616
|
+
}
|
|
3617
|
+
let rulesContent;
|
|
3618
|
+
try {
|
|
3619
|
+
rulesContent = await readFile10(rulesPath, "utf-8");
|
|
3620
|
+
} catch {
|
|
3621
|
+
console.log(chalk14.red("\u2717 Could not read .lynxprompt/rules/agents.md"));
|
|
3622
|
+
return;
|
|
3623
|
+
}
|
|
3624
|
+
const exportedFiles = [
|
|
3625
|
+
{ path: "AGENTS.md", name: "AGENTS.md" },
|
|
3626
|
+
{ path: ".cursor/rules/lynxprompt-rules.mdc", name: "Cursor" }
|
|
3627
|
+
];
|
|
3628
|
+
let hasChanges = false;
|
|
3629
|
+
for (const file of exportedFiles) {
|
|
3630
|
+
const filePath = join11(cwd, file.path);
|
|
3631
|
+
if (existsSync8(filePath)) {
|
|
3632
|
+
try {
|
|
3633
|
+
const exportedContent = await readFile10(filePath, "utf-8");
|
|
3634
|
+
let compareContent = exportedContent;
|
|
3635
|
+
if (file.path.endsWith(".mdc")) {
|
|
3636
|
+
const frontmatterEnd = exportedContent.indexOf("---", 3);
|
|
3637
|
+
if (frontmatterEnd !== -1) {
|
|
3638
|
+
compareContent = exportedContent.substring(frontmatterEnd + 3).trim();
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
compareContent = compareContent.replace(/^# AI Coding Rules\n\n> Generated by \[LynxPrompt\].*\n\n/m, "").trim();
|
|
3642
|
+
const diff = computeDiff(rulesContent.trim(), compareContent);
|
|
3643
|
+
const stats = getDiffStats(diff);
|
|
3644
|
+
if (stats.added > 0 || stats.removed > 0) {
|
|
3645
|
+
hasChanges = true;
|
|
3646
|
+
console.log(chalk14.yellow(`\u26A0 ${file.name} differs from source:`));
|
|
3647
|
+
console.log(formatDiff(diff));
|
|
3648
|
+
console.log(chalk14.gray(` ${chalk14.green(`+${stats.added}`)} ${chalk14.red(`-${stats.removed}`)} lines`));
|
|
3649
|
+
console.log();
|
|
3650
|
+
} else {
|
|
3651
|
+
console.log(chalk14.green(`\u2713 ${file.name} is in sync`));
|
|
3652
|
+
}
|
|
3653
|
+
} catch {
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
if (!hasChanges) {
|
|
3658
|
+
console.log();
|
|
3659
|
+
console.log(chalk14.green("\u2713 All exported files are in sync with .lynxprompt/rules/"));
|
|
3660
|
+
} else {
|
|
3661
|
+
console.log();
|
|
3662
|
+
console.log(chalk14.gray("Run 'lynxp sync' to update exported files from .lynxprompt/rules/"));
|
|
3663
|
+
}
|
|
3664
|
+
console.log();
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
// src/commands/link.ts
|
|
3668
|
+
import chalk15 from "chalk";
|
|
3669
|
+
import ora12 from "ora";
|
|
3670
|
+
import prompts7 from "prompts";
|
|
3671
|
+
import { join as join12 } from "path";
|
|
3672
|
+
import { existsSync as existsSync9 } from "fs";
|
|
3673
|
+
function getSourceFromVisibility2(visibility) {
|
|
3674
|
+
switch (visibility) {
|
|
3675
|
+
case "PUBLIC":
|
|
3676
|
+
return "marketplace";
|
|
3677
|
+
case "TEAM":
|
|
3678
|
+
return "team";
|
|
3679
|
+
case "PRIVATE":
|
|
3680
|
+
return "private";
|
|
3681
|
+
default:
|
|
3682
|
+
return "marketplace";
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
async function linkCommand(file, blueprintId, options = {}) {
|
|
3686
|
+
const cwd = process.cwd();
|
|
3687
|
+
if (options.list) {
|
|
3688
|
+
await listTrackedBlueprints(cwd);
|
|
3689
|
+
return;
|
|
3690
|
+
}
|
|
3691
|
+
if (!file) {
|
|
3692
|
+
console.log(chalk15.red("\u2717 Please provide a file path to link."));
|
|
3693
|
+
console.log();
|
|
3694
|
+
console.log(chalk15.gray("Usage:"));
|
|
3695
|
+
console.log(chalk15.gray(" lynxp link <file> <blueprint-id> Link a local file to a cloud blueprint"));
|
|
3696
|
+
console.log(chalk15.gray(" lynxp link --list List all tracked blueprints"));
|
|
3697
|
+
console.log();
|
|
3698
|
+
console.log(chalk15.gray("Example:"));
|
|
3699
|
+
console.log(chalk15.gray(" lynxp link AGENTS.md bp_abc123"));
|
|
3700
|
+
return;
|
|
3701
|
+
}
|
|
3702
|
+
if (!blueprintId) {
|
|
3703
|
+
console.log(chalk15.red("\u2717 Please provide a blueprint ID to link to."));
|
|
3704
|
+
console.log();
|
|
3705
|
+
console.log(chalk15.gray("Usage: lynxp link <file> <blueprint-id>"));
|
|
3706
|
+
console.log(chalk15.gray("Example: lynxp link AGENTS.md bp_abc123"));
|
|
3707
|
+
console.log();
|
|
3708
|
+
console.log(chalk15.gray("To find blueprint IDs:"));
|
|
3709
|
+
console.log(chalk15.gray(" lynxp list - Show your blueprints"));
|
|
3710
|
+
console.log(chalk15.gray(" lynxp search <query> - Search marketplace"));
|
|
3711
|
+
return;
|
|
3712
|
+
}
|
|
3713
|
+
const filePath = join12(cwd, file);
|
|
3714
|
+
if (!existsSync9(filePath)) {
|
|
3715
|
+
console.log(chalk15.red(`\u2717 File not found: ${file}`));
|
|
3716
|
+
return;
|
|
3717
|
+
}
|
|
3718
|
+
const existing = await findBlueprintByFile(cwd, file);
|
|
3719
|
+
if (existing) {
|
|
3720
|
+
console.log(chalk15.yellow(`\u26A0 This file is already linked to: ${existing.id}`));
|
|
3721
|
+
const { proceed } = await prompts7({
|
|
3722
|
+
type: "confirm",
|
|
3723
|
+
name: "proceed",
|
|
3724
|
+
message: "Replace the existing link?",
|
|
3725
|
+
initial: false
|
|
3726
|
+
});
|
|
3727
|
+
if (!proceed) {
|
|
3728
|
+
console.log(chalk15.gray("Cancelled."));
|
|
3729
|
+
return;
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
if (!isAuthenticated()) {
|
|
3733
|
+
console.log(
|
|
3734
|
+
chalk15.yellow("Not logged in. Run 'lynxp login' to authenticate.")
|
|
3735
|
+
);
|
|
3736
|
+
process.exit(1);
|
|
3737
|
+
}
|
|
3738
|
+
const spinner = ora12(`Fetching blueprint ${chalk15.cyan(blueprintId)}...`).start();
|
|
3739
|
+
try {
|
|
3740
|
+
const { blueprint } = await api.getBlueprint(blueprintId);
|
|
3741
|
+
spinner.stop();
|
|
3742
|
+
const source = getSourceFromVisibility2(blueprint.visibility);
|
|
3743
|
+
const isMarketplace = source === "marketplace";
|
|
3744
|
+
console.log();
|
|
3745
|
+
console.log(chalk15.cyan(`\u{1F431} Blueprint: ${chalk15.bold(blueprint.name)}`));
|
|
3746
|
+
if (blueprint.description) {
|
|
3747
|
+
console.log(chalk15.gray(` ${blueprint.description}`));
|
|
3748
|
+
}
|
|
3749
|
+
console.log(chalk15.gray(` Visibility: ${blueprint.visibility}`));
|
|
3750
|
+
console.log();
|
|
3751
|
+
if (isMarketplace) {
|
|
3752
|
+
console.log(chalk15.yellow("\u26A0 This is a marketplace blueprint."));
|
|
3753
|
+
console.log(chalk15.gray(" Your local changes will NOT sync back to the cloud."));
|
|
3754
|
+
console.log(chalk15.gray(" To make changes, you'll need to create your own copy."));
|
|
3755
|
+
console.log();
|
|
3756
|
+
}
|
|
3757
|
+
const { confirm } = await prompts7({
|
|
3758
|
+
type: "confirm",
|
|
3759
|
+
name: "confirm",
|
|
3760
|
+
message: `Link ${chalk15.cyan(file)} to ${chalk15.cyan(blueprint.name)}?`,
|
|
3761
|
+
initial: true
|
|
3762
|
+
});
|
|
3763
|
+
if (!confirm) {
|
|
3764
|
+
console.log(chalk15.gray("Cancelled."));
|
|
3765
|
+
return;
|
|
3766
|
+
}
|
|
3767
|
+
await linkBlueprint(cwd, file, blueprint.id, blueprint.name, source);
|
|
3768
|
+
console.log();
|
|
3769
|
+
console.log(chalk15.green(`\u2705 Linked: ${file} \u2192 ${blueprint.id}`));
|
|
3770
|
+
console.log();
|
|
3771
|
+
console.log(chalk15.gray("Next steps:"));
|
|
3772
|
+
console.log(chalk15.gray(` \u2022 Run 'lynxp pull ${blueprintId}' to update local file from cloud`));
|
|
3773
|
+
console.log(chalk15.gray(` \u2022 Run 'lynxp diff ${blueprintId}' to see differences`));
|
|
3774
|
+
console.log(chalk15.gray(` \u2022 Run 'lynxp status' to see all tracked blueprints`));
|
|
3775
|
+
if (!isMarketplace) {
|
|
3776
|
+
console.log(chalk15.gray(` \u2022 Run 'lynxp push ${file}' to push local changes to cloud`));
|
|
3777
|
+
}
|
|
3778
|
+
console.log();
|
|
3779
|
+
} catch (error) {
|
|
3780
|
+
spinner.stop();
|
|
3781
|
+
if (error instanceof ApiRequestError) {
|
|
3782
|
+
if (error.statusCode === 404) {
|
|
3783
|
+
console.log(chalk15.red(`\u2717 Blueprint not found: ${blueprintId}`));
|
|
3784
|
+
console.log(chalk15.gray(" Make sure the ID is correct. Use 'lynxp list' or 'lynxp search' to find blueprints."));
|
|
3785
|
+
} else if (error.statusCode === 403) {
|
|
3786
|
+
console.log(chalk15.red("\u2717 You don't have access to this blueprint."));
|
|
3787
|
+
} else {
|
|
3788
|
+
console.log(chalk15.red(`\u2717 Error: ${error.message}`));
|
|
3789
|
+
}
|
|
3790
|
+
} else {
|
|
3791
|
+
console.log(chalk15.red("\u2717 An unexpected error occurred."));
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
async function unlinkCommand(file) {
|
|
3796
|
+
const cwd = process.cwd();
|
|
3797
|
+
if (!file) {
|
|
3798
|
+
console.log(chalk15.red("\u2717 Please provide a file path to unlink."));
|
|
3799
|
+
console.log();
|
|
3800
|
+
console.log(chalk15.gray("Usage: lynxp unlink <file>"));
|
|
3801
|
+
console.log(chalk15.gray("Example: lynxp unlink AGENTS.md"));
|
|
3802
|
+
return;
|
|
3803
|
+
}
|
|
3804
|
+
const tracked = await findBlueprintByFile(cwd, file);
|
|
3805
|
+
if (!tracked) {
|
|
3806
|
+
console.log(chalk15.yellow(`\u26A0 File is not linked to any blueprint: ${file}`));
|
|
3807
|
+
return;
|
|
3808
|
+
}
|
|
3809
|
+
console.log();
|
|
3810
|
+
console.log(chalk15.cyan(`Currently linked to: ${tracked.id}`));
|
|
3811
|
+
console.log(chalk15.gray(` Name: ${tracked.name}`));
|
|
3812
|
+
console.log(chalk15.gray(` Source: ${tracked.source}`));
|
|
3813
|
+
console.log();
|
|
3814
|
+
const { confirm } = await prompts7({
|
|
3815
|
+
type: "confirm",
|
|
3816
|
+
name: "confirm",
|
|
3817
|
+
message: `Unlink ${chalk15.cyan(file)} from ${chalk15.cyan(tracked.name)}?`,
|
|
3818
|
+
initial: true
|
|
3819
|
+
});
|
|
3820
|
+
if (!confirm) {
|
|
3821
|
+
console.log(chalk15.gray("Cancelled."));
|
|
3822
|
+
return;
|
|
3823
|
+
}
|
|
3824
|
+
const success = await untrackBlueprint(cwd, file);
|
|
3825
|
+
if (success) {
|
|
3826
|
+
console.log();
|
|
3827
|
+
console.log(chalk15.green(`\u2705 Unlinked: ${file}`));
|
|
3828
|
+
console.log(chalk15.gray(" The file is now a standalone local file."));
|
|
3829
|
+
console.log(chalk15.gray(" It will no longer receive updates from the cloud blueprint."));
|
|
3830
|
+
console.log();
|
|
3831
|
+
} else {
|
|
3832
|
+
console.log(chalk15.red("\u2717 Failed to unlink file."));
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
async function listTrackedBlueprints(cwd) {
|
|
3836
|
+
console.log();
|
|
3837
|
+
console.log(chalk15.cyan("\u{1F431} Tracked Blueprints"));
|
|
3838
|
+
console.log();
|
|
3839
|
+
const status = await checkSyncStatus(cwd);
|
|
3840
|
+
if (status.length === 0) {
|
|
3841
|
+
console.log(chalk15.gray("No blueprints are currently tracked."));
|
|
3842
|
+
console.log();
|
|
3843
|
+
console.log(chalk15.gray("To track a blueprint:"));
|
|
3844
|
+
console.log(chalk15.gray(" lynxp pull <blueprint-id> Download and track a blueprint"));
|
|
3845
|
+
console.log(chalk15.gray(" lynxp link <file> <id> Link an existing file to a blueprint"));
|
|
3846
|
+
return;
|
|
3847
|
+
}
|
|
3848
|
+
for (const { blueprint, localModified, fileExists: fileExists2 } of status) {
|
|
3849
|
+
const statusIcon = !fileExists2 ? chalk15.red("\u2717") : localModified ? chalk15.yellow("\u25CF") : chalk15.green("\u2713");
|
|
3850
|
+
const sourceLabel = {
|
|
3851
|
+
marketplace: chalk15.gray("[marketplace]"),
|
|
3852
|
+
team: chalk15.blue("[team]"),
|
|
3853
|
+
private: chalk15.green("[private]"),
|
|
3854
|
+
local: chalk15.gray("[local]")
|
|
3855
|
+
}[blueprint.source];
|
|
3856
|
+
console.log(`${statusIcon} ${chalk15.cyan(blueprint.file)}`);
|
|
3857
|
+
console.log(` ${sourceLabel} ${blueprint.name}`);
|
|
3858
|
+
console.log(` ${chalk15.gray(`ID: ${blueprint.id}`)}`);
|
|
3859
|
+
if (!fileExists2) {
|
|
3860
|
+
console.log(chalk15.red(` \u26A0 File not found`));
|
|
3861
|
+
} else if (localModified) {
|
|
3862
|
+
console.log(chalk15.yellow(` \u26A0 Local changes detected`));
|
|
3863
|
+
}
|
|
3864
|
+
console.log();
|
|
3865
|
+
}
|
|
3866
|
+
console.log(chalk15.gray("Legend:"));
|
|
3867
|
+
console.log(chalk15.gray(` ${chalk15.green("\u2713")} In sync ${chalk15.yellow("\u25CF")} Modified locally ${chalk15.red("\u2717")} Missing`));
|
|
3868
|
+
console.log();
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
// src/index.ts
|
|
3872
|
+
var program = new Command();
|
|
3873
|
+
program.name("lynxprompt").description("CLI for LynxPrompt - Generate AI IDE configuration files").version("0.2.0");
|
|
3874
|
+
program.command("wizard").description("Generate AI IDE configuration (recommended for most users)").option("-n, --name <name>", "Project name").option("-d, --description <description>", "Project description").option("-s, --stack <stack>", "Tech stack (comma-separated)").option("-f, --format <format>", "Output format: agents, cursor, or comma-separated for multiple").option("-p, --platforms <platforms>", "Alias for --format (deprecated)").option("--persona <persona>", "AI persona (fullstack, backend, frontend, devops, data, security)").option("--boundaries <level>", "Boundary preset (conservative, standard, permissive)").option("-y, --yes", "Skip prompts, use defaults (generates AGENTS.md)").action(wizardCommand);
|
|
3875
|
+
program.command("check").description("Validate AI configuration files (for CI/CD)").option("--ci", "CI mode - exit codes only (0=pass, 1=fail)").action(checkCommand);
|
|
3876
|
+
program.command("status").description("Show current AI configuration and tracked blueprints").action(statusCommand);
|
|
3877
|
+
program.command("pull <id>").description("Download and track a blueprint from the marketplace").option("-o, --output <path>", "Output directory", ".").option("-y, --yes", "Overwrite existing files without prompting").option("--preview", "Preview content without downloading").option("--no-track", "Don't track the blueprint for future syncs").action(pullCommand);
|
|
3878
|
+
program.command("search <query>").description("Search public blueprints in the marketplace").option("-l, --limit <number>", "Number of results", "20").action(searchCommand);
|
|
3879
|
+
program.command("list").description("List your blueprints").option("-l, --limit <number>", "Number of results", "20").option("-v, --visibility <visibility>", "Filter: PRIVATE, TEAM, PUBLIC, or all").action(listCommand);
|
|
3880
|
+
program.command("push [file]").description("Push local file to LynxPrompt cloud as a blueprint").option("-n, --name <name>", "Blueprint name").option("-d, --description <desc>", "Blueprint description").option("-v, --visibility <vis>", "Visibility: PRIVATE, TEAM, or PUBLIC", "PRIVATE").option("-t, --tags <tags>", "Tags (comma-separated)").option("-y, --yes", "Skip prompts").action(pushCommand);
|
|
3881
|
+
program.command("link [file] [blueprint-id]").description("Link a local file to a cloud blueprint for tracking").option("--list", "List all tracked blueprints").action(linkCommand);
|
|
3882
|
+
program.command("unlink <file>").description("Disconnect a local file from its cloud blueprint").action(unlinkCommand);
|
|
3883
|
+
program.command("diff [blueprint-id]").description("Show changes between local and remote blueprint").option("--local", "Compare .lynxprompt/rules/ with exported files").action(diffCommand);
|
|
3884
|
+
program.command("init").description("Initialize .lynxprompt/ for multi-editor sync (advanced)").option("-y, --yes", "Skip prompts and use defaults").option("-f, --force", "Re-initialize even if already initialized").action(initCommand);
|
|
3885
|
+
program.command("sync").description("Sync .lynxprompt/rules/ to all configured agents").option("--dry-run", "Preview changes without writing files").option("-f, --force", "Skip prompts (for CI/automation)").action(syncCommand);
|
|
3886
|
+
program.command("agents [action] [agent]").description("Manage AI agents (list, enable, disable, detect)").option("-i, --interactive", "Interactive agent selection").action(agentsCommand);
|
|
3887
|
+
program.command("login").description("Authenticate with LynxPrompt (opens browser)").action(loginCommand);
|
|
3888
|
+
program.command("logout").description("Log out and remove stored credentials").action(logoutCommand);
|
|
3889
|
+
program.command("whoami").description("Show current authenticated user").action(whoamiCommand);
|
|
3890
|
+
program.addHelpText(
|
|
3891
|
+
"beforeAll",
|
|
3892
|
+
`
|
|
3893
|
+
${chalk16.cyan("\u{1F431} LynxPrompt CLI")} ${chalk16.gray("(also available as: lynxp)")}
|
|
3894
|
+
${chalk16.gray("Generate AI IDE configuration files from your terminal")}
|
|
3895
|
+
`
|
|
3896
|
+
);
|
|
3897
|
+
program.addHelpText(
|
|
3898
|
+
"after",
|
|
3899
|
+
`
|
|
3900
|
+
${chalk16.cyan("Quick Start:")}
|
|
3901
|
+
${chalk16.white("$ lynxp wizard")} ${chalk16.gray("Generate config interactively")}
|
|
3902
|
+
${chalk16.white("$ lynxp wizard -y")} ${chalk16.gray("Generate AGENTS.md with defaults")}
|
|
3903
|
+
${chalk16.white("$ lynxp wizard -f cursor")} ${chalk16.gray("Generate .cursor/rules/")}
|
|
3904
|
+
|
|
3905
|
+
${chalk16.cyan("Marketplace:")}
|
|
3906
|
+
${chalk16.white("$ lynxp search nextjs")} ${chalk16.gray("Search blueprints")}
|
|
3907
|
+
${chalk16.white("$ lynxp pull bp_abc123")} ${chalk16.gray("Download and track a blueprint")}
|
|
3908
|
+
${chalk16.white("$ lynxp push")} ${chalk16.gray("Push local file to cloud")}
|
|
3909
|
+
${chalk16.white("$ lynxp link --list")} ${chalk16.gray("Show tracked blueprints")}
|
|
3910
|
+
|
|
3911
|
+
${chalk16.cyan("Blueprint Tracking:")}
|
|
3912
|
+
${chalk16.white("$ lynxp link AGENTS.md bp_xyz")} ${chalk16.gray("Link existing file to blueprint")}
|
|
3913
|
+
${chalk16.white("$ lynxp unlink AGENTS.md")} ${chalk16.gray("Disconnect from cloud")}
|
|
3914
|
+
${chalk16.white("$ lynxp diff bp_abc123")} ${chalk16.gray("Show changes vs cloud version")}
|
|
3915
|
+
|
|
3916
|
+
${chalk16.cyan("CI/CD:")}
|
|
3917
|
+
${chalk16.white("$ lynxp check --ci")} ${chalk16.gray("Validate config (exit code)")}
|
|
1319
3918
|
|
|
1320
|
-
${
|
|
3919
|
+
${chalk16.gray("Docs: https://lynxprompt.com/docs/cli")}
|
|
1321
3920
|
`
|
|
1322
3921
|
);
|
|
1323
3922
|
program.parse();
|