lynxprompt 1.2.16 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1535 -1362
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1059,1087 +1059,862 @@ import fs from "fs";
|
|
|
1059
1059
|
import path from "path";
|
|
1060
1060
|
import { createHash as createHash2 } from "crypto";
|
|
1061
1061
|
import prompts2 from "prompts";
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1062
|
+
|
|
1063
|
+
// src/utils/detect.ts
|
|
1064
|
+
import { readFile as readFile3, access as access2, rm, mkdtemp } from "fs/promises";
|
|
1065
|
+
import { join as join3 } from "path";
|
|
1066
|
+
import { tmpdir } from "os";
|
|
1067
|
+
import { spawnSync } from "child_process";
|
|
1068
|
+
var JS_FRAMEWORK_PATTERNS = {
|
|
1069
|
+
nextjs: ["next"],
|
|
1070
|
+
react: ["react", "react-dom"],
|
|
1071
|
+
vue: ["vue"],
|
|
1072
|
+
angular: ["@angular/core"],
|
|
1073
|
+
svelte: ["svelte", "@sveltejs/kit"],
|
|
1074
|
+
solid: ["solid-js"],
|
|
1075
|
+
remix: ["@remix-run/react"],
|
|
1076
|
+
astro: ["astro"],
|
|
1077
|
+
nuxt: ["nuxt"],
|
|
1078
|
+
gatsby: ["gatsby"]
|
|
1079
|
+
};
|
|
1080
|
+
var JS_TOOL_PATTERNS = {
|
|
1081
|
+
typescript: ["typescript"],
|
|
1082
|
+
tailwind: ["tailwindcss"],
|
|
1083
|
+
prisma: ["prisma", "@prisma/client"],
|
|
1084
|
+
drizzle: ["drizzle-orm"],
|
|
1085
|
+
express: ["express"],
|
|
1086
|
+
fastify: ["fastify"],
|
|
1087
|
+
hono: ["hono"],
|
|
1088
|
+
elysia: ["elysia"],
|
|
1089
|
+
trpc: ["@trpc/server"],
|
|
1090
|
+
graphql: ["graphql", "@apollo/server"],
|
|
1091
|
+
jest: ["jest"],
|
|
1092
|
+
vitest: ["vitest"],
|
|
1093
|
+
playwright: ["@playwright/test"],
|
|
1094
|
+
cypress: ["cypress"],
|
|
1095
|
+
eslint: ["eslint"],
|
|
1096
|
+
biome: ["@biomejs/biome"],
|
|
1097
|
+
prettier: ["prettier"],
|
|
1098
|
+
vite: ["vite"],
|
|
1099
|
+
webpack: ["webpack"],
|
|
1100
|
+
turbo: ["turbo"]
|
|
1101
|
+
};
|
|
1102
|
+
async function detectExtendedCommands(cwd) {
|
|
1103
|
+
const cmds = {
|
|
1104
|
+
test: [],
|
|
1105
|
+
testCoverage: [],
|
|
1106
|
+
install: [],
|
|
1107
|
+
dev: [],
|
|
1108
|
+
build: [],
|
|
1109
|
+
lint: [],
|
|
1110
|
+
format: [],
|
|
1111
|
+
typecheck: [],
|
|
1112
|
+
clean: [],
|
|
1113
|
+
preCommit: [],
|
|
1114
|
+
additional: []
|
|
1115
|
+
};
|
|
1116
|
+
const addCmd = (category, cmd, desc) => {
|
|
1117
|
+
if (!cmds[category].some((c) => c.cmd === cmd)) {
|
|
1118
|
+
cmds[category].push({ cmd, desc });
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
const packageJsonPath = join3(cwd, "package.json");
|
|
1122
|
+
if (await fileExists(packageJsonPath)) {
|
|
1066
1123
|
try {
|
|
1067
|
-
const
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1124
|
+
const content = await readFile3(packageJsonPath, "utf-8");
|
|
1125
|
+
const pkg = JSON.parse(content);
|
|
1126
|
+
if (pkg.scripts) {
|
|
1127
|
+
for (const [name, script] of Object.entries(pkg.scripts)) {
|
|
1128
|
+
const scriptStr = String(script);
|
|
1129
|
+
const fullCmd = `npm run ${name}`;
|
|
1130
|
+
if (name.match(/^test$|^test:/i) || scriptStr.includes("jest") || scriptStr.includes("vitest") || scriptStr.includes("mocha")) {
|
|
1131
|
+
if (name.includes("cov") || scriptStr.includes("--coverage")) {
|
|
1132
|
+
addCmd("testCoverage", fullCmd, `Run ${name}`);
|
|
1133
|
+
} else {
|
|
1134
|
+
addCmd("test", fullCmd, `Run ${name}`);
|
|
1135
|
+
}
|
|
1136
|
+
} else if (name.match(/^lint$|^lint:/i) || scriptStr.includes("eslint") || scriptStr.includes("biome")) {
|
|
1137
|
+
addCmd("lint", fullCmd, `Run ${name}`);
|
|
1138
|
+
} else if (name.match(/^format$|^fmt$|format:/i) || scriptStr.includes("prettier")) {
|
|
1139
|
+
addCmd("format", fullCmd, `Run ${name}`);
|
|
1140
|
+
} else if (name.match(/^build$|^build:/i) || scriptStr.includes("tsc") || scriptStr.includes("webpack") || scriptStr.includes("vite build")) {
|
|
1141
|
+
addCmd("build", fullCmd, `Run ${name}`);
|
|
1142
|
+
} else if (name.match(/^dev$|^start$|^serve$/i)) {
|
|
1143
|
+
addCmd("dev", fullCmd, `Run ${name}`);
|
|
1144
|
+
} else if (name.match(/^typecheck$|^type-check$|^types$|^check:types/i) || scriptStr.includes("tsc --noEmit")) {
|
|
1145
|
+
addCmd("typecheck", fullCmd, `Run ${name}`);
|
|
1146
|
+
} else if (name.match(/^clean$|^clean:/i) || scriptStr.includes("rimraf") || scriptStr.includes("rm -rf")) {
|
|
1147
|
+
addCmd("clean", fullCmd, `Run ${name}`);
|
|
1148
|
+
} else if (name.match(/^prepare$|^precommit$|^pre-commit$|^husky/i)) {
|
|
1149
|
+
addCmd("preCommit", fullCmd, `Run ${name}`);
|
|
1150
|
+
} else if (name === "install" || name === "postinstall") {
|
|
1151
|
+
addCmd("install", fullCmd, `Run ${name}`);
|
|
1152
|
+
} else if (!["publish", "prepublish", "prepublishOnly", "version", "postversion"].includes(name)) {
|
|
1153
|
+
addCmd("additional", fullCmd, `Run ${name}`);
|
|
1073
1154
|
}
|
|
1074
|
-
scan(fullPath, depth + 1);
|
|
1075
|
-
} else if (entry.name === "AGENTS.md") {
|
|
1076
|
-
const relativePath = path.relative(cwd, fullPath);
|
|
1077
|
-
results.push({
|
|
1078
|
-
path: relativePath,
|
|
1079
|
-
absolutePath: fullPath,
|
|
1080
|
-
isRoot: relativePath === "AGENTS.md"
|
|
1081
|
-
});
|
|
1082
1155
|
}
|
|
1083
1156
|
}
|
|
1084
1157
|
} catch {
|
|
1085
1158
|
}
|
|
1086
1159
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
result.parentId = parentBlueprint.id;
|
|
1160
|
+
const pyprojectPath = join3(cwd, "pyproject.toml");
|
|
1161
|
+
if (await fileExists(pyprojectPath)) {
|
|
1162
|
+
try {
|
|
1163
|
+
const content = await readFile3(pyprojectPath, "utf-8");
|
|
1164
|
+
if (content.includes("pytest") || content.includes("[tool.pytest")) {
|
|
1165
|
+
addCmd("test", "python -m pytest tests/ -v --tb=short", "Run pytest");
|
|
1166
|
+
addCmd("testCoverage", "python -m pytest tests/ --cov=src --cov-report=term-missing", "Run pytest with coverage");
|
|
1167
|
+
}
|
|
1168
|
+
if (content.includes("ruff")) {
|
|
1169
|
+
addCmd("lint", "ruff check .", "Run ruff linter");
|
|
1170
|
+
addCmd("format", "ruff format .", "Run ruff formatter");
|
|
1171
|
+
}
|
|
1172
|
+
if (content.includes("black")) {
|
|
1173
|
+
addCmd("format", "black .", "Run black formatter");
|
|
1174
|
+
}
|
|
1175
|
+
if (content.includes("mypy")) {
|
|
1176
|
+
addCmd("typecheck", "mypy .", "Run mypy type checker");
|
|
1177
|
+
}
|
|
1178
|
+
const poetryScriptsMatch = content.match(/\[tool\.poetry\.scripts\]([\s\S]*?)(?=\n\[|$)/);
|
|
1179
|
+
if (poetryScriptsMatch) {
|
|
1180
|
+
const scriptLines = poetryScriptsMatch[1].split("\n").filter((l) => l.includes("="));
|
|
1181
|
+
for (const line of scriptLines) {
|
|
1182
|
+
const match = line.match(/(\w+)\s*=\s*"([^"]+)"/);
|
|
1183
|
+
if (match) {
|
|
1184
|
+
const [, name, entry] = match;
|
|
1185
|
+
addCmd("additional", `poetry run ${name}`, entry);
|
|
1186
|
+
}
|
|
1115
1187
|
}
|
|
1116
1188
|
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1189
|
+
const poeMatch = content.match(/\[tool\.poe\.tasks\]([\s\S]*?)(?=\n\[tool\.|$)/);
|
|
1190
|
+
if (poeMatch) {
|
|
1191
|
+
const taskLines = poeMatch[1].split("\n").filter((l) => l.includes("="));
|
|
1192
|
+
for (const line of taskLines) {
|
|
1193
|
+
const match = line.match(/(\w+)\s*=\s*"([^"]+)"/);
|
|
1194
|
+
if (match) {
|
|
1195
|
+
const [, name, cmd] = match;
|
|
1196
|
+
if (name.match(/test/i)) {
|
|
1197
|
+
addCmd("test", `poe ${name}`, cmd);
|
|
1198
|
+
} else if (name.match(/lint/i)) {
|
|
1199
|
+
addCmd("lint", `poe ${name}`, cmd);
|
|
1200
|
+
} else if (name.match(/format/i)) {
|
|
1201
|
+
addCmd("format", `poe ${name}`, cmd);
|
|
1202
|
+
} else {
|
|
1203
|
+
addCmd("additional", `poe ${name}`, cmd);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
if (content.includes("fastapi") || content.includes("uvicorn")) {
|
|
1209
|
+
addCmd("dev", "uvicorn src.main:app --host 0.0.0.0 --port 8000 --reload", "Run FastAPI dev server");
|
|
1210
|
+
}
|
|
1211
|
+
if (content.includes("[tool.poetry]")) {
|
|
1212
|
+
addCmd("install", "poetry install", "Install dependencies with Poetry");
|
|
1213
|
+
} else if (await fileExists(join3(cwd, "uv.lock"))) {
|
|
1214
|
+
addCmd("install", "uv sync", "Sync dependencies with uv");
|
|
1215
|
+
} else {
|
|
1216
|
+
addCmd("install", "pip install -r requirements.txt", "Install dependencies with pip");
|
|
1217
|
+
}
|
|
1218
|
+
} catch {
|
|
1119
1219
|
}
|
|
1120
|
-
} catch {
|
|
1121
|
-
}
|
|
1122
|
-
return result;
|
|
1123
|
-
}
|
|
1124
|
-
async function ensureHierarchy(_cwd, repositoryRoot, name) {
|
|
1125
|
-
try {
|
|
1126
|
-
const response = await api.createHierarchy({
|
|
1127
|
-
name,
|
|
1128
|
-
repository_root: repositoryRoot
|
|
1129
|
-
});
|
|
1130
|
-
return response.hierarchy.id;
|
|
1131
|
-
} catch (error) {
|
|
1132
|
-
console.log(chalk6.gray(" Note: Hierarchy creation skipped"));
|
|
1133
|
-
return null;
|
|
1134
1220
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
const urlMatch = gitConfig.match(/url = (.+)/);
|
|
1142
|
-
if (urlMatch) {
|
|
1143
|
-
return createHash2("sha256").update(urlMatch[1].trim()).digest("hex").substring(0, 16);
|
|
1221
|
+
const requirementsPath = join3(cwd, "requirements.txt");
|
|
1222
|
+
if (await fileExists(requirementsPath)) {
|
|
1223
|
+
try {
|
|
1224
|
+
const content = await readFile3(requirementsPath, "utf-8");
|
|
1225
|
+
if (content.includes("pytest")) {
|
|
1226
|
+
addCmd("test", "python -m pytest tests/ -v", "Run pytest");
|
|
1144
1227
|
}
|
|
1228
|
+
addCmd("install", "pip install -r requirements.txt", "Install dependencies");
|
|
1229
|
+
} catch {
|
|
1145
1230
|
}
|
|
1146
|
-
} catch {
|
|
1147
|
-
}
|
|
1148
|
-
return createHash2("sha256").update(path.resolve(rootPath)).digest("hex").substring(0, 16);
|
|
1149
|
-
}
|
|
1150
|
-
async function pushCommand(fileArg, options) {
|
|
1151
|
-
const cwd = process.cwd();
|
|
1152
|
-
if (!isAuthenticated()) {
|
|
1153
|
-
console.log(chalk6.yellow("You need to be logged in to push blueprints."));
|
|
1154
|
-
console.log(chalk6.gray("Run 'lynxp login' to authenticate."));
|
|
1155
|
-
process.exit(1);
|
|
1156
|
-
}
|
|
1157
|
-
const file = fileArg || findDefaultFile();
|
|
1158
|
-
if (!file) {
|
|
1159
|
-
console.log(chalk6.red("No AI configuration file found."));
|
|
1160
|
-
console.log(
|
|
1161
|
-
chalk6.gray("Specify a file or run in a directory with AGENTS.md, CLAUDE.md, etc.")
|
|
1162
|
-
);
|
|
1163
|
-
process.exit(1);
|
|
1164
|
-
}
|
|
1165
|
-
if (!fs.existsSync(file)) {
|
|
1166
|
-
console.log(chalk6.red(`File not found: ${file}`));
|
|
1167
|
-
process.exit(1);
|
|
1168
1231
|
}
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1232
|
+
const makefilePath = join3(cwd, "Makefile");
|
|
1233
|
+
if (await fileExists(makefilePath)) {
|
|
1234
|
+
try {
|
|
1235
|
+
const content = await readFile3(makefilePath, "utf-8");
|
|
1236
|
+
const targetMatches = content.matchAll(/^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(?:[a-zA-Z0-9_\- ]*)?$/gm);
|
|
1237
|
+
for (const match of targetMatches) {
|
|
1238
|
+
const target = match[1];
|
|
1239
|
+
const cmd = `make ${target}`;
|
|
1240
|
+
if (target.match(/^test$|^tests$/i)) {
|
|
1241
|
+
addCmd("test", cmd, `Make ${target}`);
|
|
1242
|
+
} else if (target.match(/^test[-_]?cov|^coverage$/i)) {
|
|
1243
|
+
addCmd("testCoverage", cmd, `Make ${target}`);
|
|
1244
|
+
} else if (target.match(/^lint$/i)) {
|
|
1245
|
+
addCmd("lint", cmd, `Make ${target}`);
|
|
1246
|
+
} else if (target.match(/^format$|^fmt$/i)) {
|
|
1247
|
+
addCmd("format", cmd, `Make ${target}`);
|
|
1248
|
+
} else if (target.match(/^build$/i)) {
|
|
1249
|
+
addCmd("build", cmd, `Make ${target}`);
|
|
1250
|
+
} else if (target.match(/^dev$|^run$|^serve$/i)) {
|
|
1251
|
+
addCmd("dev", cmd, `Make ${target}`);
|
|
1252
|
+
} else if (target.match(/^typecheck$|^types$/i)) {
|
|
1253
|
+
addCmd("typecheck", cmd, `Make ${target}`);
|
|
1254
|
+
} else if (target.match(/^clean$/i)) {
|
|
1255
|
+
addCmd("clean", cmd, `Make ${target}`);
|
|
1256
|
+
} else if (target.match(/^install$|^deps$/i)) {
|
|
1257
|
+
addCmd("install", cmd, `Make ${target}`);
|
|
1258
|
+
} else if (!["all", "default", ".PHONY", ".DEFAULT_GOAL"].includes(target)) {
|
|
1259
|
+
addCmd("additional", cmd, `Make ${target}`);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
} catch {
|
|
1192
1263
|
}
|
|
1193
1264
|
}
|
|
1194
|
-
const
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1265
|
+
const dockerComposePath = join3(cwd, "docker-compose.yml");
|
|
1266
|
+
const dockerComposeYamlPath = join3(cwd, "docker-compose.yaml");
|
|
1267
|
+
const composePath = await fileExists(dockerComposePath) ? dockerComposePath : await fileExists(dockerComposeYamlPath) ? dockerComposeYamlPath : null;
|
|
1268
|
+
if (composePath) {
|
|
1269
|
+
try {
|
|
1270
|
+
const content = await readFile3(composePath, "utf-8");
|
|
1271
|
+
const serviceMatches = content.matchAll(/^\s{2}([a-zA-Z_][a-zA-Z0-9_-]*):\s*$/gm);
|
|
1272
|
+
for (const match of serviceMatches) {
|
|
1273
|
+
const service = match[1];
|
|
1274
|
+
addCmd("additional", `docker compose up ${service}`, `Run ${service} service`);
|
|
1275
|
+
}
|
|
1276
|
+
addCmd("dev", "docker compose up", "Start all services");
|
|
1277
|
+
addCmd("build", "docker compose build", "Build all services");
|
|
1278
|
+
addCmd("clean", "docker compose down -v", "Stop and remove volumes");
|
|
1279
|
+
} catch {
|
|
1199
1280
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1281
|
+
}
|
|
1282
|
+
const dockerfilePath = join3(cwd, "Dockerfile");
|
|
1283
|
+
if (await fileExists(dockerfilePath)) {
|
|
1284
|
+
try {
|
|
1285
|
+
const content = await readFile3(dockerfilePath, "utf-8");
|
|
1286
|
+
const fromMatch = content.match(/FROM\s+([^\s]+)/);
|
|
1287
|
+
const imageName = fromMatch ? fromMatch[1].split(":")[0] : "app";
|
|
1288
|
+
addCmd("build", `docker build -t ${imageName} .`, "Build Docker image");
|
|
1289
|
+
addCmd("dev", `docker run -it --rm ${imageName}`, "Run Docker container");
|
|
1290
|
+
} catch {
|
|
1208
1291
|
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
if (
|
|
1213
|
-
|
|
1214
|
-
console.log(chalk6.yellow("\u26A0 Conflict: The blueprint has been modified since you last pulled it."));
|
|
1215
|
-
console.log(chalk6.gray(" Someone else may have pushed changes."));
|
|
1216
|
-
console.log();
|
|
1217
|
-
console.log(chalk6.gray("Options:"));
|
|
1218
|
-
console.log(chalk6.gray(" 1. Run 'lynxp pull " + blueprintId + "' to get the latest version"));
|
|
1219
|
-
console.log(chalk6.gray(" 2. Run 'lynxp push --force' to overwrite remote changes"));
|
|
1220
|
-
process.exit(1);
|
|
1292
|
+
}
|
|
1293
|
+
try {
|
|
1294
|
+
const dockerViewerPath = join3(cwd, "Dockerfile.viewer");
|
|
1295
|
+
if (await fileExists(dockerViewerPath)) {
|
|
1296
|
+
addCmd("build", "docker build -f Dockerfile.viewer -t app-viewer .", "Build viewer Docker image");
|
|
1221
1297
|
}
|
|
1222
|
-
|
|
1298
|
+
} catch {
|
|
1299
|
+
}
|
|
1300
|
+
const cargoPath = join3(cwd, "Cargo.toml");
|
|
1301
|
+
if (await fileExists(cargoPath)) {
|
|
1302
|
+
addCmd("build", "cargo build", "Build Rust project");
|
|
1303
|
+
addCmd("build", "cargo build --release", "Build release");
|
|
1304
|
+
addCmd("test", "cargo test", "Run Rust tests");
|
|
1305
|
+
addCmd("lint", "cargo clippy", "Run Clippy linter");
|
|
1306
|
+
addCmd("format", "cargo fmt", "Format Rust code");
|
|
1307
|
+
addCmd("dev", "cargo run", "Run Rust binary");
|
|
1308
|
+
addCmd("clean", "cargo clean", "Clean build artifacts");
|
|
1309
|
+
}
|
|
1310
|
+
const goModPath = join3(cwd, "go.mod");
|
|
1311
|
+
if (await fileExists(goModPath)) {
|
|
1312
|
+
addCmd("build", "go build", "Build Go project");
|
|
1313
|
+
addCmd("test", "go test ./...", "Run Go tests");
|
|
1314
|
+
addCmd("lint", "golangci-lint run", "Run golangci-lint");
|
|
1315
|
+
addCmd("format", "go fmt ./...", "Format Go code");
|
|
1316
|
+
addCmd("dev", "go run .", "Run Go binary");
|
|
1317
|
+
addCmd("clean", "go clean", "Clean build cache");
|
|
1318
|
+
addCmd("typecheck", "go vet ./...", "Run go vet");
|
|
1319
|
+
}
|
|
1320
|
+
const srcMainPath = join3(cwd, "src", "main.py");
|
|
1321
|
+
const mainPath = join3(cwd, "main.py");
|
|
1322
|
+
const appPath = join3(cwd, "app.py");
|
|
1323
|
+
if (await fileExists(srcMainPath)) {
|
|
1324
|
+
addCmd("dev", "python -m src.main", "Run main module");
|
|
1325
|
+
}
|
|
1326
|
+
if (await fileExists(mainPath)) {
|
|
1327
|
+
addCmd("dev", "python main.py", "Run main.py");
|
|
1328
|
+
}
|
|
1329
|
+
if (await fileExists(appPath)) {
|
|
1330
|
+
addCmd("dev", "python app.py", "Run app.py");
|
|
1331
|
+
}
|
|
1332
|
+
const schedulerPath = join3(cwd, "src", "scheduler.py");
|
|
1333
|
+
if (await fileExists(schedulerPath)) {
|
|
1334
|
+
addCmd("additional", "python -m src.scheduler", "Run scheduler");
|
|
1335
|
+
}
|
|
1336
|
+
const setupAuthPath = join3(cwd, "src", "setup_auth.py");
|
|
1337
|
+
if (await fileExists(setupAuthPath)) {
|
|
1338
|
+
addCmd("additional", "python -m src.setup_auth", "Setup authentication");
|
|
1223
1339
|
}
|
|
1340
|
+
const webMainPath = join3(cwd, "src", "web", "main.py");
|
|
1341
|
+
if (await fileExists(webMainPath)) {
|
|
1342
|
+
addCmd("dev", "uvicorn src.web.main:app --host 0.0.0.0 --port 8080", "Run web viewer");
|
|
1343
|
+
}
|
|
1344
|
+
return cmds;
|
|
1224
1345
|
}
|
|
1225
|
-
async function
|
|
1226
|
-
const
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
});
|
|
1246
|
-
shouldCreateHierarchy = createHierarchy;
|
|
1346
|
+
async function detectProject(cwd) {
|
|
1347
|
+
const detected = {
|
|
1348
|
+
name: null,
|
|
1349
|
+
stack: [],
|
|
1350
|
+
databases: [],
|
|
1351
|
+
commands: {},
|
|
1352
|
+
packageManager: null,
|
|
1353
|
+
type: "unknown"
|
|
1354
|
+
};
|
|
1355
|
+
const packageJsonPath = join3(cwd, "package.json");
|
|
1356
|
+
if (await fileExists(packageJsonPath)) {
|
|
1357
|
+
try {
|
|
1358
|
+
const content = await readFile3(packageJsonPath, "utf-8");
|
|
1359
|
+
const pkg = JSON.parse(content);
|
|
1360
|
+
detected.name = pkg.name || null;
|
|
1361
|
+
detected.description = pkg.description;
|
|
1362
|
+
if (pkg.workspaces || await fileExists(join3(cwd, "pnpm-workspace.yaml"))) {
|
|
1363
|
+
detected.type = "monorepo";
|
|
1364
|
+
} else if (pkg.main || pkg.exports) {
|
|
1365
|
+
detected.type = "library";
|
|
1247
1366
|
} else {
|
|
1248
|
-
|
|
1367
|
+
detected.type = "application";
|
|
1249
1368
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1369
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1370
|
+
for (const [framework, deps] of Object.entries(JS_FRAMEWORK_PATTERNS)) {
|
|
1371
|
+
if (deps.some((dep) => allDeps[dep])) {
|
|
1372
|
+
detected.stack.push(framework);
|
|
1373
|
+
}
|
|
1253
1374
|
}
|
|
1254
|
-
|
|
1375
|
+
for (const [tool, deps] of Object.entries(JS_TOOL_PATTERNS)) {
|
|
1376
|
+
if (deps.some((dep) => allDeps[dep])) {
|
|
1377
|
+
detected.stack.push(tool);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
if (detected.stack.length === 0 || detected.stack.length === 1 && detected.stack[0] === "typescript") {
|
|
1381
|
+
detected.stack.unshift("javascript");
|
|
1382
|
+
}
|
|
1383
|
+
if (pkg.scripts) {
|
|
1384
|
+
detected.commands.build = pkg.scripts.build;
|
|
1385
|
+
detected.commands.test = pkg.scripts.test;
|
|
1386
|
+
detected.commands.lint = pkg.scripts.lint || pkg.scripts["lint:check"];
|
|
1387
|
+
detected.commands.dev = pkg.scripts.dev || pkg.scripts.start || pkg.scripts.serve;
|
|
1388
|
+
detected.commands.format = pkg.scripts.format || pkg.scripts.prettier;
|
|
1389
|
+
}
|
|
1390
|
+
if (await fileExists(join3(cwd, "pnpm-lock.yaml"))) {
|
|
1391
|
+
detected.packageManager = "pnpm";
|
|
1392
|
+
} else if (await fileExists(join3(cwd, "yarn.lock"))) {
|
|
1393
|
+
detected.packageManager = "yarn";
|
|
1394
|
+
} else if (await fileExists(join3(cwd, "bun.lockb"))) {
|
|
1395
|
+
detected.packageManager = "bun";
|
|
1396
|
+
} else if (await fileExists(join3(cwd, "package-lock.json"))) {
|
|
1397
|
+
detected.packageManager = "npm";
|
|
1398
|
+
}
|
|
1399
|
+
if (detected.packageManager && detected.packageManager !== "npm") {
|
|
1400
|
+
const pm = detected.packageManager;
|
|
1401
|
+
for (const [key, value] of Object.entries(detected.commands)) {
|
|
1402
|
+
if (value && !value.startsWith(pm) && !value.startsWith("npx")) {
|
|
1403
|
+
detected.commands[key] = `${pm} run ${value}`;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
} else if (detected.commands) {
|
|
1407
|
+
for (const [key, value] of Object.entries(detected.commands)) {
|
|
1408
|
+
if (value && !value.startsWith("npm") && !value.startsWith("npx")) {
|
|
1409
|
+
detected.commands[key] = `npm run ${value}`;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
if (pkg.scripts) {
|
|
1414
|
+
detected.commands.build = pkg.scripts.build ? "build" : void 0;
|
|
1415
|
+
detected.commands.test = pkg.scripts.test ? "test" : void 0;
|
|
1416
|
+
detected.commands.lint = pkg.scripts.lint ? "lint" : pkg.scripts["lint:check"] ? "lint:check" : void 0;
|
|
1417
|
+
detected.commands.dev = pkg.scripts.dev ? "dev" : pkg.scripts.start ? "start" : pkg.scripts.serve ? "serve" : void 0;
|
|
1418
|
+
}
|
|
1419
|
+
return detected;
|
|
1420
|
+
} catch {
|
|
1255
1421
|
}
|
|
1256
1422
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
{
|
|
1279
|
-
|
|
1280
|
-
name: "visibility",
|
|
1281
|
-
message: "Visibility:",
|
|
1282
|
-
choices: [
|
|
1283
|
-
{ title: "Private (only you)", value: "PRIVATE" },
|
|
1284
|
-
{ title: "Team (your team members)", value: "TEAM" },
|
|
1285
|
-
{ title: "Public (visible to everyone)", value: "PUBLIC" }
|
|
1286
|
-
],
|
|
1287
|
-
initial: 0
|
|
1288
|
-
},
|
|
1289
|
-
{
|
|
1290
|
-
type: "text",
|
|
1291
|
-
name: "tags",
|
|
1292
|
-
message: "Tags (comma-separated):",
|
|
1293
|
-
initial: ""
|
|
1423
|
+
const pyprojectPath = join3(cwd, "pyproject.toml");
|
|
1424
|
+
if (await fileExists(pyprojectPath)) {
|
|
1425
|
+
try {
|
|
1426
|
+
const content = await readFile3(pyprojectPath, "utf-8");
|
|
1427
|
+
detected.stack.push("python");
|
|
1428
|
+
detected.type = "application";
|
|
1429
|
+
const nameMatch = content.match(/name\s*=\s*"([^"]+)"/);
|
|
1430
|
+
if (nameMatch) detected.name = nameMatch[1];
|
|
1431
|
+
if (content.includes("fastapi")) detected.stack.push("fastapi");
|
|
1432
|
+
if (content.includes("django")) detected.stack.push("django");
|
|
1433
|
+
if (content.includes("flask")) detected.stack.push("flask");
|
|
1434
|
+
if (content.includes("pydantic")) detected.stack.push("pydantic");
|
|
1435
|
+
if (content.includes("sqlalchemy")) detected.stack.push("sqlalchemy");
|
|
1436
|
+
if (content.includes("pytest")) detected.stack.push("pytest");
|
|
1437
|
+
if (content.includes("ruff")) detected.stack.push("ruff");
|
|
1438
|
+
if (content.includes("mypy")) detected.stack.push("mypy");
|
|
1439
|
+
detected.commands.test = "pytest";
|
|
1440
|
+
detected.commands.lint = "ruff check .";
|
|
1441
|
+
if (content.includes("[tool.poetry]")) {
|
|
1442
|
+
detected.packageManager = "yarn";
|
|
1443
|
+
detected.commands.dev = "poetry run python -m uvicorn main:app --reload";
|
|
1444
|
+
} else if (await fileExists(join3(cwd, "uv.lock"))) {
|
|
1445
|
+
detected.commands.dev = "uv run python main.py";
|
|
1294
1446
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
console.log(chalk6.yellow("Push cancelled."));
|
|
1298
|
-
return;
|
|
1447
|
+
return detected;
|
|
1448
|
+
} catch {
|
|
1299
1449
|
}
|
|
1300
|
-
name = name || responses.name;
|
|
1301
|
-
description = description || responses.description || "";
|
|
1302
|
-
visibility = responses.visibility || visibility;
|
|
1303
|
-
tags = responses.tags ? responses.tags.split(",").map((t) => t.trim()).filter(Boolean) : tags;
|
|
1304
|
-
}
|
|
1305
|
-
if (!name) {
|
|
1306
|
-
name = filename.replace(/\.(md|mdc|json|yml|yaml)$/, "");
|
|
1307
|
-
}
|
|
1308
|
-
const hierarchyInfo = await detectHierarchyInfo(cwd, file);
|
|
1309
|
-
let hierarchyId = null;
|
|
1310
|
-
if (hierarchyInfo.repositoryPath) {
|
|
1311
|
-
hierarchyId = await ensureHierarchy(cwd, hierarchyInfo.repositoryRoot, path.basename(cwd));
|
|
1312
1450
|
}
|
|
1313
|
-
const
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
}
|
|
1326
|
-
spinner.succeed("Blueprint created!");
|
|
1327
|
-
await trackBlueprint(cwd, {
|
|
1328
|
-
id: result.blueprint.id,
|
|
1329
|
-
name: result.blueprint.name,
|
|
1330
|
-
file,
|
|
1331
|
-
content,
|
|
1332
|
-
source: "private",
|
|
1333
|
-
hierarchyId: hierarchyId || void 0,
|
|
1334
|
-
repositoryPath: hierarchyInfo.repositoryPath || void 0
|
|
1335
|
-
});
|
|
1336
|
-
console.log();
|
|
1337
|
-
console.log(chalk6.green(`\u2705 Created blueprint ${chalk6.bold(result.blueprint.name)}`));
|
|
1338
|
-
console.log(chalk6.gray(` ID: ${result.blueprint.id}`));
|
|
1339
|
-
console.log(chalk6.gray(` Visibility: ${visibility}`));
|
|
1340
|
-
if (hierarchyInfo.repositoryPath) {
|
|
1341
|
-
console.log(chalk6.gray(` Path: ${hierarchyInfo.repositoryPath}`));
|
|
1342
|
-
}
|
|
1343
|
-
if (result.blueprint.hierarchy_id) {
|
|
1344
|
-
console.log(chalk6.gray(` Hierarchy: ${result.blueprint.hierarchy_id}`));
|
|
1451
|
+
const requirementsPath = join3(cwd, "requirements.txt");
|
|
1452
|
+
if (await fileExists(requirementsPath)) {
|
|
1453
|
+
try {
|
|
1454
|
+
const content = await readFile3(requirementsPath, "utf-8");
|
|
1455
|
+
detected.stack.push("python");
|
|
1456
|
+
detected.type = "application";
|
|
1457
|
+
if (content.toLowerCase().includes("fastapi")) detected.stack.push("fastapi");
|
|
1458
|
+
if (content.toLowerCase().includes("django")) detected.stack.push("django");
|
|
1459
|
+
if (content.toLowerCase().includes("flask")) detected.stack.push("flask");
|
|
1460
|
+
detected.commands.test = "pytest";
|
|
1461
|
+
detected.commands.lint = "ruff check .";
|
|
1462
|
+
return detected;
|
|
1463
|
+
} catch {
|
|
1345
1464
|
}
|
|
1346
|
-
|
|
1347
|
-
|
|
1465
|
+
}
|
|
1466
|
+
const cargoPath = join3(cwd, "Cargo.toml");
|
|
1467
|
+
if (await fileExists(cargoPath)) {
|
|
1468
|
+
try {
|
|
1469
|
+
const content = await readFile3(cargoPath, "utf-8");
|
|
1470
|
+
detected.stack.push("rust");
|
|
1471
|
+
detected.type = "application";
|
|
1472
|
+
const nameMatch = content.match(/name\s*=\s*"([^"]+)"/);
|
|
1473
|
+
if (nameMatch) detected.name = nameMatch[1];
|
|
1474
|
+
if (content.includes("actix-web")) detected.stack.push("actix");
|
|
1475
|
+
if (content.includes("axum")) detected.stack.push("axum");
|
|
1476
|
+
if (content.includes("tokio")) detected.stack.push("tokio");
|
|
1477
|
+
if (content.includes("serde")) detected.stack.push("serde");
|
|
1478
|
+
if (content.includes("sqlx")) detected.stack.push("sqlx");
|
|
1479
|
+
detected.commands.build = "cargo build";
|
|
1480
|
+
detected.commands.test = "cargo test";
|
|
1481
|
+
detected.commands.lint = "cargo clippy";
|
|
1482
|
+
detected.commands.dev = "cargo run";
|
|
1483
|
+
return detected;
|
|
1484
|
+
} catch {
|
|
1348
1485
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1486
|
+
}
|
|
1487
|
+
const goModPath = join3(cwd, "go.mod");
|
|
1488
|
+
if (await fileExists(goModPath)) {
|
|
1489
|
+
try {
|
|
1490
|
+
const content = await readFile3(goModPath, "utf-8");
|
|
1491
|
+
detected.stack.push("go");
|
|
1492
|
+
detected.type = "application";
|
|
1493
|
+
const moduleMatch = content.match(/module\s+(\S+)/);
|
|
1494
|
+
if (moduleMatch) {
|
|
1495
|
+
const parts = moduleMatch[1].split("/");
|
|
1496
|
+
detected.name = parts[parts.length - 1];
|
|
1497
|
+
}
|
|
1498
|
+
if (content.includes("gin-gonic/gin")) detected.stack.push("gin");
|
|
1499
|
+
if (content.includes("gofiber/fiber")) detected.stack.push("fiber");
|
|
1500
|
+
if (content.includes("labstack/echo")) detected.stack.push("echo");
|
|
1501
|
+
if (content.includes("gorm.io/gorm")) detected.stack.push("gorm");
|
|
1502
|
+
detected.commands.build = "go build";
|
|
1503
|
+
detected.commands.test = "go test ./...";
|
|
1504
|
+
detected.commands.lint = "golangci-lint run";
|
|
1505
|
+
detected.commands.dev = "go run .";
|
|
1506
|
+
return detected;
|
|
1507
|
+
} catch {
|
|
1351
1508
|
}
|
|
1352
|
-
console.log();
|
|
1353
|
-
console.log(chalk6.cyan("The file is now linked. Future 'lynxp push' will update this blueprint."));
|
|
1354
|
-
} catch (error) {
|
|
1355
|
-
spinner.fail("Failed to create blueprint");
|
|
1356
|
-
handleError(error);
|
|
1357
1509
|
}
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
},
|
|
1371
|
-
{
|
|
1372
|
-
type: "select",
|
|
1373
|
-
name: "visibility",
|
|
1374
|
-
message: "Visibility for all blueprints:",
|
|
1375
|
-
choices: [
|
|
1376
|
-
{ title: "Private (only you)", value: "PRIVATE" },
|
|
1377
|
-
{ title: "Team (your team members)", value: "TEAM" },
|
|
1378
|
-
{ title: "Public (visible to everyone)", value: "PUBLIC" }
|
|
1379
|
-
],
|
|
1380
|
-
initial: 0
|
|
1510
|
+
const makefilePath = join3(cwd, "Makefile");
|
|
1511
|
+
if (await fileExists(makefilePath)) {
|
|
1512
|
+
try {
|
|
1513
|
+
const content = await readFile3(makefilePath, "utf-8");
|
|
1514
|
+
if (content.includes("build:")) detected.commands.build = "make build";
|
|
1515
|
+
if (content.includes("test:")) detected.commands.test = "make test";
|
|
1516
|
+
if (content.includes("lint:")) detected.commands.lint = "make lint";
|
|
1517
|
+
if (content.includes("dev:")) detected.commands.dev = "make dev";
|
|
1518
|
+
if (content.includes("run:")) detected.commands.dev = detected.commands.dev || "make run";
|
|
1519
|
+
if (Object.keys(detected.commands).length > 0) {
|
|
1520
|
+
detected.type = "application";
|
|
1521
|
+
return detected;
|
|
1381
1522
|
}
|
|
1382
|
-
|
|
1383
|
-
if (!responses.name) {
|
|
1384
|
-
console.log(chalk6.yellow("Push cancelled."));
|
|
1385
|
-
return;
|
|
1523
|
+
} catch {
|
|
1386
1524
|
}
|
|
1387
|
-
hierarchyName = responses.name;
|
|
1388
|
-
visibility = responses.visibility || visibility;
|
|
1389
1525
|
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
let hierarchyId;
|
|
1395
|
-
try {
|
|
1396
|
-
const hierarchyResponse = await api.createHierarchy({
|
|
1397
|
-
name: hierarchyName,
|
|
1398
|
-
repository_root: repositoryRoot
|
|
1399
|
-
});
|
|
1400
|
-
hierarchyId = hierarchyResponse.hierarchy.id;
|
|
1401
|
-
console.log(chalk6.green(`\u2713 Created hierarchy: ${hierarchyId}`));
|
|
1402
|
-
} catch (error) {
|
|
1403
|
-
console.log(chalk6.red("Failed to create hierarchy"));
|
|
1404
|
-
handleError(error);
|
|
1405
|
-
return;
|
|
1526
|
+
if (await fileExists(join3(cwd, "Dockerfile")) || await fileExists(join3(cwd, "docker-compose.yml"))) {
|
|
1527
|
+
detected.stack.push("docker");
|
|
1528
|
+
detected.type = "application";
|
|
1529
|
+
detected.hasDocker = true;
|
|
1406
1530
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
let failCount = 0;
|
|
1410
|
-
for (const file of files) {
|
|
1411
|
-
const spinner = ora5(`Uploading ${file.path}...`).start();
|
|
1531
|
+
const licensePath = join3(cwd, "LICENSE");
|
|
1532
|
+
if (await fileExists(licensePath)) {
|
|
1412
1533
|
try {
|
|
1413
|
-
const
|
|
1414
|
-
|
|
1415
|
-
if (
|
|
1416
|
-
|
|
1417
|
-
} else {
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
if (file.isRoot) {
|
|
1432
|
-
rootBlueprintId = result.blueprint.id;
|
|
1433
|
-
}
|
|
1434
|
-
await trackBlueprint(cwd, {
|
|
1435
|
-
id: result.blueprint.id,
|
|
1436
|
-
name: blueprintName,
|
|
1437
|
-
file: file.path,
|
|
1438
|
-
content,
|
|
1439
|
-
source: "private",
|
|
1440
|
-
hierarchyId,
|
|
1441
|
-
hierarchyName,
|
|
1442
|
-
repositoryPath: file.path
|
|
1443
|
-
});
|
|
1444
|
-
spinner.succeed(`${file.path} \u2192 ${result.blueprint.id}`);
|
|
1445
|
-
successCount++;
|
|
1446
|
-
} catch (error) {
|
|
1447
|
-
spinner.fail(`${file.path} failed`);
|
|
1448
|
-
if (error instanceof ApiRequestError) {
|
|
1449
|
-
console.log(chalk6.red(` Error: ${error.message}`));
|
|
1534
|
+
const licenseContent = await readFile3(licensePath, "utf-8");
|
|
1535
|
+
const lowerContent = licenseContent.toLowerCase();
|
|
1536
|
+
if (lowerContent.includes("mit license") || lowerContent.includes("permission is hereby granted, free of charge")) {
|
|
1537
|
+
detected.license = "mit";
|
|
1538
|
+
} else if (lowerContent.includes("apache license") && lowerContent.includes("version 2.0")) {
|
|
1539
|
+
detected.license = "apache-2.0";
|
|
1540
|
+
} else if (lowerContent.includes("gnu general public license") && lowerContent.includes("version 3")) {
|
|
1541
|
+
detected.license = "gpl-3.0";
|
|
1542
|
+
} else if (lowerContent.includes("gnu lesser general public license")) {
|
|
1543
|
+
detected.license = "lgpl-3.0";
|
|
1544
|
+
} else if (lowerContent.includes("gnu affero general public license")) {
|
|
1545
|
+
detected.license = "agpl-3.0";
|
|
1546
|
+
} else if (lowerContent.includes("bsd 3-clause") || lowerContent.includes("redistribution and use in source and binary forms")) {
|
|
1547
|
+
detected.license = "bsd-3";
|
|
1548
|
+
} else if (lowerContent.includes("mozilla public license") && lowerContent.includes("2.0")) {
|
|
1549
|
+
detected.license = "mpl-2.0";
|
|
1550
|
+
} else if (lowerContent.includes("unlicense") || lowerContent.includes("this is free and unencumbered software")) {
|
|
1551
|
+
detected.license = "unlicense";
|
|
1450
1552
|
}
|
|
1451
|
-
|
|
1553
|
+
} catch {
|
|
1452
1554
|
}
|
|
1453
1555
|
}
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
".clinerules"
|
|
1476
|
-
];
|
|
1477
|
-
for (const candidate of candidates) {
|
|
1478
|
-
if (fs.existsSync(candidate)) {
|
|
1479
|
-
return candidate;
|
|
1556
|
+
const gitConfigPath = join3(cwd, ".git", "config");
|
|
1557
|
+
if (await fileExists(gitConfigPath)) {
|
|
1558
|
+
try {
|
|
1559
|
+
const gitConfig = await readFile3(gitConfigPath, "utf-8");
|
|
1560
|
+
const urlMatch = gitConfig.match(/url\s*=\s*(.+)/);
|
|
1561
|
+
if (urlMatch) {
|
|
1562
|
+
const repoUrl = urlMatch[1].trim();
|
|
1563
|
+
detected.repoUrl = repoUrl;
|
|
1564
|
+
if (repoUrl.includes("github.com")) {
|
|
1565
|
+
detected.repoHost = "github";
|
|
1566
|
+
} else if (repoUrl.includes("gitlab.com") || repoUrl.includes("gitlab")) {
|
|
1567
|
+
detected.repoHost = "gitlab";
|
|
1568
|
+
} else if (repoUrl.includes("bitbucket")) {
|
|
1569
|
+
detected.repoHost = "bitbucket";
|
|
1570
|
+
} else if (repoUrl.includes("gitea") || repoUrl.includes("codeberg")) {
|
|
1571
|
+
detected.repoHost = "gitea";
|
|
1572
|
+
} else if (repoUrl.includes("azure")) {
|
|
1573
|
+
detected.repoHost = "azure";
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
} catch {
|
|
1480
1577
|
}
|
|
1481
1578
|
}
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
} else {
|
|
1495
|
-
|
|
1579
|
+
if (await fileExists(join3(cwd, ".github", "workflows"))) {
|
|
1580
|
+
detected.cicd = "github_actions";
|
|
1581
|
+
} else if (await fileExists(join3(cwd, ".gitlab-ci.yml"))) {
|
|
1582
|
+
detected.cicd = "gitlab_ci";
|
|
1583
|
+
} else if (await fileExists(join3(cwd, "Jenkinsfile"))) {
|
|
1584
|
+
detected.cicd = "jenkins";
|
|
1585
|
+
} else if (await fileExists(join3(cwd, ".circleci"))) {
|
|
1586
|
+
detected.cicd = "circleci";
|
|
1587
|
+
} else if (await fileExists(join3(cwd, ".travis.yml"))) {
|
|
1588
|
+
detected.cicd = "travis";
|
|
1589
|
+
} else if (await fileExists(join3(cwd, "azure-pipelines.yml"))) {
|
|
1590
|
+
detected.cicd = "azure_devops";
|
|
1591
|
+
} else if (await fileExists(join3(cwd, "bitbucket-pipelines.yml"))) {
|
|
1592
|
+
detected.cicd = "bitbucket";
|
|
1593
|
+
} else if (await fileExists(join3(cwd, ".drone.yml"))) {
|
|
1594
|
+
detected.cicd = "drone";
|
|
1496
1595
|
}
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
var JS_FRAMEWORK_PATTERNS = {
|
|
1515
|
-
nextjs: ["next"],
|
|
1516
|
-
react: ["react", "react-dom"],
|
|
1517
|
-
vue: ["vue"],
|
|
1518
|
-
angular: ["@angular/core"],
|
|
1519
|
-
svelte: ["svelte", "@sveltejs/kit"],
|
|
1520
|
-
solid: ["solid-js"],
|
|
1521
|
-
remix: ["@remix-run/react"],
|
|
1522
|
-
astro: ["astro"],
|
|
1523
|
-
nuxt: ["nuxt"],
|
|
1524
|
-
gatsby: ["gatsby"]
|
|
1525
|
-
};
|
|
1526
|
-
var JS_TOOL_PATTERNS = {
|
|
1527
|
-
typescript: ["typescript"],
|
|
1528
|
-
tailwind: ["tailwindcss"],
|
|
1529
|
-
prisma: ["prisma", "@prisma/client"],
|
|
1530
|
-
drizzle: ["drizzle-orm"],
|
|
1531
|
-
express: ["express"],
|
|
1532
|
-
fastify: ["fastify"],
|
|
1533
|
-
hono: ["hono"],
|
|
1534
|
-
elysia: ["elysia"],
|
|
1535
|
-
trpc: ["@trpc/server"],
|
|
1536
|
-
graphql: ["graphql", "@apollo/server"],
|
|
1537
|
-
jest: ["jest"],
|
|
1538
|
-
vitest: ["vitest"],
|
|
1539
|
-
playwright: ["@playwright/test"],
|
|
1540
|
-
cypress: ["cypress"],
|
|
1541
|
-
eslint: ["eslint"],
|
|
1542
|
-
biome: ["@biomejs/biome"],
|
|
1543
|
-
prettier: ["prettier"],
|
|
1544
|
-
vite: ["vite"],
|
|
1545
|
-
webpack: ["webpack"],
|
|
1546
|
-
turbo: ["turbo"]
|
|
1547
|
-
};
|
|
1548
|
-
async function detectExtendedCommands(cwd) {
|
|
1549
|
-
const cmds = {
|
|
1550
|
-
test: [],
|
|
1551
|
-
testCoverage: [],
|
|
1552
|
-
install: [],
|
|
1553
|
-
dev: [],
|
|
1554
|
-
build: [],
|
|
1555
|
-
lint: [],
|
|
1556
|
-
format: [],
|
|
1557
|
-
typecheck: [],
|
|
1558
|
-
clean: [],
|
|
1559
|
-
preCommit: [],
|
|
1560
|
-
additional: []
|
|
1561
|
-
};
|
|
1562
|
-
const addCmd = (category, cmd, desc) => {
|
|
1563
|
-
if (!cmds[category].some((c) => c.cmd === cmd)) {
|
|
1564
|
-
cmds[category].push({ cmd, desc });
|
|
1596
|
+
detected.existingFiles = [];
|
|
1597
|
+
const staticFiles = [
|
|
1598
|
+
".editorconfig",
|
|
1599
|
+
"CONTRIBUTING.md",
|
|
1600
|
+
"CODE_OF_CONDUCT.md",
|
|
1601
|
+
"SECURITY.md",
|
|
1602
|
+
"ROADMAP.md",
|
|
1603
|
+
".gitignore",
|
|
1604
|
+
".github/FUNDING.yml",
|
|
1605
|
+
"LICENSE",
|
|
1606
|
+
"README.md",
|
|
1607
|
+
"ARCHITECTURE.md",
|
|
1608
|
+
"CHANGELOG.md"
|
|
1609
|
+
];
|
|
1610
|
+
for (const file of staticFiles) {
|
|
1611
|
+
if (await fileExists(join3(cwd, file))) {
|
|
1612
|
+
detected.existingFiles.push(file);
|
|
1565
1613
|
}
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
for (const
|
|
1574
|
-
const
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
addCmd("testCoverage", fullCmd, `Run ${name}`);
|
|
1579
|
-
} else {
|
|
1580
|
-
addCmd("test", fullCmd, `Run ${name}`);
|
|
1581
|
-
}
|
|
1582
|
-
} else if (name.match(/^lint$|^lint:/i) || scriptStr.includes("eslint") || scriptStr.includes("biome")) {
|
|
1583
|
-
addCmd("lint", fullCmd, `Run ${name}`);
|
|
1584
|
-
} else if (name.match(/^format$|^fmt$|format:/i) || scriptStr.includes("prettier")) {
|
|
1585
|
-
addCmd("format", fullCmd, `Run ${name}`);
|
|
1586
|
-
} else if (name.match(/^build$|^build:/i) || scriptStr.includes("tsc") || scriptStr.includes("webpack") || scriptStr.includes("vite build")) {
|
|
1587
|
-
addCmd("build", fullCmd, `Run ${name}`);
|
|
1588
|
-
} else if (name.match(/^dev$|^start$|^serve$/i)) {
|
|
1589
|
-
addCmd("dev", fullCmd, `Run ${name}`);
|
|
1590
|
-
} else if (name.match(/^typecheck$|^type-check$|^types$|^check:types/i) || scriptStr.includes("tsc --noEmit")) {
|
|
1591
|
-
addCmd("typecheck", fullCmd, `Run ${name}`);
|
|
1592
|
-
} else if (name.match(/^clean$|^clean:/i) || scriptStr.includes("rimraf") || scriptStr.includes("rm -rf")) {
|
|
1593
|
-
addCmd("clean", fullCmd, `Run ${name}`);
|
|
1594
|
-
} else if (name.match(/^prepare$|^precommit$|^pre-commit$|^husky/i)) {
|
|
1595
|
-
addCmd("preCommit", fullCmd, `Run ${name}`);
|
|
1596
|
-
} else if (name === "install" || name === "postinstall") {
|
|
1597
|
-
addCmd("install", fullCmd, `Run ${name}`);
|
|
1598
|
-
} else if (!["publish", "prepublish", "prepublishOnly", "version", "postversion"].includes(name)) {
|
|
1599
|
-
addCmd("additional", fullCmd, `Run ${name}`);
|
|
1614
|
+
}
|
|
1615
|
+
if (!detected.description) {
|
|
1616
|
+
const readmePath = join3(cwd, "README.md");
|
|
1617
|
+
if (await fileExists(readmePath)) {
|
|
1618
|
+
try {
|
|
1619
|
+
const readme = await readFile3(readmePath, "utf-8");
|
|
1620
|
+
const lines = readme.split("\n");
|
|
1621
|
+
for (const line of lines) {
|
|
1622
|
+
const trimmed = line.trim();
|
|
1623
|
+
if (trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("!") && !trimmed.startsWith("[") && trimmed.length > 20) {
|
|
1624
|
+
detected.description = trimmed.substring(0, 200);
|
|
1625
|
+
break;
|
|
1600
1626
|
}
|
|
1601
1627
|
}
|
|
1628
|
+
} catch {
|
|
1602
1629
|
}
|
|
1603
|
-
} catch {
|
|
1604
1630
|
}
|
|
1605
1631
|
}
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1632
|
+
detected.detectedCommands = await detectExtendedCommands(cwd);
|
|
1633
|
+
return detected.stack.length > 0 || detected.name ? detected : null;
|
|
1634
|
+
}
|
|
1635
|
+
async function fileExists(path2) {
|
|
1636
|
+
try {
|
|
1637
|
+
await access2(path2);
|
|
1638
|
+
return true;
|
|
1639
|
+
} catch {
|
|
1640
|
+
return false;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
function detectRepoHost(url) {
|
|
1644
|
+
const lower = url.toLowerCase();
|
|
1645
|
+
if (lower.includes("github.com") || lower.includes("github:")) return "github";
|
|
1646
|
+
if (lower.includes("gitlab.com") || lower.includes("gitlab")) return "gitlab";
|
|
1647
|
+
if (lower.includes("bitbucket.org") || lower.includes("bitbucket:")) return "bitbucket";
|
|
1648
|
+
if (lower.includes("gitea.") || lower.includes("gitea:") || lower.includes("codeberg.org")) return "gitea";
|
|
1649
|
+
if (lower.includes("azure.com") || lower.includes("visualstudio.com") || lower.includes("dev.azure")) return "azure";
|
|
1650
|
+
return "other";
|
|
1651
|
+
}
|
|
1652
|
+
function parseGitHubUrl(url) {
|
|
1653
|
+
const patterns = [
|
|
1654
|
+
/github\.com[/:]([^/]+)\/([^/.]+)/,
|
|
1655
|
+
/^([^/]+)\/([^/]+)$/
|
|
1656
|
+
];
|
|
1657
|
+
for (const pattern of patterns) {
|
|
1658
|
+
const match = url.match(pattern);
|
|
1659
|
+
if (match) {
|
|
1660
|
+
return { owner: match[1], repo: match[2].replace(/\.git$/, "") };
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
return null;
|
|
1664
|
+
}
|
|
1665
|
+
function parseGitLabUrl(url) {
|
|
1666
|
+
const patterns = [
|
|
1667
|
+
/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/,
|
|
1668
|
+
/^git@([^:]+):(.+?)(?:\.git)?$/
|
|
1669
|
+
];
|
|
1670
|
+
for (const pattern of patterns) {
|
|
1671
|
+
const match = url.match(pattern);
|
|
1672
|
+
if (match) {
|
|
1673
|
+
const host = match[1];
|
|
1674
|
+
const path2 = match[2].replace(/\.git$/, "");
|
|
1675
|
+
if (host.includes("gitlab") || url.toLowerCase().includes("gitlab")) {
|
|
1676
|
+
return { path: path2, host };
|
|
1620
1677
|
}
|
|
1621
|
-
|
|
1622
|
-
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
return null;
|
|
1681
|
+
}
|
|
1682
|
+
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"];
|
|
1683
|
+
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"];
|
|
1684
|
+
async function detectFromGitHubApi(repoUrl) {
|
|
1685
|
+
const parsed = parseGitHubUrl(repoUrl);
|
|
1686
|
+
if (!parsed) return null;
|
|
1687
|
+
const { owner, repo } = parsed;
|
|
1688
|
+
try {
|
|
1689
|
+
const repoRes = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
|
|
1690
|
+
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
1691
|
+
});
|
|
1692
|
+
if (!repoRes.ok) return null;
|
|
1693
|
+
const repoInfo = await repoRes.json();
|
|
1694
|
+
if (repoInfo.private) return null;
|
|
1695
|
+
const licenseId = repoInfo.license?.spdx_id?.toLowerCase() || null;
|
|
1696
|
+
const isOpenSource = !repoInfo.private && (licenseId ? OPEN_SOURCE_LICENSES.includes(licenseId) : false);
|
|
1697
|
+
const detected = {
|
|
1698
|
+
name: repoInfo.name,
|
|
1699
|
+
description: repoInfo.description ?? void 0,
|
|
1700
|
+
stack: [],
|
|
1701
|
+
databases: [],
|
|
1702
|
+
commands: {},
|
|
1703
|
+
packageManager: null,
|
|
1704
|
+
type: "application",
|
|
1705
|
+
repoHost: "github",
|
|
1706
|
+
repoUrl,
|
|
1707
|
+
license: licenseId ?? void 0,
|
|
1708
|
+
isPublicRepo: !repoInfo.private,
|
|
1709
|
+
isOpenSource,
|
|
1710
|
+
projectType: isOpenSource ? "open_source" : void 0,
|
|
1711
|
+
hasDocker: false,
|
|
1712
|
+
existingFiles: []
|
|
1713
|
+
};
|
|
1714
|
+
const filesRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/`, {
|
|
1715
|
+
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
1716
|
+
});
|
|
1717
|
+
if (!filesRes.ok) return detected;
|
|
1718
|
+
const files = await filesRes.json();
|
|
1719
|
+
const fileNames = new Set(files.map((f) => f.name.toLowerCase()));
|
|
1720
|
+
for (const file of STATIC_FILES) {
|
|
1721
|
+
if (file.includes("/")) continue;
|
|
1722
|
+
if (files.some((f) => f.name.toLowerCase() === file.toLowerCase())) {
|
|
1723
|
+
detected.existingFiles.push(file);
|
|
1623
1724
|
}
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1725
|
+
}
|
|
1726
|
+
if (files.some((f) => f.name === ".github" && f.type === "dir")) {
|
|
1727
|
+
const ghFilesRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/.github`, {
|
|
1728
|
+
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
1729
|
+
});
|
|
1730
|
+
if (ghFilesRes.ok) {
|
|
1731
|
+
const ghFiles = await ghFilesRes.json();
|
|
1732
|
+
if (ghFiles.some((f) => f.name.toLowerCase() === "funding.yml")) {
|
|
1733
|
+
detected.existingFiles.push(".github/FUNDING.yml");
|
|
1633
1734
|
}
|
|
1634
1735
|
}
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1736
|
+
}
|
|
1737
|
+
if (fileNames.has("dockerfile") || fileNames.has("docker-compose.yml") || fileNames.has("docker-compose.yaml")) {
|
|
1738
|
+
detected.hasDocker = true;
|
|
1739
|
+
detected.stack.push("docker");
|
|
1740
|
+
const dockerComposeFile = fileNames.has("docker-compose.yml") ? "docker-compose.yml" : fileNames.has("docker-compose.yaml") ? "docker-compose.yaml" : null;
|
|
1741
|
+
if (dockerComposeFile) {
|
|
1742
|
+
const composeRes = await fetch(`https://raw.githubusercontent.com/${owner}/${repo}/HEAD/${dockerComposeFile}`);
|
|
1743
|
+
if (composeRes.ok) {
|
|
1744
|
+
try {
|
|
1745
|
+
const content = await composeRes.text();
|
|
1746
|
+
const lowerContent = content.toLowerCase();
|
|
1747
|
+
if (content.includes("ghcr.io")) detected.containerRegistry = "ghcr";
|
|
1748
|
+
else if (content.includes("docker.io") || /image:\s*[a-z0-9]+\/[a-z0-9]/.test(content)) detected.containerRegistry = "dockerhub";
|
|
1749
|
+
else if (content.includes("gcr.io")) detected.containerRegistry = "gcr";
|
|
1750
|
+
else if (content.includes("ecr.") || content.includes(".amazonaws.com")) detected.containerRegistry = "ecr";
|
|
1751
|
+
else if (content.includes("azurecr.io")) detected.containerRegistry = "acr";
|
|
1752
|
+
else if (content.includes("quay.io")) detected.containerRegistry = "quay";
|
|
1753
|
+
else if (content.includes("registry.gitlab.com")) detected.containerRegistry = "gitlab_registry";
|
|
1754
|
+
if (lowerContent.includes("postgres")) detected.databases.push("postgresql");
|
|
1755
|
+
if (lowerContent.includes("mysql") && !lowerContent.includes("mysql-")) detected.databases.push("mysql");
|
|
1756
|
+
if (lowerContent.includes("mongo")) detected.databases.push("mongodb");
|
|
1757
|
+
if (lowerContent.includes("redis")) detected.databases.push("redis");
|
|
1758
|
+
if (lowerContent.includes("sqlite")) detected.databases.push("sqlite");
|
|
1759
|
+
if (lowerContent.includes("mariadb")) detected.databases.push("mariadb");
|
|
1760
|
+
} catch {
|
|
1651
1761
|
}
|
|
1652
1762
|
}
|
|
1653
1763
|
}
|
|
1654
|
-
if (content.includes("fastapi") || content.includes("uvicorn")) {
|
|
1655
|
-
addCmd("dev", "uvicorn src.main:app --host 0.0.0.0 --port 8000 --reload", "Run FastAPI dev server");
|
|
1656
|
-
}
|
|
1657
|
-
if (content.includes("[tool.poetry]")) {
|
|
1658
|
-
addCmd("install", "poetry install", "Install dependencies with Poetry");
|
|
1659
|
-
} else if (await fileExists(join3(cwd, "uv.lock"))) {
|
|
1660
|
-
addCmd("install", "uv sync", "Sync dependencies with uv");
|
|
1661
|
-
} else {
|
|
1662
|
-
addCmd("install", "pip install -r requirements.txt", "Install dependencies with pip");
|
|
1663
|
-
}
|
|
1664
|
-
} catch {
|
|
1665
1764
|
}
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1765
|
+
if (files.some((f) => f.name === ".github" && f.type === "dir")) {
|
|
1766
|
+
const ghFilesRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/.github`, {
|
|
1767
|
+
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
1768
|
+
});
|
|
1769
|
+
if (ghFilesRes.ok) {
|
|
1770
|
+
const ghFiles = await ghFilesRes.json();
|
|
1771
|
+
if (ghFiles.some((f) => f.name === "workflows")) {
|
|
1772
|
+
detected.cicd = "github_actions";
|
|
1773
|
+
}
|
|
1673
1774
|
}
|
|
1674
|
-
addCmd("install", "pip install -r requirements.txt", "Install dependencies");
|
|
1675
|
-
} catch {
|
|
1676
1775
|
}
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1776
|
+
if (fileNames.has(".gitlab-ci.yml")) detected.cicd = "gitlab_ci";
|
|
1777
|
+
if (fileNames.has("jenkinsfile")) detected.cicd = "jenkins";
|
|
1778
|
+
if (fileNames.has(".travis.yml")) detected.cicd = "travis";
|
|
1779
|
+
if (fileNames.has("azure-pipelines.yml")) detected.cicd = "azure_devops";
|
|
1780
|
+
if (fileNames.has("pyproject.toml")) {
|
|
1781
|
+
detected.stack.push("python");
|
|
1782
|
+
const pyprojectRes = await fetch(`https://raw.githubusercontent.com/${owner}/${repo}/HEAD/pyproject.toml`);
|
|
1783
|
+
if (pyprojectRes.ok) {
|
|
1784
|
+
try {
|
|
1785
|
+
const content = await pyprojectRes.text();
|
|
1786
|
+
const lowerContent = content.toLowerCase();
|
|
1787
|
+
if (lowerContent.includes("fastapi")) detected.stack.push("fastapi");
|
|
1788
|
+
if (lowerContent.includes("django")) detected.stack.push("django");
|
|
1789
|
+
if (lowerContent.includes("flask")) detected.stack.push("flask");
|
|
1790
|
+
if (lowerContent.includes("sqlalchemy")) detected.stack.push("sqlalchemy");
|
|
1791
|
+
if (lowerContent.includes("pydantic")) detected.stack.push("pydantic");
|
|
1792
|
+
if (lowerContent.includes("pytest")) detected.testFramework = "pytest";
|
|
1793
|
+
else if (lowerContent.includes("unittest")) detected.testFramework = "unittest";
|
|
1794
|
+
detected.commands.test = "pytest";
|
|
1795
|
+
if (lowerContent.includes("ruff")) detected.commands.lint = "ruff check .";
|
|
1796
|
+
if (lowerContent.includes("asyncpg") || lowerContent.includes("psycopg")) {
|
|
1797
|
+
if (!detected.databases.includes("postgresql")) detected.databases.push("postgresql");
|
|
1798
|
+
}
|
|
1799
|
+
if (lowerContent.includes("aiosqlite") || lowerContent.includes("sqlite")) {
|
|
1800
|
+
if (!detected.databases.includes("sqlite")) detected.databases.push("sqlite");
|
|
1801
|
+
}
|
|
1802
|
+
if (lowerContent.includes("pymongo") || lowerContent.includes("motor")) {
|
|
1803
|
+
if (!detected.databases.includes("mongodb")) detected.databases.push("mongodb");
|
|
1804
|
+
}
|
|
1805
|
+
if (lowerContent.includes("redis") || lowerContent.includes("aioredis")) {
|
|
1806
|
+
if (!detected.databases.includes("redis")) detected.databases.push("redis");
|
|
1807
|
+
}
|
|
1808
|
+
if (lowerContent.includes("pymysql") || lowerContent.includes("aiomysql")) {
|
|
1809
|
+
if (!detected.databases.includes("mysql")) detected.databases.push("mysql");
|
|
1810
|
+
}
|
|
1811
|
+
} catch {
|
|
1706
1812
|
}
|
|
1707
1813
|
}
|
|
1708
|
-
} catch {
|
|
1709
1814
|
}
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
addCmd("dev", "docker compose up", "Start all services");
|
|
1723
|
-
addCmd("build", "docker compose build", "Build all services");
|
|
1724
|
-
addCmd("clean", "docker compose down -v", "Stop and remove volumes");
|
|
1725
|
-
} catch {
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
const dockerfilePath = join3(cwd, "Dockerfile");
|
|
1729
|
-
if (await fileExists(dockerfilePath)) {
|
|
1730
|
-
try {
|
|
1731
|
-
const content = await readFile3(dockerfilePath, "utf-8");
|
|
1732
|
-
const fromMatch = content.match(/FROM\s+([^\s]+)/);
|
|
1733
|
-
const imageName = fromMatch ? fromMatch[1].split(":")[0] : "app";
|
|
1734
|
-
addCmd("build", `docker build -t ${imageName} .`, "Build Docker image");
|
|
1735
|
-
addCmd("dev", `docker run -it --rm ${imageName}`, "Run Docker container");
|
|
1736
|
-
} catch {
|
|
1737
|
-
}
|
|
1738
|
-
}
|
|
1739
|
-
try {
|
|
1740
|
-
const dockerViewerPath = join3(cwd, "Dockerfile.viewer");
|
|
1741
|
-
if (await fileExists(dockerViewerPath)) {
|
|
1742
|
-
addCmd("build", "docker build -f Dockerfile.viewer -t app-viewer .", "Build viewer Docker image");
|
|
1743
|
-
}
|
|
1744
|
-
} catch {
|
|
1745
|
-
}
|
|
1746
|
-
const cargoPath = join3(cwd, "Cargo.toml");
|
|
1747
|
-
if (await fileExists(cargoPath)) {
|
|
1748
|
-
addCmd("build", "cargo build", "Build Rust project");
|
|
1749
|
-
addCmd("build", "cargo build --release", "Build release");
|
|
1750
|
-
addCmd("test", "cargo test", "Run Rust tests");
|
|
1751
|
-
addCmd("lint", "cargo clippy", "Run Clippy linter");
|
|
1752
|
-
addCmd("format", "cargo fmt", "Format Rust code");
|
|
1753
|
-
addCmd("dev", "cargo run", "Run Rust binary");
|
|
1754
|
-
addCmd("clean", "cargo clean", "Clean build artifacts");
|
|
1755
|
-
}
|
|
1756
|
-
const goModPath = join3(cwd, "go.mod");
|
|
1757
|
-
if (await fileExists(goModPath)) {
|
|
1758
|
-
addCmd("build", "go build", "Build Go project");
|
|
1759
|
-
addCmd("test", "go test ./...", "Run Go tests");
|
|
1760
|
-
addCmd("lint", "golangci-lint run", "Run golangci-lint");
|
|
1761
|
-
addCmd("format", "go fmt ./...", "Format Go code");
|
|
1762
|
-
addCmd("dev", "go run .", "Run Go binary");
|
|
1763
|
-
addCmd("clean", "go clean", "Clean build cache");
|
|
1764
|
-
addCmd("typecheck", "go vet ./...", "Run go vet");
|
|
1765
|
-
}
|
|
1766
|
-
const srcMainPath = join3(cwd, "src", "main.py");
|
|
1767
|
-
const mainPath = join3(cwd, "main.py");
|
|
1768
|
-
const appPath = join3(cwd, "app.py");
|
|
1769
|
-
if (await fileExists(srcMainPath)) {
|
|
1770
|
-
addCmd("dev", "python -m src.main", "Run main module");
|
|
1771
|
-
}
|
|
1772
|
-
if (await fileExists(mainPath)) {
|
|
1773
|
-
addCmd("dev", "python main.py", "Run main.py");
|
|
1774
|
-
}
|
|
1775
|
-
if (await fileExists(appPath)) {
|
|
1776
|
-
addCmd("dev", "python app.py", "Run app.py");
|
|
1777
|
-
}
|
|
1778
|
-
const schedulerPath = join3(cwd, "src", "scheduler.py");
|
|
1779
|
-
if (await fileExists(schedulerPath)) {
|
|
1780
|
-
addCmd("additional", "python -m src.scheduler", "Run scheduler");
|
|
1781
|
-
}
|
|
1782
|
-
const setupAuthPath = join3(cwd, "src", "setup_auth.py");
|
|
1783
|
-
if (await fileExists(setupAuthPath)) {
|
|
1784
|
-
addCmd("additional", "python -m src.setup_auth", "Setup authentication");
|
|
1785
|
-
}
|
|
1786
|
-
const webMainPath = join3(cwd, "src", "web", "main.py");
|
|
1787
|
-
if (await fileExists(webMainPath)) {
|
|
1788
|
-
addCmd("dev", "uvicorn src.web.main:app --host 0.0.0.0 --port 8080", "Run web viewer");
|
|
1789
|
-
}
|
|
1790
|
-
return cmds;
|
|
1791
|
-
}
|
|
1792
|
-
async function detectProject(cwd) {
|
|
1793
|
-
const detected = {
|
|
1794
|
-
name: null,
|
|
1795
|
-
stack: [],
|
|
1796
|
-
databases: [],
|
|
1797
|
-
commands: {},
|
|
1798
|
-
packageManager: null,
|
|
1799
|
-
type: "unknown"
|
|
1800
|
-
};
|
|
1801
|
-
const packageJsonPath = join3(cwd, "package.json");
|
|
1802
|
-
if (await fileExists(packageJsonPath)) {
|
|
1803
|
-
try {
|
|
1804
|
-
const content = await readFile3(packageJsonPath, "utf-8");
|
|
1805
|
-
const pkg = JSON.parse(content);
|
|
1806
|
-
detected.name = pkg.name || null;
|
|
1807
|
-
detected.description = pkg.description;
|
|
1808
|
-
if (pkg.workspaces || await fileExists(join3(cwd, "pnpm-workspace.yaml"))) {
|
|
1809
|
-
detected.type = "monorepo";
|
|
1810
|
-
} else if (pkg.main || pkg.exports) {
|
|
1811
|
-
detected.type = "library";
|
|
1812
|
-
} else {
|
|
1813
|
-
detected.type = "application";
|
|
1814
|
-
}
|
|
1815
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1816
|
-
for (const [framework, deps] of Object.entries(JS_FRAMEWORK_PATTERNS)) {
|
|
1817
|
-
if (deps.some((dep) => allDeps[dep])) {
|
|
1818
|
-
detected.stack.push(framework);
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
for (const [tool, deps] of Object.entries(JS_TOOL_PATTERNS)) {
|
|
1822
|
-
if (deps.some((dep) => allDeps[dep])) {
|
|
1823
|
-
detected.stack.push(tool);
|
|
1824
|
-
}
|
|
1825
|
-
}
|
|
1826
|
-
if (detected.stack.length === 0 || detected.stack.length === 1 && detected.stack[0] === "typescript") {
|
|
1827
|
-
detected.stack.unshift("javascript");
|
|
1828
|
-
}
|
|
1829
|
-
if (pkg.scripts) {
|
|
1830
|
-
detected.commands.build = pkg.scripts.build;
|
|
1831
|
-
detected.commands.test = pkg.scripts.test;
|
|
1832
|
-
detected.commands.lint = pkg.scripts.lint || pkg.scripts["lint:check"];
|
|
1833
|
-
detected.commands.dev = pkg.scripts.dev || pkg.scripts.start || pkg.scripts.serve;
|
|
1834
|
-
detected.commands.format = pkg.scripts.format || pkg.scripts.prettier;
|
|
1835
|
-
}
|
|
1836
|
-
if (await fileExists(join3(cwd, "pnpm-lock.yaml"))) {
|
|
1837
|
-
detected.packageManager = "pnpm";
|
|
1838
|
-
} else if (await fileExists(join3(cwd, "yarn.lock"))) {
|
|
1839
|
-
detected.packageManager = "yarn";
|
|
1840
|
-
} else if (await fileExists(join3(cwd, "bun.lockb"))) {
|
|
1841
|
-
detected.packageManager = "bun";
|
|
1842
|
-
} else if (await fileExists(join3(cwd, "package-lock.json"))) {
|
|
1843
|
-
detected.packageManager = "npm";
|
|
1844
|
-
}
|
|
1845
|
-
if (detected.packageManager && detected.packageManager !== "npm") {
|
|
1846
|
-
const pm = detected.packageManager;
|
|
1847
|
-
for (const [key, value] of Object.entries(detected.commands)) {
|
|
1848
|
-
if (value && !value.startsWith(pm) && !value.startsWith("npx")) {
|
|
1849
|
-
detected.commands[key] = `${pm} run ${value}`;
|
|
1815
|
+
if (fileNames.has("requirements.txt")) {
|
|
1816
|
+
const reqRes = await fetch(`https://raw.githubusercontent.com/${owner}/${repo}/HEAD/requirements.txt`);
|
|
1817
|
+
if (reqRes.ok) {
|
|
1818
|
+
try {
|
|
1819
|
+
const content = (await reqRes.text()).toLowerCase();
|
|
1820
|
+
if (!detected.stack.includes("python")) detected.stack.push("python");
|
|
1821
|
+
if (content.includes("fastapi") && !detected.stack.includes("fastapi")) detected.stack.push("fastapi");
|
|
1822
|
+
if (content.includes("django") && !detected.stack.includes("django")) detected.stack.push("django");
|
|
1823
|
+
if (content.includes("flask") && !detected.stack.includes("flask")) detected.stack.push("flask");
|
|
1824
|
+
if (content.includes("sqlalchemy") && !detected.stack.includes("sqlalchemy")) detected.stack.push("sqlalchemy");
|
|
1825
|
+
if (content.includes("asyncpg") || content.includes("psycopg")) {
|
|
1826
|
+
if (!detected.databases.includes("postgresql")) detected.databases.push("postgresql");
|
|
1850
1827
|
}
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
for (const [key, value] of Object.entries(detected.commands)) {
|
|
1854
|
-
if (value && !value.startsWith("npm") && !value.startsWith("npx")) {
|
|
1855
|
-
detected.commands[key] = `npm run ${value}`;
|
|
1828
|
+
if (content.includes("aiosqlite") || content.includes("sqlite")) {
|
|
1829
|
+
if (!detected.databases.includes("sqlite")) detected.databases.push("sqlite");
|
|
1856
1830
|
}
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
if (pkg.scripts) {
|
|
1860
|
-
detected.commands.build = pkg.scripts.build ? "build" : void 0;
|
|
1861
|
-
detected.commands.test = pkg.scripts.test ? "test" : void 0;
|
|
1862
|
-
detected.commands.lint = pkg.scripts.lint ? "lint" : pkg.scripts["lint:check"] ? "lint:check" : void 0;
|
|
1863
|
-
detected.commands.dev = pkg.scripts.dev ? "dev" : pkg.scripts.start ? "start" : pkg.scripts.serve ? "serve" : void 0;
|
|
1864
|
-
}
|
|
1865
|
-
return detected;
|
|
1866
|
-
} catch {
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
const pyprojectPath = join3(cwd, "pyproject.toml");
|
|
1870
|
-
if (await fileExists(pyprojectPath)) {
|
|
1871
|
-
try {
|
|
1872
|
-
const content = await readFile3(pyprojectPath, "utf-8");
|
|
1873
|
-
detected.stack.push("python");
|
|
1874
|
-
detected.type = "application";
|
|
1875
|
-
const nameMatch = content.match(/name\s*=\s*"([^"]+)"/);
|
|
1876
|
-
if (nameMatch) detected.name = nameMatch[1];
|
|
1877
|
-
if (content.includes("fastapi")) detected.stack.push("fastapi");
|
|
1878
|
-
if (content.includes("django")) detected.stack.push("django");
|
|
1879
|
-
if (content.includes("flask")) detected.stack.push("flask");
|
|
1880
|
-
if (content.includes("pydantic")) detected.stack.push("pydantic");
|
|
1881
|
-
if (content.includes("sqlalchemy")) detected.stack.push("sqlalchemy");
|
|
1882
|
-
if (content.includes("pytest")) detected.stack.push("pytest");
|
|
1883
|
-
if (content.includes("ruff")) detected.stack.push("ruff");
|
|
1884
|
-
if (content.includes("mypy")) detected.stack.push("mypy");
|
|
1885
|
-
detected.commands.test = "pytest";
|
|
1886
|
-
detected.commands.lint = "ruff check .";
|
|
1887
|
-
if (content.includes("[tool.poetry]")) {
|
|
1888
|
-
detected.packageManager = "yarn";
|
|
1889
|
-
detected.commands.dev = "poetry run python -m uvicorn main:app --reload";
|
|
1890
|
-
} else if (await fileExists(join3(cwd, "uv.lock"))) {
|
|
1891
|
-
detected.commands.dev = "uv run python main.py";
|
|
1892
|
-
}
|
|
1893
|
-
return detected;
|
|
1894
|
-
} catch {
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
const requirementsPath = join3(cwd, "requirements.txt");
|
|
1898
|
-
if (await fileExists(requirementsPath)) {
|
|
1899
|
-
try {
|
|
1900
|
-
const content = await readFile3(requirementsPath, "utf-8");
|
|
1901
|
-
detected.stack.push("python");
|
|
1902
|
-
detected.type = "application";
|
|
1903
|
-
if (content.toLowerCase().includes("fastapi")) detected.stack.push("fastapi");
|
|
1904
|
-
if (content.toLowerCase().includes("django")) detected.stack.push("django");
|
|
1905
|
-
if (content.toLowerCase().includes("flask")) detected.stack.push("flask");
|
|
1906
|
-
detected.commands.test = "pytest";
|
|
1907
|
-
detected.commands.lint = "ruff check .";
|
|
1908
|
-
return detected;
|
|
1909
|
-
} catch {
|
|
1910
|
-
}
|
|
1911
|
-
}
|
|
1912
|
-
const cargoPath = join3(cwd, "Cargo.toml");
|
|
1913
|
-
if (await fileExists(cargoPath)) {
|
|
1914
|
-
try {
|
|
1915
|
-
const content = await readFile3(cargoPath, "utf-8");
|
|
1916
|
-
detected.stack.push("rust");
|
|
1917
|
-
detected.type = "application";
|
|
1918
|
-
const nameMatch = content.match(/name\s*=\s*"([^"]+)"/);
|
|
1919
|
-
if (nameMatch) detected.name = nameMatch[1];
|
|
1920
|
-
if (content.includes("actix-web")) detected.stack.push("actix");
|
|
1921
|
-
if (content.includes("axum")) detected.stack.push("axum");
|
|
1922
|
-
if (content.includes("tokio")) detected.stack.push("tokio");
|
|
1923
|
-
if (content.includes("serde")) detected.stack.push("serde");
|
|
1924
|
-
if (content.includes("sqlx")) detected.stack.push("sqlx");
|
|
1925
|
-
detected.commands.build = "cargo build";
|
|
1926
|
-
detected.commands.test = "cargo test";
|
|
1927
|
-
detected.commands.lint = "cargo clippy";
|
|
1928
|
-
detected.commands.dev = "cargo run";
|
|
1929
|
-
return detected;
|
|
1930
|
-
} catch {
|
|
1931
|
-
}
|
|
1932
|
-
}
|
|
1933
|
-
const goModPath = join3(cwd, "go.mod");
|
|
1934
|
-
if (await fileExists(goModPath)) {
|
|
1935
|
-
try {
|
|
1936
|
-
const content = await readFile3(goModPath, "utf-8");
|
|
1937
|
-
detected.stack.push("go");
|
|
1938
|
-
detected.type = "application";
|
|
1939
|
-
const moduleMatch = content.match(/module\s+(\S+)/);
|
|
1940
|
-
if (moduleMatch) {
|
|
1941
|
-
const parts = moduleMatch[1].split("/");
|
|
1942
|
-
detected.name = parts[parts.length - 1];
|
|
1943
|
-
}
|
|
1944
|
-
if (content.includes("gin-gonic/gin")) detected.stack.push("gin");
|
|
1945
|
-
if (content.includes("gofiber/fiber")) detected.stack.push("fiber");
|
|
1946
|
-
if (content.includes("labstack/echo")) detected.stack.push("echo");
|
|
1947
|
-
if (content.includes("gorm.io/gorm")) detected.stack.push("gorm");
|
|
1948
|
-
detected.commands.build = "go build";
|
|
1949
|
-
detected.commands.test = "go test ./...";
|
|
1950
|
-
detected.commands.lint = "golangci-lint run";
|
|
1951
|
-
detected.commands.dev = "go run .";
|
|
1952
|
-
return detected;
|
|
1953
|
-
} catch {
|
|
1954
|
-
}
|
|
1955
|
-
}
|
|
1956
|
-
const makefilePath = join3(cwd, "Makefile");
|
|
1957
|
-
if (await fileExists(makefilePath)) {
|
|
1958
|
-
try {
|
|
1959
|
-
const content = await readFile3(makefilePath, "utf-8");
|
|
1960
|
-
if (content.includes("build:")) detected.commands.build = "make build";
|
|
1961
|
-
if (content.includes("test:")) detected.commands.test = "make test";
|
|
1962
|
-
if (content.includes("lint:")) detected.commands.lint = "make lint";
|
|
1963
|
-
if (content.includes("dev:")) detected.commands.dev = "make dev";
|
|
1964
|
-
if (content.includes("run:")) detected.commands.dev = detected.commands.dev || "make run";
|
|
1965
|
-
if (Object.keys(detected.commands).length > 0) {
|
|
1966
|
-
detected.type = "application";
|
|
1967
|
-
return detected;
|
|
1968
|
-
}
|
|
1969
|
-
} catch {
|
|
1970
|
-
}
|
|
1971
|
-
}
|
|
1972
|
-
if (await fileExists(join3(cwd, "Dockerfile")) || await fileExists(join3(cwd, "docker-compose.yml"))) {
|
|
1973
|
-
detected.stack.push("docker");
|
|
1974
|
-
detected.type = "application";
|
|
1975
|
-
detected.hasDocker = true;
|
|
1976
|
-
}
|
|
1977
|
-
const licensePath = join3(cwd, "LICENSE");
|
|
1978
|
-
if (await fileExists(licensePath)) {
|
|
1979
|
-
try {
|
|
1980
|
-
const licenseContent = await readFile3(licensePath, "utf-8");
|
|
1981
|
-
const lowerContent = licenseContent.toLowerCase();
|
|
1982
|
-
if (lowerContent.includes("mit license") || lowerContent.includes("permission is hereby granted, free of charge")) {
|
|
1983
|
-
detected.license = "mit";
|
|
1984
|
-
} else if (lowerContent.includes("apache license") && lowerContent.includes("version 2.0")) {
|
|
1985
|
-
detected.license = "apache-2.0";
|
|
1986
|
-
} else if (lowerContent.includes("gnu general public license") && lowerContent.includes("version 3")) {
|
|
1987
|
-
detected.license = "gpl-3.0";
|
|
1988
|
-
} else if (lowerContent.includes("gnu lesser general public license")) {
|
|
1989
|
-
detected.license = "lgpl-3.0";
|
|
1990
|
-
} else if (lowerContent.includes("gnu affero general public license")) {
|
|
1991
|
-
detected.license = "agpl-3.0";
|
|
1992
|
-
} else if (lowerContent.includes("bsd 3-clause") || lowerContent.includes("redistribution and use in source and binary forms")) {
|
|
1993
|
-
detected.license = "bsd-3";
|
|
1994
|
-
} else if (lowerContent.includes("mozilla public license") && lowerContent.includes("2.0")) {
|
|
1995
|
-
detected.license = "mpl-2.0";
|
|
1996
|
-
} else if (lowerContent.includes("unlicense") || lowerContent.includes("this is free and unencumbered software")) {
|
|
1997
|
-
detected.license = "unlicense";
|
|
1998
|
-
}
|
|
1999
|
-
} catch {
|
|
2000
|
-
}
|
|
2001
|
-
}
|
|
2002
|
-
const gitConfigPath = join3(cwd, ".git", "config");
|
|
2003
|
-
if (await fileExists(gitConfigPath)) {
|
|
2004
|
-
try {
|
|
2005
|
-
const gitConfig = await readFile3(gitConfigPath, "utf-8");
|
|
2006
|
-
const urlMatch = gitConfig.match(/url\s*=\s*(.+)/);
|
|
2007
|
-
if (urlMatch) {
|
|
2008
|
-
const repoUrl = urlMatch[1].trim();
|
|
2009
|
-
detected.repoUrl = repoUrl;
|
|
2010
|
-
if (repoUrl.includes("github.com")) {
|
|
2011
|
-
detected.repoHost = "github";
|
|
2012
|
-
} else if (repoUrl.includes("gitlab.com") || repoUrl.includes("gitlab")) {
|
|
2013
|
-
detected.repoHost = "gitlab";
|
|
2014
|
-
} else if (repoUrl.includes("bitbucket")) {
|
|
2015
|
-
detected.repoHost = "bitbucket";
|
|
2016
|
-
} else if (repoUrl.includes("gitea") || repoUrl.includes("codeberg")) {
|
|
2017
|
-
detected.repoHost = "gitea";
|
|
2018
|
-
} else if (repoUrl.includes("azure")) {
|
|
2019
|
-
detected.repoHost = "azure";
|
|
2020
|
-
}
|
|
2021
|
-
}
|
|
2022
|
-
} catch {
|
|
2023
|
-
}
|
|
2024
|
-
}
|
|
2025
|
-
if (await fileExists(join3(cwd, ".github", "workflows"))) {
|
|
2026
|
-
detected.cicd = "github_actions";
|
|
2027
|
-
} else if (await fileExists(join3(cwd, ".gitlab-ci.yml"))) {
|
|
2028
|
-
detected.cicd = "gitlab_ci";
|
|
2029
|
-
} else if (await fileExists(join3(cwd, "Jenkinsfile"))) {
|
|
2030
|
-
detected.cicd = "jenkins";
|
|
2031
|
-
} else if (await fileExists(join3(cwd, ".circleci"))) {
|
|
2032
|
-
detected.cicd = "circleci";
|
|
2033
|
-
} else if (await fileExists(join3(cwd, ".travis.yml"))) {
|
|
2034
|
-
detected.cicd = "travis";
|
|
2035
|
-
} else if (await fileExists(join3(cwd, "azure-pipelines.yml"))) {
|
|
2036
|
-
detected.cicd = "azure_devops";
|
|
2037
|
-
} else if (await fileExists(join3(cwd, "bitbucket-pipelines.yml"))) {
|
|
2038
|
-
detected.cicd = "bitbucket";
|
|
2039
|
-
} else if (await fileExists(join3(cwd, ".drone.yml"))) {
|
|
2040
|
-
detected.cicd = "drone";
|
|
2041
|
-
}
|
|
2042
|
-
detected.existingFiles = [];
|
|
2043
|
-
const staticFiles = [
|
|
2044
|
-
".editorconfig",
|
|
2045
|
-
"CONTRIBUTING.md",
|
|
2046
|
-
"CODE_OF_CONDUCT.md",
|
|
2047
|
-
"SECURITY.md",
|
|
2048
|
-
"ROADMAP.md",
|
|
2049
|
-
".gitignore",
|
|
2050
|
-
".github/FUNDING.yml",
|
|
2051
|
-
"LICENSE",
|
|
2052
|
-
"README.md",
|
|
2053
|
-
"ARCHITECTURE.md",
|
|
2054
|
-
"CHANGELOG.md"
|
|
2055
|
-
];
|
|
2056
|
-
for (const file of staticFiles) {
|
|
2057
|
-
if (await fileExists(join3(cwd, file))) {
|
|
2058
|
-
detected.existingFiles.push(file);
|
|
2059
|
-
}
|
|
2060
|
-
}
|
|
2061
|
-
if (!detected.description) {
|
|
2062
|
-
const readmePath = join3(cwd, "README.md");
|
|
2063
|
-
if (await fileExists(readmePath)) {
|
|
2064
|
-
try {
|
|
2065
|
-
const readme = await readFile3(readmePath, "utf-8");
|
|
2066
|
-
const lines = readme.split("\n");
|
|
2067
|
-
for (const line of lines) {
|
|
2068
|
-
const trimmed = line.trim();
|
|
2069
|
-
if (trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("!") && !trimmed.startsWith("[") && trimmed.length > 20) {
|
|
2070
|
-
detected.description = trimmed.substring(0, 200);
|
|
2071
|
-
break;
|
|
1831
|
+
if (content.includes("pymongo") || content.includes("motor")) {
|
|
1832
|
+
if (!detected.databases.includes("mongodb")) detected.databases.push("mongodb");
|
|
2072
1833
|
}
|
|
1834
|
+
} catch {
|
|
2073
1835
|
}
|
|
2074
|
-
} catch {
|
|
2075
1836
|
}
|
|
2076
1837
|
}
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
1838
|
+
if (fileNames.has("package.json")) {
|
|
1839
|
+
const pkgRes = await fetch(`https://raw.githubusercontent.com/${owner}/${repo}/HEAD/package.json`);
|
|
1840
|
+
if (pkgRes.ok) {
|
|
1841
|
+
try {
|
|
1842
|
+
const pkg = await pkgRes.json();
|
|
1843
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1844
|
+
if (allDeps["next"]) detected.stack.push("nextjs");
|
|
1845
|
+
if (allDeps["react"]) detected.stack.push("react");
|
|
1846
|
+
if (allDeps["vue"]) detected.stack.push("vue");
|
|
1847
|
+
if (allDeps["svelte"]) detected.stack.push("svelte");
|
|
1848
|
+
if (allDeps["express"]) detected.stack.push("express");
|
|
1849
|
+
if (allDeps["fastify"]) detected.stack.push("fastify");
|
|
1850
|
+
if (allDeps["hono"]) detected.stack.push("hono");
|
|
1851
|
+
if (allDeps["typescript"]) detected.stack.push("typescript");
|
|
1852
|
+
if (allDeps["tailwindcss"]) detected.stack.push("tailwind");
|
|
1853
|
+
if (allDeps["prisma"]) detected.stack.push("prisma");
|
|
1854
|
+
if (allDeps["drizzle-orm"]) detected.stack.push("drizzle");
|
|
1855
|
+
if (allDeps["vitest"]) detected.testFramework = "vitest";
|
|
1856
|
+
else if (allDeps["jest"]) detected.testFramework = "jest";
|
|
1857
|
+
else if (allDeps["@playwright/test"]) detected.testFramework = "playwright";
|
|
1858
|
+
else if (allDeps["cypress"]) detected.testFramework = "cypress";
|
|
1859
|
+
else if (allDeps["mocha"]) detected.testFramework = "mocha";
|
|
1860
|
+
if (detected.stack.length === 0 || detected.stack.length === 1 && detected.stack[0] === "typescript") {
|
|
1861
|
+
detected.stack.unshift("javascript");
|
|
1862
|
+
}
|
|
1863
|
+
if (pkg.scripts) {
|
|
1864
|
+
if (pkg.scripts.build) detected.commands.build = "npm run build";
|
|
1865
|
+
if (pkg.scripts.test) detected.commands.test = "npm run test";
|
|
1866
|
+
if (pkg.scripts.lint) detected.commands.lint = "npm run lint";
|
|
1867
|
+
if (pkg.scripts.dev) detected.commands.dev = "npm run dev";
|
|
1868
|
+
else if (pkg.scripts.start) detected.commands.dev = "npm run start";
|
|
1869
|
+
}
|
|
1870
|
+
if (allDeps["pg"] || allDeps["postgres"] || allDeps["@neondatabase/serverless"]) {
|
|
1871
|
+
if (!detected.databases.includes("postgresql")) detected.databases.push("postgresql");
|
|
1872
|
+
}
|
|
1873
|
+
if (allDeps["better-sqlite3"] || allDeps["sql.js"] || allDeps["sqlite3"]) {
|
|
1874
|
+
if (!detected.databases.includes("sqlite")) detected.databases.push("sqlite");
|
|
1875
|
+
}
|
|
1876
|
+
if (allDeps["mongodb"] || allDeps["mongoose"]) {
|
|
1877
|
+
if (!detected.databases.includes("mongodb")) detected.databases.push("mongodb");
|
|
1878
|
+
}
|
|
1879
|
+
if (allDeps["redis"] || allDeps["ioredis"]) {
|
|
1880
|
+
if (!detected.databases.includes("redis")) detected.databases.push("redis");
|
|
1881
|
+
}
|
|
1882
|
+
if (allDeps["mysql"] || allDeps["mysql2"]) {
|
|
1883
|
+
if (!detected.databases.includes("mysql")) detected.databases.push("mysql");
|
|
1884
|
+
}
|
|
1885
|
+
} catch {
|
|
1886
|
+
}
|
|
2123
1887
|
}
|
|
2124
1888
|
}
|
|
1889
|
+
if (fileNames.has("cargo.toml")) {
|
|
1890
|
+
detected.stack.push("rust");
|
|
1891
|
+
detected.commands.build = "cargo build";
|
|
1892
|
+
detected.commands.test = "cargo test";
|
|
1893
|
+
}
|
|
1894
|
+
if (fileNames.has("go.mod")) {
|
|
1895
|
+
detected.stack.push("go");
|
|
1896
|
+
detected.commands.build = "go build";
|
|
1897
|
+
detected.commands.test = "go test ./...";
|
|
1898
|
+
}
|
|
1899
|
+
return detected;
|
|
1900
|
+
} catch {
|
|
1901
|
+
return null;
|
|
2125
1902
|
}
|
|
2126
|
-
return null;
|
|
2127
1903
|
}
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
async function detectFromGitHubApi(repoUrl) {
|
|
2131
|
-
const parsed = parseGitHubUrl(repoUrl);
|
|
1904
|
+
async function detectFromGitLabApi(repoUrl) {
|
|
1905
|
+
const parsed = parseGitLabUrl(repoUrl);
|
|
2132
1906
|
if (!parsed) return null;
|
|
2133
|
-
const {
|
|
1907
|
+
const { path: projectPath, host } = parsed;
|
|
1908
|
+
const encodedPath = encodeURIComponent(projectPath);
|
|
2134
1909
|
try {
|
|
2135
|
-
const repoRes = await fetch(`https
|
|
1910
|
+
const repoRes = await fetch(`https://${host}/api/v4/projects/${encodedPath}`, {
|
|
2136
1911
|
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
2137
1912
|
});
|
|
2138
1913
|
if (!repoRes.ok) return null;
|
|
2139
1914
|
const repoInfo = await repoRes.json();
|
|
2140
|
-
if (repoInfo.private) return null;
|
|
2141
|
-
const licenseId = repoInfo.license?.
|
|
2142
|
-
const isOpenSource =
|
|
1915
|
+
if (repoInfo.visibility === "private") return null;
|
|
1916
|
+
const licenseId = repoInfo.license?.key?.toLowerCase() || null;
|
|
1917
|
+
const isOpenSource = repoInfo.visibility === "public" && (licenseId ? OPEN_SOURCE_LICENSES.includes(licenseId) : false);
|
|
2143
1918
|
const detected = {
|
|
2144
1919
|
name: repoInfo.name,
|
|
2145
1920
|
description: repoInfo.description ?? void 0,
|
|
@@ -2148,425 +1923,744 @@ async function detectFromGitHubApi(repoUrl) {
|
|
|
2148
1923
|
commands: {},
|
|
2149
1924
|
packageManager: null,
|
|
2150
1925
|
type: "application",
|
|
2151
|
-
repoHost: "
|
|
1926
|
+
repoHost: "gitlab",
|
|
2152
1927
|
repoUrl,
|
|
2153
1928
|
license: licenseId ?? void 0,
|
|
2154
|
-
isPublicRepo:
|
|
1929
|
+
isPublicRepo: repoInfo.visibility === "public",
|
|
2155
1930
|
isOpenSource,
|
|
2156
1931
|
projectType: isOpenSource ? "open_source" : void 0,
|
|
2157
1932
|
hasDocker: false,
|
|
2158
1933
|
existingFiles: []
|
|
2159
1934
|
};
|
|
2160
|
-
const filesRes = await fetch(`https
|
|
1935
|
+
const filesRes = await fetch(`https://${host}/api/v4/projects/${encodedPath}/repository/tree?per_page=100`, {
|
|
2161
1936
|
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
2162
1937
|
});
|
|
2163
1938
|
if (!filesRes.ok) return detected;
|
|
2164
1939
|
const files = await filesRes.json();
|
|
2165
1940
|
const fileNames = new Set(files.map((f) => f.name.toLowerCase()));
|
|
2166
1941
|
for (const file of STATIC_FILES) {
|
|
2167
|
-
if (file.includes("/")) continue;
|
|
2168
1942
|
if (files.some((f) => f.name.toLowerCase() === file.toLowerCase())) {
|
|
2169
1943
|
detected.existingFiles.push(file);
|
|
2170
1944
|
}
|
|
2171
1945
|
}
|
|
2172
|
-
if (
|
|
2173
|
-
|
|
2174
|
-
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
2175
|
-
});
|
|
2176
|
-
if (ghFilesRes.ok) {
|
|
2177
|
-
const ghFiles = await ghFilesRes.json();
|
|
2178
|
-
if (ghFiles.some((f) => f.name.toLowerCase() === "funding.yml")) {
|
|
2179
|
-
detected.existingFiles.push(".github/FUNDING.yml");
|
|
2180
|
-
}
|
|
2181
|
-
}
|
|
2182
|
-
}
|
|
1946
|
+
if (fileNames.has(".gitlab-ci.yml")) detected.cicd = "gitlab_ci";
|
|
1947
|
+
if (fileNames.has("jenkinsfile")) detected.cicd = "jenkins";
|
|
2183
1948
|
if (fileNames.has("dockerfile") || fileNames.has("docker-compose.yml") || fileNames.has("docker-compose.yaml")) {
|
|
2184
1949
|
detected.hasDocker = true;
|
|
2185
1950
|
detected.stack.push("docker");
|
|
2186
1951
|
const dockerComposeFile = fileNames.has("docker-compose.yml") ? "docker-compose.yml" : fileNames.has("docker-compose.yaml") ? "docker-compose.yaml" : null;
|
|
2187
1952
|
if (dockerComposeFile) {
|
|
2188
|
-
const composeRes = await fetch(`https
|
|
1953
|
+
const composeRes = await fetch(`https://${host}/api/v4/projects/${encodedPath}/repository/files/${encodeURIComponent(dockerComposeFile)}/raw?ref=HEAD`, {
|
|
1954
|
+
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
1955
|
+
});
|
|
2189
1956
|
if (composeRes.ok) {
|
|
2190
1957
|
try {
|
|
2191
1958
|
const content = await composeRes.text();
|
|
2192
1959
|
const lowerContent = content.toLowerCase();
|
|
2193
|
-
if (content.includes("
|
|
1960
|
+
if (content.includes("registry.gitlab.com")) detected.containerRegistry = "gitlab_registry";
|
|
1961
|
+
else if (content.includes("ghcr.io")) detected.containerRegistry = "ghcr";
|
|
2194
1962
|
else if (content.includes("docker.io") || /image:\s*[a-z0-9]+\/[a-z0-9]/.test(content)) detected.containerRegistry = "dockerhub";
|
|
2195
1963
|
else if (content.includes("gcr.io")) detected.containerRegistry = "gcr";
|
|
2196
|
-
else if (content.includes("ecr.") || content.includes(".amazonaws.com")) detected.containerRegistry = "ecr";
|
|
2197
|
-
else if (content.includes("azurecr.io")) detected.containerRegistry = "acr";
|
|
2198
|
-
else if (content.includes("quay.io")) detected.containerRegistry = "quay";
|
|
2199
|
-
else if (content.includes("registry.gitlab.com")) detected.containerRegistry = "gitlab_registry";
|
|
2200
1964
|
if (lowerContent.includes("postgres")) detected.databases.push("postgresql");
|
|
2201
1965
|
if (lowerContent.includes("mysql") && !lowerContent.includes("mysql-")) detected.databases.push("mysql");
|
|
2202
1966
|
if (lowerContent.includes("mongo")) detected.databases.push("mongodb");
|
|
2203
1967
|
if (lowerContent.includes("redis")) detected.databases.push("redis");
|
|
2204
1968
|
if (lowerContent.includes("sqlite")) detected.databases.push("sqlite");
|
|
2205
|
-
if (lowerContent.includes("mariadb")) detected.databases.push("mariadb");
|
|
2206
1969
|
} catch {
|
|
2207
1970
|
}
|
|
2208
1971
|
}
|
|
2209
1972
|
}
|
|
2210
1973
|
}
|
|
2211
|
-
if (
|
|
2212
|
-
const
|
|
2213
|
-
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
2214
|
-
});
|
|
2215
|
-
if (
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
if (
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
1974
|
+
if (fileNames.has("package.json")) {
|
|
1975
|
+
const pkgRes = await fetch(`https://${host}/api/v4/projects/${encodedPath}/repository/files/package.json/raw?ref=HEAD`, {
|
|
1976
|
+
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
1977
|
+
});
|
|
1978
|
+
if (pkgRes.ok) {
|
|
1979
|
+
try {
|
|
1980
|
+
const pkg = await pkgRes.json();
|
|
1981
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1982
|
+
if (allDeps["next"]) detected.stack.push("nextjs");
|
|
1983
|
+
if (allDeps["react"]) detected.stack.push("react");
|
|
1984
|
+
if (allDeps["vue"]) detected.stack.push("vue");
|
|
1985
|
+
if (allDeps["svelte"]) detected.stack.push("svelte");
|
|
1986
|
+
if (allDeps["express"]) detected.stack.push("express");
|
|
1987
|
+
if (allDeps["fastify"]) detected.stack.push("fastify");
|
|
1988
|
+
if (allDeps["typescript"]) detected.stack.push("typescript");
|
|
1989
|
+
if (allDeps["tailwindcss"]) detected.stack.push("tailwind");
|
|
1990
|
+
if (allDeps["prisma"]) detected.stack.push("prisma");
|
|
1991
|
+
if (allDeps["vitest"]) detected.testFramework = "vitest";
|
|
1992
|
+
else if (allDeps["jest"]) detected.testFramework = "jest";
|
|
1993
|
+
else if (allDeps["@playwright/test"]) detected.testFramework = "playwright";
|
|
1994
|
+
if (detected.stack.length === 0 || detected.stack.length === 1 && detected.stack[0] === "typescript") {
|
|
1995
|
+
detected.stack.unshift("javascript");
|
|
1996
|
+
}
|
|
1997
|
+
if (pkg.scripts) {
|
|
1998
|
+
if (pkg.scripts.build) detected.commands.build = "npm run build";
|
|
1999
|
+
if (pkg.scripts.test) detected.commands.test = "npm run test";
|
|
2000
|
+
if (pkg.scripts.lint) detected.commands.lint = "npm run lint";
|
|
2001
|
+
if (pkg.scripts.dev) detected.commands.dev = "npm run dev";
|
|
2002
|
+
}
|
|
2003
|
+
if (allDeps["pg"] || allDeps["postgres"]) {
|
|
2004
|
+
if (!detected.databases.includes("postgresql")) detected.databases.push("postgresql");
|
|
2005
|
+
}
|
|
2006
|
+
if (allDeps["better-sqlite3"] || allDeps["sqlite3"]) {
|
|
2007
|
+
if (!detected.databases.includes("sqlite")) detected.databases.push("sqlite");
|
|
2008
|
+
}
|
|
2009
|
+
if (allDeps["mongodb"] || allDeps["mongoose"]) {
|
|
2010
|
+
if (!detected.databases.includes("mongodb")) detected.databases.push("mongodb");
|
|
2011
|
+
}
|
|
2012
|
+
if (allDeps["redis"] || allDeps["ioredis"]) {
|
|
2013
|
+
if (!detected.databases.includes("redis")) detected.databases.push("redis");
|
|
2014
|
+
}
|
|
2015
|
+
} catch {
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
if (fileNames.has("pyproject.toml")) {
|
|
2020
|
+
detected.stack.push("python");
|
|
2021
|
+
const pyRes = await fetch(`https://${host}/api/v4/projects/${encodedPath}/repository/files/pyproject.toml/raw?ref=HEAD`, {
|
|
2022
|
+
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
2023
|
+
});
|
|
2024
|
+
if (pyRes.ok) {
|
|
2025
|
+
try {
|
|
2026
|
+
const content = (await pyRes.text()).toLowerCase();
|
|
2027
|
+
if (content.includes("fastapi")) detected.stack.push("fastapi");
|
|
2028
|
+
if (content.includes("django")) detected.stack.push("django");
|
|
2029
|
+
if (content.includes("flask")) detected.stack.push("flask");
|
|
2030
|
+
if (content.includes("sqlalchemy")) detected.stack.push("sqlalchemy");
|
|
2031
|
+
if (content.includes("pytest")) detected.testFramework = "pytest";
|
|
2032
|
+
if (content.includes("asyncpg") || content.includes("psycopg")) {
|
|
2033
|
+
if (!detected.databases.includes("postgresql")) detected.databases.push("postgresql");
|
|
2034
|
+
}
|
|
2035
|
+
if (content.includes("aiosqlite") || content.includes("sqlite")) {
|
|
2036
|
+
if (!detected.databases.includes("sqlite")) detected.databases.push("sqlite");
|
|
2037
|
+
}
|
|
2038
|
+
} catch {
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
} else if (fileNames.has("requirements.txt")) {
|
|
2042
|
+
detected.stack.push("python");
|
|
2043
|
+
}
|
|
2044
|
+
if (fileNames.has("cargo.toml")) {
|
|
2045
|
+
detected.stack.push("rust");
|
|
2046
|
+
detected.commands.build = "cargo build";
|
|
2047
|
+
detected.commands.test = "cargo test";
|
|
2048
|
+
}
|
|
2049
|
+
if (fileNames.has("go.mod")) {
|
|
2050
|
+
detected.stack.push("go");
|
|
2051
|
+
detected.commands.build = "go build";
|
|
2052
|
+
detected.commands.test = "go test ./...";
|
|
2053
|
+
}
|
|
2054
|
+
return detected;
|
|
2055
|
+
} catch {
|
|
2056
|
+
return null;
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
function isValidGitUrl(url) {
|
|
2060
|
+
const trimmed = url.trim();
|
|
2061
|
+
if (trimmed.startsWith("https://") || trimmed.startsWith("http://") || trimmed.startsWith("git://") || trimmed.startsWith("git@") || trimmed.startsWith("ssh://")) {
|
|
2062
|
+
const dangerousChars = /[;&|`$(){}[\]<>\\'"!#*?~]/;
|
|
2063
|
+
return !dangerousChars.test(trimmed);
|
|
2064
|
+
}
|
|
2065
|
+
return false;
|
|
2066
|
+
}
|
|
2067
|
+
async function detectFromShallowClone(repoUrl) {
|
|
2068
|
+
let tempDir = null;
|
|
2069
|
+
if (!isValidGitUrl(repoUrl)) {
|
|
2070
|
+
return null;
|
|
2071
|
+
}
|
|
2072
|
+
try {
|
|
2073
|
+
tempDir = await mkdtemp(join3(tmpdir(), "lynxprompt-detect-"));
|
|
2074
|
+
try {
|
|
2075
|
+
const result = spawnSync("git", ["clone", "--depth", "1", "--quiet", repoUrl, tempDir], {
|
|
2076
|
+
stdio: "pipe",
|
|
2077
|
+
timeout: 3e4
|
|
2078
|
+
});
|
|
2079
|
+
if (result.status !== 0) {
|
|
2080
|
+
return null;
|
|
2081
|
+
}
|
|
2082
|
+
} catch {
|
|
2083
|
+
return null;
|
|
2084
|
+
}
|
|
2085
|
+
const detected = await detectProject(tempDir);
|
|
2086
|
+
if (detected) {
|
|
2087
|
+
detected.repoHost = detectRepoHost(repoUrl);
|
|
2088
|
+
detected.repoUrl = repoUrl;
|
|
2089
|
+
}
|
|
2090
|
+
return detected;
|
|
2091
|
+
} catch {
|
|
2092
|
+
return null;
|
|
2093
|
+
} finally {
|
|
2094
|
+
if (tempDir) {
|
|
2095
|
+
try {
|
|
2096
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
2097
|
+
} catch {
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
async function detectFromRemoteUrl(repoUrl) {
|
|
2103
|
+
const host = detectRepoHost(repoUrl);
|
|
2104
|
+
if (host === "github") {
|
|
2105
|
+
const result = await detectFromGitHubApi(repoUrl);
|
|
2106
|
+
if (result) return result;
|
|
2107
|
+
}
|
|
2108
|
+
if (host === "gitlab") {
|
|
2109
|
+
const result = await detectFromGitLabApi(repoUrl);
|
|
2110
|
+
if (result) return result;
|
|
2111
|
+
}
|
|
2112
|
+
return detectFromShallowClone(repoUrl);
|
|
2113
|
+
}
|
|
2114
|
+
function isGitUrl(str) {
|
|
2115
|
+
const patterns = [
|
|
2116
|
+
/^https?:\/\/[^/]+\/.*$/,
|
|
2117
|
+
/^git@[^:]+:.*$/,
|
|
2118
|
+
/^git:\/\/.*$/,
|
|
2119
|
+
/^ssh:\/\/.*$/
|
|
2120
|
+
];
|
|
2121
|
+
return patterns.some((p) => p.test(str.trim()));
|
|
2122
|
+
}
|
|
2123
|
+
var COMMAND_DIRECTORIES = [
|
|
2124
|
+
{ directory: ".cursor/commands", type: "cursor-command", platform: "cursor", templateType: "CURSOR_COMMAND" },
|
|
2125
|
+
{ directory: ".claude/commands", type: "claude-command", platform: "claude", templateType: "CLAUDE_COMMAND" },
|
|
2126
|
+
{ directory: ".windsurf/workflows", type: "windsurf-workflow", platform: "windsurf", templateType: "WINDSURF_WORKFLOW" },
|
|
2127
|
+
{ directory: ".copilot/prompts", type: "copilot-prompt", platform: "copilot", templateType: "COPILOT_PROMPT" },
|
|
2128
|
+
{ directory: ".continue/prompts", type: "continue-prompt", platform: "continue", templateType: "CONTINUE_PROMPT" },
|
|
2129
|
+
{ directory: ".opencode/commands", type: "opencode-command", platform: "opencode", templateType: "OPENCODE_COMMAND" }
|
|
2130
|
+
];
|
|
2131
|
+
async function detectCommandFiles(cwd) {
|
|
2132
|
+
const commands = [];
|
|
2133
|
+
const { readdir: readdir4, readFile: readFileAsync } = await import("fs/promises");
|
|
2134
|
+
for (const cmdDir of COMMAND_DIRECTORIES) {
|
|
2135
|
+
const dirPath = join3(cwd, cmdDir.directory);
|
|
2136
|
+
try {
|
|
2137
|
+
await access2(dirPath);
|
|
2138
|
+
const entries = await readdir4(dirPath, { withFileTypes: true });
|
|
2139
|
+
for (const entry of entries) {
|
|
2140
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
2141
|
+
const filePath = join3(dirPath, entry.name);
|
|
2142
|
+
try {
|
|
2143
|
+
const content = await readFileAsync(filePath, "utf-8");
|
|
2144
|
+
const name = entry.name.replace(/\.md$/, "");
|
|
2145
|
+
commands.push({
|
|
2146
|
+
path: filePath,
|
|
2147
|
+
name,
|
|
2148
|
+
type: cmdDir.type,
|
|
2149
|
+
content,
|
|
2150
|
+
platform: cmdDir.platform,
|
|
2151
|
+
templateType: cmdDir.templateType
|
|
2152
|
+
});
|
|
2153
|
+
} catch {
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
} catch {
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
return commands;
|
|
2161
|
+
}
|
|
2162
|
+
function inferCommandTypeFromPath(filePath) {
|
|
2163
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
2164
|
+
const match = COMMAND_DIRECTORIES.find((cmd) => normalizedPath.includes(cmd.directory));
|
|
2165
|
+
if (match) {
|
|
2166
|
+
return {
|
|
2167
|
+
type: match.type,
|
|
2168
|
+
platform: match.platform,
|
|
2169
|
+
templateType: match.templateType
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
return null;
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
// src/commands/push.ts
|
|
2176
|
+
function scanForAgentFiles(cwd, maxDepth = 5) {
|
|
2177
|
+
const results = [];
|
|
2178
|
+
function scan(dir, depth) {
|
|
2179
|
+
if (depth > maxDepth) return;
|
|
2180
|
+
try {
|
|
2181
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
2182
|
+
for (const entry of entries) {
|
|
2183
|
+
const fullPath = path.join(dir, entry.name);
|
|
2184
|
+
if (entry.isDirectory()) {
|
|
2185
|
+
if (["node_modules", ".git", "dist", "build", ".next", "__pycache__", "venv", ".venv"].includes(entry.name)) {
|
|
2186
|
+
continue;
|
|
2256
2187
|
}
|
|
2257
|
-
|
|
2188
|
+
scan(fullPath, depth + 1);
|
|
2189
|
+
} else if (entry.name === "AGENTS.md") {
|
|
2190
|
+
const relativePath = path.relative(cwd, fullPath);
|
|
2191
|
+
results.push({
|
|
2192
|
+
path: relativePath,
|
|
2193
|
+
absolutePath: fullPath,
|
|
2194
|
+
isRoot: relativePath === "AGENTS.md"
|
|
2195
|
+
});
|
|
2258
2196
|
}
|
|
2259
2197
|
}
|
|
2198
|
+
} catch {
|
|
2260
2199
|
}
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2200
|
+
}
|
|
2201
|
+
scan(cwd, 0);
|
|
2202
|
+
results.sort((a, b) => {
|
|
2203
|
+
if (a.isRoot && !b.isRoot) return -1;
|
|
2204
|
+
if (!a.isRoot && b.isRoot) return 1;
|
|
2205
|
+
return a.path.localeCompare(b.path);
|
|
2206
|
+
});
|
|
2207
|
+
return results;
|
|
2208
|
+
}
|
|
2209
|
+
async function detectHierarchyInfo(cwd, file) {
|
|
2210
|
+
const repositoryRoot = createRepositoryRoot(cwd);
|
|
2211
|
+
const result = {
|
|
2212
|
+
repositoryPath: null,
|
|
2213
|
+
hierarchyId: null,
|
|
2214
|
+
parentId: null,
|
|
2215
|
+
repositoryRoot
|
|
2216
|
+
};
|
|
2217
|
+
try {
|
|
2218
|
+
const relativePath = path.relative(cwd, path.resolve(file));
|
|
2219
|
+
if (relativePath.includes(path.sep) && !relativePath.startsWith("..")) {
|
|
2220
|
+
result.repositoryPath = relativePath;
|
|
2221
|
+
const rootAgentsMd = path.join(cwd, "AGENTS.md");
|
|
2222
|
+
if (fs.existsSync(rootAgentsMd) && path.resolve(file) !== rootAgentsMd) {
|
|
2223
|
+
const blueprints = await loadBlueprints(cwd);
|
|
2224
|
+
const parentBlueprint = blueprints.blueprints.find(
|
|
2225
|
+
(b) => b.file === "AGENTS.md"
|
|
2226
|
+
);
|
|
2227
|
+
if (parentBlueprint) {
|
|
2228
|
+
result.parentId = parentBlueprint.id;
|
|
2281
2229
|
}
|
|
2282
2230
|
}
|
|
2231
|
+
} else if (relativePath === "AGENTS.md" || relativePath === path.basename(file)) {
|
|
2232
|
+
result.repositoryPath = relativePath;
|
|
2283
2233
|
}
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
}
|
|
2309
|
-
if (pkg.scripts) {
|
|
2310
|
-
if (pkg.scripts.build) detected.commands.build = "npm run build";
|
|
2311
|
-
if (pkg.scripts.test) detected.commands.test = "npm run test";
|
|
2312
|
-
if (pkg.scripts.lint) detected.commands.lint = "npm run lint";
|
|
2313
|
-
if (pkg.scripts.dev) detected.commands.dev = "npm run dev";
|
|
2314
|
-
else if (pkg.scripts.start) detected.commands.dev = "npm run start";
|
|
2315
|
-
}
|
|
2316
|
-
if (allDeps["pg"] || allDeps["postgres"] || allDeps["@neondatabase/serverless"]) {
|
|
2317
|
-
if (!detected.databases.includes("postgresql")) detected.databases.push("postgresql");
|
|
2318
|
-
}
|
|
2319
|
-
if (allDeps["better-sqlite3"] || allDeps["sql.js"] || allDeps["sqlite3"]) {
|
|
2320
|
-
if (!detected.databases.includes("sqlite")) detected.databases.push("sqlite");
|
|
2321
|
-
}
|
|
2322
|
-
if (allDeps["mongodb"] || allDeps["mongoose"]) {
|
|
2323
|
-
if (!detected.databases.includes("mongodb")) detected.databases.push("mongodb");
|
|
2324
|
-
}
|
|
2325
|
-
if (allDeps["redis"] || allDeps["ioredis"]) {
|
|
2326
|
-
if (!detected.databases.includes("redis")) detected.databases.push("redis");
|
|
2327
|
-
}
|
|
2328
|
-
if (allDeps["mysql"] || allDeps["mysql2"]) {
|
|
2329
|
-
if (!detected.databases.includes("mysql")) detected.databases.push("mysql");
|
|
2330
|
-
}
|
|
2331
|
-
} catch {
|
|
2332
|
-
}
|
|
2234
|
+
} catch {
|
|
2235
|
+
}
|
|
2236
|
+
return result;
|
|
2237
|
+
}
|
|
2238
|
+
async function ensureHierarchy(_cwd, repositoryRoot, name) {
|
|
2239
|
+
try {
|
|
2240
|
+
const response = await api.createHierarchy({
|
|
2241
|
+
name,
|
|
2242
|
+
repository_root: repositoryRoot
|
|
2243
|
+
});
|
|
2244
|
+
return response.hierarchy.id;
|
|
2245
|
+
} catch (error) {
|
|
2246
|
+
console.log(chalk6.gray(" Note: Hierarchy creation skipped"));
|
|
2247
|
+
return null;
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
function createRepositoryRoot(rootPath) {
|
|
2251
|
+
try {
|
|
2252
|
+
const gitConfigPath = path.join(rootPath, ".git", "config");
|
|
2253
|
+
if (fs.existsSync(gitConfigPath)) {
|
|
2254
|
+
const gitConfig = fs.readFileSync(gitConfigPath, "utf-8");
|
|
2255
|
+
const urlMatch = gitConfig.match(/url = (.+)/);
|
|
2256
|
+
if (urlMatch) {
|
|
2257
|
+
return createHash2("sha256").update(urlMatch[1].trim()).digest("hex").substring(0, 16);
|
|
2333
2258
|
}
|
|
2334
2259
|
}
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
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
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
async function updateBlueprint(cwd, file, blueprintId, content, options, expectedChecksum) {
|
|
2293
|
+
console.log(chalk6.cyan(`
|
|
2294
|
+
\u{1F4E4} Updating blueprint ${chalk6.bold(blueprintId)}...`));
|
|
2295
|
+
console.log(chalk6.gray(` File: ${file}`));
|
|
2296
|
+
if (!options.yes) {
|
|
2297
|
+
const confirm = await prompts2({
|
|
2298
|
+
type: "confirm",
|
|
2299
|
+
name: "value",
|
|
2300
|
+
message: `Push changes to ${blueprintId}?`,
|
|
2301
|
+
initial: true
|
|
2302
|
+
});
|
|
2303
|
+
if (!confirm.value) {
|
|
2304
|
+
console.log(chalk6.yellow("Push cancelled."));
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
const spinner = ora5("Pushing changes...").start();
|
|
2309
|
+
try {
|
|
2310
|
+
const updateData = { content };
|
|
2311
|
+
if (expectedChecksum && !options.force) {
|
|
2312
|
+
updateData.expected_checksum = expectedChecksum;
|
|
2313
|
+
}
|
|
2314
|
+
const result = await api.updateBlueprint(blueprintId, updateData);
|
|
2315
|
+
spinner.succeed("Blueprint updated!");
|
|
2316
|
+
await updateChecksum(cwd, file, content);
|
|
2317
|
+
console.log();
|
|
2318
|
+
console.log(chalk6.green(`\u2705 Successfully updated ${chalk6.bold(result.blueprint.name)}`));
|
|
2319
|
+
console.log(chalk6.gray(` ID: ${blueprintId}`));
|
|
2320
|
+
if (result.blueprint.content_checksum) {
|
|
2321
|
+
console.log(chalk6.gray(` Checksum: ${result.blueprint.content_checksum}`));
|
|
2339
2322
|
}
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2323
|
+
console.log(chalk6.gray(` View: https://lynxprompt.com/templates/${blueprintId.replace("bp_", "")}`));
|
|
2324
|
+
} catch (error) {
|
|
2325
|
+
spinner.fail("Failed to update blueprint");
|
|
2326
|
+
if (error instanceof ApiRequestError && error.statusCode === 409) {
|
|
2327
|
+
console.log();
|
|
2328
|
+
console.log(chalk6.yellow("\u26A0 Conflict: The blueprint has been modified since you last pulled it."));
|
|
2329
|
+
console.log(chalk6.gray(" Someone else may have pushed changes."));
|
|
2330
|
+
console.log();
|
|
2331
|
+
console.log(chalk6.gray("Options:"));
|
|
2332
|
+
console.log(chalk6.gray(" 1. Run 'lynxp pull " + blueprintId + "' to get the latest version"));
|
|
2333
|
+
console.log(chalk6.gray(" 2. Run 'lynxp push --force' to overwrite remote changes"));
|
|
2334
|
+
process.exit(1);
|
|
2344
2335
|
}
|
|
2345
|
-
|
|
2346
|
-
} catch {
|
|
2347
|
-
return null;
|
|
2336
|
+
handleError(error);
|
|
2348
2337
|
}
|
|
2349
2338
|
}
|
|
2350
|
-
async function
|
|
2351
|
-
const
|
|
2352
|
-
if (
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
if (repoInfo.visibility === "private") return null;
|
|
2362
|
-
const licenseId = repoInfo.license?.key?.toLowerCase() || null;
|
|
2363
|
-
const isOpenSource = repoInfo.visibility === "public" && (licenseId ? OPEN_SOURCE_LICENSES.includes(licenseId) : false);
|
|
2364
|
-
const detected = {
|
|
2365
|
-
name: repoInfo.name,
|
|
2366
|
-
description: repoInfo.description ?? void 0,
|
|
2367
|
-
stack: [],
|
|
2368
|
-
databases: [],
|
|
2369
|
-
commands: {},
|
|
2370
|
-
packageManager: null,
|
|
2371
|
-
type: "application",
|
|
2372
|
-
repoHost: "gitlab",
|
|
2373
|
-
repoUrl,
|
|
2374
|
-
license: licenseId ?? void 0,
|
|
2375
|
-
isPublicRepo: repoInfo.visibility === "public",
|
|
2376
|
-
isOpenSource,
|
|
2377
|
-
projectType: isOpenSource ? "open_source" : void 0,
|
|
2378
|
-
hasDocker: false,
|
|
2379
|
-
existingFiles: []
|
|
2380
|
-
};
|
|
2381
|
-
const filesRes = await fetch(`https://${host}/api/v4/projects/${encodedPath}/repository/tree?per_page=100`, {
|
|
2382
|
-
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
2383
|
-
});
|
|
2384
|
-
if (!filesRes.ok) return detected;
|
|
2385
|
-
const files = await filesRes.json();
|
|
2386
|
-
const fileNames = new Set(files.map((f) => f.name.toLowerCase()));
|
|
2387
|
-
for (const file of STATIC_FILES) {
|
|
2388
|
-
if (files.some((f) => f.name.toLowerCase() === file.toLowerCase())) {
|
|
2389
|
-
detected.existingFiles.push(file);
|
|
2339
|
+
async function createOrLinkBlueprint(cwd, file, filename, content, options) {
|
|
2340
|
+
const isAgentsMd = filename === "AGENTS.md";
|
|
2341
|
+
if (isAgentsMd) {
|
|
2342
|
+
const discoveredFiles = scanForAgentFiles(cwd);
|
|
2343
|
+
if (discoveredFiles.length > 1) {
|
|
2344
|
+
console.log();
|
|
2345
|
+
console.log(chalk6.cyan(`\u{1F4C1} Found ${discoveredFiles.length} AGENTS.md files:`));
|
|
2346
|
+
console.log();
|
|
2347
|
+
for (const f of discoveredFiles) {
|
|
2348
|
+
const icon = f.isRoot ? "\u{1F4C4}" : " \u2514\u2500";
|
|
2349
|
+
console.log(chalk6.gray(` ${icon} ${f.path}`));
|
|
2390
2350
|
}
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
const composeRes = await fetch(`https://${host}/api/v4/projects/${encodedPath}/repository/files/${encodeURIComponent(dockerComposeFile)}/raw?ref=HEAD`, {
|
|
2400
|
-
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
2351
|
+
console.log();
|
|
2352
|
+
let shouldCreateHierarchy = options.yes;
|
|
2353
|
+
if (!options.yes) {
|
|
2354
|
+
const { createHierarchy } = await prompts2({
|
|
2355
|
+
type: "confirm",
|
|
2356
|
+
name: "createHierarchy",
|
|
2357
|
+
message: `Create a hierarchy with all ${discoveredFiles.length} AGENTS.md files?`,
|
|
2358
|
+
initial: true
|
|
2401
2359
|
});
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
const lowerContent = content.toLowerCase();
|
|
2406
|
-
if (content.includes("registry.gitlab.com")) detected.containerRegistry = "gitlab_registry";
|
|
2407
|
-
else if (content.includes("ghcr.io")) detected.containerRegistry = "ghcr";
|
|
2408
|
-
else if (content.includes("docker.io") || /image:\s*[a-z0-9]+\/[a-z0-9]/.test(content)) detected.containerRegistry = "dockerhub";
|
|
2409
|
-
else if (content.includes("gcr.io")) detected.containerRegistry = "gcr";
|
|
2410
|
-
if (lowerContent.includes("postgres")) detected.databases.push("postgresql");
|
|
2411
|
-
if (lowerContent.includes("mysql") && !lowerContent.includes("mysql-")) detected.databases.push("mysql");
|
|
2412
|
-
if (lowerContent.includes("mongo")) detected.databases.push("mongodb");
|
|
2413
|
-
if (lowerContent.includes("redis")) detected.databases.push("redis");
|
|
2414
|
-
if (lowerContent.includes("sqlite")) detected.databases.push("sqlite");
|
|
2415
|
-
} catch {
|
|
2416
|
-
}
|
|
2417
|
-
}
|
|
2360
|
+
shouldCreateHierarchy = createHierarchy;
|
|
2361
|
+
} else {
|
|
2362
|
+
console.log(chalk6.cyan(`Auto-creating hierarchy with ${discoveredFiles.length} files...`));
|
|
2418
2363
|
}
|
|
2364
|
+
if (shouldCreateHierarchy) {
|
|
2365
|
+
await pushHierarchy(cwd, discoveredFiles, options);
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2368
|
+
console.log(chalk6.gray("Proceeding with single file push..."));
|
|
2419
2369
|
}
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
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
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2370
|
+
}
|
|
2371
|
+
const inferredType = inferBlueprintType(file);
|
|
2372
|
+
const COMMAND_TYPES = [
|
|
2373
|
+
"CURSOR_COMMAND",
|
|
2374
|
+
"CLAUDE_COMMAND",
|
|
2375
|
+
"WINDSURF_WORKFLOW",
|
|
2376
|
+
"COPILOT_PROMPT",
|
|
2377
|
+
"CONTINUE_PROMPT",
|
|
2378
|
+
"OPENCODE_COMMAND"
|
|
2379
|
+
];
|
|
2380
|
+
const isCommandFile = COMMAND_TYPES.includes(inferredType);
|
|
2381
|
+
const commandNames = {
|
|
2382
|
+
"CURSOR_COMMAND": "Cursor",
|
|
2383
|
+
"CLAUDE_COMMAND": "Claude Code",
|
|
2384
|
+
"WINDSURF_WORKFLOW": "Windsurf",
|
|
2385
|
+
"COPILOT_PROMPT": "Copilot",
|
|
2386
|
+
"CONTINUE_PROMPT": "Continue",
|
|
2387
|
+
"OPENCODE_COMMAND": "OpenCode"
|
|
2388
|
+
};
|
|
2389
|
+
const typeLabel = isCommandFile ? chalk6.magenta(`[${commandNames[inferredType] || "Command"} Command]`) : "";
|
|
2390
|
+
console.log(chalk6.cyan("\n\u{1F4E4} Push new blueprint"));
|
|
2391
|
+
console.log(chalk6.gray(` File: ${file}`));
|
|
2392
|
+
if (isCommandFile) {
|
|
2393
|
+
console.log(chalk6.gray(` Type: ${typeLabel}`));
|
|
2394
|
+
}
|
|
2395
|
+
let name = options.name;
|
|
2396
|
+
let description = options.description;
|
|
2397
|
+
let visibility = options.visibility || "PRIVATE";
|
|
2398
|
+
let tags = options.tags ? options.tags.split(",").map((t) => t.trim()) : [];
|
|
2399
|
+
if (!options.yes) {
|
|
2400
|
+
const responses = await prompts2([
|
|
2401
|
+
{
|
|
2402
|
+
type: name ? null : "text",
|
|
2403
|
+
name: "name",
|
|
2404
|
+
message: "Blueprint name:",
|
|
2405
|
+
initial: filename.replace(/\.(md|mdc|json|yml|yaml)$/, ""),
|
|
2406
|
+
validate: (v) => v.length > 0 || "Name is required"
|
|
2407
|
+
},
|
|
2408
|
+
{
|
|
2409
|
+
type: description ? null : "text",
|
|
2410
|
+
name: "description",
|
|
2411
|
+
message: "Description:",
|
|
2412
|
+
initial: ""
|
|
2413
|
+
},
|
|
2414
|
+
{
|
|
2415
|
+
type: "select",
|
|
2416
|
+
name: "visibility",
|
|
2417
|
+
message: "Visibility:",
|
|
2418
|
+
choices: [
|
|
2419
|
+
{ title: "Private (only you)", value: "PRIVATE" },
|
|
2420
|
+
{ title: "Team (your team members)", value: "TEAM" },
|
|
2421
|
+
{ title: "Public (visible to everyone)", value: "PUBLIC" }
|
|
2422
|
+
],
|
|
2423
|
+
initial: 0
|
|
2424
|
+
},
|
|
2425
|
+
{
|
|
2426
|
+
type: "text",
|
|
2427
|
+
name: "tags",
|
|
2428
|
+
message: "Tags (comma-separated):",
|
|
2429
|
+
initial: ""
|
|
2463
2430
|
}
|
|
2431
|
+
]);
|
|
2432
|
+
if (!responses.name && !name) {
|
|
2433
|
+
console.log(chalk6.yellow("Push cancelled."));
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
name = name || responses.name;
|
|
2437
|
+
description = description || responses.description || "";
|
|
2438
|
+
visibility = responses.visibility || visibility;
|
|
2439
|
+
tags = responses.tags ? responses.tags.split(",").map((t) => t.trim()).filter(Boolean) : tags;
|
|
2440
|
+
}
|
|
2441
|
+
if (!name) {
|
|
2442
|
+
name = filename.replace(/\.(md|mdc|json|yml|yaml)$/, "");
|
|
2443
|
+
}
|
|
2444
|
+
const hierarchyInfo = await detectHierarchyInfo(cwd, file);
|
|
2445
|
+
let hierarchyId = null;
|
|
2446
|
+
if (hierarchyInfo.repositoryPath) {
|
|
2447
|
+
hierarchyId = await ensureHierarchy(cwd, hierarchyInfo.repositoryRoot, path.basename(cwd));
|
|
2448
|
+
}
|
|
2449
|
+
const spinner = ora5("Creating blueprint...").start();
|
|
2450
|
+
try {
|
|
2451
|
+
const result = await api.createBlueprint({
|
|
2452
|
+
name,
|
|
2453
|
+
description: description || "",
|
|
2454
|
+
content,
|
|
2455
|
+
visibility,
|
|
2456
|
+
tags,
|
|
2457
|
+
type: inferredType,
|
|
2458
|
+
// Include the inferred type (AGENTS_MD, CURSOR_COMMAND, etc.)
|
|
2459
|
+
// Include hierarchy info if detected
|
|
2460
|
+
hierarchy_id: hierarchyId,
|
|
2461
|
+
parent_id: hierarchyInfo.parentId,
|
|
2462
|
+
repository_path: hierarchyInfo.repositoryPath
|
|
2463
|
+
});
|
|
2464
|
+
spinner.succeed("Blueprint created!");
|
|
2465
|
+
await trackBlueprint(cwd, {
|
|
2466
|
+
id: result.blueprint.id,
|
|
2467
|
+
name: result.blueprint.name,
|
|
2468
|
+
file,
|
|
2469
|
+
content,
|
|
2470
|
+
source: "private",
|
|
2471
|
+
hierarchyId: hierarchyId || void 0,
|
|
2472
|
+
repositoryPath: hierarchyInfo.repositoryPath || void 0
|
|
2473
|
+
});
|
|
2474
|
+
console.log();
|
|
2475
|
+
console.log(chalk6.green(`\u2705 Created blueprint ${chalk6.bold(result.blueprint.name)}`));
|
|
2476
|
+
console.log(chalk6.gray(` ID: ${result.blueprint.id}`));
|
|
2477
|
+
console.log(chalk6.gray(` Visibility: ${visibility}`));
|
|
2478
|
+
if (hierarchyInfo.repositoryPath) {
|
|
2479
|
+
console.log(chalk6.gray(` Path: ${hierarchyInfo.repositoryPath}`));
|
|
2464
2480
|
}
|
|
2465
|
-
if (
|
|
2466
|
-
|
|
2467
|
-
const pyRes = await fetch(`https://${host}/api/v4/projects/${encodedPath}/repository/files/pyproject.toml/raw?ref=HEAD`, {
|
|
2468
|
-
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
2469
|
-
});
|
|
2470
|
-
if (pyRes.ok) {
|
|
2471
|
-
try {
|
|
2472
|
-
const content = (await pyRes.text()).toLowerCase();
|
|
2473
|
-
if (content.includes("fastapi")) detected.stack.push("fastapi");
|
|
2474
|
-
if (content.includes("django")) detected.stack.push("django");
|
|
2475
|
-
if (content.includes("flask")) detected.stack.push("flask");
|
|
2476
|
-
if (content.includes("sqlalchemy")) detected.stack.push("sqlalchemy");
|
|
2477
|
-
if (content.includes("pytest")) detected.testFramework = "pytest";
|
|
2478
|
-
if (content.includes("asyncpg") || content.includes("psycopg")) {
|
|
2479
|
-
if (!detected.databases.includes("postgresql")) detected.databases.push("postgresql");
|
|
2480
|
-
}
|
|
2481
|
-
if (content.includes("aiosqlite") || content.includes("sqlite")) {
|
|
2482
|
-
if (!detected.databases.includes("sqlite")) detected.databases.push("sqlite");
|
|
2483
|
-
}
|
|
2484
|
-
} catch {
|
|
2485
|
-
}
|
|
2486
|
-
}
|
|
2487
|
-
} else if (fileNames.has("requirements.txt")) {
|
|
2488
|
-
detected.stack.push("python");
|
|
2481
|
+
if (result.blueprint.hierarchy_id) {
|
|
2482
|
+
console.log(chalk6.gray(` Hierarchy: ${result.blueprint.hierarchy_id}`));
|
|
2489
2483
|
}
|
|
2490
|
-
if (
|
|
2491
|
-
|
|
2492
|
-
detected.commands.build = "cargo build";
|
|
2493
|
-
detected.commands.test = "cargo test";
|
|
2484
|
+
if (hierarchyInfo.parentId) {
|
|
2485
|
+
console.log(chalk6.cyan(` \u21B3 Linked to parent blueprint: ${hierarchyInfo.parentId}`));
|
|
2494
2486
|
}
|
|
2495
|
-
if (
|
|
2496
|
-
|
|
2497
|
-
detected.commands.build = "go build";
|
|
2498
|
-
detected.commands.test = "go test ./...";
|
|
2487
|
+
if (visibility === "PUBLIC") {
|
|
2488
|
+
console.log(chalk6.gray(` View: https://lynxprompt.com/templates/${result.blueprint.id.replace("bp_", "")}`));
|
|
2499
2489
|
}
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
function isValidGitUrl(url) {
|
|
2506
|
-
const trimmed = url.trim();
|
|
2507
|
-
if (trimmed.startsWith("https://") || trimmed.startsWith("http://") || trimmed.startsWith("git://") || trimmed.startsWith("git@") || trimmed.startsWith("ssh://")) {
|
|
2508
|
-
const dangerousChars = /[;&|`$(){}[\]<>\\'"!#*?~]/;
|
|
2509
|
-
return !dangerousChars.test(trimmed);
|
|
2490
|
+
console.log();
|
|
2491
|
+
console.log(chalk6.cyan("The file is now linked. Future 'lynxp push' will update this blueprint."));
|
|
2492
|
+
} catch (error) {
|
|
2493
|
+
spinner.fail("Failed to create blueprint");
|
|
2494
|
+
handleError(error);
|
|
2510
2495
|
}
|
|
2511
|
-
return false;
|
|
2512
2496
|
}
|
|
2513
|
-
async function
|
|
2514
|
-
let
|
|
2515
|
-
|
|
2516
|
-
|
|
2497
|
+
async function pushHierarchy(cwd, files, options) {
|
|
2498
|
+
let hierarchyName = options.name || path.basename(cwd);
|
|
2499
|
+
let visibility = options.visibility || "PRIVATE";
|
|
2500
|
+
if (!options.yes) {
|
|
2501
|
+
const responses = await prompts2([
|
|
2502
|
+
{
|
|
2503
|
+
type: "text",
|
|
2504
|
+
name: "name",
|
|
2505
|
+
message: "Hierarchy name:",
|
|
2506
|
+
initial: hierarchyName,
|
|
2507
|
+
validate: (v) => v.length > 0 || "Name is required"
|
|
2508
|
+
},
|
|
2509
|
+
{
|
|
2510
|
+
type: "select",
|
|
2511
|
+
name: "visibility",
|
|
2512
|
+
message: "Visibility for all blueprints:",
|
|
2513
|
+
choices: [
|
|
2514
|
+
{ title: "Private (only you)", value: "PRIVATE" },
|
|
2515
|
+
{ title: "Team (your team members)", value: "TEAM" },
|
|
2516
|
+
{ title: "Public (visible to everyone)", value: "PUBLIC" }
|
|
2517
|
+
],
|
|
2518
|
+
initial: 0
|
|
2519
|
+
}
|
|
2520
|
+
]);
|
|
2521
|
+
if (!responses.name) {
|
|
2522
|
+
console.log(chalk6.yellow("Push cancelled."));
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2525
|
+
hierarchyName = responses.name;
|
|
2526
|
+
visibility = responses.visibility || visibility;
|
|
2517
2527
|
}
|
|
2528
|
+
console.log();
|
|
2529
|
+
console.log(chalk6.cyan(`\u{1F4C1} Creating hierarchy "${hierarchyName}" with ${files.length} files...`));
|
|
2530
|
+
console.log();
|
|
2531
|
+
const repositoryRoot = createRepositoryRoot(cwd);
|
|
2532
|
+
let hierarchyId;
|
|
2518
2533
|
try {
|
|
2519
|
-
|
|
2534
|
+
const hierarchyResponse = await api.createHierarchy({
|
|
2535
|
+
name: hierarchyName,
|
|
2536
|
+
repository_root: repositoryRoot
|
|
2537
|
+
});
|
|
2538
|
+
hierarchyId = hierarchyResponse.hierarchy.id;
|
|
2539
|
+
console.log(chalk6.green(`\u2713 Created hierarchy: ${hierarchyId}`));
|
|
2540
|
+
} catch (error) {
|
|
2541
|
+
console.log(chalk6.red("Failed to create hierarchy"));
|
|
2542
|
+
handleError(error);
|
|
2543
|
+
return;
|
|
2544
|
+
}
|
|
2545
|
+
let rootBlueprintId = null;
|
|
2546
|
+
let successCount = 0;
|
|
2547
|
+
let failCount = 0;
|
|
2548
|
+
for (const file of files) {
|
|
2549
|
+
const spinner = ora5(`Uploading ${file.path}...`).start();
|
|
2520
2550
|
try {
|
|
2521
|
-
const
|
|
2522
|
-
|
|
2523
|
-
|
|
2551
|
+
const content = fs.readFileSync(file.absolutePath, "utf-8");
|
|
2552
|
+
let blueprintName;
|
|
2553
|
+
if (file.isRoot) {
|
|
2554
|
+
blueprintName = hierarchyName;
|
|
2555
|
+
} else {
|
|
2556
|
+
const dirname5 = path.dirname(file.path);
|
|
2557
|
+
blueprintName = dirname5.replace(/[/\\]/g, " / ");
|
|
2558
|
+
}
|
|
2559
|
+
const result = await api.createBlueprint({
|
|
2560
|
+
name: blueprintName,
|
|
2561
|
+
description: "",
|
|
2562
|
+
content,
|
|
2563
|
+
visibility,
|
|
2564
|
+
tags: [],
|
|
2565
|
+
hierarchy_id: hierarchyId,
|
|
2566
|
+
parent_id: file.isRoot ? null : rootBlueprintId,
|
|
2567
|
+
repository_path: file.path
|
|
2524
2568
|
});
|
|
2525
|
-
if (
|
|
2526
|
-
|
|
2569
|
+
if (file.isRoot) {
|
|
2570
|
+
rootBlueprintId = result.blueprint.id;
|
|
2527
2571
|
}
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2572
|
+
await trackBlueprint(cwd, {
|
|
2573
|
+
id: result.blueprint.id,
|
|
2574
|
+
name: blueprintName,
|
|
2575
|
+
file: file.path,
|
|
2576
|
+
content,
|
|
2577
|
+
source: "private",
|
|
2578
|
+
hierarchyId,
|
|
2579
|
+
hierarchyName,
|
|
2580
|
+
repositoryPath: file.path
|
|
2581
|
+
});
|
|
2582
|
+
spinner.succeed(`${file.path} \u2192 ${result.blueprint.id}`);
|
|
2583
|
+
successCount++;
|
|
2584
|
+
} catch (error) {
|
|
2585
|
+
spinner.fail(`${file.path} failed`);
|
|
2586
|
+
if (error instanceof ApiRequestError) {
|
|
2587
|
+
console.log(chalk6.red(` Error: ${error.message}`));
|
|
2544
2588
|
}
|
|
2589
|
+
failCount++;
|
|
2545
2590
|
}
|
|
2546
2591
|
}
|
|
2592
|
+
console.log();
|
|
2593
|
+
console.log(chalk6.green(`\u2705 Hierarchy created successfully!`));
|
|
2594
|
+
console.log(chalk6.gray(` Hierarchy: ${hierarchyId}`));
|
|
2595
|
+
console.log(chalk6.gray(` Name: ${hierarchyName}`));
|
|
2596
|
+
console.log(chalk6.gray(` Blueprints: ${successCount} uploaded${failCount > 0 ? `, ${failCount} failed` : ""}`));
|
|
2597
|
+
console.log();
|
|
2598
|
+
console.log(chalk6.cyan("Tips:"));
|
|
2599
|
+
console.log(chalk6.gray(` \u2022 Run 'lynxp status' to see all tracked blueprints`));
|
|
2600
|
+
console.log(chalk6.gray(` \u2022 Run 'lynxp pull ${hierarchyId}' to download the entire hierarchy`));
|
|
2601
|
+
console.log(chalk6.gray(` \u2022 Run 'lynxp push' in any subfolder to update individual blueprints`));
|
|
2602
|
+
console.log();
|
|
2547
2603
|
}
|
|
2548
|
-
|
|
2549
|
-
const
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2604
|
+
function findDefaultFile() {
|
|
2605
|
+
const candidates = [
|
|
2606
|
+
"AGENTS.md",
|
|
2607
|
+
"CLAUDE.md",
|
|
2608
|
+
".cursor/rules/project.mdc",
|
|
2609
|
+
".github/copilot-instructions.md",
|
|
2610
|
+
".windsurfrules",
|
|
2611
|
+
"AIDER.md",
|
|
2612
|
+
"GEMINI.md",
|
|
2613
|
+
".clinerules"
|
|
2614
|
+
];
|
|
2615
|
+
for (const candidate of candidates) {
|
|
2616
|
+
if (fs.existsSync(candidate)) {
|
|
2617
|
+
return candidate;
|
|
2618
|
+
}
|
|
2557
2619
|
}
|
|
2558
|
-
return
|
|
2620
|
+
return null;
|
|
2559
2621
|
}
|
|
2560
|
-
function
|
|
2561
|
-
const
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2622
|
+
function inferBlueprintType(filePath) {
|
|
2623
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
2624
|
+
const commandInfo = inferCommandTypeFromPath(filePath);
|
|
2625
|
+
if (commandInfo) {
|
|
2626
|
+
return commandInfo.templateType;
|
|
2627
|
+
}
|
|
2628
|
+
if (normalizedPath.includes(".cursor/rules/")) return "CURSOR_RULES";
|
|
2629
|
+
if (normalizedPath.endsWith("CLAUDE.md")) return "CLAUDE_MD";
|
|
2630
|
+
if (normalizedPath.endsWith(".windsurfrules")) return "WINDSURF_RULES";
|
|
2631
|
+
if (normalizedPath.endsWith(".clinerules")) return "CLINE_RULES";
|
|
2632
|
+
if (normalizedPath.includes(".github/copilot-instructions.md")) return "COPILOT_INSTRUCTIONS";
|
|
2633
|
+
if (normalizedPath.endsWith("GEMINI.md")) return "GEMINI_MD";
|
|
2634
|
+
if (normalizedPath.endsWith("AIDER.md")) return "AGENTS_MD";
|
|
2635
|
+
if (normalizedPath.endsWith("AGENTS.md")) return "AGENTS_MD";
|
|
2636
|
+
if (normalizedPath.endsWith(".md")) return "AGENTS_MD";
|
|
2637
|
+
return "CUSTOM";
|
|
2638
|
+
}
|
|
2639
|
+
function handleError(error) {
|
|
2640
|
+
if (error instanceof ApiRequestError) {
|
|
2641
|
+
console.error(chalk6.red(`Error: ${error.message}`));
|
|
2642
|
+
if (error.statusCode === 401) {
|
|
2643
|
+
console.error(chalk6.gray("Your session may have expired. Run 'lynxp login' to re-authenticate."));
|
|
2644
|
+
} else if (error.statusCode === 403) {
|
|
2645
|
+
console.error(chalk6.gray("You don't have permission to modify this blueprint."));
|
|
2646
|
+
} else if (error.statusCode === 404) {
|
|
2647
|
+
console.error(chalk6.gray("Blueprint not found. It may have been deleted."));
|
|
2648
|
+
}
|
|
2649
|
+
} else {
|
|
2650
|
+
console.error(chalk6.red("An unexpected error occurred."));
|
|
2651
|
+
}
|
|
2652
|
+
process.exit(1);
|
|
2568
2653
|
}
|
|
2569
2654
|
|
|
2655
|
+
// src/commands/wizard.ts
|
|
2656
|
+
import chalk7 from "chalk";
|
|
2657
|
+
import prompts3 from "prompts";
|
|
2658
|
+
import ora6 from "ora";
|
|
2659
|
+
import * as readline from "readline";
|
|
2660
|
+
import * as os from "os";
|
|
2661
|
+
import { writeFile as writeFile3, mkdir as mkdir3, access as access3, readFile as readFile4 } from "fs/promises";
|
|
2662
|
+
import { join as join4, dirname as dirname3 } from "path";
|
|
2663
|
+
|
|
2570
2664
|
// src/utils/generator.ts
|
|
2571
2665
|
function bpVar(blueprintMode, varName, defaultValue) {
|
|
2572
2666
|
if (!blueprintMode || !defaultValue) return defaultValue;
|
|
@@ -3931,7 +4025,7 @@ function generateYamlConfig(options, platform2) {
|
|
|
3931
4025
|
|
|
3932
4026
|
// src/commands/wizard.ts
|
|
3933
4027
|
var DRAFTS_DIR = ".lynxprompt/drafts";
|
|
3934
|
-
var CLI_VERSION = "1.
|
|
4028
|
+
var CLI_VERSION = "1.4.0";
|
|
3935
4029
|
async function saveDraftLocally(name, config2, stepReached) {
|
|
3936
4030
|
const draftsPath = join4(process.cwd(), DRAFTS_DIR);
|
|
3937
4031
|
await mkdir3(draftsPath, { recursive: true });
|
|
@@ -6634,9 +6728,9 @@ async function runInteractiveWizard(options, detected, userTier) {
|
|
|
6634
6728
|
}, promptConfig);
|
|
6635
6729
|
answers.selfImprove = selfImproveResponse.selfImprove || false;
|
|
6636
6730
|
console.log();
|
|
6637
|
-
console.log(chalk7.gray(" \u2601\uFE0F Store your config on LynxPrompt cloud for
|
|
6731
|
+
console.log(chalk7.gray(" \u2601\uFE0F Store your config on LynxPrompt cloud for syncing and version control."));
|
|
6638
6732
|
console.log(chalk7.gray(" Benefits:"));
|
|
6639
|
-
console.log(chalk7.gray(" \u2022
|
|
6733
|
+
console.log(chalk7.gray(" \u2022 Sync configs across all your devices"));
|
|
6640
6734
|
console.log(chalk7.gray(" \u2022 Track changes and rollback if needed"));
|
|
6641
6735
|
console.log(chalk7.gray(" \u2022 Instructions added to config so AI can sync automatically"));
|
|
6642
6736
|
const enableAutoUpdateResponse = await prompts3({
|
|
@@ -8562,7 +8656,14 @@ var TARGET_FILES = {
|
|
|
8562
8656
|
aider: ".aider.conf.yml",
|
|
8563
8657
|
codex: "codex.md",
|
|
8564
8658
|
supermaven: "supermaven.md",
|
|
8565
|
-
goose: ".goose/rules.txt"
|
|
8659
|
+
goose: ".goose/rules.txt",
|
|
8660
|
+
// Command targets - all plain markdown, no conversion needed
|
|
8661
|
+
"cursor-command": ".cursor/commands/command.md",
|
|
8662
|
+
"claude-command": ".claude/commands/command.md",
|
|
8663
|
+
"windsurf-workflow": ".windsurf/workflows/workflow.md",
|
|
8664
|
+
"copilot-prompt": ".copilot/prompts/prompt.md",
|
|
8665
|
+
"continue-prompt": ".continue/prompts/prompt.md",
|
|
8666
|
+
"opencode-command": ".opencode/commands/command.md"
|
|
8566
8667
|
};
|
|
8567
8668
|
var PLATFORM_NAMES = {
|
|
8568
8669
|
agents: "AGENTS.md (Universal)",
|
|
@@ -8575,8 +8676,24 @@ var PLATFORM_NAMES = {
|
|
|
8575
8676
|
aider: "Aider Config",
|
|
8576
8677
|
codex: "Codex",
|
|
8577
8678
|
supermaven: "Supermaven",
|
|
8578
|
-
goose: "Goose Rules"
|
|
8679
|
+
goose: "Goose Rules",
|
|
8680
|
+
// Commands
|
|
8681
|
+
"cursor-command": "Cursor Command",
|
|
8682
|
+
"claude-command": "Claude Code Command",
|
|
8683
|
+
"windsurf-workflow": "Windsurf Workflow",
|
|
8684
|
+
"copilot-prompt": "Copilot Prompt",
|
|
8685
|
+
"continue-prompt": "Continue Prompt",
|
|
8686
|
+
"opencode-command": "OpenCode Command"
|
|
8579
8687
|
};
|
|
8688
|
+
var COMMAND_DIRS = {
|
|
8689
|
+
".cursor/commands": "cursor-command",
|
|
8690
|
+
".claude/commands": "claude-command",
|
|
8691
|
+
".windsurf/workflows": "windsurf-workflow",
|
|
8692
|
+
".copilot/prompts": "copilot-prompt",
|
|
8693
|
+
".continue/prompts": "continue-prompt",
|
|
8694
|
+
".opencode/commands": "opencode-command"
|
|
8695
|
+
};
|
|
8696
|
+
var isCommandTarget = (target) => target.includes("-command") || target.includes("-prompt") || target.includes("-workflow");
|
|
8580
8697
|
async function detectSourceFile(cwd) {
|
|
8581
8698
|
for (const [pattern, platform2] of Object.entries(SOURCE_FILES)) {
|
|
8582
8699
|
try {
|
|
@@ -8598,6 +8715,15 @@ async function detectSourceFile(cwd) {
|
|
|
8598
8715
|
}
|
|
8599
8716
|
return null;
|
|
8600
8717
|
}
|
|
8718
|
+
function detectCommandPlatform(sourcePath) {
|
|
8719
|
+
const normalized = sourcePath.replace(/\\/g, "/").toLowerCase();
|
|
8720
|
+
for (const [dir, platform2] of Object.entries(COMMAND_DIRS)) {
|
|
8721
|
+
if (normalized.includes(dir)) {
|
|
8722
|
+
return platform2;
|
|
8723
|
+
}
|
|
8724
|
+
}
|
|
8725
|
+
return null;
|
|
8726
|
+
}
|
|
8601
8727
|
function parseMarkdownConfig(content) {
|
|
8602
8728
|
const config2 = {};
|
|
8603
8729
|
const sections = content.split(/^##\s+/m);
|
|
@@ -8613,6 +8739,9 @@ function parseMarkdownConfig(content) {
|
|
|
8613
8739
|
}
|
|
8614
8740
|
function generateTargetContent(config2, targetPlatform) {
|
|
8615
8741
|
const rawContent = config2._raw || "";
|
|
8742
|
+
if (isCommandTarget(targetPlatform)) {
|
|
8743
|
+
return rawContent;
|
|
8744
|
+
}
|
|
8616
8745
|
switch (targetPlatform) {
|
|
8617
8746
|
case "cursor":
|
|
8618
8747
|
return `---
|
|
@@ -8663,8 +8792,13 @@ async function convertCommand(source, target, options) {
|
|
|
8663
8792
|
let sourcePlatform;
|
|
8664
8793
|
if (source) {
|
|
8665
8794
|
sourcePath = join9(cwd, source);
|
|
8666
|
-
const
|
|
8667
|
-
|
|
8795
|
+
const cmdPlatform = detectCommandPlatform(source);
|
|
8796
|
+
if (cmdPlatform) {
|
|
8797
|
+
sourcePlatform = cmdPlatform;
|
|
8798
|
+
} else {
|
|
8799
|
+
const sourceBasename = basename(source).toLowerCase();
|
|
8800
|
+
sourcePlatform = SOURCE_FILES[sourceBasename] || "unknown";
|
|
8801
|
+
}
|
|
8668
8802
|
} else {
|
|
8669
8803
|
const detected = await detectSourceFile(cwd);
|
|
8670
8804
|
if (!detected) {
|
|
@@ -8675,8 +8809,14 @@ async function convertCommand(source, target, options) {
|
|
|
8675
8809
|
console.log(chalk14.gray(` \u2022 ${file}`));
|
|
8676
8810
|
}
|
|
8677
8811
|
console.log();
|
|
8812
|
+
console.log(chalk14.gray(" Command directories (for slash commands):"));
|
|
8813
|
+
for (const dir of Object.keys(COMMAND_DIRS)) {
|
|
8814
|
+
console.log(chalk14.gray(` \u2022 ${dir}/*.md`));
|
|
8815
|
+
}
|
|
8816
|
+
console.log();
|
|
8678
8817
|
console.log(chalk14.gray(" Usage: lynxp convert <source> <target>"));
|
|
8679
8818
|
console.log(chalk14.gray(" Example: lynxp convert AGENTS.md cursor"));
|
|
8819
|
+
console.log(chalk14.gray(" Example: lynxp convert .cursor/commands/deploy.md claude-command"));
|
|
8680
8820
|
process.exit(1);
|
|
8681
8821
|
}
|
|
8682
8822
|
sourcePath = detected.path;
|
|
@@ -8706,7 +8846,12 @@ async function convertCommand(source, target, options) {
|
|
|
8706
8846
|
const config2 = parseMarkdownConfig(sourceContent);
|
|
8707
8847
|
const targetContent = generateTargetContent(config2, normalizedTarget);
|
|
8708
8848
|
spinner.stop();
|
|
8709
|
-
|
|
8849
|
+
let outputFilename = options.output || TARGET_FILES[normalizedTarget];
|
|
8850
|
+
if (isCommandTarget(normalizedTarget) && !options.output) {
|
|
8851
|
+
const originalFilename = basename(sourcePath);
|
|
8852
|
+
const targetDir = TARGET_FILES[normalizedTarget].split("/").slice(0, -1).join("/");
|
|
8853
|
+
outputFilename = `${targetDir}/${originalFilename}`;
|
|
8854
|
+
}
|
|
8710
8855
|
const outputPath = join9(cwd, outputFilename);
|
|
8711
8856
|
try {
|
|
8712
8857
|
await access5(outputPath);
|
|
@@ -9152,7 +9297,7 @@ function displayResults(result, options) {
|
|
|
9152
9297
|
async function importCommand(path2 = ".", options) {
|
|
9153
9298
|
console.log();
|
|
9154
9299
|
console.log(chalk16.cyan.bold(" \u{1F4E5} LynxPrompt Import"));
|
|
9155
|
-
console.log(chalk16.gray(" Scan and import AGENTS.md files from your repository"));
|
|
9300
|
+
console.log(chalk16.gray(" Scan and import AGENTS.md files and AI commands from your repository"));
|
|
9156
9301
|
console.log();
|
|
9157
9302
|
const rootPath = join11(process.cwd(), path2);
|
|
9158
9303
|
try {
|
|
@@ -9161,25 +9306,53 @@ async function importCommand(path2 = ".", options) {
|
|
|
9161
9306
|
console.log(chalk16.red(` \u2717 Path not found: ${rootPath}`));
|
|
9162
9307
|
process.exit(1);
|
|
9163
9308
|
}
|
|
9164
|
-
const spinner = ora14("Scanning for configuration files...").start();
|
|
9309
|
+
const spinner = ora14("Scanning for configuration files and commands...").start();
|
|
9165
9310
|
try {
|
|
9166
9311
|
const files = await scanDirectory(rootPath, options);
|
|
9167
|
-
|
|
9168
|
-
|
|
9312
|
+
const commands = await detectCommandFiles(rootPath);
|
|
9313
|
+
const totalFound = files.length + commands.length;
|
|
9314
|
+
if (totalFound === 0) {
|
|
9315
|
+
spinner.warn("No configuration files or commands found");
|
|
9169
9316
|
console.log();
|
|
9170
|
-
console.log(chalk16.gray(" Looking for:
|
|
9317
|
+
console.log(chalk16.gray(" Looking for:"));
|
|
9318
|
+
console.log(chalk16.gray(" \u2022 Rules: AGENTS.md, CLAUDE.md, .cursorrules, .windsurfrules"));
|
|
9319
|
+
console.log(chalk16.gray(" \u2022 Commands: .cursor/commands/*.md, .claude/commands/*.md"));
|
|
9171
9320
|
console.log(chalk16.gray(" Try specifying a different path or use --pattern for custom filenames"));
|
|
9172
9321
|
return;
|
|
9173
9322
|
}
|
|
9174
|
-
spinner.succeed(`Found ${files.length} configuration file(s)`);
|
|
9323
|
+
spinner.succeed(`Found ${files.length} configuration file(s) and ${commands.length} command(s)`);
|
|
9175
9324
|
const hierarchy = buildHierarchy(files, rootPath);
|
|
9176
9325
|
const result = {
|
|
9177
|
-
totalFound
|
|
9326
|
+
totalFound,
|
|
9178
9327
|
files,
|
|
9179
9328
|
hierarchy,
|
|
9180
9329
|
errors: []
|
|
9181
9330
|
};
|
|
9182
9331
|
displayResults(result, options);
|
|
9332
|
+
if (commands.length > 0) {
|
|
9333
|
+
console.log();
|
|
9334
|
+
console.log(chalk16.cyan.bold(" \u26A1 AI Agent Commands"));
|
|
9335
|
+
console.log();
|
|
9336
|
+
const cursorCommands = commands.filter((c) => c.type === "cursor-command");
|
|
9337
|
+
const claudeCommands = commands.filter((c) => c.type === "claude-command");
|
|
9338
|
+
if (cursorCommands.length > 0) {
|
|
9339
|
+
console.log(chalk16.blue(` Cursor Commands (${cursorCommands.length}):`));
|
|
9340
|
+
for (const cmd of cursorCommands) {
|
|
9341
|
+
console.log(chalk16.gray(` \u26A1 ${cmd.name}`));
|
|
9342
|
+
console.log(chalk16.gray(` ${relative(rootPath, cmd.path)}`));
|
|
9343
|
+
}
|
|
9344
|
+
console.log();
|
|
9345
|
+
}
|
|
9346
|
+
if (claudeCommands.length > 0) {
|
|
9347
|
+
console.log(chalk16.yellow(` Claude Commands (${claudeCommands.length}):`));
|
|
9348
|
+
for (const cmd of claudeCommands) {
|
|
9349
|
+
console.log(chalk16.gray(` \u{1F9E0} ${cmd.name}`));
|
|
9350
|
+
console.log(chalk16.gray(` ${relative(rootPath, cmd.path)}`));
|
|
9351
|
+
}
|
|
9352
|
+
console.log();
|
|
9353
|
+
}
|
|
9354
|
+
console.log(chalk16.cyan(" \u{1F4A1} Push commands with: lynxp push .cursor/commands/my-command.md"));
|
|
9355
|
+
}
|
|
9183
9356
|
if (options.dryRun) {
|
|
9184
9357
|
console.log(chalk16.yellow(" \u{1F4CB} Dry run complete - no changes made"));
|
|
9185
9358
|
console.log(chalk16.gray(" Remove --dry-run to proceed with import"));
|
|
@@ -9322,7 +9495,7 @@ function handleError2(error) {
|
|
|
9322
9495
|
}
|
|
9323
9496
|
|
|
9324
9497
|
// src/index.ts
|
|
9325
|
-
var CLI_VERSION2 = "1.
|
|
9498
|
+
var CLI_VERSION2 = "1.4.0";
|
|
9326
9499
|
var program = new Command();
|
|
9327
9500
|
program.name("lynxprompt").description("CLI for LynxPrompt - Generate AI IDE configuration files").version(CLI_VERSION2);
|
|
9328
9501
|
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);
|