lynxprompt 1.2.15 → 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 +1521 -1347
- 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
|
}
|
|
1831
|
+
if (content.includes("pymongo") || content.includes("motor")) {
|
|
1832
|
+
if (!detected.databases.includes("mongodb")) detected.databases.push("mongodb");
|
|
1833
|
+
}
|
|
1834
|
+
} catch {
|
|
1857
1835
|
}
|
|
1858
1836
|
}
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
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;
|
|
1837
|
+
}
|
|
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";
|
|
2072
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 {
|
|
2073
1886
|
}
|
|
2074
|
-
} catch {
|
|
2075
1887
|
}
|
|
2076
1888
|
}
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
async function fileExists(path2) {
|
|
2082
|
-
try {
|
|
2083
|
-
await access2(path2);
|
|
2084
|
-
return true;
|
|
2085
|
-
} catch {
|
|
2086
|
-
return false;
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
function detectRepoHost(url) {
|
|
2090
|
-
const lower = url.toLowerCase();
|
|
2091
|
-
if (lower.includes("github.com") || lower.includes("github:")) return "github";
|
|
2092
|
-
if (lower.includes("gitlab.com") || lower.includes("gitlab")) return "gitlab";
|
|
2093
|
-
if (lower.includes("bitbucket.org") || lower.includes("bitbucket:")) return "bitbucket";
|
|
2094
|
-
if (lower.includes("gitea.") || lower.includes("gitea:") || lower.includes("codeberg.org")) return "gitea";
|
|
2095
|
-
if (lower.includes("azure.com") || lower.includes("visualstudio.com") || lower.includes("dev.azure")) return "azure";
|
|
2096
|
-
return "other";
|
|
2097
|
-
}
|
|
2098
|
-
function parseGitHubUrl(url) {
|
|
2099
|
-
const patterns = [
|
|
2100
|
-
/github\.com[/:]([^/]+)\/([^/.]+)/,
|
|
2101
|
-
/^([^/]+)\/([^/]+)$/
|
|
2102
|
-
];
|
|
2103
|
-
for (const pattern of patterns) {
|
|
2104
|
-
const match = url.match(pattern);
|
|
2105
|
-
if (match) {
|
|
2106
|
-
return { owner: match[1], repo: match[2].replace(/\.git$/, "") };
|
|
1889
|
+
if (fileNames.has("cargo.toml")) {
|
|
1890
|
+
detected.stack.push("rust");
|
|
1891
|
+
detected.commands.build = "cargo build";
|
|
1892
|
+
detected.commands.test = "cargo test";
|
|
2107
1893
|
}
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
const patterns = [
|
|
2113
|
-
/^https?:\/\/([^/]+)\/(.+?)(?:\.git)?$/,
|
|
2114
|
-
/^git@([^:]+):(.+?)(?:\.git)?$/
|
|
2115
|
-
];
|
|
2116
|
-
for (const pattern of patterns) {
|
|
2117
|
-
const match = url.match(pattern);
|
|
2118
|
-
if (match) {
|
|
2119
|
-
const host = match[1];
|
|
2120
|
-
const path2 = match[2].replace(/\.git$/, "");
|
|
2121
|
-
if (host.includes("gitlab") || url.toLowerCase().includes("gitlab")) {
|
|
2122
|
-
return { path: path2, host };
|
|
2123
|
-
}
|
|
1894
|
+
if (fileNames.has("go.mod")) {
|
|
1895
|
+
detected.stack.push("go");
|
|
1896
|
+
detected.commands.build = "go build";
|
|
1897
|
+
detected.commands.test = "go test ./...";
|
|
2124
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,141 +1923,58 @@ 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
|
|
1974
|
+
if (fileNames.has("package.json")) {
|
|
1975
|
+
const pkgRes = await fetch(`https://${host}/api/v4/projects/${encodedPath}/repository/files/package.json/raw?ref=HEAD`, {
|
|
2213
1976
|
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
2214
1977
|
});
|
|
2215
|
-
if (ghFilesRes.ok) {
|
|
2216
|
-
const ghFiles = await ghFilesRes.json();
|
|
2217
|
-
if (ghFiles.some((f) => f.name === "workflows")) {
|
|
2218
|
-
detected.cicd = "github_actions";
|
|
2219
|
-
}
|
|
2220
|
-
}
|
|
2221
|
-
}
|
|
2222
|
-
if (fileNames.has(".gitlab-ci.yml")) detected.cicd = "gitlab_ci";
|
|
2223
|
-
if (fileNames.has("jenkinsfile")) detected.cicd = "jenkins";
|
|
2224
|
-
if (fileNames.has(".travis.yml")) detected.cicd = "travis";
|
|
2225
|
-
if (fileNames.has("azure-pipelines.yml")) detected.cicd = "azure_devops";
|
|
2226
|
-
if (fileNames.has("pyproject.toml")) {
|
|
2227
|
-
detected.stack.push("python");
|
|
2228
|
-
const pyprojectRes = await fetch(`https://raw.githubusercontent.com/${owner}/${repo}/HEAD/pyproject.toml`);
|
|
2229
|
-
if (pyprojectRes.ok) {
|
|
2230
|
-
try {
|
|
2231
|
-
const content = await pyprojectRes.text();
|
|
2232
|
-
const lowerContent = content.toLowerCase();
|
|
2233
|
-
if (lowerContent.includes("fastapi")) detected.stack.push("fastapi");
|
|
2234
|
-
if (lowerContent.includes("django")) detected.stack.push("django");
|
|
2235
|
-
if (lowerContent.includes("flask")) detected.stack.push("flask");
|
|
2236
|
-
if (lowerContent.includes("sqlalchemy")) detected.stack.push("sqlalchemy");
|
|
2237
|
-
if (lowerContent.includes("pydantic")) detected.stack.push("pydantic");
|
|
2238
|
-
if (lowerContent.includes("pytest")) detected.testFramework = "pytest";
|
|
2239
|
-
else if (lowerContent.includes("unittest")) detected.testFramework = "unittest";
|
|
2240
|
-
detected.commands.test = "pytest";
|
|
2241
|
-
if (lowerContent.includes("ruff")) detected.commands.lint = "ruff check .";
|
|
2242
|
-
if (lowerContent.includes("asyncpg") || lowerContent.includes("psycopg")) {
|
|
2243
|
-
if (!detected.databases.includes("postgresql")) detected.databases.push("postgresql");
|
|
2244
|
-
}
|
|
2245
|
-
if (lowerContent.includes("aiosqlite") || lowerContent.includes("sqlite")) {
|
|
2246
|
-
if (!detected.databases.includes("sqlite")) detected.databases.push("sqlite");
|
|
2247
|
-
}
|
|
2248
|
-
if (lowerContent.includes("pymongo") || lowerContent.includes("motor")) {
|
|
2249
|
-
if (!detected.databases.includes("mongodb")) detected.databases.push("mongodb");
|
|
2250
|
-
}
|
|
2251
|
-
if (lowerContent.includes("redis") || lowerContent.includes("aioredis")) {
|
|
2252
|
-
if (!detected.databases.includes("redis")) detected.databases.push("redis");
|
|
2253
|
-
}
|
|
2254
|
-
if (lowerContent.includes("pymysql") || lowerContent.includes("aiomysql")) {
|
|
2255
|
-
if (!detected.databases.includes("mysql")) detected.databases.push("mysql");
|
|
2256
|
-
}
|
|
2257
|
-
} catch {
|
|
2258
|
-
}
|
|
2259
|
-
}
|
|
2260
|
-
}
|
|
2261
|
-
if (fileNames.has("requirements.txt")) {
|
|
2262
|
-
const reqRes = await fetch(`https://raw.githubusercontent.com/${owner}/${repo}/HEAD/requirements.txt`);
|
|
2263
|
-
if (reqRes.ok) {
|
|
2264
|
-
try {
|
|
2265
|
-
const content = (await reqRes.text()).toLowerCase();
|
|
2266
|
-
if (!detected.stack.includes("python")) detected.stack.push("python");
|
|
2267
|
-
if (content.includes("fastapi") && !detected.stack.includes("fastapi")) detected.stack.push("fastapi");
|
|
2268
|
-
if (content.includes("django") && !detected.stack.includes("django")) detected.stack.push("django");
|
|
2269
|
-
if (content.includes("flask") && !detected.stack.includes("flask")) detected.stack.push("flask");
|
|
2270
|
-
if (content.includes("sqlalchemy") && !detected.stack.includes("sqlalchemy")) detected.stack.push("sqlalchemy");
|
|
2271
|
-
if (content.includes("asyncpg") || content.includes("psycopg")) {
|
|
2272
|
-
if (!detected.databases.includes("postgresql")) detected.databases.push("postgresql");
|
|
2273
|
-
}
|
|
2274
|
-
if (content.includes("aiosqlite") || content.includes("sqlite")) {
|
|
2275
|
-
if (!detected.databases.includes("sqlite")) detected.databases.push("sqlite");
|
|
2276
|
-
}
|
|
2277
|
-
if (content.includes("pymongo") || content.includes("motor")) {
|
|
2278
|
-
if (!detected.databases.includes("mongodb")) detected.databases.push("mongodb");
|
|
2279
|
-
}
|
|
2280
|
-
} catch {
|
|
2281
|
-
}
|
|
2282
|
-
}
|
|
2283
|
-
}
|
|
2284
|
-
if (fileNames.has("package.json")) {
|
|
2285
|
-
const pkgRes = await fetch(`https://raw.githubusercontent.com/${owner}/${repo}/HEAD/package.json`);
|
|
2286
1978
|
if (pkgRes.ok) {
|
|
2287
1979
|
try {
|
|
2288
1980
|
const pkg = await pkgRes.json();
|
|
@@ -2293,16 +1985,12 @@ async function detectFromGitHubApi(repoUrl) {
|
|
|
2293
1985
|
if (allDeps["svelte"]) detected.stack.push("svelte");
|
|
2294
1986
|
if (allDeps["express"]) detected.stack.push("express");
|
|
2295
1987
|
if (allDeps["fastify"]) detected.stack.push("fastify");
|
|
2296
|
-
if (allDeps["hono"]) detected.stack.push("hono");
|
|
2297
1988
|
if (allDeps["typescript"]) detected.stack.push("typescript");
|
|
2298
1989
|
if (allDeps["tailwindcss"]) detected.stack.push("tailwind");
|
|
2299
1990
|
if (allDeps["prisma"]) detected.stack.push("prisma");
|
|
2300
|
-
if (allDeps["drizzle-orm"]) detected.stack.push("drizzle");
|
|
2301
1991
|
if (allDeps["vitest"]) detected.testFramework = "vitest";
|
|
2302
1992
|
else if (allDeps["jest"]) detected.testFramework = "jest";
|
|
2303
1993
|
else if (allDeps["@playwright/test"]) detected.testFramework = "playwright";
|
|
2304
|
-
else if (allDeps["cypress"]) detected.testFramework = "cypress";
|
|
2305
|
-
else if (allDeps["mocha"]) detected.testFramework = "mocha";
|
|
2306
1994
|
if (detected.stack.length === 0 || detected.stack.length === 1 && detected.stack[0] === "typescript") {
|
|
2307
1995
|
detected.stack.unshift("javascript");
|
|
2308
1996
|
}
|
|
@@ -2311,262 +1999,668 @@ async function detectFromGitHubApi(repoUrl) {
|
|
|
2311
1999
|
if (pkg.scripts.test) detected.commands.test = "npm run test";
|
|
2312
2000
|
if (pkg.scripts.lint) detected.commands.lint = "npm run lint";
|
|
2313
2001
|
if (pkg.scripts.dev) detected.commands.dev = "npm run dev";
|
|
2314
|
-
else if (pkg.scripts.start) detected.commands.dev = "npm run start";
|
|
2315
2002
|
}
|
|
2316
|
-
if (allDeps["pg"] || allDeps["postgres"]
|
|
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")) {
|
|
2317
2033
|
if (!detected.databases.includes("postgresql")) detected.databases.push("postgresql");
|
|
2318
2034
|
}
|
|
2319
|
-
if (
|
|
2035
|
+
if (content.includes("aiosqlite") || content.includes("sqlite")) {
|
|
2320
2036
|
if (!detected.databases.includes("sqlite")) detected.databases.push("sqlite");
|
|
2321
2037
|
}
|
|
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
2038
|
} catch {
|
|
2332
2039
|
}
|
|
2333
2040
|
}
|
|
2041
|
+
} else if (fileNames.has("requirements.txt")) {
|
|
2042
|
+
detected.stack.push("python");
|
|
2334
2043
|
}
|
|
2335
2044
|
if (fileNames.has("cargo.toml")) {
|
|
2336
2045
|
detected.stack.push("rust");
|
|
2337
2046
|
detected.commands.build = "cargo build";
|
|
2338
2047
|
detected.commands.test = "cargo test";
|
|
2339
2048
|
}
|
|
2340
|
-
if (fileNames.has("go.mod")) {
|
|
2341
|
-
detected.stack.push("go");
|
|
2342
|
-
detected.commands.build = "go build";
|
|
2343
|
-
detected.commands.test = "go test ./...";
|
|
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;
|
|
2187
|
+
}
|
|
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
|
+
});
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
} catch {
|
|
2199
|
+
}
|
|
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;
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
} else if (relativePath === "AGENTS.md" || relativePath === path.basename(file)) {
|
|
2232
|
+
result.repositoryPath = relativePath;
|
|
2233
|
+
}
|
|
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);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
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}`));
|
|
2344
2322
|
}
|
|
2345
|
-
|
|
2346
|
-
} catch {
|
|
2347
|
-
|
|
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);
|
|
2335
|
+
}
|
|
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
|
}
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
headers: { "User-Agent": "LynxPrompt-CLI" }
|
|
2423
|
-
});
|
|
2424
|
-
if (pkgRes.ok) {
|
|
2425
|
-
try {
|
|
2426
|
-
const pkg = await pkgRes.json();
|
|
2427
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2428
|
-
if (allDeps["next"]) detected.stack.push("nextjs");
|
|
2429
|
-
if (allDeps["react"]) detected.stack.push("react");
|
|
2430
|
-
if (allDeps["vue"]) detected.stack.push("vue");
|
|
2431
|
-
if (allDeps["svelte"]) detected.stack.push("svelte");
|
|
2432
|
-
if (allDeps["express"]) detected.stack.push("express");
|
|
2433
|
-
if (allDeps["fastify"]) detected.stack.push("fastify");
|
|
2434
|
-
if (allDeps["typescript"]) detected.stack.push("typescript");
|
|
2435
|
-
if (allDeps["tailwindcss"]) detected.stack.push("tailwind");
|
|
2436
|
-
if (allDeps["prisma"]) detected.stack.push("prisma");
|
|
2437
|
-
if (allDeps["vitest"]) detected.testFramework = "vitest";
|
|
2438
|
-
else if (allDeps["jest"]) detected.testFramework = "jest";
|
|
2439
|
-
else if (allDeps["@playwright/test"]) detected.testFramework = "playwright";
|
|
2440
|
-
if (detected.stack.length === 0 || detected.stack.length === 1 && detected.stack[0] === "typescript") {
|
|
2441
|
-
detected.stack.unshift("javascript");
|
|
2442
|
-
}
|
|
2443
|
-
if (pkg.scripts) {
|
|
2444
|
-
if (pkg.scripts.build) detected.commands.build = "npm run build";
|
|
2445
|
-
if (pkg.scripts.test) detected.commands.test = "npm run test";
|
|
2446
|
-
if (pkg.scripts.lint) detected.commands.lint = "npm run lint";
|
|
2447
|
-
if (pkg.scripts.dev) detected.commands.dev = "npm run dev";
|
|
2448
|
-
}
|
|
2449
|
-
if (allDeps["pg"] || allDeps["postgres"]) {
|
|
2450
|
-
if (!detected.databases.includes("postgresql")) detected.databases.push("postgresql");
|
|
2451
|
-
}
|
|
2452
|
-
if (allDeps["better-sqlite3"] || allDeps["sqlite3"]) {
|
|
2453
|
-
if (!detected.databases.includes("sqlite")) detected.databases.push("sqlite");
|
|
2454
|
-
}
|
|
2455
|
-
if (allDeps["mongodb"] || allDeps["mongoose"]) {
|
|
2456
|
-
if (!detected.databases.includes("mongodb")) detected.databases.push("mongodb");
|
|
2457
|
-
}
|
|
2458
|
-
if (allDeps["redis"] || allDeps["ioredis"]) {
|
|
2459
|
-
if (!detected.databases.includes("redis")) detected.databases.push("redis");
|
|
2460
|
-
}
|
|
2461
|
-
} catch {
|
|
2462
|
-
}
|
|
2364
|
+
if (shouldCreateHierarchy) {
|
|
2365
|
+
await pushHierarchy(cwd, discoveredFiles, options);
|
|
2366
|
+
return;
|
|
2463
2367
|
}
|
|
2368
|
+
console.log(chalk6.gray("Proceeding with single file push..."));
|
|
2464
2369
|
}
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
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: ""
|
|
2486
2430
|
}
|
|
2487
|
-
|
|
2488
|
-
|
|
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}`));
|
|
2489
2480
|
}
|
|
2490
|
-
if (
|
|
2491
|
-
|
|
2492
|
-
detected.commands.build = "cargo build";
|
|
2493
|
-
detected.commands.test = "cargo test";
|
|
2481
|
+
if (result.blueprint.hierarchy_id) {
|
|
2482
|
+
console.log(chalk6.gray(` Hierarchy: ${result.blueprint.hierarchy_id}`));
|
|
2494
2483
|
}
|
|
2495
|
-
if (
|
|
2496
|
-
|
|
2497
|
-
detected.commands.build = "go build";
|
|
2498
|
-
detected.commands.test = "go test ./...";
|
|
2484
|
+
if (hierarchyInfo.parentId) {
|
|
2485
|
+
console.log(chalk6.cyan(` \u21B3 Linked to parent blueprint: ${hierarchyInfo.parentId}`));
|
|
2499
2486
|
}
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
const dangerousChars = /[;&|`$(){}[\]<>\\'"!#*?~]/;
|
|
2509
|
-
return !dangerousChars.test(trimmed);
|
|
2487
|
+
if (visibility === "PUBLIC") {
|
|
2488
|
+
console.log(chalk6.gray(` View: https://lynxprompt.com/templates/${result.blueprint.id.replace("bp_", "")}`));
|
|
2489
|
+
}
|
|
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 });
|
|
@@ -5092,21 +5186,7 @@ async function runWizardWithDraftProtection(options) {
|
|
|
5092
5186
|
const userTier = userPlanRaw === "teams" ? "teams" : "users";
|
|
5093
5187
|
const userPlanDisplay = userTier === "teams" ? "TEAMS" : "USERS";
|
|
5094
5188
|
if (!authenticated) {
|
|
5095
|
-
|
|
5096
|
-
const y = chalk7.yellow;
|
|
5097
|
-
const pad = (s, len) => s + " ".repeat(Math.max(0, len - s.length));
|
|
5098
|
-
console.log(y("\u250C" + "\u2500".repeat(W) + "\u2510"));
|
|
5099
|
-
console.log(y("\u2502") + pad(" \u{1F4A1} Log in for full wizard features:", W - 1) + y("\u2502"));
|
|
5100
|
-
console.log(y("\u2502") + " ".repeat(W) + y("\u2502"));
|
|
5101
|
-
console.log(y("\u2502") + pad(" \u2022 Full wizard with all steps", W) + y("\u2502"));
|
|
5102
|
-
console.log(y("\u2502") + pad(" \u2022 Auto-detect from repos [TEAMS]", W) + y("\u2502"));
|
|
5103
|
-
console.log(y("\u2502") + pad(" \u2022 AI assistant for configs [TEAMS]", W) + y("\u2502"));
|
|
5104
|
-
console.log(y("\u2502") + pad(" \u2022 Save preferences to your profile", W) + y("\u2502"));
|
|
5105
|
-
console.log(y("\u2502") + pad(" \u2022 Push configs to cloud (lynxp push)", W) + y("\u2502"));
|
|
5106
|
-
console.log(y("\u2502") + pad(" \u2022 Share across devices (lynxp push/pull)", W) + y("\u2502"));
|
|
5107
|
-
console.log(y("\u2502") + " ".repeat(W) + y("\u2502"));
|
|
5108
|
-
console.log(y("\u2502") + pad(" Run: " + chalk7.cyan("lynxp login"), W + 10) + y("\u2502"));
|
|
5109
|
-
console.log(y("\u2514" + "\u2500".repeat(W) + "\u2518"));
|
|
5189
|
+
console.log(chalk7.gray(` \u{1F464} Running as guest. ${chalk7.cyan("lynxp login")} for cloud sync & sharing.`));
|
|
5110
5190
|
console.log();
|
|
5111
5191
|
} else {
|
|
5112
5192
|
const planEmoji = userTier === "teams" ? "\u{1F465}" : "\u{1F193}";
|
|
@@ -5331,12 +5411,27 @@ async function runWizardWithDraftProtection(options) {
|
|
|
5331
5411
|
nextStepsLines.push(chalk7.cyan(" lynxp push ") + chalk7.gray("Upload to cloud"));
|
|
5332
5412
|
nextStepsLines.push(chalk7.cyan(" lynxp link ") + chalk7.gray("Link to a blueprint"));
|
|
5333
5413
|
nextStepsLines.push(chalk7.cyan(" lynxp diff ") + chalk7.gray("Compare with cloud blueprint"));
|
|
5334
|
-
} else {
|
|
5335
|
-
nextStepsLines.push(chalk7.gray(" lynxp login ") + chalk7.yellow("Log in to push & sync"));
|
|
5336
5414
|
}
|
|
5337
5415
|
nextStepsLines.push(chalk7.cyan(" lynxp status ") + chalk7.gray("View current setup"));
|
|
5338
5416
|
printBox(nextStepsLines, chalk7.gray);
|
|
5339
5417
|
console.log();
|
|
5418
|
+
if (!authenticated) {
|
|
5419
|
+
const W = 60;
|
|
5420
|
+
const y = chalk7.yellow;
|
|
5421
|
+
const g = chalk7.green;
|
|
5422
|
+
const pad = (s, len) => s + " ".repeat(Math.max(0, len - s.length));
|
|
5423
|
+
console.log(y(" \u256D" + "\u2500".repeat(W) + "\u256E"));
|
|
5424
|
+
console.log(y(" \u2502") + g(pad(" \u{1F680} Unlock LynxPrompt Cloud (FREE)", W)) + y("\u2502"));
|
|
5425
|
+
console.log(y(" \u2502") + " ".repeat(W) + y("\u2502"));
|
|
5426
|
+
console.log(y(" \u2502") + pad(" \u2713 Sync configs across all your devices", W) + y("\u2502"));
|
|
5427
|
+
console.log(y(" \u2502") + pad(" \u2713 Share blueprints with your team", W) + y("\u2502"));
|
|
5428
|
+
console.log(y(" \u2502") + pad(" \u2713 Save preferences for future wizards", W) + y("\u2502"));
|
|
5429
|
+
console.log(y(" \u2502") + pad(" \u2713 Auto-update configs via lynxp push/pull", W) + y("\u2502"));
|
|
5430
|
+
console.log(y(" \u2502") + " ".repeat(W) + y("\u2502"));
|
|
5431
|
+
console.log(y(" \u2502") + pad(" Sign in now: " + chalk7.cyan("lynxp login"), W + 10) + y("\u2502"));
|
|
5432
|
+
console.log(y(" \u2570" + "\u2500".repeat(W) + "\u256F"));
|
|
5433
|
+
console.log();
|
|
5434
|
+
}
|
|
5340
5435
|
if (options.saveDraft) {
|
|
5341
5436
|
try {
|
|
5342
5437
|
await saveDraftLocally(options.saveDraft, config2);
|
|
@@ -6633,9 +6728,9 @@ async function runInteractiveWizard(options, detected, userTier) {
|
|
|
6633
6728
|
}, promptConfig);
|
|
6634
6729
|
answers.selfImprove = selfImproveResponse.selfImprove || false;
|
|
6635
6730
|
console.log();
|
|
6636
|
-
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."));
|
|
6637
6732
|
console.log(chalk7.gray(" Benefits:"));
|
|
6638
|
-
console.log(chalk7.gray(" \u2022
|
|
6733
|
+
console.log(chalk7.gray(" \u2022 Sync configs across all your devices"));
|
|
6639
6734
|
console.log(chalk7.gray(" \u2022 Track changes and rollback if needed"));
|
|
6640
6735
|
console.log(chalk7.gray(" \u2022 Instructions added to config so AI can sync automatically"));
|
|
6641
6736
|
const enableAutoUpdateResponse = await prompts3({
|
|
@@ -8561,7 +8656,14 @@ var TARGET_FILES = {
|
|
|
8561
8656
|
aider: ".aider.conf.yml",
|
|
8562
8657
|
codex: "codex.md",
|
|
8563
8658
|
supermaven: "supermaven.md",
|
|
8564
|
-
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"
|
|
8565
8667
|
};
|
|
8566
8668
|
var PLATFORM_NAMES = {
|
|
8567
8669
|
agents: "AGENTS.md (Universal)",
|
|
@@ -8574,8 +8676,24 @@ var PLATFORM_NAMES = {
|
|
|
8574
8676
|
aider: "Aider Config",
|
|
8575
8677
|
codex: "Codex",
|
|
8576
8678
|
supermaven: "Supermaven",
|
|
8577
|
-
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"
|
|
8578
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");
|
|
8579
8697
|
async function detectSourceFile(cwd) {
|
|
8580
8698
|
for (const [pattern, platform2] of Object.entries(SOURCE_FILES)) {
|
|
8581
8699
|
try {
|
|
@@ -8597,6 +8715,15 @@ async function detectSourceFile(cwd) {
|
|
|
8597
8715
|
}
|
|
8598
8716
|
return null;
|
|
8599
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
|
+
}
|
|
8600
8727
|
function parseMarkdownConfig(content) {
|
|
8601
8728
|
const config2 = {};
|
|
8602
8729
|
const sections = content.split(/^##\s+/m);
|
|
@@ -8612,6 +8739,9 @@ function parseMarkdownConfig(content) {
|
|
|
8612
8739
|
}
|
|
8613
8740
|
function generateTargetContent(config2, targetPlatform) {
|
|
8614
8741
|
const rawContent = config2._raw || "";
|
|
8742
|
+
if (isCommandTarget(targetPlatform)) {
|
|
8743
|
+
return rawContent;
|
|
8744
|
+
}
|
|
8615
8745
|
switch (targetPlatform) {
|
|
8616
8746
|
case "cursor":
|
|
8617
8747
|
return `---
|
|
@@ -8662,8 +8792,13 @@ async function convertCommand(source, target, options) {
|
|
|
8662
8792
|
let sourcePlatform;
|
|
8663
8793
|
if (source) {
|
|
8664
8794
|
sourcePath = join9(cwd, source);
|
|
8665
|
-
const
|
|
8666
|
-
|
|
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
|
+
}
|
|
8667
8802
|
} else {
|
|
8668
8803
|
const detected = await detectSourceFile(cwd);
|
|
8669
8804
|
if (!detected) {
|
|
@@ -8674,8 +8809,14 @@ async function convertCommand(source, target, options) {
|
|
|
8674
8809
|
console.log(chalk14.gray(` \u2022 ${file}`));
|
|
8675
8810
|
}
|
|
8676
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();
|
|
8677
8817
|
console.log(chalk14.gray(" Usage: lynxp convert <source> <target>"));
|
|
8678
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"));
|
|
8679
8820
|
process.exit(1);
|
|
8680
8821
|
}
|
|
8681
8822
|
sourcePath = detected.path;
|
|
@@ -8705,7 +8846,12 @@ async function convertCommand(source, target, options) {
|
|
|
8705
8846
|
const config2 = parseMarkdownConfig(sourceContent);
|
|
8706
8847
|
const targetContent = generateTargetContent(config2, normalizedTarget);
|
|
8707
8848
|
spinner.stop();
|
|
8708
|
-
|
|
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
|
+
}
|
|
8709
8855
|
const outputPath = join9(cwd, outputFilename);
|
|
8710
8856
|
try {
|
|
8711
8857
|
await access5(outputPath);
|
|
@@ -9151,7 +9297,7 @@ function displayResults(result, options) {
|
|
|
9151
9297
|
async function importCommand(path2 = ".", options) {
|
|
9152
9298
|
console.log();
|
|
9153
9299
|
console.log(chalk16.cyan.bold(" \u{1F4E5} LynxPrompt Import"));
|
|
9154
|
-
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"));
|
|
9155
9301
|
console.log();
|
|
9156
9302
|
const rootPath = join11(process.cwd(), path2);
|
|
9157
9303
|
try {
|
|
@@ -9160,25 +9306,53 @@ async function importCommand(path2 = ".", options) {
|
|
|
9160
9306
|
console.log(chalk16.red(` \u2717 Path not found: ${rootPath}`));
|
|
9161
9307
|
process.exit(1);
|
|
9162
9308
|
}
|
|
9163
|
-
const spinner = ora14("Scanning for configuration files...").start();
|
|
9309
|
+
const spinner = ora14("Scanning for configuration files and commands...").start();
|
|
9164
9310
|
try {
|
|
9165
9311
|
const files = await scanDirectory(rootPath, options);
|
|
9166
|
-
|
|
9167
|
-
|
|
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");
|
|
9168
9316
|
console.log();
|
|
9169
|
-
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"));
|
|
9170
9320
|
console.log(chalk16.gray(" Try specifying a different path or use --pattern for custom filenames"));
|
|
9171
9321
|
return;
|
|
9172
9322
|
}
|
|
9173
|
-
spinner.succeed(`Found ${files.length} configuration file(s)`);
|
|
9323
|
+
spinner.succeed(`Found ${files.length} configuration file(s) and ${commands.length} command(s)`);
|
|
9174
9324
|
const hierarchy = buildHierarchy(files, rootPath);
|
|
9175
9325
|
const result = {
|
|
9176
|
-
totalFound
|
|
9326
|
+
totalFound,
|
|
9177
9327
|
files,
|
|
9178
9328
|
hierarchy,
|
|
9179
9329
|
errors: []
|
|
9180
9330
|
};
|
|
9181
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
|
+
}
|
|
9182
9356
|
if (options.dryRun) {
|
|
9183
9357
|
console.log(chalk16.yellow(" \u{1F4CB} Dry run complete - no changes made"));
|
|
9184
9358
|
console.log(chalk16.gray(" Remove --dry-run to proceed with import"));
|
|
@@ -9321,7 +9495,7 @@ function handleError2(error) {
|
|
|
9321
9495
|
}
|
|
9322
9496
|
|
|
9323
9497
|
// src/index.ts
|
|
9324
|
-
var CLI_VERSION2 = "1.
|
|
9498
|
+
var CLI_VERSION2 = "1.4.0";
|
|
9325
9499
|
var program = new Command();
|
|
9326
9500
|
program.name("lynxprompt").description("CLI for LynxPrompt - Generate AI IDE configuration files").version(CLI_VERSION2);
|
|
9327
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);
|