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