lynxprompt 2.0.11 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1631 -1473
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1034,1644 +1034,1801 @@ import fs from "fs";
|
|
|
1034
1034
|
import path from "path";
|
|
1035
1035
|
import { createHash as createHash2 } from "crypto";
|
|
1036
1036
|
import prompts2 from "prompts";
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1037
|
+
var EXCLUDED_DIRS = /* @__PURE__ */ new Set([
|
|
1038
|
+
"node_modules",
|
|
1039
|
+
".git",
|
|
1040
|
+
"dist",
|
|
1041
|
+
"build",
|
|
1042
|
+
".next",
|
|
1043
|
+
"__pycache__",
|
|
1044
|
+
"venv",
|
|
1045
|
+
".venv",
|
|
1046
|
+
"target",
|
|
1047
|
+
"vendor",
|
|
1048
|
+
"out",
|
|
1049
|
+
".output",
|
|
1050
|
+
".nuxt",
|
|
1051
|
+
"coverage",
|
|
1052
|
+
".cache",
|
|
1053
|
+
"tmp",
|
|
1054
|
+
".lynxprompt"
|
|
1055
|
+
]);
|
|
1056
|
+
function normPath(p) {
|
|
1057
|
+
return p.replace(/\\/g, "/").toLowerCase();
|
|
1058
|
+
}
|
|
1059
|
+
var CONFIG_PATTERNS = [
|
|
1060
|
+
// Commands (most specific paths first)
|
|
1061
|
+
{ match: (p) => normPath(p).includes(".cursor/commands/") && p.endsWith(".md"), type: "CURSOR_COMMAND", label: "Cursor Command" },
|
|
1062
|
+
{ match: (p) => normPath(p).includes(".claude/commands/") && p.endsWith(".md"), type: "CLAUDE_COMMAND", label: "Claude Command" },
|
|
1063
|
+
{ match: (p) => normPath(p).includes(".windsurf/workflows/") && p.endsWith(".md"), type: "WINDSURF_WORKFLOW", label: "Windsurf Workflow" },
|
|
1064
|
+
{ match: (p) => normPath(p).includes(".copilot/prompts/") && p.endsWith(".md"), type: "COPILOT_PROMPT", label: "Copilot Prompt" },
|
|
1065
|
+
{ match: (p) => normPath(p).includes(".continue/prompts/") && p.endsWith(".md"), type: "CONTINUE_PROMPT", label: "Continue Prompt" },
|
|
1066
|
+
{ match: (p) => normPath(p).includes(".opencode/commands/") && p.endsWith(".md"), type: "OPENCODE_COMMAND", label: "OpenCode Command" },
|
|
1067
|
+
// Directory-based rules
|
|
1068
|
+
{ match: (p) => normPath(p).includes(".cursor/rules/") && p.endsWith(".mdc"), type: "CURSOR_RULES", label: "Cursor Rules" },
|
|
1069
|
+
{ match: (p) => normPath(p).includes(".trae/rules/") && p.endsWith(".mdc"), type: "TRAE_RULES", label: "Trae Rules" },
|
|
1070
|
+
{ match: (p) => normPath(p).includes(".idx/") && p.endsWith(".mdc"), type: "FIREBASE_RULES", label: "Firebase Rules" },
|
|
1071
|
+
{ match: (p) => normPath(p).includes(".roo/rules/") && p.endsWith(".mdc"), type: "ROO_RULES", label: "Roo Rules" },
|
|
1072
|
+
{ match: (p) => normPath(p).includes(".amazonq/rules/") && p.endsWith(".mdc"), type: "AMAZONQ_RULES", label: "Amazon Q Rules" },
|
|
1073
|
+
{ match: (p) => normPath(p).includes(".augment/rules/") && p.endsWith(".mdc"), type: "AUGMENT_RULES", label: "Augment Rules" },
|
|
1074
|
+
{ match: (p) => normPath(p).includes(".kilocode/rules/") && p.endsWith(".mdc"), type: "KILOCODE_RULES", label: "Kilo Code Rules" },
|
|
1075
|
+
{ match: (p) => normPath(p).includes(".kiro/steering/") && p.endsWith(".mdc"), type: "KIRO_STEERING", label: "Kiro Steering" },
|
|
1076
|
+
// Path-based configs
|
|
1077
|
+
{ match: (p) => normPath(p).includes(".github/copilot-instructions.md"), type: "COPILOT_INSTRUCTIONS", label: "Copilot Instructions" },
|
|
1078
|
+
{ match: (p) => normPath(p).includes(".zed/instructions.md"), type: "ZED_INSTRUCTIONS", label: "Zed Instructions" },
|
|
1079
|
+
{ match: (p) => normPath(p).includes(".openhands/microagents/repo.md"), type: "OPENHANDS_CONFIG", label: "OpenHands Config" },
|
|
1080
|
+
{ match: (p) => normPath(p).includes(".junie/guidelines.md"), type: "JUNIE_GUIDELINES", label: "Junie Guidelines" },
|
|
1081
|
+
{ match: (p) => normPath(p).includes(".void/config.json"), type: "VOID_CONFIG", label: "Void Config" },
|
|
1082
|
+
{ match: (p) => normPath(p).includes(".continue/config.json"), type: "CONTINUE_CONFIG", label: "Continue Config" },
|
|
1083
|
+
{ match: (p) => normPath(p).includes(".cody/config.json"), type: "CODY_CONFIG", label: "Cody Config" },
|
|
1084
|
+
{ match: (p) => normPath(p).includes(".supermaven/config.json"), type: "SUPERMAVEN_CONFIG", label: "Supermaven Config" },
|
|
1085
|
+
{ match: (p) => normPath(p).includes(".codegpt/config.json"), type: "CODEGPT_CONFIG", label: "CodeGPT Config" },
|
|
1086
|
+
// Basename rules
|
|
1087
|
+
{ match: (p) => path.basename(p) === "AGENTS.md", type: "AGENTS_MD", label: "AGENTS.md" },
|
|
1088
|
+
{ match: (p) => path.basename(p) === "CLAUDE.md", type: "CLAUDE_MD", label: "CLAUDE.md" },
|
|
1089
|
+
{ match: (p) => path.basename(p) === "AIDER.md", type: "AIDER_MD", label: "AIDER.md" },
|
|
1090
|
+
{ match: (p) => path.basename(p) === "GEMINI.md", type: "GEMINI_MD", label: "GEMINI.md" },
|
|
1091
|
+
{ match: (p) => path.basename(p) === "WARP.md", type: "WARP_MD", label: "WARP.md" },
|
|
1092
|
+
{ match: (p) => path.basename(p) === "CRUSH.md", type: "CRUSH_MD", label: "CRUSH.md" },
|
|
1093
|
+
{ match: (p) => path.basename(p) === ".windsurfrules", type: "WINDSURF_RULES", label: "Windsurf Rules" },
|
|
1094
|
+
{ match: (p) => path.basename(p) === ".clinerules", type: "CLINE_RULES", label: "Cline Rules" },
|
|
1095
|
+
{ match: (p) => path.basename(p) === ".goosehints", type: "GOOSE_HINTS", label: "Goose Hints" },
|
|
1096
|
+
{ match: (p) => path.basename(p) === ".tabnine.yaml", type: "TABNINE_CONFIG", label: "Tabnine Config" },
|
|
1097
|
+
{ match: (p) => path.basename(p) === "opencode.json", type: "OPENCODE_CONFIG", label: "OpenCode Config" },
|
|
1098
|
+
{ match: (p) => path.basename(p) === "firebender.json", type: "FIREBENDER_CONFIG", label: "Firebender Config" }
|
|
1099
|
+
];
|
|
1100
|
+
function matchConfigFile(relativePath) {
|
|
1101
|
+
for (const pattern of CONFIG_PATTERNS) {
|
|
1102
|
+
if (pattern.match(relativePath)) {
|
|
1103
|
+
return { type: pattern.type, label: pattern.label };
|
|
1094
1104
|
}
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1105
|
+
}
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1108
|
+
function scanForAgentFiles(cwd, maxDepth = 5) {
|
|
1109
|
+
const results = [];
|
|
1110
|
+
function scan(dir, depth) {
|
|
1111
|
+
if (depth > maxDepth) return;
|
|
1098
1112
|
try {
|
|
1099
|
-
const
|
|
1100
|
-
const
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
}
|
|
1114
|
-
addCmd("format", fullCmd, `Run ${name}`);
|
|
1115
|
-
} else if (name.match(/^build$|^build:/i) || scriptStr.includes("tsc") || scriptStr.includes("webpack") || scriptStr.includes("vite build")) {
|
|
1116
|
-
addCmd("build", fullCmd, `Run ${name}`);
|
|
1117
|
-
} else if (name.match(/^dev$|^start$|^serve$/i)) {
|
|
1118
|
-
addCmd("dev", fullCmd, `Run ${name}`);
|
|
1119
|
-
} else if (name.match(/^typecheck$|^type-check$|^types$|^check:types/i) || scriptStr.includes("tsc --noEmit")) {
|
|
1120
|
-
addCmd("typecheck", fullCmd, `Run ${name}`);
|
|
1121
|
-
} else if (name.match(/^clean$|^clean:/i) || scriptStr.includes("rimraf") || scriptStr.includes("rm -rf")) {
|
|
1122
|
-
addCmd("clean", fullCmd, `Run ${name}`);
|
|
1123
|
-
} else if (name.match(/^prepare$|^precommit$|^pre-commit$|^husky/i)) {
|
|
1124
|
-
addCmd("preCommit", fullCmd, `Run ${name}`);
|
|
1125
|
-
} else if (name === "install" || name === "postinstall") {
|
|
1126
|
-
addCmd("install", fullCmd, `Run ${name}`);
|
|
1127
|
-
} else if (!["publish", "prepublish", "prepublishOnly", "version", "postversion"].includes(name)) {
|
|
1128
|
-
addCmd("additional", fullCmd, `Run ${name}`);
|
|
1129
|
-
}
|
|
1113
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1114
|
+
for (const entry of entries) {
|
|
1115
|
+
const fullPath = path.join(dir, entry.name);
|
|
1116
|
+
if (entry.isDirectory()) {
|
|
1117
|
+
if (EXCLUDED_DIRS.has(entry.name)) continue;
|
|
1118
|
+
scan(fullPath, depth + 1);
|
|
1119
|
+
} else if (entry.name === "AGENTS.md") {
|
|
1120
|
+
const relativePath = path.relative(cwd, fullPath);
|
|
1121
|
+
results.push({
|
|
1122
|
+
path: relativePath,
|
|
1123
|
+
absolutePath: fullPath,
|
|
1124
|
+
isRoot: relativePath === "AGENTS.md",
|
|
1125
|
+
type: "AGENTS_MD",
|
|
1126
|
+
label: "AGENTS.md"
|
|
1127
|
+
});
|
|
1130
1128
|
}
|
|
1131
1129
|
}
|
|
1132
1130
|
} catch {
|
|
1133
1131
|
}
|
|
1134
1132
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1133
|
+
scan(cwd, 0);
|
|
1134
|
+
results.sort((a, b) => {
|
|
1135
|
+
if (a.isRoot && !b.isRoot) return -1;
|
|
1136
|
+
if (!a.isRoot && b.isRoot) return 1;
|
|
1137
|
+
return a.path.localeCompare(b.path);
|
|
1138
|
+
});
|
|
1139
|
+
return results;
|
|
1140
|
+
}
|
|
1141
|
+
function scanForAllConfigFiles(cwd, maxDepth = 5) {
|
|
1142
|
+
const results = [];
|
|
1143
|
+
const MAX_FILE_SIZE = 1024 * 1024;
|
|
1144
|
+
function scan(dir, depth) {
|
|
1145
|
+
if (depth > maxDepth) return;
|
|
1137
1146
|
try {
|
|
1138
|
-
const
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
const poetryScriptsMatch = content.match(/\[tool\.poetry\.scripts\]([\s\S]*?)(?=\n\[|$)/);
|
|
1154
|
-
if (poetryScriptsMatch) {
|
|
1155
|
-
const scriptLines = poetryScriptsMatch[1].split("\n").filter((l) => l.includes("="));
|
|
1156
|
-
for (const line of scriptLines) {
|
|
1157
|
-
const match = line.match(/(\w+)\s*=\s*"([^"]+)"/);
|
|
1158
|
-
if (match) {
|
|
1159
|
-
const [, name, entry] = match;
|
|
1160
|
-
addCmd("additional", `poetry run ${name}`, entry);
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
const poeMatch = content.match(/\[tool\.poe\.tasks\]([\s\S]*?)(?=\n\[tool\.|$)/);
|
|
1165
|
-
if (poeMatch) {
|
|
1166
|
-
const taskLines = poeMatch[1].split("\n").filter((l) => l.includes("="));
|
|
1167
|
-
for (const line of taskLines) {
|
|
1168
|
-
const match = line.match(/(\w+)\s*=\s*"([^"]+)"/);
|
|
1169
|
-
if (match) {
|
|
1170
|
-
const [, name, cmd] = match;
|
|
1171
|
-
if (name.match(/test/i)) {
|
|
1172
|
-
addCmd("test", `poe ${name}`, cmd);
|
|
1173
|
-
} else if (name.match(/lint/i)) {
|
|
1174
|
-
addCmd("lint", `poe ${name}`, cmd);
|
|
1175
|
-
} else if (name.match(/format/i)) {
|
|
1176
|
-
addCmd("format", `poe ${name}`, cmd);
|
|
1177
|
-
} else {
|
|
1178
|
-
addCmd("additional", `poe ${name}`, cmd);
|
|
1147
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1148
|
+
for (const entry of entries) {
|
|
1149
|
+
const fullPath = path.join(dir, entry.name);
|
|
1150
|
+
if (entry.isDirectory()) {
|
|
1151
|
+
if (EXCLUDED_DIRS.has(entry.name)) continue;
|
|
1152
|
+
scan(fullPath, depth + 1);
|
|
1153
|
+
} else {
|
|
1154
|
+
const relativePath = path.relative(cwd, fullPath);
|
|
1155
|
+
const configMatch = matchConfigFile(relativePath);
|
|
1156
|
+
if (configMatch) {
|
|
1157
|
+
try {
|
|
1158
|
+
const stat2 = fs.statSync(fullPath);
|
|
1159
|
+
if (stat2.size === 0 || stat2.size > MAX_FILE_SIZE) continue;
|
|
1160
|
+
} catch {
|
|
1161
|
+
continue;
|
|
1179
1162
|
}
|
|
1163
|
+
results.push({
|
|
1164
|
+
path: relativePath,
|
|
1165
|
+
absolutePath: fullPath,
|
|
1166
|
+
isRoot: relativePath === "AGENTS.md",
|
|
1167
|
+
type: configMatch.type,
|
|
1168
|
+
label: configMatch.label
|
|
1169
|
+
});
|
|
1180
1170
|
}
|
|
1181
1171
|
}
|
|
1182
1172
|
}
|
|
1183
|
-
if (content.includes("fastapi") || content.includes("uvicorn")) {
|
|
1184
|
-
addCmd("dev", "uvicorn src.main:app --host 0.0.0.0 --port 8000 --reload", "Run FastAPI dev server");
|
|
1185
|
-
}
|
|
1186
|
-
if (content.includes("[tool.poetry]")) {
|
|
1187
|
-
addCmd("install", "poetry install", "Install dependencies with Poetry");
|
|
1188
|
-
} else if (await fileExists(join3(cwd, "uv.lock"))) {
|
|
1189
|
-
addCmd("install", "uv sync", "Sync dependencies with uv");
|
|
1190
|
-
} else {
|
|
1191
|
-
addCmd("install", "pip install -r requirements.txt", "Install dependencies with pip");
|
|
1192
|
-
}
|
|
1193
|
-
} catch {
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
const requirementsPath = join3(cwd, "requirements.txt");
|
|
1197
|
-
if (await fileExists(requirementsPath)) {
|
|
1198
|
-
try {
|
|
1199
|
-
const content = await readFile3(requirementsPath, "utf-8");
|
|
1200
|
-
if (content.includes("pytest")) {
|
|
1201
|
-
addCmd("test", "python -m pytest tests/ -v", "Run pytest");
|
|
1202
|
-
}
|
|
1203
|
-
addCmd("install", "pip install -r requirements.txt", "Install dependencies");
|
|
1204
1173
|
} catch {
|
|
1205
1174
|
}
|
|
1206
1175
|
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1176
|
+
scan(cwd, 0);
|
|
1177
|
+
results.sort((a, b) => {
|
|
1178
|
+
if (a.isRoot && !b.isRoot) return -1;
|
|
1179
|
+
if (!a.isRoot && b.isRoot) return 1;
|
|
1180
|
+
return a.path.localeCompare(b.path);
|
|
1181
|
+
});
|
|
1182
|
+
return results;
|
|
1183
|
+
}
|
|
1184
|
+
async function detectHierarchyInfo(cwd, file) {
|
|
1185
|
+
const repositoryRoot = createRepositoryRoot(cwd);
|
|
1186
|
+
const result = {
|
|
1187
|
+
repositoryPath: null,
|
|
1188
|
+
hierarchyId: null,
|
|
1189
|
+
parentId: null,
|
|
1190
|
+
repositoryRoot
|
|
1191
|
+
};
|
|
1192
|
+
try {
|
|
1193
|
+
const relativePath = path.relative(cwd, path.resolve(file));
|
|
1194
|
+
if (relativePath.includes(path.sep) && !relativePath.startsWith("..")) {
|
|
1195
|
+
result.repositoryPath = relativePath;
|
|
1196
|
+
const rootAgentsMd = path.join(cwd, "AGENTS.md");
|
|
1197
|
+
if (fs.existsSync(rootAgentsMd) && path.resolve(file) !== rootAgentsMd) {
|
|
1198
|
+
const blueprints = await loadBlueprints(cwd);
|
|
1199
|
+
const parentBlueprint = blueprints.blueprints.find(
|
|
1200
|
+
(b) => b.file === "AGENTS.md"
|
|
1201
|
+
);
|
|
1202
|
+
if (parentBlueprint) {
|
|
1203
|
+
result.parentId = parentBlueprint.id;
|
|
1235
1204
|
}
|
|
1236
1205
|
}
|
|
1237
|
-
}
|
|
1206
|
+
} else if (relativePath === "AGENTS.md" || relativePath === path.basename(file)) {
|
|
1207
|
+
result.repositoryPath = relativePath;
|
|
1238
1208
|
}
|
|
1209
|
+
} catch {
|
|
1239
1210
|
}
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
addCmd("build", "docker compose build", "Build all services");
|
|
1253
|
-
addCmd("clean", "docker compose down -v", "Stop and remove volumes");
|
|
1254
|
-
} catch {
|
|
1255
|
-
}
|
|
1211
|
+
return result;
|
|
1212
|
+
}
|
|
1213
|
+
async function ensureHierarchy(_cwd, repositoryRoot, name) {
|
|
1214
|
+
try {
|
|
1215
|
+
const response = await api.createHierarchy({
|
|
1216
|
+
name,
|
|
1217
|
+
repository_root: repositoryRoot
|
|
1218
|
+
});
|
|
1219
|
+
return response.hierarchy.id;
|
|
1220
|
+
} catch (error) {
|
|
1221
|
+
console.log(chalk6.gray(" Note: Hierarchy creation skipped"));
|
|
1222
|
+
return null;
|
|
1256
1223
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1224
|
+
}
|
|
1225
|
+
async function findExistingBlueprintOnServer(repositoryPath, hierarchyId) {
|
|
1226
|
+
if (!repositoryPath) return null;
|
|
1227
|
+
try {
|
|
1228
|
+
let offset = 0;
|
|
1229
|
+
const limit = 50;
|
|
1230
|
+
while (true) {
|
|
1231
|
+
const response = await api.listBlueprints({ limit, offset });
|
|
1232
|
+
for (const bp of response.blueprints) {
|
|
1233
|
+
if (bp.repository_path === repositoryPath) {
|
|
1234
|
+
if (hierarchyId) {
|
|
1235
|
+
if (bp.hierarchy_id === hierarchyId) {
|
|
1236
|
+
return { id: bp.id, name: bp.name };
|
|
1237
|
+
}
|
|
1238
|
+
} else {
|
|
1239
|
+
return { id: bp.id, name: bp.name };
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
if (!response.has_more) break;
|
|
1244
|
+
offset += limit;
|
|
1266
1245
|
}
|
|
1246
|
+
} catch {
|
|
1267
1247
|
}
|
|
1248
|
+
return null;
|
|
1249
|
+
}
|
|
1250
|
+
function createRepositoryRoot(rootPath) {
|
|
1268
1251
|
try {
|
|
1269
|
-
const
|
|
1270
|
-
if (
|
|
1271
|
-
|
|
1252
|
+
const gitConfigPath = path.join(rootPath, ".git", "config");
|
|
1253
|
+
if (fs.existsSync(gitConfigPath)) {
|
|
1254
|
+
const gitConfig = fs.readFileSync(gitConfigPath, "utf-8");
|
|
1255
|
+
const urlMatch = gitConfig.match(/url = (.+)/);
|
|
1256
|
+
if (urlMatch) {
|
|
1257
|
+
return createHash2("sha256").update(urlMatch[1].trim()).digest("hex").substring(0, 16);
|
|
1258
|
+
}
|
|
1272
1259
|
}
|
|
1273
1260
|
} catch {
|
|
1274
1261
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
addCmd("clean", "cargo clean", "Clean build artifacts");
|
|
1284
|
-
}
|
|
1285
|
-
const goModPath = join3(cwd, "go.mod");
|
|
1286
|
-
if (await fileExists(goModPath)) {
|
|
1287
|
-
addCmd("build", "go build", "Build Go project");
|
|
1288
|
-
addCmd("test", "go test ./...", "Run Go tests");
|
|
1289
|
-
addCmd("lint", "golangci-lint run", "Run golangci-lint");
|
|
1290
|
-
addCmd("format", "go fmt ./...", "Format Go code");
|
|
1291
|
-
addCmd("dev", "go run .", "Run Go binary");
|
|
1292
|
-
addCmd("clean", "go clean", "Clean build cache");
|
|
1293
|
-
addCmd("typecheck", "go vet ./...", "Run go vet");
|
|
1294
|
-
}
|
|
1295
|
-
const srcMainPath = join3(cwd, "src", "main.py");
|
|
1296
|
-
const mainPath = join3(cwd, "main.py");
|
|
1297
|
-
const appPath = join3(cwd, "app.py");
|
|
1298
|
-
if (await fileExists(srcMainPath)) {
|
|
1299
|
-
addCmd("dev", "python -m src.main", "Run main module");
|
|
1262
|
+
return createHash2("sha256").update(path.resolve(rootPath)).digest("hex").substring(0, 16);
|
|
1263
|
+
}
|
|
1264
|
+
async function pushCommand(fileArg, options) {
|
|
1265
|
+
const cwd = process.cwd();
|
|
1266
|
+
if (!isAuthenticated()) {
|
|
1267
|
+
console.log(chalk6.yellow("You need to be logged in to push blueprints."));
|
|
1268
|
+
console.log(chalk6.gray("Run 'lynxp login' to authenticate."));
|
|
1269
|
+
process.exit(1);
|
|
1300
1270
|
}
|
|
1301
|
-
if (
|
|
1302
|
-
|
|
1271
|
+
if (options.all) {
|
|
1272
|
+
const discoveredFiles = scanForAllConfigFiles(cwd);
|
|
1273
|
+
if (discoveredFiles.length === 0) {
|
|
1274
|
+
console.log(chalk6.yellow("No AI configuration files found."));
|
|
1275
|
+
console.log(chalk6.gray("Run from a directory containing AGENTS.md, CLAUDE.md, .cursor/rules/, etc."));
|
|
1276
|
+
process.exit(1);
|
|
1277
|
+
}
|
|
1278
|
+
console.log(chalk6.cyan(`
|
|
1279
|
+
\u{1F4C1} Found ${discoveredFiles.length} AI config files:
|
|
1280
|
+
`));
|
|
1281
|
+
const byType = /* @__PURE__ */ new Map();
|
|
1282
|
+
for (const f of discoveredFiles) {
|
|
1283
|
+
const key = f.label || "Unknown";
|
|
1284
|
+
if (!byType.has(key)) byType.set(key, []);
|
|
1285
|
+
byType.get(key).push(f);
|
|
1286
|
+
}
|
|
1287
|
+
for (const [label, files] of byType) {
|
|
1288
|
+
console.log(chalk6.white(` ${label} (${files.length}):`));
|
|
1289
|
+
for (const f of files) {
|
|
1290
|
+
console.log(chalk6.gray(` ${f.path}`));
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
console.log();
|
|
1294
|
+
if (!options.yes) {
|
|
1295
|
+
const { confirm } = await prompts2({
|
|
1296
|
+
type: "confirm",
|
|
1297
|
+
name: "confirm",
|
|
1298
|
+
message: `Push all ${discoveredFiles.length} files as a hierarchy?`,
|
|
1299
|
+
initial: true
|
|
1300
|
+
});
|
|
1301
|
+
if (!confirm) {
|
|
1302
|
+
console.log(chalk6.yellow("Push cancelled."));
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
await pushHierarchy(cwd, discoveredFiles, options);
|
|
1307
|
+
return;
|
|
1303
1308
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1309
|
+
const file = fileArg || findDefaultFile();
|
|
1310
|
+
if (!file) {
|
|
1311
|
+
console.log(chalk6.red("No AI configuration file found."));
|
|
1312
|
+
console.log(
|
|
1313
|
+
chalk6.gray("Specify a file or run in a directory with AGENTS.md, CLAUDE.md, etc.")
|
|
1314
|
+
);
|
|
1315
|
+
console.log(chalk6.gray("Or use 'lynxp push --all' to scan recursively for all config files."));
|
|
1316
|
+
process.exit(1);
|
|
1306
1317
|
}
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1318
|
+
if (!fs.existsSync(file)) {
|
|
1319
|
+
console.log(chalk6.red(`File not found: ${file}`));
|
|
1320
|
+
process.exit(1);
|
|
1310
1321
|
}
|
|
1311
|
-
const
|
|
1312
|
-
|
|
1313
|
-
|
|
1322
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
1323
|
+
const filename = path.basename(file);
|
|
1324
|
+
const linked = await findBlueprintByFile(cwd, file);
|
|
1325
|
+
if (linked) {
|
|
1326
|
+
await updateBlueprint(cwd, file, linked.id, content, options, linked.checksum);
|
|
1327
|
+
} else {
|
|
1328
|
+
await createOrLinkBlueprint(cwd, file, filename, content, options);
|
|
1314
1329
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1330
|
+
}
|
|
1331
|
+
async function updateBlueprint(cwd, file, blueprintId, content, options, expectedChecksum) {
|
|
1332
|
+
console.log(chalk6.cyan(`
|
|
1333
|
+
\u{1F4E4} Updating blueprint ${chalk6.bold(blueprintId)}...`));
|
|
1334
|
+
console.log(chalk6.gray(` File: ${file}`));
|
|
1335
|
+
const spinner = ora5("Pushing changes...").start();
|
|
1336
|
+
try {
|
|
1337
|
+
const updateData = { content };
|
|
1338
|
+
if (expectedChecksum && !options.force) {
|
|
1339
|
+
updateData.expected_checksum = expectedChecksum;
|
|
1340
|
+
}
|
|
1341
|
+
const result = await api.updateBlueprint(blueprintId, updateData);
|
|
1342
|
+
spinner.succeed("Blueprint updated!");
|
|
1343
|
+
await updateChecksum(cwd, file, content);
|
|
1344
|
+
console.log();
|
|
1345
|
+
console.log(chalk6.green(`\u2705 Successfully updated ${chalk6.bold(result.blueprint.name)}`));
|
|
1346
|
+
console.log(chalk6.gray(` ID: ${blueprintId}`));
|
|
1347
|
+
if (result.blueprint.content_checksum) {
|
|
1348
|
+
console.log(chalk6.gray(` Checksum: ${result.blueprint.content_checksum}`));
|
|
1349
|
+
}
|
|
1350
|
+
console.log(chalk6.gray(` View: https://lynxprompt.com/templates/${blueprintId.replace("bp_", "")}`));
|
|
1351
|
+
} catch (error) {
|
|
1352
|
+
spinner.fail("Failed to update blueprint");
|
|
1353
|
+
if (error instanceof ApiRequestError && error.statusCode === 409) {
|
|
1354
|
+
console.log();
|
|
1355
|
+
console.log(chalk6.yellow("\u26A0 Conflict: The blueprint has been modified since you last pulled it."));
|
|
1356
|
+
console.log(chalk6.gray(" Someone else may have pushed changes."));
|
|
1357
|
+
console.log();
|
|
1358
|
+
console.log(chalk6.gray("Options:"));
|
|
1359
|
+
console.log(chalk6.gray(" 1. Run 'lynxp pull " + blueprintId + "' to get the latest version"));
|
|
1360
|
+
console.log(chalk6.gray(" 2. Run 'lynxp push --force' to overwrite remote changes"));
|
|
1361
|
+
process.exit(1);
|
|
1362
|
+
}
|
|
1363
|
+
handleError(error);
|
|
1318
1364
|
}
|
|
1319
|
-
return cmds;
|
|
1320
1365
|
}
|
|
1321
|
-
async function
|
|
1322
|
-
const
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
try {
|
|
1333
|
-
const content = await readFile3(packageJsonPath, "utf-8");
|
|
1334
|
-
const pkg = JSON.parse(content);
|
|
1335
|
-
detected.name = pkg.name || null;
|
|
1336
|
-
detected.description = pkg.description;
|
|
1337
|
-
if (pkg.workspaces || await fileExists(join3(cwd, "pnpm-workspace.yaml"))) {
|
|
1338
|
-
detected.type = "monorepo";
|
|
1339
|
-
} else if (pkg.main || pkg.exports) {
|
|
1340
|
-
detected.type = "library";
|
|
1341
|
-
} else {
|
|
1342
|
-
detected.type = "application";
|
|
1343
|
-
}
|
|
1344
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1345
|
-
for (const [framework, deps] of Object.entries(JS_FRAMEWORK_PATTERNS)) {
|
|
1346
|
-
if (deps.some((dep) => allDeps[dep])) {
|
|
1347
|
-
detected.stack.push(framework);
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
for (const [tool, deps] of Object.entries(JS_TOOL_PATTERNS)) {
|
|
1351
|
-
if (deps.some((dep) => allDeps[dep])) {
|
|
1352
|
-
detected.stack.push(tool);
|
|
1353
|
-
}
|
|
1366
|
+
async function createOrLinkBlueprint(cwd, file, filename, content, options) {
|
|
1367
|
+
const isAgentsMd = filename === "AGENTS.md";
|
|
1368
|
+
if (isAgentsMd) {
|
|
1369
|
+
const discoveredFiles = scanForAgentFiles(cwd);
|
|
1370
|
+
if (discoveredFiles.length > 1) {
|
|
1371
|
+
console.log();
|
|
1372
|
+
console.log(chalk6.cyan(`\u{1F4C1} Found ${discoveredFiles.length} AGENTS.md files:`));
|
|
1373
|
+
console.log();
|
|
1374
|
+
for (const f of discoveredFiles) {
|
|
1375
|
+
const icon = f.isRoot ? "\u{1F4C4}" : " \u2514\u2500";
|
|
1376
|
+
console.log(chalk6.gray(` ${icon} ${f.path}`));
|
|
1354
1377
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1378
|
+
console.log();
|
|
1379
|
+
let shouldCreateHierarchy = options.yes;
|
|
1380
|
+
if (!options.yes) {
|
|
1381
|
+
const { createHierarchy } = await prompts2({
|
|
1382
|
+
type: "confirm",
|
|
1383
|
+
name: "createHierarchy",
|
|
1384
|
+
message: `Create a hierarchy with all ${discoveredFiles.length} AGENTS.md files?`,
|
|
1385
|
+
initial: true
|
|
1386
|
+
});
|
|
1387
|
+
shouldCreateHierarchy = createHierarchy;
|
|
1388
|
+
} else {
|
|
1389
|
+
console.log(chalk6.cyan(`Auto-creating hierarchy with ${discoveredFiles.length} files...`));
|
|
1357
1390
|
}
|
|
1358
|
-
if (
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
detected.commands.lint = pkg.scripts.lint || pkg.scripts["lint:check"];
|
|
1362
|
-
detected.commands.dev = pkg.scripts.dev || pkg.scripts.start || pkg.scripts.serve;
|
|
1363
|
-
detected.commands.format = pkg.scripts.format || pkg.scripts.prettier;
|
|
1391
|
+
if (shouldCreateHierarchy) {
|
|
1392
|
+
await pushHierarchy(cwd, discoveredFiles, options);
|
|
1393
|
+
return;
|
|
1364
1394
|
}
|
|
1365
|
-
|
|
1366
|
-
detected.packageManager = "pnpm";
|
|
1367
|
-
} else if (await fileExists(join3(cwd, "yarn.lock"))) {
|
|
1368
|
-
detected.packageManager = "yarn";
|
|
1369
|
-
} else if (await fileExists(join3(cwd, "bun.lockb"))) {
|
|
1370
|
-
detected.packageManager = "bun";
|
|
1371
|
-
} else if (await fileExists(join3(cwd, "package-lock.json"))) {
|
|
1372
|
-
detected.packageManager = "npm";
|
|
1373
|
-
}
|
|
1374
|
-
if (detected.packageManager && detected.packageManager !== "npm") {
|
|
1375
|
-
const pm = detected.packageManager;
|
|
1376
|
-
for (const [key, value] of Object.entries(detected.commands)) {
|
|
1377
|
-
if (value && !value.startsWith(pm) && !value.startsWith("npx")) {
|
|
1378
|
-
detected.commands[key] = `${pm} run ${value}`;
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
} else if (detected.commands) {
|
|
1382
|
-
for (const [key, value] of Object.entries(detected.commands)) {
|
|
1383
|
-
if (value && !value.startsWith("npm") && !value.startsWith("npx")) {
|
|
1384
|
-
detected.commands[key] = `npm run ${value}`;
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
if (pkg.scripts) {
|
|
1389
|
-
detected.commands.build = pkg.scripts.build ? "build" : void 0;
|
|
1390
|
-
detected.commands.test = pkg.scripts.test ? "test" : void 0;
|
|
1391
|
-
detected.commands.lint = pkg.scripts.lint ? "lint" : pkg.scripts["lint:check"] ? "lint:check" : void 0;
|
|
1392
|
-
detected.commands.dev = pkg.scripts.dev ? "dev" : pkg.scripts.start ? "start" : pkg.scripts.serve ? "serve" : void 0;
|
|
1393
|
-
}
|
|
1394
|
-
return detected;
|
|
1395
|
-
} catch {
|
|
1396
|
-
}
|
|
1397
|
-
}
|
|
1398
|
-
const pyprojectPath = join3(cwd, "pyproject.toml");
|
|
1399
|
-
if (await fileExists(pyprojectPath)) {
|
|
1400
|
-
try {
|
|
1401
|
-
const content = await readFile3(pyprojectPath, "utf-8");
|
|
1402
|
-
detected.stack.push("python");
|
|
1403
|
-
detected.type = "application";
|
|
1404
|
-
const nameMatch = content.match(/name\s*=\s*"([^"]+)"/);
|
|
1405
|
-
if (nameMatch) detected.name = nameMatch[1];
|
|
1406
|
-
if (content.includes("fastapi")) detected.stack.push("fastapi");
|
|
1407
|
-
if (content.includes("django")) detected.stack.push("django");
|
|
1408
|
-
if (content.includes("flask")) detected.stack.push("flask");
|
|
1409
|
-
if (content.includes("pydantic")) detected.stack.push("pydantic");
|
|
1410
|
-
if (content.includes("sqlalchemy")) detected.stack.push("sqlalchemy");
|
|
1411
|
-
if (content.includes("pytest")) detected.stack.push("pytest");
|
|
1412
|
-
if (content.includes("ruff")) detected.stack.push("ruff");
|
|
1413
|
-
if (content.includes("mypy")) detected.stack.push("mypy");
|
|
1414
|
-
detected.commands.test = "pytest";
|
|
1415
|
-
detected.commands.lint = "ruff check .";
|
|
1416
|
-
if (content.includes("[tool.poetry]")) {
|
|
1417
|
-
detected.packageManager = "yarn";
|
|
1418
|
-
detected.commands.dev = "poetry run python -m uvicorn main:app --reload";
|
|
1419
|
-
} else if (await fileExists(join3(cwd, "uv.lock"))) {
|
|
1420
|
-
detected.commands.dev = "uv run python main.py";
|
|
1421
|
-
}
|
|
1422
|
-
return detected;
|
|
1423
|
-
} catch {
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
const requirementsPath = join3(cwd, "requirements.txt");
|
|
1427
|
-
if (await fileExists(requirementsPath)) {
|
|
1428
|
-
try {
|
|
1429
|
-
const content = await readFile3(requirementsPath, "utf-8");
|
|
1430
|
-
detected.stack.push("python");
|
|
1431
|
-
detected.type = "application";
|
|
1432
|
-
if (content.toLowerCase().includes("fastapi")) detected.stack.push("fastapi");
|
|
1433
|
-
if (content.toLowerCase().includes("django")) detected.stack.push("django");
|
|
1434
|
-
if (content.toLowerCase().includes("flask")) detected.stack.push("flask");
|
|
1435
|
-
detected.commands.test = "pytest";
|
|
1436
|
-
detected.commands.lint = "ruff check .";
|
|
1437
|
-
return detected;
|
|
1438
|
-
} catch {
|
|
1395
|
+
console.log(chalk6.gray("Proceeding with single file push..."));
|
|
1439
1396
|
}
|
|
1440
1397
|
}
|
|
1441
|
-
const
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1398
|
+
const inferredType = inferBlueprintType(file);
|
|
1399
|
+
const COMMAND_TYPES = [
|
|
1400
|
+
"CURSOR_COMMAND",
|
|
1401
|
+
"CLAUDE_COMMAND",
|
|
1402
|
+
"WINDSURF_WORKFLOW",
|
|
1403
|
+
"COPILOT_PROMPT",
|
|
1404
|
+
"CONTINUE_PROMPT",
|
|
1405
|
+
"OPENCODE_COMMAND"
|
|
1406
|
+
];
|
|
1407
|
+
const isCommandFile = COMMAND_TYPES.includes(inferredType);
|
|
1408
|
+
const commandNames = {
|
|
1409
|
+
"CURSOR_COMMAND": "Cursor",
|
|
1410
|
+
"CLAUDE_COMMAND": "Claude Code",
|
|
1411
|
+
"WINDSURF_WORKFLOW": "Windsurf",
|
|
1412
|
+
"COPILOT_PROMPT": "Copilot",
|
|
1413
|
+
"CONTINUE_PROMPT": "Continue",
|
|
1414
|
+
"OPENCODE_COMMAND": "OpenCode"
|
|
1415
|
+
};
|
|
1416
|
+
console.log(chalk6.cyan("\n\u{1F4E4} Push new blueprint"));
|
|
1417
|
+
console.log(chalk6.gray(` File: ${file}`));
|
|
1418
|
+
if (isCommandFile) {
|
|
1419
|
+
console.log(chalk6.magenta(` Type: ${commandNames[inferredType] || "Command"} Command`));
|
|
1420
|
+
} else {
|
|
1421
|
+
console.log(chalk6.gray(` Type: ${inferredType.replace(/_/g, " ")}`));
|
|
1461
1422
|
}
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1423
|
+
let name = options.name;
|
|
1424
|
+
let description = options.description;
|
|
1425
|
+
let visibility = options.visibility || "PRIVATE";
|
|
1426
|
+
let tags = options.tags ? options.tags.split(",").map((t) => t.trim()) : [];
|
|
1427
|
+
if (!options.yes) {
|
|
1428
|
+
const responses = await prompts2([
|
|
1429
|
+
{
|
|
1430
|
+
type: name ? null : "text",
|
|
1431
|
+
name: "name",
|
|
1432
|
+
message: "Blueprint name:",
|
|
1433
|
+
initial: filename.replace(/\.(md|mdc|json|yml|yaml)$/, ""),
|
|
1434
|
+
validate: (v) => v.length > 0 || "Name is required"
|
|
1435
|
+
},
|
|
1436
|
+
{
|
|
1437
|
+
type: description ? null : "text",
|
|
1438
|
+
name: "description",
|
|
1439
|
+
message: "Description:",
|
|
1440
|
+
initial: ""
|
|
1441
|
+
},
|
|
1442
|
+
{
|
|
1443
|
+
type: "select",
|
|
1444
|
+
name: "visibility",
|
|
1445
|
+
message: "Visibility:",
|
|
1446
|
+
choices: [
|
|
1447
|
+
{ title: "Private (only you)", value: "PRIVATE" },
|
|
1448
|
+
{ title: "Team (your team members)", value: "TEAM" },
|
|
1449
|
+
{ title: "Public (visible to everyone)", value: "PUBLIC" }
|
|
1450
|
+
],
|
|
1451
|
+
initial: 0
|
|
1452
|
+
},
|
|
1453
|
+
{
|
|
1454
|
+
type: "text",
|
|
1455
|
+
name: "tags",
|
|
1456
|
+
message: "Tags (comma-separated):",
|
|
1457
|
+
initial: ""
|
|
1472
1458
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
detected.commands.build = "go build";
|
|
1478
|
-
detected.commands.test = "go test ./...";
|
|
1479
|
-
detected.commands.lint = "golangci-lint run";
|
|
1480
|
-
detected.commands.dev = "go run .";
|
|
1481
|
-
return detected;
|
|
1482
|
-
} catch {
|
|
1459
|
+
]);
|
|
1460
|
+
if (!responses.name && !name) {
|
|
1461
|
+
console.log(chalk6.yellow("Push cancelled."));
|
|
1462
|
+
return;
|
|
1483
1463
|
}
|
|
1464
|
+
name = name || responses.name;
|
|
1465
|
+
description = description || responses.description || "";
|
|
1466
|
+
visibility = responses.visibility || visibility;
|
|
1467
|
+
tags = responses.tags ? responses.tags.split(",").map((t) => t.trim()).filter(Boolean) : tags;
|
|
1484
1468
|
}
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
try {
|
|
1488
|
-
const content = await readFile3(makefilePath, "utf-8");
|
|
1489
|
-
if (content.includes("build:")) detected.commands.build = "make build";
|
|
1490
|
-
if (content.includes("test:")) detected.commands.test = "make test";
|
|
1491
|
-
if (content.includes("lint:")) detected.commands.lint = "make lint";
|
|
1492
|
-
if (content.includes("dev:")) detected.commands.dev = "make dev";
|
|
1493
|
-
if (content.includes("run:")) detected.commands.dev = detected.commands.dev || "make run";
|
|
1494
|
-
if (Object.keys(detected.commands).length > 0) {
|
|
1495
|
-
detected.type = "application";
|
|
1496
|
-
return detected;
|
|
1497
|
-
}
|
|
1498
|
-
} catch {
|
|
1499
|
-
}
|
|
1469
|
+
if (!name) {
|
|
1470
|
+
name = filename.replace(/\.(md|mdc|json|yml|yaml)$/, "");
|
|
1500
1471
|
}
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1472
|
+
const hierarchyInfo = await detectHierarchyInfo(cwd, file);
|
|
1473
|
+
let hierarchyId = null;
|
|
1474
|
+
if (hierarchyInfo.repositoryPath) {
|
|
1475
|
+
hierarchyId = await ensureHierarchy(cwd, hierarchyInfo.repositoryRoot, path.basename(cwd));
|
|
1505
1476
|
}
|
|
1506
|
-
const
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
} else if (lowerContent.includes("unlicense") || lowerContent.includes("this is free and unencumbered software")) {
|
|
1526
|
-
detected.license = "unlicense";
|
|
1527
|
-
}
|
|
1528
|
-
} catch {
|
|
1529
|
-
}
|
|
1477
|
+
const existingBlueprint = await findExistingBlueprintOnServer(
|
|
1478
|
+
hierarchyInfo.repositoryPath,
|
|
1479
|
+
hierarchyId
|
|
1480
|
+
);
|
|
1481
|
+
if (existingBlueprint) {
|
|
1482
|
+
console.log(chalk6.cyan(`
|
|
1483
|
+
\u2139 Linked to existing blueprint "${existingBlueprint.name}" (${existingBlueprint.id}).`));
|
|
1484
|
+
console.log(chalk6.gray(" Pushing as an update."));
|
|
1485
|
+
await trackBlueprint(cwd, {
|
|
1486
|
+
id: existingBlueprint.id,
|
|
1487
|
+
name: existingBlueprint.name,
|
|
1488
|
+
file,
|
|
1489
|
+
content,
|
|
1490
|
+
source: "private",
|
|
1491
|
+
hierarchyId: hierarchyId || void 0,
|
|
1492
|
+
repositoryPath: hierarchyInfo.repositoryPath || void 0
|
|
1493
|
+
});
|
|
1494
|
+
await updateBlueprint(cwd, file, existingBlueprint.id, content, options);
|
|
1495
|
+
return;
|
|
1530
1496
|
}
|
|
1531
|
-
const
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1497
|
+
const spinner = ora5("Creating blueprint...").start();
|
|
1498
|
+
try {
|
|
1499
|
+
const result = await api.createBlueprint({
|
|
1500
|
+
name,
|
|
1501
|
+
description: description || "",
|
|
1502
|
+
content,
|
|
1503
|
+
visibility,
|
|
1504
|
+
tags,
|
|
1505
|
+
type: inferredType,
|
|
1506
|
+
// Include the inferred type (AGENTS_MD, CURSOR_COMMAND, etc.)
|
|
1507
|
+
// Include hierarchy info if detected
|
|
1508
|
+
hierarchy_id: hierarchyId,
|
|
1509
|
+
parent_id: hierarchyInfo.parentId,
|
|
1510
|
+
repository_path: hierarchyInfo.repositoryPath
|
|
1511
|
+
});
|
|
1512
|
+
spinner.succeed("Blueprint created!");
|
|
1513
|
+
await trackBlueprint(cwd, {
|
|
1514
|
+
id: result.blueprint.id,
|
|
1515
|
+
name: result.blueprint.name,
|
|
1516
|
+
file,
|
|
1517
|
+
content,
|
|
1518
|
+
source: "private",
|
|
1519
|
+
hierarchyId: hierarchyId || void 0,
|
|
1520
|
+
repositoryPath: hierarchyInfo.repositoryPath || void 0
|
|
1521
|
+
});
|
|
1522
|
+
console.log();
|
|
1523
|
+
const typeDesc = isCommandFile ? `${commandNames[inferredType]} Command` : "Blueprint";
|
|
1524
|
+
console.log(chalk6.green(`\u2705 Created ${typeDesc} ${chalk6.bold(result.blueprint.name)}`));
|
|
1525
|
+
console.log(chalk6.gray(` ID: ${result.blueprint.id}`));
|
|
1526
|
+
console.log(chalk6.gray(` Type: ${inferredType}`));
|
|
1527
|
+
console.log(chalk6.gray(` Visibility: ${visibility}`));
|
|
1528
|
+
if (hierarchyInfo.repositoryPath) {
|
|
1529
|
+
console.log(chalk6.gray(` Path: ${hierarchyInfo.repositoryPath}`));
|
|
1552
1530
|
}
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
detected.cicd = "circleci";
|
|
1562
|
-
} else if (await fileExists(join3(cwd, ".travis.yml"))) {
|
|
1563
|
-
detected.cicd = "travis";
|
|
1564
|
-
} else if (await fileExists(join3(cwd, "azure-pipelines.yml"))) {
|
|
1565
|
-
detected.cicd = "azure_devops";
|
|
1566
|
-
} else if (await fileExists(join3(cwd, "bitbucket-pipelines.yml"))) {
|
|
1567
|
-
detected.cicd = "bitbucket";
|
|
1568
|
-
} else if (await fileExists(join3(cwd, ".drone.yml"))) {
|
|
1569
|
-
detected.cicd = "drone";
|
|
1570
|
-
}
|
|
1571
|
-
detected.existingFiles = [];
|
|
1572
|
-
const staticFiles = [
|
|
1573
|
-
".editorconfig",
|
|
1574
|
-
"CONTRIBUTING.md",
|
|
1575
|
-
"CODE_OF_CONDUCT.md",
|
|
1576
|
-
"SECURITY.md",
|
|
1577
|
-
"ROADMAP.md",
|
|
1578
|
-
".gitignore",
|
|
1579
|
-
".github/FUNDING.yml",
|
|
1580
|
-
"LICENSE",
|
|
1581
|
-
"README.md",
|
|
1582
|
-
"ARCHITECTURE.md",
|
|
1583
|
-
"CHANGELOG.md"
|
|
1584
|
-
];
|
|
1585
|
-
for (const file of staticFiles) {
|
|
1586
|
-
if (await fileExists(join3(cwd, file))) {
|
|
1587
|
-
detected.existingFiles.push(file);
|
|
1531
|
+
if (result.blueprint.hierarchy_id) {
|
|
1532
|
+
console.log(chalk6.gray(` Hierarchy: ${result.blueprint.hierarchy_id}`));
|
|
1533
|
+
}
|
|
1534
|
+
if (hierarchyInfo.parentId) {
|
|
1535
|
+
console.log(chalk6.cyan(` \u21B3 Linked to parent blueprint: ${hierarchyInfo.parentId}`));
|
|
1536
|
+
}
|
|
1537
|
+
if (visibility === "PUBLIC") {
|
|
1538
|
+
console.log(chalk6.gray(` View: https://lynxprompt.com/templates/${result.blueprint.id.replace("bp_", "")}`));
|
|
1588
1539
|
}
|
|
1540
|
+
console.log();
|
|
1541
|
+
console.log(chalk6.cyan("The file is now linked. Future 'lynxp push' will update this blueprint."));
|
|
1542
|
+
} catch (error) {
|
|
1543
|
+
spinner.fail("Failed to create blueprint");
|
|
1544
|
+
handleError(error);
|
|
1589
1545
|
}
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1546
|
+
}
|
|
1547
|
+
async function pushHierarchy(cwd, files, options) {
|
|
1548
|
+
let hierarchyName = options.name || path.basename(cwd);
|
|
1549
|
+
let visibility = options.visibility || "PRIVATE";
|
|
1550
|
+
if (!options.yes) {
|
|
1551
|
+
const responses = await prompts2([
|
|
1552
|
+
{
|
|
1553
|
+
type: "text",
|
|
1554
|
+
name: "name",
|
|
1555
|
+
message: "Hierarchy name:",
|
|
1556
|
+
initial: hierarchyName,
|
|
1557
|
+
validate: (v) => v.length > 0 || "Name is required"
|
|
1558
|
+
},
|
|
1559
|
+
{
|
|
1560
|
+
type: "select",
|
|
1561
|
+
name: "visibility",
|
|
1562
|
+
message: "Visibility for all blueprints:",
|
|
1563
|
+
choices: [
|
|
1564
|
+
{ title: "Private (only you)", value: "PRIVATE" },
|
|
1565
|
+
{ title: "Team (your team members)", value: "TEAM" },
|
|
1566
|
+
{ title: "Public (visible to everyone)", value: "PUBLIC" }
|
|
1567
|
+
],
|
|
1568
|
+
initial: 0
|
|
1604
1569
|
}
|
|
1570
|
+
]);
|
|
1571
|
+
if (!responses.name) {
|
|
1572
|
+
console.log(chalk6.yellow("Push cancelled."));
|
|
1573
|
+
return;
|
|
1605
1574
|
}
|
|
1575
|
+
hierarchyName = responses.name;
|
|
1576
|
+
visibility = responses.visibility || visibility;
|
|
1606
1577
|
}
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1578
|
+
console.log();
|
|
1579
|
+
console.log(chalk6.cyan(`\u{1F4C1} Syncing hierarchy "${hierarchyName}" with ${files.length} files...`));
|
|
1580
|
+
console.log();
|
|
1581
|
+
const repositoryRoot = createRepositoryRoot(cwd);
|
|
1582
|
+
let hierarchyId;
|
|
1611
1583
|
try {
|
|
1612
|
-
await
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1584
|
+
const hierarchyResponse = await api.createHierarchy({
|
|
1585
|
+
name: hierarchyName,
|
|
1586
|
+
repository_root: repositoryRoot
|
|
1587
|
+
});
|
|
1588
|
+
hierarchyId = hierarchyResponse.hierarchy.id;
|
|
1589
|
+
console.log(chalk6.green(`\u2713 Hierarchy: ${hierarchyId}`));
|
|
1590
|
+
} catch (error) {
|
|
1591
|
+
console.log(chalk6.red("Failed to create hierarchy"));
|
|
1592
|
+
handleError(error);
|
|
1593
|
+
return;
|
|
1616
1594
|
}
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1595
|
+
let rootBlueprintId = null;
|
|
1596
|
+
let createCount = 0;
|
|
1597
|
+
let updateCount = 0;
|
|
1598
|
+
let failCount = 0;
|
|
1599
|
+
for (const file of files) {
|
|
1600
|
+
const spinner = ora5(`Processing ${file.path}...`).start();
|
|
1601
|
+
try {
|
|
1602
|
+
const content = fs.readFileSync(file.absolutePath, "utf-8");
|
|
1603
|
+
const tracked = await findBlueprintByFile(cwd, file.path);
|
|
1604
|
+
if (tracked) {
|
|
1605
|
+
const updateData = { content };
|
|
1606
|
+
if (tracked.checksum && !options.force) {
|
|
1607
|
+
updateData.expected_checksum = tracked.checksum;
|
|
1608
|
+
}
|
|
1609
|
+
await api.updateBlueprint(tracked.id, updateData);
|
|
1610
|
+
await updateChecksum(cwd, file.path, content);
|
|
1611
|
+
if (file.isRoot) rootBlueprintId = tracked.id;
|
|
1612
|
+
spinner.succeed(`${file.path} \u2192 updated (${tracked.id})`);
|
|
1613
|
+
updateCount++;
|
|
1614
|
+
continue;
|
|
1615
|
+
}
|
|
1616
|
+
const existing = await findExistingBlueprintOnServer(file.path, hierarchyId);
|
|
1617
|
+
if (existing) {
|
|
1618
|
+
await api.updateBlueprint(existing.id, { content });
|
|
1619
|
+
await trackBlueprint(cwd, {
|
|
1620
|
+
id: existing.id,
|
|
1621
|
+
name: existing.name,
|
|
1622
|
+
file: file.path,
|
|
1623
|
+
content,
|
|
1624
|
+
source: "private",
|
|
1625
|
+
hierarchyId,
|
|
1626
|
+
hierarchyName,
|
|
1627
|
+
repositoryPath: file.path
|
|
1628
|
+
});
|
|
1629
|
+
if (file.isRoot) rootBlueprintId = existing.id;
|
|
1630
|
+
spinner.succeed(`${file.path} \u2192 linked & updated (${existing.id})`);
|
|
1631
|
+
updateCount++;
|
|
1632
|
+
continue;
|
|
1633
|
+
}
|
|
1634
|
+
const blueprintName = file.isRoot ? hierarchyName : path.basename(file.path) === "AGENTS.md" ? path.dirname(file.path).replace(/[/\\]/g, " / ") : file.path.replace(/\\/g, "/");
|
|
1635
|
+
const parentId = !file.isRoot && file.type === "AGENTS_MD" && rootBlueprintId ? rootBlueprintId : null;
|
|
1636
|
+
const result = await api.createBlueprint({
|
|
1637
|
+
name: blueprintName,
|
|
1638
|
+
description: "",
|
|
1639
|
+
content,
|
|
1640
|
+
visibility,
|
|
1641
|
+
tags: [],
|
|
1642
|
+
type: file.type || inferBlueprintType(file.path),
|
|
1643
|
+
hierarchy_id: hierarchyId,
|
|
1644
|
+
parent_id: parentId,
|
|
1645
|
+
repository_path: file.path
|
|
1646
|
+
});
|
|
1647
|
+
if (file.isRoot) rootBlueprintId = result.blueprint.id;
|
|
1648
|
+
await trackBlueprint(cwd, {
|
|
1649
|
+
id: result.blueprint.id,
|
|
1650
|
+
name: blueprintName,
|
|
1651
|
+
file: file.path,
|
|
1652
|
+
content,
|
|
1653
|
+
source: "private",
|
|
1654
|
+
hierarchyId,
|
|
1655
|
+
hierarchyName,
|
|
1656
|
+
repositoryPath: file.path
|
|
1657
|
+
});
|
|
1658
|
+
spinner.succeed(`${file.path} \u2192 created (${result.blueprint.id})`);
|
|
1659
|
+
createCount++;
|
|
1660
|
+
} catch (error) {
|
|
1661
|
+
spinner.fail(`${file.path} failed`);
|
|
1662
|
+
if (error instanceof ApiRequestError) {
|
|
1663
|
+
console.log(chalk6.red(` Error: ${error.message}`));
|
|
1664
|
+
}
|
|
1665
|
+
failCount++;
|
|
1636
1666
|
}
|
|
1637
1667
|
}
|
|
1638
|
-
|
|
1668
|
+
console.log();
|
|
1669
|
+
console.log(chalk6.green(`\u2705 Hierarchy sync complete!`));
|
|
1670
|
+
console.log(chalk6.gray(` Hierarchy: ${hierarchyId}`));
|
|
1671
|
+
console.log(chalk6.gray(` Name: ${hierarchyName}`));
|
|
1672
|
+
const parts = [];
|
|
1673
|
+
if (createCount > 0) parts.push(`${createCount} created`);
|
|
1674
|
+
if (updateCount > 0) parts.push(`${updateCount} updated`);
|
|
1675
|
+
if (failCount > 0) parts.push(`${failCount} failed`);
|
|
1676
|
+
console.log(chalk6.gray(` Results: ${parts.join(", ")}`));
|
|
1677
|
+
console.log();
|
|
1678
|
+
console.log(chalk6.cyan("Tips:"));
|
|
1679
|
+
console.log(chalk6.gray(` \u2022 Run 'lynxp status' to see all tracked blueprints`));
|
|
1680
|
+
console.log(chalk6.gray(` \u2022 Run 'lynxp push --all' again to sync changes`));
|
|
1681
|
+
console.log();
|
|
1639
1682
|
}
|
|
1640
|
-
function
|
|
1641
|
-
const
|
|
1642
|
-
|
|
1643
|
-
|
|
1683
|
+
function findDefaultFile() {
|
|
1684
|
+
const candidates = [
|
|
1685
|
+
"AGENTS.md",
|
|
1686
|
+
"CLAUDE.md",
|
|
1687
|
+
".cursor/rules/project.mdc",
|
|
1688
|
+
".github/copilot-instructions.md",
|
|
1689
|
+
".windsurfrules",
|
|
1690
|
+
"AIDER.md",
|
|
1691
|
+
"GEMINI.md",
|
|
1692
|
+
".clinerules"
|
|
1644
1693
|
];
|
|
1645
|
-
for (const
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
const host = match[1];
|
|
1649
|
-
const path2 = match[2].replace(/\.git$/, "");
|
|
1650
|
-
if (host.includes("gitlab") || url.toLowerCase().includes("gitlab")) {
|
|
1651
|
-
return { path: path2, host };
|
|
1652
|
-
}
|
|
1694
|
+
for (const candidate of candidates) {
|
|
1695
|
+
if (fs.existsSync(candidate)) {
|
|
1696
|
+
return candidate;
|
|
1653
1697
|
}
|
|
1654
1698
|
}
|
|
1655
1699
|
return null;
|
|
1656
1700
|
}
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
const detected = {
|
|
1673
|
-
name: repoInfo.name,
|
|
1674
|
-
description: repoInfo.description ?? void 0,
|
|
1675
|
-
stack: [],
|
|
1676
|
-
databases: [],
|
|
1677
|
-
commands: {},
|
|
1678
|
-
packageManager: null,
|
|
1679
|
-
type: "application",
|
|
1680
|
-
repoHost: "github",
|
|
1681
|
-
repoUrl,
|
|
1682
|
-
license: licenseId ?? void 0,
|
|
1683
|
-
isPublicRepo: !repoInfo.private,
|
|
1684
|
-
isOpenSource,
|
|
1685
|
-
projectType: isOpenSource ? "open_source" : void 0,
|
|
1686
|
-
hasDocker: false,
|
|
1687
|
-
existingFiles: []
|
|
1688
|
-
};
|
|
1689
|
-
const filesRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/`, {
|
|
1690
|
-
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
1691
|
-
});
|
|
1692
|
-
if (!filesRes.ok) return detected;
|
|
1693
|
-
const files = await filesRes.json();
|
|
1694
|
-
const fileNames = new Set(files.map((f) => f.name.toLowerCase()));
|
|
1695
|
-
for (const file of STATIC_FILES) {
|
|
1696
|
-
if (file.includes("/")) continue;
|
|
1697
|
-
if (files.some((f) => f.name.toLowerCase() === file.toLowerCase())) {
|
|
1698
|
-
detected.existingFiles.push(file);
|
|
1699
|
-
}
|
|
1701
|
+
function inferBlueprintType(filePath) {
|
|
1702
|
+
const configMatch = matchConfigFile(filePath);
|
|
1703
|
+
if (configMatch) return configMatch.type;
|
|
1704
|
+
if (filePath.endsWith(".md") || filePath.endsWith(".mdc")) return "AGENTS_MD";
|
|
1705
|
+
return "CUSTOM";
|
|
1706
|
+
}
|
|
1707
|
+
function handleError(error) {
|
|
1708
|
+
if (error instanceof ApiRequestError) {
|
|
1709
|
+
console.error(chalk6.red(`Error: ${error.message}`));
|
|
1710
|
+
if (error.statusCode === 401) {
|
|
1711
|
+
console.error(chalk6.gray("Your session may have expired. Run 'lynxp login' to re-authenticate."));
|
|
1712
|
+
} else if (error.statusCode === 403) {
|
|
1713
|
+
console.error(chalk6.gray("You don't have permission to modify this blueprint."));
|
|
1714
|
+
} else if (error.statusCode === 404) {
|
|
1715
|
+
console.error(chalk6.gray("Blueprint not found. It may have been deleted."));
|
|
1700
1716
|
}
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1717
|
+
} else {
|
|
1718
|
+
console.error(chalk6.red("An unexpected error occurred."));
|
|
1719
|
+
}
|
|
1720
|
+
process.exit(1);
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
// src/commands/wizard.ts
|
|
1724
|
+
import chalk7 from "chalk";
|
|
1725
|
+
import prompts3 from "prompts";
|
|
1726
|
+
import ora6 from "ora";
|
|
1727
|
+
import * as readline from "readline";
|
|
1728
|
+
import * as os from "os";
|
|
1729
|
+
import { writeFile as writeFile3, mkdir as mkdir3, access as access3, readFile as readFile4 } from "fs/promises";
|
|
1730
|
+
import { join as join4, dirname as dirname3 } from "path";
|
|
1731
|
+
|
|
1732
|
+
// src/utils/detect.ts
|
|
1733
|
+
import { readFile as readFile3, access as access2, rm, mkdtemp } from "fs/promises";
|
|
1734
|
+
import { join as join3 } from "path";
|
|
1735
|
+
import { tmpdir } from "os";
|
|
1736
|
+
import { spawnSync } from "child_process";
|
|
1737
|
+
var JS_FRAMEWORK_PATTERNS = {
|
|
1738
|
+
nextjs: ["next"],
|
|
1739
|
+
react: ["react", "react-dom"],
|
|
1740
|
+
vue: ["vue"],
|
|
1741
|
+
angular: ["@angular/core"],
|
|
1742
|
+
svelte: ["svelte", "@sveltejs/kit"],
|
|
1743
|
+
solid: ["solid-js"],
|
|
1744
|
+
remix: ["@remix-run/react"],
|
|
1745
|
+
astro: ["astro"],
|
|
1746
|
+
nuxt: ["nuxt"],
|
|
1747
|
+
gatsby: ["gatsby"]
|
|
1748
|
+
};
|
|
1749
|
+
var JS_TOOL_PATTERNS = {
|
|
1750
|
+
typescript: ["typescript"],
|
|
1751
|
+
tailwind: ["tailwindcss"],
|
|
1752
|
+
prisma: ["prisma", "@prisma/client"],
|
|
1753
|
+
drizzle: ["drizzle-orm"],
|
|
1754
|
+
express: ["express"],
|
|
1755
|
+
fastify: ["fastify"],
|
|
1756
|
+
hono: ["hono"],
|
|
1757
|
+
elysia: ["elysia"],
|
|
1758
|
+
trpc: ["@trpc/server"],
|
|
1759
|
+
graphql: ["graphql", "@apollo/server"],
|
|
1760
|
+
jest: ["jest"],
|
|
1761
|
+
vitest: ["vitest"],
|
|
1762
|
+
playwright: ["@playwright/test"],
|
|
1763
|
+
cypress: ["cypress"],
|
|
1764
|
+
eslint: ["eslint"],
|
|
1765
|
+
biome: ["@biomejs/biome"],
|
|
1766
|
+
prettier: ["prettier"],
|
|
1767
|
+
vite: ["vite"],
|
|
1768
|
+
webpack: ["webpack"],
|
|
1769
|
+
turbo: ["turbo"]
|
|
1770
|
+
};
|
|
1771
|
+
async function detectExtendedCommands(cwd) {
|
|
1772
|
+
const cmds = {
|
|
1773
|
+
test: [],
|
|
1774
|
+
testCoverage: [],
|
|
1775
|
+
install: [],
|
|
1776
|
+
dev: [],
|
|
1777
|
+
build: [],
|
|
1778
|
+
lint: [],
|
|
1779
|
+
format: [],
|
|
1780
|
+
typecheck: [],
|
|
1781
|
+
clean: [],
|
|
1782
|
+
preCommit: [],
|
|
1783
|
+
additional: []
|
|
1784
|
+
};
|
|
1785
|
+
const addCmd = (category, cmd, desc) => {
|
|
1786
|
+
if (!cmds[category].some((c) => c.cmd === cmd)) {
|
|
1787
|
+
cmds[category].push({ cmd, desc });
|
|
1711
1788
|
}
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
else
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1789
|
+
};
|
|
1790
|
+
const packageJsonPath = join3(cwd, "package.json");
|
|
1791
|
+
if (await fileExists(packageJsonPath)) {
|
|
1792
|
+
try {
|
|
1793
|
+
const content = await readFile3(packageJsonPath, "utf-8");
|
|
1794
|
+
const pkg = JSON.parse(content);
|
|
1795
|
+
if (pkg.scripts) {
|
|
1796
|
+
for (const [name, script] of Object.entries(pkg.scripts)) {
|
|
1797
|
+
const scriptStr = String(script);
|
|
1798
|
+
const fullCmd = `npm run ${name}`;
|
|
1799
|
+
if (name.match(/^test$|^test:/i) || scriptStr.includes("jest") || scriptStr.includes("vitest") || scriptStr.includes("mocha")) {
|
|
1800
|
+
if (name.includes("cov") || scriptStr.includes("--coverage")) {
|
|
1801
|
+
addCmd("testCoverage", fullCmd, `Run ${name}`);
|
|
1802
|
+
} else {
|
|
1803
|
+
addCmd("test", fullCmd, `Run ${name}`);
|
|
1804
|
+
}
|
|
1805
|
+
} else if (name.match(/^lint$|^lint:/i) || scriptStr.includes("eslint") || scriptStr.includes("biome")) {
|
|
1806
|
+
addCmd("lint", fullCmd, `Run ${name}`);
|
|
1807
|
+
} else if (name.match(/^format$|^fmt$|format:/i) || scriptStr.includes("prettier")) {
|
|
1808
|
+
addCmd("format", fullCmd, `Run ${name}`);
|
|
1809
|
+
} else if (name.match(/^build$|^build:/i) || scriptStr.includes("tsc") || scriptStr.includes("webpack") || scriptStr.includes("vite build")) {
|
|
1810
|
+
addCmd("build", fullCmd, `Run ${name}`);
|
|
1811
|
+
} else if (name.match(/^dev$|^start$|^serve$/i)) {
|
|
1812
|
+
addCmd("dev", fullCmd, `Run ${name}`);
|
|
1813
|
+
} else if (name.match(/^typecheck$|^type-check$|^types$|^check:types/i) || scriptStr.includes("tsc --noEmit")) {
|
|
1814
|
+
addCmd("typecheck", fullCmd, `Run ${name}`);
|
|
1815
|
+
} else if (name.match(/^clean$|^clean:/i) || scriptStr.includes("rimraf") || scriptStr.includes("rm -rf")) {
|
|
1816
|
+
addCmd("clean", fullCmd, `Run ${name}`);
|
|
1817
|
+
} else if (name.match(/^prepare$|^precommit$|^pre-commit$|^husky/i)) {
|
|
1818
|
+
addCmd("preCommit", fullCmd, `Run ${name}`);
|
|
1819
|
+
} else if (name === "install" || name === "postinstall") {
|
|
1820
|
+
addCmd("install", fullCmd, `Run ${name}`);
|
|
1821
|
+
} else if (!["publish", "prepublish", "prepublishOnly", "version", "postversion"].includes(name)) {
|
|
1822
|
+
addCmd("additional", fullCmd, `Run ${name}`);
|
|
1736
1823
|
}
|
|
1737
1824
|
}
|
|
1738
1825
|
}
|
|
1826
|
+
} catch {
|
|
1739
1827
|
}
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
}
|
|
1828
|
+
}
|
|
1829
|
+
const pyprojectPath = join3(cwd, "pyproject.toml");
|
|
1830
|
+
if (await fileExists(pyprojectPath)) {
|
|
1831
|
+
try {
|
|
1832
|
+
const content = await readFile3(pyprojectPath, "utf-8");
|
|
1833
|
+
if (content.includes("pytest") || content.includes("[tool.pytest")) {
|
|
1834
|
+
addCmd("test", "python -m pytest tests/ -v --tb=short", "Run pytest");
|
|
1835
|
+
addCmd("testCoverage", "python -m pytest tests/ --cov=src --cov-report=term-missing", "Run pytest with coverage");
|
|
1749
1836
|
}
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
if (
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
else if (lowerContent.includes("unittest")) detected.testFramework = "unittest";
|
|
1769
|
-
detected.commands.test = "pytest";
|
|
1770
|
-
if (lowerContent.includes("ruff")) detected.commands.lint = "ruff check .";
|
|
1771
|
-
if (lowerContent.includes("asyncpg") || lowerContent.includes("psycopg")) {
|
|
1772
|
-
if (!detected.databases.includes("postgresql")) detected.databases.push("postgresql");
|
|
1773
|
-
}
|
|
1774
|
-
if (lowerContent.includes("aiosqlite") || lowerContent.includes("sqlite")) {
|
|
1775
|
-
if (!detected.databases.includes("sqlite")) detected.databases.push("sqlite");
|
|
1776
|
-
}
|
|
1777
|
-
if (lowerContent.includes("pymongo") || lowerContent.includes("motor")) {
|
|
1778
|
-
if (!detected.databases.includes("mongodb")) detected.databases.push("mongodb");
|
|
1779
|
-
}
|
|
1780
|
-
if (lowerContent.includes("redis") || lowerContent.includes("aioredis")) {
|
|
1781
|
-
if (!detected.databases.includes("redis")) detected.databases.push("redis");
|
|
1782
|
-
}
|
|
1783
|
-
if (lowerContent.includes("pymysql") || lowerContent.includes("aiomysql")) {
|
|
1784
|
-
if (!detected.databases.includes("mysql")) detected.databases.push("mysql");
|
|
1837
|
+
if (content.includes("ruff")) {
|
|
1838
|
+
addCmd("lint", "ruff check .", "Run ruff linter");
|
|
1839
|
+
addCmd("format", "ruff format .", "Run ruff formatter");
|
|
1840
|
+
}
|
|
1841
|
+
if (content.includes("black")) {
|
|
1842
|
+
addCmd("format", "black .", "Run black formatter");
|
|
1843
|
+
}
|
|
1844
|
+
if (content.includes("mypy")) {
|
|
1845
|
+
addCmd("typecheck", "mypy .", "Run mypy type checker");
|
|
1846
|
+
}
|
|
1847
|
+
const poetryScriptsMatch = content.match(/\[tool\.poetry\.scripts\]([\s\S]*?)(?=\n\[|$)/);
|
|
1848
|
+
if (poetryScriptsMatch) {
|
|
1849
|
+
const scriptLines = poetryScriptsMatch[1].split("\n").filter((l) => l.includes("="));
|
|
1850
|
+
for (const line of scriptLines) {
|
|
1851
|
+
const match = line.match(/(\w+)\s*=\s*"([^"]+)"/);
|
|
1852
|
+
if (match) {
|
|
1853
|
+
const [, name, entry] = match;
|
|
1854
|
+
addCmd("additional", `poetry run ${name}`, entry);
|
|
1785
1855
|
}
|
|
1786
|
-
} catch {
|
|
1787
1856
|
}
|
|
1788
1857
|
}
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
}
|
|
1806
|
-
if (content.includes("pymongo") || content.includes("motor")) {
|
|
1807
|
-
if (!detected.databases.includes("mongodb")) detected.databases.push("mongodb");
|
|
1858
|
+
const poeMatch = content.match(/\[tool\.poe\.tasks\]([\s\S]*?)(?=\n\[tool\.|$)/);
|
|
1859
|
+
if (poeMatch) {
|
|
1860
|
+
const taskLines = poeMatch[1].split("\n").filter((l) => l.includes("="));
|
|
1861
|
+
for (const line of taskLines) {
|
|
1862
|
+
const match = line.match(/(\w+)\s*=\s*"([^"]+)"/);
|
|
1863
|
+
if (match) {
|
|
1864
|
+
const [, name, cmd] = match;
|
|
1865
|
+
if (name.match(/test/i)) {
|
|
1866
|
+
addCmd("test", `poe ${name}`, cmd);
|
|
1867
|
+
} else if (name.match(/lint/i)) {
|
|
1868
|
+
addCmd("lint", `poe ${name}`, cmd);
|
|
1869
|
+
} else if (name.match(/format/i)) {
|
|
1870
|
+
addCmd("format", `poe ${name}`, cmd);
|
|
1871
|
+
} else {
|
|
1872
|
+
addCmd("additional", `poe ${name}`, cmd);
|
|
1873
|
+
}
|
|
1808
1874
|
}
|
|
1809
|
-
} catch {
|
|
1810
1875
|
}
|
|
1811
1876
|
}
|
|
1877
|
+
if (content.includes("fastapi") || content.includes("uvicorn")) {
|
|
1878
|
+
addCmd("dev", "uvicorn src.main:app --host 0.0.0.0 --port 8000 --reload", "Run FastAPI dev server");
|
|
1879
|
+
}
|
|
1880
|
+
if (content.includes("[tool.poetry]")) {
|
|
1881
|
+
addCmd("install", "poetry install", "Install dependencies with Poetry");
|
|
1882
|
+
} else if (await fileExists(join3(cwd, "uv.lock"))) {
|
|
1883
|
+
addCmd("install", "uv sync", "Sync dependencies with uv");
|
|
1884
|
+
} else {
|
|
1885
|
+
addCmd("install", "pip install -r requirements.txt", "Install dependencies with pip");
|
|
1886
|
+
}
|
|
1887
|
+
} catch {
|
|
1812
1888
|
}
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
}
|
|
1854
|
-
if (allDeps["redis"] || allDeps["ioredis"]) {
|
|
1855
|
-
if (!detected.databases.includes("redis")) detected.databases.push("redis");
|
|
1856
|
-
}
|
|
1857
|
-
if (allDeps["mysql"] || allDeps["mysql2"]) {
|
|
1858
|
-
if (!detected.databases.includes("mysql")) detected.databases.push("mysql");
|
|
1859
|
-
}
|
|
1860
|
-
} catch {
|
|
1889
|
+
}
|
|
1890
|
+
const requirementsPath = join3(cwd, "requirements.txt");
|
|
1891
|
+
if (await fileExists(requirementsPath)) {
|
|
1892
|
+
try {
|
|
1893
|
+
const content = await readFile3(requirementsPath, "utf-8");
|
|
1894
|
+
if (content.includes("pytest")) {
|
|
1895
|
+
addCmd("test", "python -m pytest tests/ -v", "Run pytest");
|
|
1896
|
+
}
|
|
1897
|
+
addCmd("install", "pip install -r requirements.txt", "Install dependencies");
|
|
1898
|
+
} catch {
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
const makefilePath = join3(cwd, "Makefile");
|
|
1902
|
+
if (await fileExists(makefilePath)) {
|
|
1903
|
+
try {
|
|
1904
|
+
const content = await readFile3(makefilePath, "utf-8");
|
|
1905
|
+
const targetMatches = content.matchAll(/^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(?:[a-zA-Z0-9_\- ]*)?$/gm);
|
|
1906
|
+
for (const match of targetMatches) {
|
|
1907
|
+
const target = match[1];
|
|
1908
|
+
const cmd = `make ${target}`;
|
|
1909
|
+
if (target.match(/^test$|^tests$/i)) {
|
|
1910
|
+
addCmd("test", cmd, `Make ${target}`);
|
|
1911
|
+
} else if (target.match(/^test[-_]?cov|^coverage$/i)) {
|
|
1912
|
+
addCmd("testCoverage", cmd, `Make ${target}`);
|
|
1913
|
+
} else if (target.match(/^lint$/i)) {
|
|
1914
|
+
addCmd("lint", cmd, `Make ${target}`);
|
|
1915
|
+
} else if (target.match(/^format$|^fmt$/i)) {
|
|
1916
|
+
addCmd("format", cmd, `Make ${target}`);
|
|
1917
|
+
} else if (target.match(/^build$/i)) {
|
|
1918
|
+
addCmd("build", cmd, `Make ${target}`);
|
|
1919
|
+
} else if (target.match(/^dev$|^run$|^serve$/i)) {
|
|
1920
|
+
addCmd("dev", cmd, `Make ${target}`);
|
|
1921
|
+
} else if (target.match(/^typecheck$|^types$/i)) {
|
|
1922
|
+
addCmd("typecheck", cmd, `Make ${target}`);
|
|
1923
|
+
} else if (target.match(/^clean$/i)) {
|
|
1924
|
+
addCmd("clean", cmd, `Make ${target}`);
|
|
1925
|
+
} else if (target.match(/^install$|^deps$/i)) {
|
|
1926
|
+
addCmd("install", cmd, `Make ${target}`);
|
|
1927
|
+
} else if (!["all", "default", ".PHONY", ".DEFAULT_GOAL"].includes(target)) {
|
|
1928
|
+
addCmd("additional", cmd, `Make ${target}`);
|
|
1861
1929
|
}
|
|
1862
1930
|
}
|
|
1931
|
+
} catch {
|
|
1863
1932
|
}
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1933
|
+
}
|
|
1934
|
+
const dockerComposePath = join3(cwd, "docker-compose.yml");
|
|
1935
|
+
const dockerComposeYamlPath = join3(cwd, "docker-compose.yaml");
|
|
1936
|
+
const composePath = await fileExists(dockerComposePath) ? dockerComposePath : await fileExists(dockerComposeYamlPath) ? dockerComposeYamlPath : null;
|
|
1937
|
+
if (composePath) {
|
|
1938
|
+
try {
|
|
1939
|
+
const content = await readFile3(composePath, "utf-8");
|
|
1940
|
+
const serviceMatches = content.matchAll(/^\s{2}([a-zA-Z_][a-zA-Z0-9_-]*):\s*$/gm);
|
|
1941
|
+
for (const match of serviceMatches) {
|
|
1942
|
+
const service = match[1];
|
|
1943
|
+
addCmd("additional", `docker compose up ${service}`, `Run ${service} service`);
|
|
1944
|
+
}
|
|
1945
|
+
addCmd("dev", "docker compose up", "Start all services");
|
|
1946
|
+
addCmd("build", "docker compose build", "Build all services");
|
|
1947
|
+
addCmd("clean", "docker compose down -v", "Stop and remove volumes");
|
|
1948
|
+
} catch {
|
|
1868
1949
|
}
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1950
|
+
}
|
|
1951
|
+
const dockerfilePath = join3(cwd, "Dockerfile");
|
|
1952
|
+
if (await fileExists(dockerfilePath)) {
|
|
1953
|
+
try {
|
|
1954
|
+
const content = await readFile3(dockerfilePath, "utf-8");
|
|
1955
|
+
const fromMatch = content.match(/FROM\s+([^\s]+)/);
|
|
1956
|
+
const imageName = fromMatch ? fromMatch[1].split(":")[0] : "app";
|
|
1957
|
+
addCmd("build", `docker build -t ${imageName} .`, "Build Docker image");
|
|
1958
|
+
addCmd("dev", `docker run -it --rm ${imageName}`, "Run Docker container");
|
|
1959
|
+
} catch {
|
|
1873
1960
|
}
|
|
1874
|
-
return detected;
|
|
1875
|
-
} catch {
|
|
1876
|
-
return null;
|
|
1877
1961
|
}
|
|
1878
|
-
}
|
|
1879
|
-
async function detectFromGitLabApi(repoUrl) {
|
|
1880
|
-
const parsed = parseGitLabUrl(repoUrl);
|
|
1881
|
-
if (!parsed) return null;
|
|
1882
|
-
const { path: projectPath, host } = parsed;
|
|
1883
|
-
const encodedPath = encodeURIComponent(projectPath);
|
|
1884
1962
|
try {
|
|
1885
|
-
const
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
if (!repoRes.ok) return null;
|
|
1889
|
-
const repoInfo = await repoRes.json();
|
|
1890
|
-
if (repoInfo.visibility === "private") return null;
|
|
1891
|
-
const licenseId = repoInfo.license?.key?.toLowerCase() || null;
|
|
1892
|
-
const isOpenSource = repoInfo.visibility === "public" && (licenseId ? OPEN_SOURCE_LICENSES.includes(licenseId) : false);
|
|
1893
|
-
const detected = {
|
|
1894
|
-
name: repoInfo.name,
|
|
1895
|
-
description: repoInfo.description ?? void 0,
|
|
1896
|
-
stack: [],
|
|
1897
|
-
databases: [],
|
|
1898
|
-
commands: {},
|
|
1899
|
-
packageManager: null,
|
|
1900
|
-
type: "application",
|
|
1901
|
-
repoHost: "gitlab",
|
|
1902
|
-
repoUrl,
|
|
1903
|
-
license: licenseId ?? void 0,
|
|
1904
|
-
isPublicRepo: repoInfo.visibility === "public",
|
|
1905
|
-
isOpenSource,
|
|
1906
|
-
projectType: isOpenSource ? "open_source" : void 0,
|
|
1907
|
-
hasDocker: false,
|
|
1908
|
-
existingFiles: []
|
|
1909
|
-
};
|
|
1910
|
-
const filesRes = await fetch(`https://${host}/api/v4/projects/${encodedPath}/repository/tree?per_page=100`, {
|
|
1911
|
-
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
1912
|
-
});
|
|
1913
|
-
if (!filesRes.ok) return detected;
|
|
1914
|
-
const files = await filesRes.json();
|
|
1915
|
-
const fileNames = new Set(files.map((f) => f.name.toLowerCase()));
|
|
1916
|
-
for (const file of STATIC_FILES) {
|
|
1917
|
-
if (files.some((f) => f.name.toLowerCase() === file.toLowerCase())) {
|
|
1918
|
-
detected.existingFiles.push(file);
|
|
1919
|
-
}
|
|
1920
|
-
}
|
|
1921
|
-
if (fileNames.has(".gitlab-ci.yml")) detected.cicd = "gitlab_ci";
|
|
1922
|
-
if (fileNames.has("jenkinsfile")) detected.cicd = "jenkins";
|
|
1923
|
-
if (fileNames.has("dockerfile") || fileNames.has("docker-compose.yml") || fileNames.has("docker-compose.yaml")) {
|
|
1924
|
-
detected.hasDocker = true;
|
|
1925
|
-
detected.stack.push("docker");
|
|
1926
|
-
const dockerComposeFile = fileNames.has("docker-compose.yml") ? "docker-compose.yml" : fileNames.has("docker-compose.yaml") ? "docker-compose.yaml" : null;
|
|
1927
|
-
if (dockerComposeFile) {
|
|
1928
|
-
const composeRes = await fetch(`https://${host}/api/v4/projects/${encodedPath}/repository/files/${encodeURIComponent(dockerComposeFile)}/raw?ref=HEAD`, {
|
|
1929
|
-
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
1930
|
-
});
|
|
1931
|
-
if (composeRes.ok) {
|
|
1932
|
-
try {
|
|
1933
|
-
const content = await composeRes.text();
|
|
1934
|
-
const lowerContent = content.toLowerCase();
|
|
1935
|
-
if (content.includes("registry.gitlab.com")) detected.containerRegistry = "gitlab_registry";
|
|
1936
|
-
else if (content.includes("ghcr.io")) detected.containerRegistry = "ghcr";
|
|
1937
|
-
else if (content.includes("docker.io") || /image:\s*[a-z0-9]+\/[a-z0-9]/.test(content)) detected.containerRegistry = "dockerhub";
|
|
1938
|
-
else if (content.includes("gcr.io")) detected.containerRegistry = "gcr";
|
|
1939
|
-
if (lowerContent.includes("postgres")) detected.databases.push("postgresql");
|
|
1940
|
-
if (lowerContent.includes("mysql") && !lowerContent.includes("mysql-")) detected.databases.push("mysql");
|
|
1941
|
-
if (lowerContent.includes("mongo")) detected.databases.push("mongodb");
|
|
1942
|
-
if (lowerContent.includes("redis")) detected.databases.push("redis");
|
|
1943
|
-
if (lowerContent.includes("sqlite")) detected.databases.push("sqlite");
|
|
1944
|
-
} catch {
|
|
1945
|
-
}
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1963
|
+
const dockerViewerPath = join3(cwd, "Dockerfile.viewer");
|
|
1964
|
+
if (await fileExists(dockerViewerPath)) {
|
|
1965
|
+
addCmd("build", "docker build -f Dockerfile.viewer -t app-viewer .", "Build viewer Docker image");
|
|
1948
1966
|
}
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1967
|
+
} catch {
|
|
1968
|
+
}
|
|
1969
|
+
const cargoPath = join3(cwd, "Cargo.toml");
|
|
1970
|
+
if (await fileExists(cargoPath)) {
|
|
1971
|
+
addCmd("build", "cargo build", "Build Rust project");
|
|
1972
|
+
addCmd("build", "cargo build --release", "Build release");
|
|
1973
|
+
addCmd("test", "cargo test", "Run Rust tests");
|
|
1974
|
+
addCmd("lint", "cargo clippy", "Run Clippy linter");
|
|
1975
|
+
addCmd("format", "cargo fmt", "Format Rust code");
|
|
1976
|
+
addCmd("dev", "cargo run", "Run Rust binary");
|
|
1977
|
+
addCmd("clean", "cargo clean", "Clean build artifacts");
|
|
1978
|
+
}
|
|
1979
|
+
const goModPath = join3(cwd, "go.mod");
|
|
1980
|
+
if (await fileExists(goModPath)) {
|
|
1981
|
+
addCmd("build", "go build", "Build Go project");
|
|
1982
|
+
addCmd("test", "go test ./...", "Run Go tests");
|
|
1983
|
+
addCmd("lint", "golangci-lint run", "Run golangci-lint");
|
|
1984
|
+
addCmd("format", "go fmt ./...", "Format Go code");
|
|
1985
|
+
addCmd("dev", "go run .", "Run Go binary");
|
|
1986
|
+
addCmd("clean", "go clean", "Clean build cache");
|
|
1987
|
+
addCmd("typecheck", "go vet ./...", "Run go vet");
|
|
1988
|
+
}
|
|
1989
|
+
const srcMainPath = join3(cwd, "src", "main.py");
|
|
1990
|
+
const mainPath = join3(cwd, "main.py");
|
|
1991
|
+
const appPath = join3(cwd, "app.py");
|
|
1992
|
+
if (await fileExists(srcMainPath)) {
|
|
1993
|
+
addCmd("dev", "python -m src.main", "Run main module");
|
|
1994
|
+
}
|
|
1995
|
+
if (await fileExists(mainPath)) {
|
|
1996
|
+
addCmd("dev", "python main.py", "Run main.py");
|
|
1997
|
+
}
|
|
1998
|
+
if (await fileExists(appPath)) {
|
|
1999
|
+
addCmd("dev", "python app.py", "Run app.py");
|
|
2000
|
+
}
|
|
2001
|
+
const schedulerPath = join3(cwd, "src", "scheduler.py");
|
|
2002
|
+
if (await fileExists(schedulerPath)) {
|
|
2003
|
+
addCmd("additional", "python -m src.scheduler", "Run scheduler");
|
|
2004
|
+
}
|
|
2005
|
+
const setupAuthPath = join3(cwd, "src", "setup_auth.py");
|
|
2006
|
+
if (await fileExists(setupAuthPath)) {
|
|
2007
|
+
addCmd("additional", "python -m src.setup_auth", "Setup authentication");
|
|
2008
|
+
}
|
|
2009
|
+
const webMainPath = join3(cwd, "src", "web", "main.py");
|
|
2010
|
+
if (await fileExists(webMainPath)) {
|
|
2011
|
+
addCmd("dev", "uvicorn src.web.main:app --host 0.0.0.0 --port 8080", "Run web viewer");
|
|
2012
|
+
}
|
|
2013
|
+
return cmds;
|
|
2014
|
+
}
|
|
2015
|
+
async function detectProject(cwd) {
|
|
2016
|
+
const detected = {
|
|
2017
|
+
name: null,
|
|
2018
|
+
stack: [],
|
|
2019
|
+
databases: [],
|
|
2020
|
+
commands: {},
|
|
2021
|
+
packageManager: null,
|
|
2022
|
+
type: "unknown"
|
|
2023
|
+
};
|
|
2024
|
+
const packageJsonPath = join3(cwd, "package.json");
|
|
2025
|
+
if (await fileExists(packageJsonPath)) {
|
|
2026
|
+
try {
|
|
2027
|
+
const content = await readFile3(packageJsonPath, "utf-8");
|
|
2028
|
+
const pkg = JSON.parse(content);
|
|
2029
|
+
detected.name = pkg.name || null;
|
|
2030
|
+
detected.description = pkg.description;
|
|
2031
|
+
if (pkg.workspaces || await fileExists(join3(cwd, "pnpm-workspace.yaml"))) {
|
|
2032
|
+
detected.type = "monorepo";
|
|
2033
|
+
} else if (pkg.main || pkg.exports) {
|
|
2034
|
+
detected.type = "library";
|
|
2035
|
+
} else {
|
|
2036
|
+
detected.type = "application";
|
|
2037
|
+
}
|
|
2038
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2039
|
+
for (const [framework, deps] of Object.entries(JS_FRAMEWORK_PATTERNS)) {
|
|
2040
|
+
if (deps.some((dep) => allDeps[dep])) {
|
|
2041
|
+
detected.stack.push(framework);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
for (const [tool, deps] of Object.entries(JS_TOOL_PATTERNS)) {
|
|
2045
|
+
if (deps.some((dep) => allDeps[dep])) {
|
|
2046
|
+
detected.stack.push(tool);
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
if (detected.stack.length === 0 || detected.stack.length === 1 && detected.stack[0] === "typescript") {
|
|
2050
|
+
detected.stack.unshift("javascript");
|
|
2051
|
+
}
|
|
2052
|
+
if (pkg.scripts) {
|
|
2053
|
+
detected.commands.build = pkg.scripts.build;
|
|
2054
|
+
detected.commands.test = pkg.scripts.test;
|
|
2055
|
+
detected.commands.lint = pkg.scripts.lint || pkg.scripts["lint:check"];
|
|
2056
|
+
detected.commands.dev = pkg.scripts.dev || pkg.scripts.start || pkg.scripts.serve;
|
|
2057
|
+
detected.commands.format = pkg.scripts.format || pkg.scripts.prettier;
|
|
2058
|
+
}
|
|
2059
|
+
if (await fileExists(join3(cwd, "pnpm-lock.yaml"))) {
|
|
2060
|
+
detected.packageManager = "pnpm";
|
|
2061
|
+
} else if (await fileExists(join3(cwd, "yarn.lock"))) {
|
|
2062
|
+
detected.packageManager = "yarn";
|
|
2063
|
+
} else if (await fileExists(join3(cwd, "bun.lockb"))) {
|
|
2064
|
+
detected.packageManager = "bun";
|
|
2065
|
+
} else if (await fileExists(join3(cwd, "package-lock.json"))) {
|
|
2066
|
+
detected.packageManager = "npm";
|
|
2067
|
+
}
|
|
2068
|
+
if (detected.packageManager && detected.packageManager !== "npm") {
|
|
2069
|
+
const pm = detected.packageManager;
|
|
2070
|
+
for (const [key, value] of Object.entries(detected.commands)) {
|
|
2071
|
+
if (value && !value.startsWith(pm) && !value.startsWith("npx")) {
|
|
2072
|
+
detected.commands[key] = `${pm} run ${value}`;
|
|
1986
2073
|
}
|
|
1987
|
-
|
|
1988
|
-
|
|
2074
|
+
}
|
|
2075
|
+
} else if (detected.commands) {
|
|
2076
|
+
for (const [key, value] of Object.entries(detected.commands)) {
|
|
2077
|
+
if (value && !value.startsWith("npm") && !value.startsWith("npx")) {
|
|
2078
|
+
detected.commands[key] = `npm run ${value}`;
|
|
1989
2079
|
}
|
|
1990
|
-
} catch {
|
|
1991
2080
|
}
|
|
1992
2081
|
}
|
|
2082
|
+
if (pkg.scripts) {
|
|
2083
|
+
detected.commands.build = pkg.scripts.build ? "build" : void 0;
|
|
2084
|
+
detected.commands.test = pkg.scripts.test ? "test" : void 0;
|
|
2085
|
+
detected.commands.lint = pkg.scripts.lint ? "lint" : pkg.scripts["lint:check"] ? "lint:check" : void 0;
|
|
2086
|
+
detected.commands.dev = pkg.scripts.dev ? "dev" : pkg.scripts.start ? "start" : pkg.scripts.serve ? "serve" : void 0;
|
|
2087
|
+
}
|
|
2088
|
+
return detected;
|
|
2089
|
+
} catch {
|
|
1993
2090
|
}
|
|
1994
|
-
|
|
2091
|
+
}
|
|
2092
|
+
const pyprojectPath = join3(cwd, "pyproject.toml");
|
|
2093
|
+
if (await fileExists(pyprojectPath)) {
|
|
2094
|
+
try {
|
|
2095
|
+
const content = await readFile3(pyprojectPath, "utf-8");
|
|
1995
2096
|
detected.stack.push("python");
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
if (
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
}
|
|
2097
|
+
detected.type = "application";
|
|
2098
|
+
const nameMatch = content.match(/name\s*=\s*"([^"]+)"/);
|
|
2099
|
+
if (nameMatch) detected.name = nameMatch[1];
|
|
2100
|
+
if (content.includes("fastapi")) detected.stack.push("fastapi");
|
|
2101
|
+
if (content.includes("django")) detected.stack.push("django");
|
|
2102
|
+
if (content.includes("flask")) detected.stack.push("flask");
|
|
2103
|
+
if (content.includes("pydantic")) detected.stack.push("pydantic");
|
|
2104
|
+
if (content.includes("sqlalchemy")) detected.stack.push("sqlalchemy");
|
|
2105
|
+
if (content.includes("pytest")) detected.stack.push("pytest");
|
|
2106
|
+
if (content.includes("ruff")) detected.stack.push("ruff");
|
|
2107
|
+
if (content.includes("mypy")) detected.stack.push("mypy");
|
|
2108
|
+
detected.commands.test = "pytest";
|
|
2109
|
+
detected.commands.lint = "ruff check .";
|
|
2110
|
+
if (content.includes("[tool.poetry]")) {
|
|
2111
|
+
detected.packageManager = "yarn";
|
|
2112
|
+
detected.commands.dev = "poetry run python -m uvicorn main:app --reload";
|
|
2113
|
+
} else if (await fileExists(join3(cwd, "uv.lock"))) {
|
|
2114
|
+
detected.commands.dev = "uv run python main.py";
|
|
2015
2115
|
}
|
|
2016
|
-
|
|
2116
|
+
return detected;
|
|
2117
|
+
} catch {
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
const requirementsPath = join3(cwd, "requirements.txt");
|
|
2121
|
+
if (await fileExists(requirementsPath)) {
|
|
2122
|
+
try {
|
|
2123
|
+
const content = await readFile3(requirementsPath, "utf-8");
|
|
2017
2124
|
detected.stack.push("python");
|
|
2125
|
+
detected.type = "application";
|
|
2126
|
+
if (content.toLowerCase().includes("fastapi")) detected.stack.push("fastapi");
|
|
2127
|
+
if (content.toLowerCase().includes("django")) detected.stack.push("django");
|
|
2128
|
+
if (content.toLowerCase().includes("flask")) detected.stack.push("flask");
|
|
2129
|
+
detected.commands.test = "pytest";
|
|
2130
|
+
detected.commands.lint = "ruff check .";
|
|
2131
|
+
return detected;
|
|
2132
|
+
} catch {
|
|
2018
2133
|
}
|
|
2019
|
-
|
|
2134
|
+
}
|
|
2135
|
+
const cargoPath = join3(cwd, "Cargo.toml");
|
|
2136
|
+
if (await fileExists(cargoPath)) {
|
|
2137
|
+
try {
|
|
2138
|
+
const content = await readFile3(cargoPath, "utf-8");
|
|
2020
2139
|
detected.stack.push("rust");
|
|
2140
|
+
detected.type = "application";
|
|
2141
|
+
const nameMatch = content.match(/name\s*=\s*"([^"]+)"/);
|
|
2142
|
+
if (nameMatch) detected.name = nameMatch[1];
|
|
2143
|
+
if (content.includes("actix-web")) detected.stack.push("actix");
|
|
2144
|
+
if (content.includes("axum")) detected.stack.push("axum");
|
|
2145
|
+
if (content.includes("tokio")) detected.stack.push("tokio");
|
|
2146
|
+
if (content.includes("serde")) detected.stack.push("serde");
|
|
2147
|
+
if (content.includes("sqlx")) detected.stack.push("sqlx");
|
|
2021
2148
|
detected.commands.build = "cargo build";
|
|
2022
2149
|
detected.commands.test = "cargo test";
|
|
2150
|
+
detected.commands.lint = "cargo clippy";
|
|
2151
|
+
detected.commands.dev = "cargo run";
|
|
2152
|
+
return detected;
|
|
2153
|
+
} catch {
|
|
2023
2154
|
}
|
|
2024
|
-
|
|
2155
|
+
}
|
|
2156
|
+
const goModPath = join3(cwd, "go.mod");
|
|
2157
|
+
if (await fileExists(goModPath)) {
|
|
2158
|
+
try {
|
|
2159
|
+
const content = await readFile3(goModPath, "utf-8");
|
|
2025
2160
|
detected.stack.push("go");
|
|
2161
|
+
detected.type = "application";
|
|
2162
|
+
const moduleMatch = content.match(/module\s+(\S+)/);
|
|
2163
|
+
if (moduleMatch) {
|
|
2164
|
+
const parts = moduleMatch[1].split("/");
|
|
2165
|
+
detected.name = parts[parts.length - 1];
|
|
2166
|
+
}
|
|
2167
|
+
if (content.includes("gin-gonic/gin")) detected.stack.push("gin");
|
|
2168
|
+
if (content.includes("gofiber/fiber")) detected.stack.push("fiber");
|
|
2169
|
+
if (content.includes("labstack/echo")) detected.stack.push("echo");
|
|
2170
|
+
if (content.includes("gorm.io/gorm")) detected.stack.push("gorm");
|
|
2026
2171
|
detected.commands.build = "go build";
|
|
2027
2172
|
detected.commands.test = "go test ./...";
|
|
2173
|
+
detected.commands.lint = "golangci-lint run";
|
|
2174
|
+
detected.commands.dev = "go run .";
|
|
2175
|
+
return detected;
|
|
2176
|
+
} catch {
|
|
2028
2177
|
}
|
|
2029
|
-
return detected;
|
|
2030
|
-
} catch {
|
|
2031
|
-
return null;
|
|
2032
|
-
}
|
|
2033
|
-
}
|
|
2034
|
-
function isValidGitUrl(url) {
|
|
2035
|
-
const trimmed = url.trim();
|
|
2036
|
-
if (trimmed.startsWith("https://") || trimmed.startsWith("http://") || trimmed.startsWith("git://") || trimmed.startsWith("git@") || trimmed.startsWith("ssh://")) {
|
|
2037
|
-
const dangerousChars = /[;&|`$(){}[\]<>\\'"!#*?~]/;
|
|
2038
|
-
return !dangerousChars.test(trimmed);
|
|
2039
2178
|
}
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
async function detectFromShallowClone(repoUrl) {
|
|
2043
|
-
let tempDir = null;
|
|
2044
|
-
if (!isValidGitUrl(repoUrl)) {
|
|
2045
|
-
return null;
|
|
2046
|
-
}
|
|
2047
|
-
try {
|
|
2048
|
-
tempDir = await mkdtemp(join3(tmpdir(), "lynxprompt-detect-"));
|
|
2179
|
+
const makefilePath = join3(cwd, "Makefile");
|
|
2180
|
+
if (await fileExists(makefilePath)) {
|
|
2049
2181
|
try {
|
|
2050
|
-
const
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
if (
|
|
2055
|
-
|
|
2182
|
+
const content = await readFile3(makefilePath, "utf-8");
|
|
2183
|
+
if (content.includes("build:")) detected.commands.build = "make build";
|
|
2184
|
+
if (content.includes("test:")) detected.commands.test = "make test";
|
|
2185
|
+
if (content.includes("lint:")) detected.commands.lint = "make lint";
|
|
2186
|
+
if (content.includes("dev:")) detected.commands.dev = "make dev";
|
|
2187
|
+
if (content.includes("run:")) detected.commands.dev = detected.commands.dev || "make run";
|
|
2188
|
+
if (Object.keys(detected.commands).length > 0) {
|
|
2189
|
+
detected.type = "application";
|
|
2190
|
+
return detected;
|
|
2056
2191
|
}
|
|
2057
2192
|
} catch {
|
|
2058
|
-
return null;
|
|
2059
|
-
}
|
|
2060
|
-
const detected = await detectProject(tempDir);
|
|
2061
|
-
if (detected) {
|
|
2062
|
-
detected.repoHost = detectRepoHost(repoUrl);
|
|
2063
|
-
detected.repoUrl = repoUrl;
|
|
2064
|
-
}
|
|
2065
|
-
return detected;
|
|
2066
|
-
} catch {
|
|
2067
|
-
return null;
|
|
2068
|
-
} finally {
|
|
2069
|
-
if (tempDir) {
|
|
2070
|
-
try {
|
|
2071
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
2072
|
-
} catch {
|
|
2073
|
-
}
|
|
2074
2193
|
}
|
|
2075
2194
|
}
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
const result = await detectFromGitHubApi(repoUrl);
|
|
2081
|
-
if (result) return result;
|
|
2082
|
-
}
|
|
2083
|
-
if (host === "gitlab") {
|
|
2084
|
-
const result = await detectFromGitLabApi(repoUrl);
|
|
2085
|
-
if (result) return result;
|
|
2195
|
+
if (await fileExists(join3(cwd, "Dockerfile")) || await fileExists(join3(cwd, "docker-compose.yml"))) {
|
|
2196
|
+
detected.stack.push("docker");
|
|
2197
|
+
detected.type = "application";
|
|
2198
|
+
detected.hasDocker = true;
|
|
2086
2199
|
}
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
function isGitUrl(str) {
|
|
2090
|
-
const patterns = [
|
|
2091
|
-
/^https?:\/\/[^/]+\/.*$/,
|
|
2092
|
-
/^git@[^:]+:.*$/,
|
|
2093
|
-
/^git:\/\/.*$/,
|
|
2094
|
-
/^ssh:\/\/.*$/
|
|
2095
|
-
];
|
|
2096
|
-
return patterns.some((p) => p.test(str.trim()));
|
|
2097
|
-
}
|
|
2098
|
-
var COMMAND_DIRECTORIES = [
|
|
2099
|
-
{ directory: ".cursor/commands", type: "cursor-command", platform: "cursor", templateType: "CURSOR_COMMAND" },
|
|
2100
|
-
{ directory: ".claude/commands", type: "claude-command", platform: "claude", templateType: "CLAUDE_COMMAND" },
|
|
2101
|
-
{ directory: ".windsurf/workflows", type: "windsurf-workflow", platform: "windsurf", templateType: "WINDSURF_WORKFLOW" },
|
|
2102
|
-
{ directory: ".copilot/prompts", type: "copilot-prompt", platform: "copilot", templateType: "COPILOT_PROMPT" },
|
|
2103
|
-
{ directory: ".continue/prompts", type: "continue-prompt", platform: "continue", templateType: "CONTINUE_PROMPT" },
|
|
2104
|
-
{ directory: ".opencode/commands", type: "opencode-command", platform: "opencode", templateType: "OPENCODE_COMMAND" }
|
|
2105
|
-
];
|
|
2106
|
-
async function detectCommandFiles(cwd) {
|
|
2107
|
-
const commands = [];
|
|
2108
|
-
const { readdir: readdir4, readFile: readFileAsync } = await import("fs/promises");
|
|
2109
|
-
for (const cmdDir of COMMAND_DIRECTORIES) {
|
|
2110
|
-
const dirPath = join3(cwd, cmdDir.directory);
|
|
2200
|
+
const licensePath = join3(cwd, "LICENSE");
|
|
2201
|
+
if (await fileExists(licensePath)) {
|
|
2111
2202
|
try {
|
|
2112
|
-
await
|
|
2113
|
-
const
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
}
|
|
2203
|
+
const licenseContent = await readFile3(licensePath, "utf-8");
|
|
2204
|
+
const lowerContent = licenseContent.toLowerCase();
|
|
2205
|
+
if (lowerContent.includes("mit license") || lowerContent.includes("permission is hereby granted, free of charge")) {
|
|
2206
|
+
detected.license = "mit";
|
|
2207
|
+
} else if (lowerContent.includes("apache license") && lowerContent.includes("version 2.0")) {
|
|
2208
|
+
detected.license = "apache-2.0";
|
|
2209
|
+
} else if (lowerContent.includes("gnu general public license") && lowerContent.includes("version 3")) {
|
|
2210
|
+
detected.license = "gpl-3.0";
|
|
2211
|
+
} else if (lowerContent.includes("gnu lesser general public license")) {
|
|
2212
|
+
detected.license = "lgpl-3.0";
|
|
2213
|
+
} else if (lowerContent.includes("gnu affero general public license")) {
|
|
2214
|
+
detected.license = "agpl-3.0";
|
|
2215
|
+
} else if (lowerContent.includes("bsd 3-clause") || lowerContent.includes("redistribution and use in source and binary forms")) {
|
|
2216
|
+
detected.license = "bsd-3";
|
|
2217
|
+
} else if (lowerContent.includes("mozilla public license") && lowerContent.includes("2.0")) {
|
|
2218
|
+
detected.license = "mpl-2.0";
|
|
2219
|
+
} else if (lowerContent.includes("unlicense") || lowerContent.includes("this is free and unencumbered software")) {
|
|
2220
|
+
detected.license = "unlicense";
|
|
2131
2221
|
}
|
|
2132
2222
|
} catch {
|
|
2133
2223
|
}
|
|
2134
2224
|
}
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
function inferCommandTypeFromPath(filePath) {
|
|
2138
|
-
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
2139
|
-
const match = COMMAND_DIRECTORIES.find((cmd) => normalizedPath.includes(cmd.directory));
|
|
2140
|
-
if (match) {
|
|
2141
|
-
return {
|
|
2142
|
-
type: match.type,
|
|
2143
|
-
platform: match.platform,
|
|
2144
|
-
templateType: match.templateType
|
|
2145
|
-
};
|
|
2146
|
-
}
|
|
2147
|
-
return null;
|
|
2148
|
-
}
|
|
2149
|
-
|
|
2150
|
-
// src/commands/push.ts
|
|
2151
|
-
function scanForAgentFiles(cwd, maxDepth = 5) {
|
|
2152
|
-
const results = [];
|
|
2153
|
-
function scan(dir, depth) {
|
|
2154
|
-
if (depth > maxDepth) return;
|
|
2225
|
+
const gitConfigPath = join3(cwd, ".git", "config");
|
|
2226
|
+
if (await fileExists(gitConfigPath)) {
|
|
2155
2227
|
try {
|
|
2156
|
-
const
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2228
|
+
const gitConfig = await readFile3(gitConfigPath, "utf-8");
|
|
2229
|
+
const urlMatch = gitConfig.match(/url\s*=\s*(.+)/);
|
|
2230
|
+
if (urlMatch) {
|
|
2231
|
+
const repoUrl = urlMatch[1].trim();
|
|
2232
|
+
detected.repoUrl = repoUrl;
|
|
2233
|
+
if (repoUrl.includes("github.com")) {
|
|
2234
|
+
detected.repoHost = "github";
|
|
2235
|
+
} else if (repoUrl.includes("gitlab.com") || repoUrl.includes("gitlab")) {
|
|
2236
|
+
detected.repoHost = "gitlab";
|
|
2237
|
+
} else if (repoUrl.includes("bitbucket")) {
|
|
2238
|
+
detected.repoHost = "bitbucket";
|
|
2239
|
+
} else if (repoUrl.includes("gitea") || repoUrl.includes("codeberg")) {
|
|
2240
|
+
detected.repoHost = "gitea";
|
|
2241
|
+
} else if (repoUrl.includes("azure")) {
|
|
2242
|
+
detected.repoHost = "azure";
|
|
2171
2243
|
}
|
|
2172
2244
|
}
|
|
2173
2245
|
} catch {
|
|
2174
2246
|
}
|
|
2175
2247
|
}
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2248
|
+
if (await fileExists(join3(cwd, ".github", "workflows"))) {
|
|
2249
|
+
detected.cicd = "github_actions";
|
|
2250
|
+
} else if (await fileExists(join3(cwd, ".gitlab-ci.yml"))) {
|
|
2251
|
+
detected.cicd = "gitlab_ci";
|
|
2252
|
+
} else if (await fileExists(join3(cwd, "Jenkinsfile"))) {
|
|
2253
|
+
detected.cicd = "jenkins";
|
|
2254
|
+
} else if (await fileExists(join3(cwd, ".circleci"))) {
|
|
2255
|
+
detected.cicd = "circleci";
|
|
2256
|
+
} else if (await fileExists(join3(cwd, ".travis.yml"))) {
|
|
2257
|
+
detected.cicd = "travis";
|
|
2258
|
+
} else if (await fileExists(join3(cwd, "azure-pipelines.yml"))) {
|
|
2259
|
+
detected.cicd = "azure_devops";
|
|
2260
|
+
} else if (await fileExists(join3(cwd, "bitbucket-pipelines.yml"))) {
|
|
2261
|
+
detected.cicd = "bitbucket";
|
|
2262
|
+
} else if (await fileExists(join3(cwd, ".drone.yml"))) {
|
|
2263
|
+
detected.cicd = "drone";
|
|
2264
|
+
}
|
|
2265
|
+
detected.existingFiles = [];
|
|
2266
|
+
const staticFiles = [
|
|
2267
|
+
".editorconfig",
|
|
2268
|
+
"CONTRIBUTING.md",
|
|
2269
|
+
"CODE_OF_CONDUCT.md",
|
|
2270
|
+
"SECURITY.md",
|
|
2271
|
+
"ROADMAP.md",
|
|
2272
|
+
".gitignore",
|
|
2273
|
+
".github/FUNDING.yml",
|
|
2274
|
+
"LICENSE",
|
|
2275
|
+
"README.md",
|
|
2276
|
+
"ARCHITECTURE.md",
|
|
2277
|
+
"CHANGELOG.md"
|
|
2278
|
+
];
|
|
2279
|
+
for (const file of staticFiles) {
|
|
2280
|
+
if (await fileExists(join3(cwd, file))) {
|
|
2281
|
+
detected.existingFiles.push(file);
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
if (!detected.description) {
|
|
2285
|
+
const readmePath = join3(cwd, "README.md");
|
|
2286
|
+
if (await fileExists(readmePath)) {
|
|
2287
|
+
try {
|
|
2288
|
+
const readme = await readFile3(readmePath, "utf-8");
|
|
2289
|
+
const lines = readme.split("\n");
|
|
2290
|
+
for (const line of lines) {
|
|
2291
|
+
const trimmed = line.trim();
|
|
2292
|
+
if (trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("!") && !trimmed.startsWith("[") && trimmed.length > 20) {
|
|
2293
|
+
detected.description = trimmed.substring(0, 200);
|
|
2294
|
+
break;
|
|
2295
|
+
}
|
|
2204
2296
|
}
|
|
2297
|
+
} catch {
|
|
2205
2298
|
}
|
|
2206
|
-
} else if (relativePath === "AGENTS.md" || relativePath === path.basename(file)) {
|
|
2207
|
-
result.repositoryPath = relativePath;
|
|
2208
2299
|
}
|
|
2209
|
-
} catch {
|
|
2210
2300
|
}
|
|
2211
|
-
|
|
2301
|
+
detected.detectedCommands = await detectExtendedCommands(cwd);
|
|
2302
|
+
return detected.stack.length > 0 || detected.name ? detected : null;
|
|
2212
2303
|
}
|
|
2213
|
-
async function
|
|
2304
|
+
async function fileExists(path2) {
|
|
2214
2305
|
try {
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
return response.hierarchy.id;
|
|
2220
|
-
} catch (error) {
|
|
2221
|
-
console.log(chalk6.gray(" Note: Hierarchy creation skipped"));
|
|
2222
|
-
return null;
|
|
2306
|
+
await access2(path2);
|
|
2307
|
+
return true;
|
|
2308
|
+
} catch {
|
|
2309
|
+
return false;
|
|
2223
2310
|
}
|
|
2224
2311
|
}
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
}
|
|
2243
|
-
if (!response.has_more) break;
|
|
2244
|
-
offset += limit;
|
|
2312
|
+
function detectRepoHost(url) {
|
|
2313
|
+
const lower = url.toLowerCase();
|
|
2314
|
+
if (lower.includes("github.com") || lower.includes("github:")) return "github";
|
|
2315
|
+
if (lower.includes("gitlab.com") || lower.includes("gitlab")) return "gitlab";
|
|
2316
|
+
if (lower.includes("bitbucket.org") || lower.includes("bitbucket:")) return "bitbucket";
|
|
2317
|
+
if (lower.includes("gitea.") || lower.includes("gitea:") || lower.includes("codeberg.org")) return "gitea";
|
|
2318
|
+
if (lower.includes("azure.com") || lower.includes("visualstudio.com") || lower.includes("dev.azure")) return "azure";
|
|
2319
|
+
return "other";
|
|
2320
|
+
}
|
|
2321
|
+
function parseGitHubUrl(url) {
|
|
2322
|
+
const patterns = [
|
|
2323
|
+
/github\.com[/:]([^/]+)\/([^/.]+)/,
|
|
2324
|
+
/^([^/]+)\/([^/]+)$/
|
|
2325
|
+
];
|
|
2326
|
+
for (const pattern of patterns) {
|
|
2327
|
+
const match = url.match(pattern);
|
|
2328
|
+
if (match) {
|
|
2329
|
+
return { owner: match[1], repo: match[2].replace(/\.git$/, "") };
|
|
2245
2330
|
}
|
|
2246
|
-
} catch {
|
|
2247
2331
|
}
|
|
2248
2332
|
return null;
|
|
2249
2333
|
}
|
|
2250
|
-
function
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2334
|
+
function parseGitLabUrl(url) {
|
|
2335
|
+
const patterns = [
|
|
2336
|
+
/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/,
|
|
2337
|
+
/^git@([^:]+):(.+?)(?:\.git)?$/
|
|
2338
|
+
];
|
|
2339
|
+
for (const pattern of patterns) {
|
|
2340
|
+
const match = url.match(pattern);
|
|
2341
|
+
if (match) {
|
|
2342
|
+
const host = match[1];
|
|
2343
|
+
const path2 = match[2].replace(/\.git$/, "");
|
|
2344
|
+
if (host.includes("gitlab") || url.toLowerCase().includes("gitlab")) {
|
|
2345
|
+
return { path: path2, host };
|
|
2258
2346
|
}
|
|
2259
2347
|
}
|
|
2260
|
-
} catch {
|
|
2261
|
-
}
|
|
2262
|
-
return createHash2("sha256").update(path.resolve(rootPath)).digest("hex").substring(0, 16);
|
|
2263
|
-
}
|
|
2264
|
-
async function pushCommand(fileArg, options) {
|
|
2265
|
-
const cwd = process.cwd();
|
|
2266
|
-
if (!isAuthenticated()) {
|
|
2267
|
-
console.log(chalk6.yellow("You need to be logged in to push blueprints."));
|
|
2268
|
-
console.log(chalk6.gray("Run 'lynxp login' to authenticate."));
|
|
2269
|
-
process.exit(1);
|
|
2270
|
-
}
|
|
2271
|
-
const file = fileArg || findDefaultFile();
|
|
2272
|
-
if (!file) {
|
|
2273
|
-
console.log(chalk6.red("No AI configuration file found."));
|
|
2274
|
-
console.log(
|
|
2275
|
-
chalk6.gray("Specify a file or run in a directory with AGENTS.md, CLAUDE.md, etc.")
|
|
2276
|
-
);
|
|
2277
|
-
process.exit(1);
|
|
2278
|
-
}
|
|
2279
|
-
if (!fs.existsSync(file)) {
|
|
2280
|
-
console.log(chalk6.red(`File not found: ${file}`));
|
|
2281
|
-
process.exit(1);
|
|
2282
|
-
}
|
|
2283
|
-
const content = fs.readFileSync(file, "utf-8");
|
|
2284
|
-
const filename = path.basename(file);
|
|
2285
|
-
const linked = await findBlueprintByFile(cwd, file);
|
|
2286
|
-
if (linked) {
|
|
2287
|
-
await updateBlueprint(cwd, file, linked.id, content, options, linked.checksum);
|
|
2288
|
-
} else {
|
|
2289
|
-
await createOrLinkBlueprint(cwd, file, filename, content, options);
|
|
2290
2348
|
}
|
|
2349
|
+
return null;
|
|
2291
2350
|
}
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2351
|
+
var OPEN_SOURCE_LICENSES = ["mit", "apache-2.0", "gpl-3.0", "lgpl-3.0", "agpl-3.0", "bsd-2-clause", "bsd-3-clause", "mpl-2.0", "unlicense", "cc0-1.0", "isc"];
|
|
2352
|
+
var STATIC_FILES = [".editorconfig", "CONTRIBUTING.md", "CODE_OF_CONDUCT.md", "SECURITY.md", "ROADMAP.md", ".gitignore", ".github/FUNDING.yml", "LICENSE", "README.md", "ARCHITECTURE.md", "CHANGELOG.md"];
|
|
2353
|
+
async function detectFromGitHubApi(repoUrl) {
|
|
2354
|
+
const parsed = parseGitHubUrl(repoUrl);
|
|
2355
|
+
if (!parsed) return null;
|
|
2356
|
+
const { owner, repo } = parsed;
|
|
2297
2357
|
try {
|
|
2298
|
-
const
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
const
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2358
|
+
const repoRes = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
|
|
2359
|
+
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
2360
|
+
});
|
|
2361
|
+
if (!repoRes.ok) return null;
|
|
2362
|
+
const repoInfo = await repoRes.json();
|
|
2363
|
+
if (repoInfo.private) return null;
|
|
2364
|
+
const licenseId = repoInfo.license?.spdx_id?.toLowerCase() || null;
|
|
2365
|
+
const isOpenSource = !repoInfo.private && (licenseId ? OPEN_SOURCE_LICENSES.includes(licenseId) : false);
|
|
2366
|
+
const detected = {
|
|
2367
|
+
name: repoInfo.name,
|
|
2368
|
+
description: repoInfo.description ?? void 0,
|
|
2369
|
+
stack: [],
|
|
2370
|
+
databases: [],
|
|
2371
|
+
commands: {},
|
|
2372
|
+
packageManager: null,
|
|
2373
|
+
type: "application",
|
|
2374
|
+
repoHost: "github",
|
|
2375
|
+
repoUrl,
|
|
2376
|
+
license: licenseId ?? void 0,
|
|
2377
|
+
isPublicRepo: !repoInfo.private,
|
|
2378
|
+
isOpenSource,
|
|
2379
|
+
projectType: isOpenSource ? "open_source" : void 0,
|
|
2380
|
+
hasDocker: false,
|
|
2381
|
+
existingFiles: []
|
|
2382
|
+
};
|
|
2383
|
+
const filesRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/`, {
|
|
2384
|
+
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
2385
|
+
});
|
|
2386
|
+
if (!filesRes.ok) return detected;
|
|
2387
|
+
const files = await filesRes.json();
|
|
2388
|
+
const fileNames = new Set(files.map((f) => f.name.toLowerCase()));
|
|
2389
|
+
for (const file of STATIC_FILES) {
|
|
2390
|
+
if (file.includes("/")) continue;
|
|
2391
|
+
if (files.some((f) => f.name.toLowerCase() === file.toLowerCase())) {
|
|
2392
|
+
detected.existingFiles.push(file);
|
|
2393
|
+
}
|
|
2323
2394
|
}
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
}
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
console.log(chalk6.cyan(`\u{1F4C1} Found ${discoveredFiles.length} AGENTS.md files:`));
|
|
2334
|
-
console.log();
|
|
2335
|
-
for (const f of discoveredFiles) {
|
|
2336
|
-
const icon = f.isRoot ? "\u{1F4C4}" : " \u2514\u2500";
|
|
2337
|
-
console.log(chalk6.gray(` ${icon} ${f.path}`));
|
|
2395
|
+
if (files.some((f) => f.name === ".github" && f.type === "dir")) {
|
|
2396
|
+
const ghFilesRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/.github`, {
|
|
2397
|
+
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
2398
|
+
});
|
|
2399
|
+
if (ghFilesRes.ok) {
|
|
2400
|
+
const ghFiles = await ghFilesRes.json();
|
|
2401
|
+
if (ghFiles.some((f) => f.name.toLowerCase() === "funding.yml")) {
|
|
2402
|
+
detected.existingFiles.push(".github/FUNDING.yml");
|
|
2403
|
+
}
|
|
2338
2404
|
}
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2405
|
+
}
|
|
2406
|
+
if (fileNames.has("dockerfile") || fileNames.has("docker-compose.yml") || fileNames.has("docker-compose.yaml")) {
|
|
2407
|
+
detected.hasDocker = true;
|
|
2408
|
+
detected.stack.push("docker");
|
|
2409
|
+
const dockerComposeFile = fileNames.has("docker-compose.yml") ? "docker-compose.yml" : fileNames.has("docker-compose.yaml") ? "docker-compose.yaml" : null;
|
|
2410
|
+
if (dockerComposeFile) {
|
|
2411
|
+
const composeRes = await fetch(`https://raw.githubusercontent.com/${owner}/${repo}/HEAD/${dockerComposeFile}`);
|
|
2412
|
+
if (composeRes.ok) {
|
|
2413
|
+
try {
|
|
2414
|
+
const content = await composeRes.text();
|
|
2415
|
+
const lowerContent = content.toLowerCase();
|
|
2416
|
+
if (content.includes("ghcr.io")) detected.containerRegistry = "ghcr";
|
|
2417
|
+
else if (content.includes("docker.io") || /image:\s*[a-z0-9]+\/[a-z0-9]/.test(content)) detected.containerRegistry = "dockerhub";
|
|
2418
|
+
else if (content.includes("gcr.io")) detected.containerRegistry = "gcr";
|
|
2419
|
+
else if (content.includes("ecr.") || content.includes(".amazonaws.com")) detected.containerRegistry = "ecr";
|
|
2420
|
+
else if (content.includes("azurecr.io")) detected.containerRegistry = "acr";
|
|
2421
|
+
else if (content.includes("quay.io")) detected.containerRegistry = "quay";
|
|
2422
|
+
else if (content.includes("registry.gitlab.com")) detected.containerRegistry = "gitlab_registry";
|
|
2423
|
+
if (lowerContent.includes("postgres")) detected.databases.push("postgresql");
|
|
2424
|
+
if (lowerContent.includes("mysql") && !lowerContent.includes("mysql-")) detected.databases.push("mysql");
|
|
2425
|
+
if (lowerContent.includes("mongo")) detected.databases.push("mongodb");
|
|
2426
|
+
if (lowerContent.includes("redis")) detected.databases.push("redis");
|
|
2427
|
+
if (lowerContent.includes("sqlite")) detected.databases.push("sqlite");
|
|
2428
|
+
if (lowerContent.includes("mariadb")) detected.databases.push("mariadb");
|
|
2429
|
+
} catch {
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2351
2432
|
}
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2433
|
+
}
|
|
2434
|
+
if (files.some((f) => f.name === ".github" && f.type === "dir")) {
|
|
2435
|
+
const ghFilesRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/.github`, {
|
|
2436
|
+
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
2437
|
+
});
|
|
2438
|
+
if (ghFilesRes.ok) {
|
|
2439
|
+
const ghFiles = await ghFilesRes.json();
|
|
2440
|
+
if (ghFiles.some((f) => f.name === "workflows")) {
|
|
2441
|
+
detected.cicd = "github_actions";
|
|
2442
|
+
}
|
|
2355
2443
|
}
|
|
2356
|
-
console.log(chalk6.gray("Proceeding with single file push..."));
|
|
2357
2444
|
}
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
"
|
|
2362
|
-
"
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
validate: (v) => v.length > 0 || "Name is required"
|
|
2396
|
-
},
|
|
2397
|
-
{
|
|
2398
|
-
type: description ? null : "text",
|
|
2399
|
-
name: "description",
|
|
2400
|
-
message: "Description:",
|
|
2401
|
-
initial: ""
|
|
2402
|
-
},
|
|
2403
|
-
{
|
|
2404
|
-
type: "select",
|
|
2405
|
-
name: "visibility",
|
|
2406
|
-
message: "Visibility:",
|
|
2407
|
-
choices: [
|
|
2408
|
-
{ title: "Private (only you)", value: "PRIVATE" },
|
|
2409
|
-
{ title: "Team (your team members)", value: "TEAM" },
|
|
2410
|
-
{ title: "Public (visible to everyone)", value: "PUBLIC" }
|
|
2411
|
-
],
|
|
2412
|
-
initial: 0
|
|
2413
|
-
},
|
|
2414
|
-
{
|
|
2415
|
-
type: "text",
|
|
2416
|
-
name: "tags",
|
|
2417
|
-
message: "Tags (comma-separated):",
|
|
2418
|
-
initial: ""
|
|
2445
|
+
if (fileNames.has(".gitlab-ci.yml")) detected.cicd = "gitlab_ci";
|
|
2446
|
+
if (fileNames.has("jenkinsfile")) detected.cicd = "jenkins";
|
|
2447
|
+
if (fileNames.has(".travis.yml")) detected.cicd = "travis";
|
|
2448
|
+
if (fileNames.has("azure-pipelines.yml")) detected.cicd = "azure_devops";
|
|
2449
|
+
if (fileNames.has("pyproject.toml")) {
|
|
2450
|
+
detected.stack.push("python");
|
|
2451
|
+
const pyprojectRes = await fetch(`https://raw.githubusercontent.com/${owner}/${repo}/HEAD/pyproject.toml`);
|
|
2452
|
+
if (pyprojectRes.ok) {
|
|
2453
|
+
try {
|
|
2454
|
+
const content = await pyprojectRes.text();
|
|
2455
|
+
const lowerContent = content.toLowerCase();
|
|
2456
|
+
if (lowerContent.includes("fastapi")) detected.stack.push("fastapi");
|
|
2457
|
+
if (lowerContent.includes("django")) detected.stack.push("django");
|
|
2458
|
+
if (lowerContent.includes("flask")) detected.stack.push("flask");
|
|
2459
|
+
if (lowerContent.includes("sqlalchemy")) detected.stack.push("sqlalchemy");
|
|
2460
|
+
if (lowerContent.includes("pydantic")) detected.stack.push("pydantic");
|
|
2461
|
+
if (lowerContent.includes("pytest")) detected.testFramework = "pytest";
|
|
2462
|
+
else if (lowerContent.includes("unittest")) detected.testFramework = "unittest";
|
|
2463
|
+
detected.commands.test = "pytest";
|
|
2464
|
+
if (lowerContent.includes("ruff")) detected.commands.lint = "ruff check .";
|
|
2465
|
+
if (lowerContent.includes("asyncpg") || lowerContent.includes("psycopg")) {
|
|
2466
|
+
if (!detected.databases.includes("postgresql")) detected.databases.push("postgresql");
|
|
2467
|
+
}
|
|
2468
|
+
if (lowerContent.includes("aiosqlite") || lowerContent.includes("sqlite")) {
|
|
2469
|
+
if (!detected.databases.includes("sqlite")) detected.databases.push("sqlite");
|
|
2470
|
+
}
|
|
2471
|
+
if (lowerContent.includes("pymongo") || lowerContent.includes("motor")) {
|
|
2472
|
+
if (!detected.databases.includes("mongodb")) detected.databases.push("mongodb");
|
|
2473
|
+
}
|
|
2474
|
+
if (lowerContent.includes("redis") || lowerContent.includes("aioredis")) {
|
|
2475
|
+
if (!detected.databases.includes("redis")) detected.databases.push("redis");
|
|
2476
|
+
}
|
|
2477
|
+
if (lowerContent.includes("pymysql") || lowerContent.includes("aiomysql")) {
|
|
2478
|
+
if (!detected.databases.includes("mysql")) detected.databases.push("mysql");
|
|
2479
|
+
}
|
|
2480
|
+
} catch {
|
|
2481
|
+
}
|
|
2419
2482
|
}
|
|
2420
|
-
]);
|
|
2421
|
-
if (!responses.name && !name) {
|
|
2422
|
-
console.log(chalk6.yellow("Push cancelled."));
|
|
2423
|
-
return;
|
|
2424
2483
|
}
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2484
|
+
if (fileNames.has("requirements.txt")) {
|
|
2485
|
+
const reqRes = await fetch(`https://raw.githubusercontent.com/${owner}/${repo}/HEAD/requirements.txt`);
|
|
2486
|
+
if (reqRes.ok) {
|
|
2487
|
+
try {
|
|
2488
|
+
const content = (await reqRes.text()).toLowerCase();
|
|
2489
|
+
if (!detected.stack.includes("python")) detected.stack.push("python");
|
|
2490
|
+
if (content.includes("fastapi") && !detected.stack.includes("fastapi")) detected.stack.push("fastapi");
|
|
2491
|
+
if (content.includes("django") && !detected.stack.includes("django")) detected.stack.push("django");
|
|
2492
|
+
if (content.includes("flask") && !detected.stack.includes("flask")) detected.stack.push("flask");
|
|
2493
|
+
if (content.includes("sqlalchemy") && !detected.stack.includes("sqlalchemy")) detected.stack.push("sqlalchemy");
|
|
2494
|
+
if (content.includes("asyncpg") || content.includes("psycopg")) {
|
|
2495
|
+
if (!detected.databases.includes("postgresql")) detected.databases.push("postgresql");
|
|
2496
|
+
}
|
|
2497
|
+
if (content.includes("aiosqlite") || content.includes("sqlite")) {
|
|
2498
|
+
if (!detected.databases.includes("sqlite")) detected.databases.push("sqlite");
|
|
2499
|
+
}
|
|
2500
|
+
if (content.includes("pymongo") || content.includes("motor")) {
|
|
2501
|
+
if (!detected.databases.includes("mongodb")) detected.databases.push("mongodb");
|
|
2502
|
+
}
|
|
2503
|
+
} catch {
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
if (fileNames.has("package.json")) {
|
|
2508
|
+
const pkgRes = await fetch(`https://raw.githubusercontent.com/${owner}/${repo}/HEAD/package.json`);
|
|
2509
|
+
if (pkgRes.ok) {
|
|
2510
|
+
try {
|
|
2511
|
+
const pkg = await pkgRes.json();
|
|
2512
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2513
|
+
if (allDeps["next"]) detected.stack.push("nextjs");
|
|
2514
|
+
if (allDeps["react"]) detected.stack.push("react");
|
|
2515
|
+
if (allDeps["vue"]) detected.stack.push("vue");
|
|
2516
|
+
if (allDeps["svelte"]) detected.stack.push("svelte");
|
|
2517
|
+
if (allDeps["express"]) detected.stack.push("express");
|
|
2518
|
+
if (allDeps["fastify"]) detected.stack.push("fastify");
|
|
2519
|
+
if (allDeps["hono"]) detected.stack.push("hono");
|
|
2520
|
+
if (allDeps["typescript"]) detected.stack.push("typescript");
|
|
2521
|
+
if (allDeps["tailwindcss"]) detected.stack.push("tailwind");
|
|
2522
|
+
if (allDeps["prisma"]) detected.stack.push("prisma");
|
|
2523
|
+
if (allDeps["drizzle-orm"]) detected.stack.push("drizzle");
|
|
2524
|
+
if (allDeps["vitest"]) detected.testFramework = "vitest";
|
|
2525
|
+
else if (allDeps["jest"]) detected.testFramework = "jest";
|
|
2526
|
+
else if (allDeps["@playwright/test"]) detected.testFramework = "playwright";
|
|
2527
|
+
else if (allDeps["cypress"]) detected.testFramework = "cypress";
|
|
2528
|
+
else if (allDeps["mocha"]) detected.testFramework = "mocha";
|
|
2529
|
+
if (detected.stack.length === 0 || detected.stack.length === 1 && detected.stack[0] === "typescript") {
|
|
2530
|
+
detected.stack.unshift("javascript");
|
|
2531
|
+
}
|
|
2532
|
+
if (pkg.scripts) {
|
|
2533
|
+
if (pkg.scripts.build) detected.commands.build = "npm run build";
|
|
2534
|
+
if (pkg.scripts.test) detected.commands.test = "npm run test";
|
|
2535
|
+
if (pkg.scripts.lint) detected.commands.lint = "npm run lint";
|
|
2536
|
+
if (pkg.scripts.dev) detected.commands.dev = "npm run dev";
|
|
2537
|
+
else if (pkg.scripts.start) detected.commands.dev = "npm run start";
|
|
2538
|
+
}
|
|
2539
|
+
if (allDeps["pg"] || allDeps["postgres"] || allDeps["@neondatabase/serverless"]) {
|
|
2540
|
+
if (!detected.databases.includes("postgresql")) detected.databases.push("postgresql");
|
|
2541
|
+
}
|
|
2542
|
+
if (allDeps["better-sqlite3"] || allDeps["sql.js"] || allDeps["sqlite3"]) {
|
|
2543
|
+
if (!detected.databases.includes("sqlite")) detected.databases.push("sqlite");
|
|
2544
|
+
}
|
|
2545
|
+
if (allDeps["mongodb"] || allDeps["mongoose"]) {
|
|
2546
|
+
if (!detected.databases.includes("mongodb")) detected.databases.push("mongodb");
|
|
2547
|
+
}
|
|
2548
|
+
if (allDeps["redis"] || allDeps["ioredis"]) {
|
|
2549
|
+
if (!detected.databases.includes("redis")) detected.databases.push("redis");
|
|
2550
|
+
}
|
|
2551
|
+
if (allDeps["mysql"] || allDeps["mysql2"]) {
|
|
2552
|
+
if (!detected.databases.includes("mysql")) detected.databases.push("mysql");
|
|
2553
|
+
}
|
|
2554
|
+
} catch {
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
if (fileNames.has("cargo.toml")) {
|
|
2559
|
+
detected.stack.push("rust");
|
|
2560
|
+
detected.commands.build = "cargo build";
|
|
2561
|
+
detected.commands.test = "cargo test";
|
|
2562
|
+
}
|
|
2563
|
+
if (fileNames.has("go.mod")) {
|
|
2564
|
+
detected.stack.push("go");
|
|
2565
|
+
detected.commands.build = "go build";
|
|
2566
|
+
detected.commands.test = "go test ./...";
|
|
2567
|
+
}
|
|
2568
|
+
return detected;
|
|
2569
|
+
} catch {
|
|
2570
|
+
return null;
|
|
2457
2571
|
}
|
|
2458
|
-
|
|
2572
|
+
}
|
|
2573
|
+
async function detectFromGitLabApi(repoUrl) {
|
|
2574
|
+
const parsed = parseGitLabUrl(repoUrl);
|
|
2575
|
+
if (!parsed) return null;
|
|
2576
|
+
const { path: projectPath, host } = parsed;
|
|
2577
|
+
const encodedPath = encodeURIComponent(projectPath);
|
|
2459
2578
|
try {
|
|
2460
|
-
const
|
|
2461
|
-
|
|
2462
|
-
description: description || "",
|
|
2463
|
-
content,
|
|
2464
|
-
visibility,
|
|
2465
|
-
tags,
|
|
2466
|
-
type: inferredType,
|
|
2467
|
-
// Include the inferred type (AGENTS_MD, CURSOR_COMMAND, etc.)
|
|
2468
|
-
// Include hierarchy info if detected
|
|
2469
|
-
hierarchy_id: hierarchyId,
|
|
2470
|
-
parent_id: hierarchyInfo.parentId,
|
|
2471
|
-
repository_path: hierarchyInfo.repositoryPath
|
|
2579
|
+
const repoRes = await fetch(`https://${host}/api/v4/projects/${encodedPath}`, {
|
|
2580
|
+
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
2472
2581
|
});
|
|
2473
|
-
|
|
2474
|
-
await
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2582
|
+
if (!repoRes.ok) return null;
|
|
2583
|
+
const repoInfo = await repoRes.json();
|
|
2584
|
+
if (repoInfo.visibility === "private") return null;
|
|
2585
|
+
const licenseId = repoInfo.license?.key?.toLowerCase() || null;
|
|
2586
|
+
const isOpenSource = repoInfo.visibility === "public" && (licenseId ? OPEN_SOURCE_LICENSES.includes(licenseId) : false);
|
|
2587
|
+
const detected = {
|
|
2588
|
+
name: repoInfo.name,
|
|
2589
|
+
description: repoInfo.description ?? void 0,
|
|
2590
|
+
stack: [],
|
|
2591
|
+
databases: [],
|
|
2592
|
+
commands: {},
|
|
2593
|
+
packageManager: null,
|
|
2594
|
+
type: "application",
|
|
2595
|
+
repoHost: "gitlab",
|
|
2596
|
+
repoUrl,
|
|
2597
|
+
license: licenseId ?? void 0,
|
|
2598
|
+
isPublicRepo: repoInfo.visibility === "public",
|
|
2599
|
+
isOpenSource,
|
|
2600
|
+
projectType: isOpenSource ? "open_source" : void 0,
|
|
2601
|
+
hasDocker: false,
|
|
2602
|
+
existingFiles: []
|
|
2603
|
+
};
|
|
2604
|
+
const filesRes = await fetch(`https://${host}/api/v4/projects/${encodedPath}/repository/tree?per_page=100`, {
|
|
2605
|
+
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
2482
2606
|
});
|
|
2483
|
-
|
|
2484
|
-
const
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2607
|
+
if (!filesRes.ok) return detected;
|
|
2608
|
+
const files = await filesRes.json();
|
|
2609
|
+
const fileNames = new Set(files.map((f) => f.name.toLowerCase()));
|
|
2610
|
+
for (const file of STATIC_FILES) {
|
|
2611
|
+
if (files.some((f) => f.name.toLowerCase() === file.toLowerCase())) {
|
|
2612
|
+
detected.existingFiles.push(file);
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
if (fileNames.has(".gitlab-ci.yml")) detected.cicd = "gitlab_ci";
|
|
2616
|
+
if (fileNames.has("jenkinsfile")) detected.cicd = "jenkins";
|
|
2617
|
+
if (fileNames.has("dockerfile") || fileNames.has("docker-compose.yml") || fileNames.has("docker-compose.yaml")) {
|
|
2618
|
+
detected.hasDocker = true;
|
|
2619
|
+
detected.stack.push("docker");
|
|
2620
|
+
const dockerComposeFile = fileNames.has("docker-compose.yml") ? "docker-compose.yml" : fileNames.has("docker-compose.yaml") ? "docker-compose.yaml" : null;
|
|
2621
|
+
if (dockerComposeFile) {
|
|
2622
|
+
const composeRes = await fetch(`https://${host}/api/v4/projects/${encodedPath}/repository/files/${encodeURIComponent(dockerComposeFile)}/raw?ref=HEAD`, {
|
|
2623
|
+
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
2624
|
+
});
|
|
2625
|
+
if (composeRes.ok) {
|
|
2626
|
+
try {
|
|
2627
|
+
const content = await composeRes.text();
|
|
2628
|
+
const lowerContent = content.toLowerCase();
|
|
2629
|
+
if (content.includes("registry.gitlab.com")) detected.containerRegistry = "gitlab_registry";
|
|
2630
|
+
else if (content.includes("ghcr.io")) detected.containerRegistry = "ghcr";
|
|
2631
|
+
else if (content.includes("docker.io") || /image:\s*[a-z0-9]+\/[a-z0-9]/.test(content)) detected.containerRegistry = "dockerhub";
|
|
2632
|
+
else if (content.includes("gcr.io")) detected.containerRegistry = "gcr";
|
|
2633
|
+
if (lowerContent.includes("postgres")) detected.databases.push("postgresql");
|
|
2634
|
+
if (lowerContent.includes("mysql") && !lowerContent.includes("mysql-")) detected.databases.push("mysql");
|
|
2635
|
+
if (lowerContent.includes("mongo")) detected.databases.push("mongodb");
|
|
2636
|
+
if (lowerContent.includes("redis")) detected.databases.push("redis");
|
|
2637
|
+
if (lowerContent.includes("sqlite")) detected.databases.push("sqlite");
|
|
2638
|
+
} catch {
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
if (fileNames.has("package.json")) {
|
|
2644
|
+
const pkgRes = await fetch(`https://${host}/api/v4/projects/${encodedPath}/repository/files/package.json/raw?ref=HEAD`, {
|
|
2645
|
+
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
2646
|
+
});
|
|
2647
|
+
if (pkgRes.ok) {
|
|
2648
|
+
try {
|
|
2649
|
+
const pkg = await pkgRes.json();
|
|
2650
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2651
|
+
if (allDeps["next"]) detected.stack.push("nextjs");
|
|
2652
|
+
if (allDeps["react"]) detected.stack.push("react");
|
|
2653
|
+
if (allDeps["vue"]) detected.stack.push("vue");
|
|
2654
|
+
if (allDeps["svelte"]) detected.stack.push("svelte");
|
|
2655
|
+
if (allDeps["express"]) detected.stack.push("express");
|
|
2656
|
+
if (allDeps["fastify"]) detected.stack.push("fastify");
|
|
2657
|
+
if (allDeps["typescript"]) detected.stack.push("typescript");
|
|
2658
|
+
if (allDeps["tailwindcss"]) detected.stack.push("tailwind");
|
|
2659
|
+
if (allDeps["prisma"]) detected.stack.push("prisma");
|
|
2660
|
+
if (allDeps["vitest"]) detected.testFramework = "vitest";
|
|
2661
|
+
else if (allDeps["jest"]) detected.testFramework = "jest";
|
|
2662
|
+
else if (allDeps["@playwright/test"]) detected.testFramework = "playwright";
|
|
2663
|
+
if (detected.stack.length === 0 || detected.stack.length === 1 && detected.stack[0] === "typescript") {
|
|
2664
|
+
detected.stack.unshift("javascript");
|
|
2665
|
+
}
|
|
2666
|
+
if (pkg.scripts) {
|
|
2667
|
+
if (pkg.scripts.build) detected.commands.build = "npm run build";
|
|
2668
|
+
if (pkg.scripts.test) detected.commands.test = "npm run test";
|
|
2669
|
+
if (pkg.scripts.lint) detected.commands.lint = "npm run lint";
|
|
2670
|
+
if (pkg.scripts.dev) detected.commands.dev = "npm run dev";
|
|
2671
|
+
}
|
|
2672
|
+
if (allDeps["pg"] || allDeps["postgres"]) {
|
|
2673
|
+
if (!detected.databases.includes("postgresql")) detected.databases.push("postgresql");
|
|
2674
|
+
}
|
|
2675
|
+
if (allDeps["better-sqlite3"] || allDeps["sqlite3"]) {
|
|
2676
|
+
if (!detected.databases.includes("sqlite")) detected.databases.push("sqlite");
|
|
2677
|
+
}
|
|
2678
|
+
if (allDeps["mongodb"] || allDeps["mongoose"]) {
|
|
2679
|
+
if (!detected.databases.includes("mongodb")) detected.databases.push("mongodb");
|
|
2680
|
+
}
|
|
2681
|
+
if (allDeps["redis"] || allDeps["ioredis"]) {
|
|
2682
|
+
if (!detected.databases.includes("redis")) detected.databases.push("redis");
|
|
2683
|
+
}
|
|
2684
|
+
} catch {
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2491
2687
|
}
|
|
2492
|
-
if (
|
|
2493
|
-
|
|
2688
|
+
if (fileNames.has("pyproject.toml")) {
|
|
2689
|
+
detected.stack.push("python");
|
|
2690
|
+
const pyRes = await fetch(`https://${host}/api/v4/projects/${encodedPath}/repository/files/pyproject.toml/raw?ref=HEAD`, {
|
|
2691
|
+
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
2692
|
+
});
|
|
2693
|
+
if (pyRes.ok) {
|
|
2694
|
+
try {
|
|
2695
|
+
const content = (await pyRes.text()).toLowerCase();
|
|
2696
|
+
if (content.includes("fastapi")) detected.stack.push("fastapi");
|
|
2697
|
+
if (content.includes("django")) detected.stack.push("django");
|
|
2698
|
+
if (content.includes("flask")) detected.stack.push("flask");
|
|
2699
|
+
if (content.includes("sqlalchemy")) detected.stack.push("sqlalchemy");
|
|
2700
|
+
if (content.includes("pytest")) detected.testFramework = "pytest";
|
|
2701
|
+
if (content.includes("asyncpg") || content.includes("psycopg")) {
|
|
2702
|
+
if (!detected.databases.includes("postgresql")) detected.databases.push("postgresql");
|
|
2703
|
+
}
|
|
2704
|
+
if (content.includes("aiosqlite") || content.includes("sqlite")) {
|
|
2705
|
+
if (!detected.databases.includes("sqlite")) detected.databases.push("sqlite");
|
|
2706
|
+
}
|
|
2707
|
+
} catch {
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
} else if (fileNames.has("requirements.txt")) {
|
|
2711
|
+
detected.stack.push("python");
|
|
2494
2712
|
}
|
|
2495
|
-
if (
|
|
2496
|
-
|
|
2713
|
+
if (fileNames.has("cargo.toml")) {
|
|
2714
|
+
detected.stack.push("rust");
|
|
2715
|
+
detected.commands.build = "cargo build";
|
|
2716
|
+
detected.commands.test = "cargo test";
|
|
2497
2717
|
}
|
|
2498
|
-
if (
|
|
2499
|
-
|
|
2718
|
+
if (fileNames.has("go.mod")) {
|
|
2719
|
+
detected.stack.push("go");
|
|
2720
|
+
detected.commands.build = "go build";
|
|
2721
|
+
detected.commands.test = "go test ./...";
|
|
2500
2722
|
}
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
spinner.fail("Failed to create blueprint");
|
|
2505
|
-
handleError(error);
|
|
2723
|
+
return detected;
|
|
2724
|
+
} catch {
|
|
2725
|
+
return null;
|
|
2506
2726
|
}
|
|
2507
2727
|
}
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
{
|
|
2514
|
-
type: "text",
|
|
2515
|
-
name: "name",
|
|
2516
|
-
message: "Hierarchy name:",
|
|
2517
|
-
initial: hierarchyName,
|
|
2518
|
-
validate: (v) => v.length > 0 || "Name is required"
|
|
2519
|
-
},
|
|
2520
|
-
{
|
|
2521
|
-
type: "select",
|
|
2522
|
-
name: "visibility",
|
|
2523
|
-
message: "Visibility for all blueprints:",
|
|
2524
|
-
choices: [
|
|
2525
|
-
{ title: "Private (only you)", value: "PRIVATE" },
|
|
2526
|
-
{ title: "Team (your team members)", value: "TEAM" },
|
|
2527
|
-
{ title: "Public (visible to everyone)", value: "PUBLIC" }
|
|
2528
|
-
],
|
|
2529
|
-
initial: 0
|
|
2530
|
-
}
|
|
2531
|
-
]);
|
|
2532
|
-
if (!responses.name) {
|
|
2533
|
-
console.log(chalk6.yellow("Push cancelled."));
|
|
2534
|
-
return;
|
|
2535
|
-
}
|
|
2536
|
-
hierarchyName = responses.name;
|
|
2537
|
-
visibility = responses.visibility || visibility;
|
|
2728
|
+
function isValidGitUrl(url) {
|
|
2729
|
+
const trimmed = url.trim();
|
|
2730
|
+
if (trimmed.startsWith("https://") || trimmed.startsWith("http://") || trimmed.startsWith("git://") || trimmed.startsWith("git@") || trimmed.startsWith("ssh://")) {
|
|
2731
|
+
const dangerousChars = /[;&|`$(){}[\]<>\\'"!#*?~]/;
|
|
2732
|
+
return !dangerousChars.test(trimmed);
|
|
2538
2733
|
}
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
const hierarchyResponse = await api.createHierarchy({
|
|
2546
|
-
name: hierarchyName,
|
|
2547
|
-
repository_root: repositoryRoot
|
|
2548
|
-
});
|
|
2549
|
-
hierarchyId = hierarchyResponse.hierarchy.id;
|
|
2550
|
-
console.log(chalk6.green(`\u2713 Created hierarchy: ${hierarchyId}`));
|
|
2551
|
-
} catch (error) {
|
|
2552
|
-
console.log(chalk6.red("Failed to create hierarchy"));
|
|
2553
|
-
handleError(error);
|
|
2554
|
-
return;
|
|
2734
|
+
return false;
|
|
2735
|
+
}
|
|
2736
|
+
async function detectFromShallowClone(repoUrl) {
|
|
2737
|
+
let tempDir = null;
|
|
2738
|
+
if (!isValidGitUrl(repoUrl)) {
|
|
2739
|
+
return null;
|
|
2555
2740
|
}
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
let failCount = 0;
|
|
2559
|
-
for (const file of files) {
|
|
2560
|
-
const spinner = ora5(`Uploading ${file.path}...`).start();
|
|
2741
|
+
try {
|
|
2742
|
+
tempDir = await mkdtemp(join3(tmpdir(), "lynxprompt-detect-"));
|
|
2561
2743
|
try {
|
|
2562
|
-
const
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
blueprintName = hierarchyName;
|
|
2566
|
-
} else {
|
|
2567
|
-
const dirname5 = path.dirname(file.path);
|
|
2568
|
-
blueprintName = dirname5.replace(/[/\\]/g, " / ");
|
|
2569
|
-
}
|
|
2570
|
-
const result = await api.createBlueprint({
|
|
2571
|
-
name: blueprintName,
|
|
2572
|
-
description: "",
|
|
2573
|
-
content,
|
|
2574
|
-
visibility,
|
|
2575
|
-
tags: [],
|
|
2576
|
-
hierarchy_id: hierarchyId,
|
|
2577
|
-
parent_id: file.isRoot ? null : rootBlueprintId,
|
|
2578
|
-
repository_path: file.path
|
|
2744
|
+
const result = spawnSync("git", ["clone", "--depth", "1", "--quiet", repoUrl, tempDir], {
|
|
2745
|
+
stdio: "pipe",
|
|
2746
|
+
timeout: 3e4
|
|
2579
2747
|
});
|
|
2580
|
-
if (
|
|
2581
|
-
|
|
2748
|
+
if (result.status !== 0) {
|
|
2749
|
+
return null;
|
|
2582
2750
|
}
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2751
|
+
} catch {
|
|
2752
|
+
return null;
|
|
2753
|
+
}
|
|
2754
|
+
const detected = await detectProject(tempDir);
|
|
2755
|
+
if (detected) {
|
|
2756
|
+
detected.repoHost = detectRepoHost(repoUrl);
|
|
2757
|
+
detected.repoUrl = repoUrl;
|
|
2758
|
+
}
|
|
2759
|
+
return detected;
|
|
2760
|
+
} catch {
|
|
2761
|
+
return null;
|
|
2762
|
+
} finally {
|
|
2763
|
+
if (tempDir) {
|
|
2764
|
+
try {
|
|
2765
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
2766
|
+
} catch {
|
|
2599
2767
|
}
|
|
2600
|
-
failCount++;
|
|
2601
2768
|
}
|
|
2602
2769
|
}
|
|
2603
|
-
console.log();
|
|
2604
|
-
console.log(chalk6.green(`\u2705 Hierarchy created successfully!`));
|
|
2605
|
-
console.log(chalk6.gray(` Hierarchy: ${hierarchyId}`));
|
|
2606
|
-
console.log(chalk6.gray(` Name: ${hierarchyName}`));
|
|
2607
|
-
console.log(chalk6.gray(` Blueprints: ${successCount} uploaded${failCount > 0 ? `, ${failCount} failed` : ""}`));
|
|
2608
|
-
console.log();
|
|
2609
|
-
console.log(chalk6.cyan("Tips:"));
|
|
2610
|
-
console.log(chalk6.gray(` \u2022 Run 'lynxp status' to see all tracked blueprints`));
|
|
2611
|
-
console.log(chalk6.gray(` \u2022 Run 'lynxp pull ${hierarchyId}' to download the entire hierarchy`));
|
|
2612
|
-
console.log(chalk6.gray(` \u2022 Run 'lynxp push' in any subfolder to update individual blueprints`));
|
|
2613
|
-
console.log();
|
|
2614
2770
|
}
|
|
2615
|
-
function
|
|
2616
|
-
const
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
".github/copilot-instructions.md",
|
|
2621
|
-
".windsurfrules",
|
|
2622
|
-
"AIDER.md",
|
|
2623
|
-
"GEMINI.md",
|
|
2624
|
-
".clinerules"
|
|
2625
|
-
];
|
|
2626
|
-
for (const candidate of candidates) {
|
|
2627
|
-
if (fs.existsSync(candidate)) {
|
|
2628
|
-
return candidate;
|
|
2629
|
-
}
|
|
2771
|
+
async function detectFromRemoteUrl(repoUrl) {
|
|
2772
|
+
const host = detectRepoHost(repoUrl);
|
|
2773
|
+
if (host === "github") {
|
|
2774
|
+
const result = await detectFromGitHubApi(repoUrl);
|
|
2775
|
+
if (result) return result;
|
|
2630
2776
|
}
|
|
2631
|
-
|
|
2777
|
+
if (host === "gitlab") {
|
|
2778
|
+
const result = await detectFromGitLabApi(repoUrl);
|
|
2779
|
+
if (result) return result;
|
|
2780
|
+
}
|
|
2781
|
+
return detectFromShallowClone(repoUrl);
|
|
2632
2782
|
}
|
|
2633
|
-
function
|
|
2634
|
-
const
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
if (normalizedPath.endsWith(".windsurfrules")) return "WINDSURF_RULES";
|
|
2642
|
-
if (normalizedPath.endsWith(".clinerules")) return "CLINE_RULES";
|
|
2643
|
-
if (normalizedPath.includes(".github/copilot-instructions.md")) return "COPILOT_INSTRUCTIONS";
|
|
2644
|
-
if (normalizedPath.endsWith("GEMINI.md")) return "GEMINI_MD";
|
|
2645
|
-
if (normalizedPath.endsWith("AIDER.md")) return "AGENTS_MD";
|
|
2646
|
-
if (normalizedPath.endsWith("AGENTS.md")) return "AGENTS_MD";
|
|
2647
|
-
if (normalizedPath.endsWith(".md")) return "AGENTS_MD";
|
|
2648
|
-
return "CUSTOM";
|
|
2783
|
+
function isGitUrl(str) {
|
|
2784
|
+
const patterns = [
|
|
2785
|
+
/^https?:\/\/[^/]+\/.*$/,
|
|
2786
|
+
/^git@[^:]+:.*$/,
|
|
2787
|
+
/^git:\/\/.*$/,
|
|
2788
|
+
/^ssh:\/\/.*$/
|
|
2789
|
+
];
|
|
2790
|
+
return patterns.some((p) => p.test(str.trim()));
|
|
2649
2791
|
}
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2792
|
+
var COMMAND_DIRECTORIES = [
|
|
2793
|
+
{ directory: ".cursor/commands", type: "cursor-command", platform: "cursor", templateType: "CURSOR_COMMAND" },
|
|
2794
|
+
{ directory: ".claude/commands", type: "claude-command", platform: "claude", templateType: "CLAUDE_COMMAND" },
|
|
2795
|
+
{ directory: ".windsurf/workflows", type: "windsurf-workflow", platform: "windsurf", templateType: "WINDSURF_WORKFLOW" },
|
|
2796
|
+
{ directory: ".copilot/prompts", type: "copilot-prompt", platform: "copilot", templateType: "COPILOT_PROMPT" },
|
|
2797
|
+
{ directory: ".continue/prompts", type: "continue-prompt", platform: "continue", templateType: "CONTINUE_PROMPT" },
|
|
2798
|
+
{ directory: ".opencode/commands", type: "opencode-command", platform: "opencode", templateType: "OPENCODE_COMMAND" }
|
|
2799
|
+
];
|
|
2800
|
+
async function detectCommandFiles(cwd) {
|
|
2801
|
+
const commands = [];
|
|
2802
|
+
const { readdir: readdir4, readFile: readFileAsync } = await import("fs/promises");
|
|
2803
|
+
for (const cmdDir of COMMAND_DIRECTORIES) {
|
|
2804
|
+
const dirPath = join3(cwd, cmdDir.directory);
|
|
2805
|
+
try {
|
|
2806
|
+
await access2(dirPath);
|
|
2807
|
+
const entries = await readdir4(dirPath, { withFileTypes: true });
|
|
2808
|
+
for (const entry of entries) {
|
|
2809
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
2810
|
+
const filePath = join3(dirPath, entry.name);
|
|
2811
|
+
try {
|
|
2812
|
+
const content = await readFileAsync(filePath, "utf-8");
|
|
2813
|
+
const name = entry.name.replace(/\.md$/, "");
|
|
2814
|
+
commands.push({
|
|
2815
|
+
path: filePath,
|
|
2816
|
+
name,
|
|
2817
|
+
type: cmdDir.type,
|
|
2818
|
+
content,
|
|
2819
|
+
platform: cmdDir.platform,
|
|
2820
|
+
templateType: cmdDir.templateType
|
|
2821
|
+
});
|
|
2822
|
+
} catch {
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
} catch {
|
|
2659
2827
|
}
|
|
2660
|
-
} else {
|
|
2661
|
-
console.error(chalk6.red("An unexpected error occurred."));
|
|
2662
2828
|
}
|
|
2663
|
-
|
|
2829
|
+
return commands;
|
|
2664
2830
|
}
|
|
2665
2831
|
|
|
2666
|
-
// src/commands/wizard.ts
|
|
2667
|
-
import chalk7 from "chalk";
|
|
2668
|
-
import prompts3 from "prompts";
|
|
2669
|
-
import ora6 from "ora";
|
|
2670
|
-
import * as readline from "readline";
|
|
2671
|
-
import * as os from "os";
|
|
2672
|
-
import { writeFile as writeFile3, mkdir as mkdir3, access as access3, readFile as readFile4 } from "fs/promises";
|
|
2673
|
-
import { join as join4, dirname as dirname3 } from "path";
|
|
2674
|
-
|
|
2675
2832
|
// src/utils/generator.ts
|
|
2676
2833
|
function bpVar(blueprintMode, varName, defaultValue) {
|
|
2677
2834
|
if (!blueprintMode || !defaultValue) return defaultValue;
|
|
@@ -5529,7 +5686,7 @@ var TEST_FRAMEWORKS2 = TEST_FRAMEWORKS;
|
|
|
5529
5686
|
|
|
5530
5687
|
// src/commands/wizard.ts
|
|
5531
5688
|
var DRAFTS_DIR = ".lynxprompt/drafts";
|
|
5532
|
-
var CLI_VERSION = "2.
|
|
5689
|
+
var CLI_VERSION = "2.1.1";
|
|
5533
5690
|
async function saveDraftLocally(name, config2, stepReached) {
|
|
5534
5691
|
const draftsPath = join4(process.cwd(), DRAFTS_DIR);
|
|
5535
5692
|
await mkdir3(draftsPath, { recursive: true });
|
|
@@ -11181,7 +11338,7 @@ async function configCommand(action, valueArg) {
|
|
|
11181
11338
|
}
|
|
11182
11339
|
|
|
11183
11340
|
// src/index.ts
|
|
11184
|
-
var CLI_VERSION2 = "2.
|
|
11341
|
+
var CLI_VERSION2 = "2.1.1";
|
|
11185
11342
|
var program = new Command();
|
|
11186
11343
|
program.name("lynxprompt").description("CLI for LynxPrompt - Generate AI IDE configuration files").version(CLI_VERSION2);
|
|
11187
11344
|
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)").option("-o, --output <dir>", "Output directory (default: current directory)").option("--repo-url <url>", "Analyze remote repository URL (GitHub/GitLab supported)").option("--blueprint", "Generate with [[VARIABLE|default]] placeholders for templates").option("--license <type>", "License type (mit, apache-2.0, gpl-3.0, etc.)").option("--ci-cd <platform>", "CI/CD platform (github_actions, gitlab_ci, jenkins, etc.)").option("--project-type <type>", "Project type (work, leisure, opensource, learning)").option("--detect-only", "Only detect project info, don't generate files").option("--load-draft <name>", "Load a saved wizard draft").option("--save-draft <name>", "Save wizard state as a draft (auto-saves at end)").option("--vars <values>", "Fill variables: VAR1=value1,VAR2=value2").action(wizardCommand);
|
|
@@ -11194,7 +11351,7 @@ program.command("status").description("Show current AI configuration and tracked
|
|
|
11194
11351
|
program.command("pull <id>").description("Download a blueprint (bp_xxx) or entire hierarchy (ha_xxx)").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);
|
|
11195
11352
|
program.command("search <query>").description("Search public blueprints in the marketplace").option("-l, --limit <number>", "Number of results", "20").action(searchCommand);
|
|
11196
11353
|
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);
|
|
11197
|
-
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").option("-f, --force", "Force push (overwrite remote changes)").action(pushCommand);
|
|
11354
|
+
program.command("push [file]").description("Push local file to LynxPrompt cloud as a blueprint").option("-a, --all", "Scan recursively and push ALL AI config files (40+ types)").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").option("-f, --force", "Force push (overwrite remote changes)").action(pushCommand);
|
|
11198
11355
|
program.command("hierarchies").description("List your blueprint hierarchies (monorepo groupings)").option("-l, --limit <number>", "Number of results", "50").option("-j, --json", "Output as JSON").action(hierarchiesCommand);
|
|
11199
11356
|
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);
|
|
11200
11357
|
program.command("unlink [file]").description("Disconnect a local file from its cloud blueprint").action(unlinkCommand);
|
|
@@ -11232,6 +11389,7 @@ ${chalk19.cyan("Marketplace:")}
|
|
|
11232
11389
|
${chalk19.white("$ lynxp pull bp_abc123")} ${chalk19.gray("Download and track a blueprint")}
|
|
11233
11390
|
${chalk19.white("$ lynxp pull ha_xyz789")} ${chalk19.gray("Download entire hierarchy")}
|
|
11234
11391
|
${chalk19.white("$ lynxp push")} ${chalk19.gray("Push local file to cloud")}
|
|
11392
|
+
${chalk19.white("$ lynxp push --all")} ${chalk19.gray("Push ALL config files recursively")}
|
|
11235
11393
|
${chalk19.white("$ lynxp hierarchies")} ${chalk19.gray("List your hierarchies")}
|
|
11236
11394
|
${chalk19.white("$ lynxp link --list")} ${chalk19.gray("Show tracked blueprints")}
|
|
11237
11395
|
|