@ulpi/cli 0.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.
Files changed (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +200 -0
  3. package/dist/auth-PN7TMQHV-2W4ICG64.js +15 -0
  4. package/dist/chunk-247GVVKK.js +2259 -0
  5. package/dist/chunk-2CLNOKPA.js +793 -0
  6. package/dist/chunk-2HEE5OKX.js +79 -0
  7. package/dist/chunk-2MZER6ND.js +415 -0
  8. package/dist/chunk-3SBPZRB5.js +772 -0
  9. package/dist/chunk-4VNS5WPM.js +42 -0
  10. package/dist/chunk-6JCMYYBT.js +1546 -0
  11. package/dist/chunk-6OCEY7JY.js +422 -0
  12. package/dist/chunk-74WVVWJ4.js +375 -0
  13. package/dist/chunk-7AL4DOEJ.js +131 -0
  14. package/dist/chunk-7LXY5UVC.js +330 -0
  15. package/dist/chunk-DBMUNBNB.js +3048 -0
  16. package/dist/chunk-JWUUVXIV.js +13694 -0
  17. package/dist/chunk-KIKPIH6N.js +4048 -0
  18. package/dist/chunk-KLEASXUR.js +70 -0
  19. package/dist/chunk-MIAQVCFW.js +39 -0
  20. package/dist/chunk-NNUWU6CV.js +1610 -0
  21. package/dist/chunk-PKD4ASEM.js +115 -0
  22. package/dist/chunk-Q4HIY43N.js +4230 -0
  23. package/dist/chunk-QJ5GSMEC.js +146 -0
  24. package/dist/chunk-SIAQVRKG.js +2163 -0
  25. package/dist/chunk-SPOI23SB.js +197 -0
  26. package/dist/chunk-YM2HV4IA.js +505 -0
  27. package/dist/codemap-RRJIDBQ5.js +636 -0
  28. package/dist/config-EGAXXCGL.js +127 -0
  29. package/dist/dist-6G7JC2RA.js +90 -0
  30. package/dist/dist-7LHZ65GC.js +418 -0
  31. package/dist/dist-LZKZFPVX.js +140 -0
  32. package/dist/dist-R5F4MX3I.js +107 -0
  33. package/dist/dist-R5ZJ4LX5.js +56 -0
  34. package/dist/dist-RJGCUS3L.js +87 -0
  35. package/dist/dist-RKOGLK7R.js +151 -0
  36. package/dist/dist-W7K4WPAF.js +597 -0
  37. package/dist/export-import-4A5MWLIA.js +53 -0
  38. package/dist/history-ATTUKOHO.js +934 -0
  39. package/dist/index.js +2120 -0
  40. package/dist/init-AY5C2ZAS.js +393 -0
  41. package/dist/launchd-LF2QMSKZ.js +148 -0
  42. package/dist/log-TVTUXAYD.js +75 -0
  43. package/dist/mcp-installer-NQCGKQ23.js +124 -0
  44. package/dist/memory-J3G24QHS.js +406 -0
  45. package/dist/ollama-3XCUZMZT-FYKHW4TZ.js +7 -0
  46. package/dist/openai-E7G2YAHU-UYY4ZWON.js +8 -0
  47. package/dist/projects-ATHDD3D6.js +271 -0
  48. package/dist/review-ADUPV3PN.js +152 -0
  49. package/dist/rules-E427DKYJ.js +134 -0
  50. package/dist/server-MOYPE4SM-N7SE2AN7.js +18 -0
  51. package/dist/server-X5P6WH2M-7K2RY34N.js +11 -0
  52. package/dist/skills/ulpi-generate-guardian/SKILL.md +511 -0
  53. package/dist/skills/ulpi-generate-guardian/references/framework-rules.md +692 -0
  54. package/dist/skills/ulpi-generate-guardian/references/language-rules.md +596 -0
  55. package/dist/skills-CX73O3IV.js +76 -0
  56. package/dist/status-4DFHDJMN.js +66 -0
  57. package/dist/templates/biome.yml +24 -0
  58. package/dist/templates/conventional-commits.yml +18 -0
  59. package/dist/templates/django.yml +30 -0
  60. package/dist/templates/docker.yml +30 -0
  61. package/dist/templates/eslint.yml +13 -0
  62. package/dist/templates/express.yml +20 -0
  63. package/dist/templates/fastapi.yml +23 -0
  64. package/dist/templates/git-flow.yml +26 -0
  65. package/dist/templates/github-flow.yml +27 -0
  66. package/dist/templates/go.yml +33 -0
  67. package/dist/templates/jest.yml +24 -0
  68. package/dist/templates/laravel.yml +30 -0
  69. package/dist/templates/monorepo.yml +26 -0
  70. package/dist/templates/nestjs.yml +21 -0
  71. package/dist/templates/nextjs.yml +31 -0
  72. package/dist/templates/nodejs.yml +33 -0
  73. package/dist/templates/npm.yml +15 -0
  74. package/dist/templates/php.yml +25 -0
  75. package/dist/templates/pnpm.yml +15 -0
  76. package/dist/templates/prettier.yml +23 -0
  77. package/dist/templates/prisma.yml +21 -0
  78. package/dist/templates/python.yml +33 -0
  79. package/dist/templates/quality-of-life.yml +111 -0
  80. package/dist/templates/ruby.yml +25 -0
  81. package/dist/templates/rust.yml +34 -0
  82. package/dist/templates/typescript.yml +14 -0
  83. package/dist/templates/vitest.yml +24 -0
  84. package/dist/templates/yarn.yml +15 -0
  85. package/dist/templates-U7T6MARD.js +156 -0
  86. package/dist/ui-L7UAWXDY.js +167 -0
  87. package/dist/ui.html +698 -0
  88. package/dist/ulpi-RMMCUAGP-JCJ273T6.js +161 -0
  89. package/dist/uninstall-6SW35IK4.js +25 -0
  90. package/dist/update-M2B4RLGH.js +61 -0
  91. package/dist/version-checker-ANCS3IHR.js +10 -0
  92. package/package.json +92 -0
@@ -0,0 +1,793 @@
1
+ // ../../packages/stack-engine/dist/index.js
2
+ import { existsSync, readdirSync } from "fs";
3
+ import { join } from "path";
4
+ import { existsSync as existsSync2, readFileSync, statSync } from "fs";
5
+ import { join as join2 } from "path";
6
+ import { existsSync as existsSync3 } from "fs";
7
+ import { join as join3 } from "path";
8
+ import { existsSync as existsSync4 } from "fs";
9
+ import { join as join4 } from "path";
10
+ import { existsSync as existsSync5, readdirSync as readdirSync2 } from "fs";
11
+ import { join as join5 } from "path";
12
+ import { existsSync as existsSync6, readdirSync as readdirSync3 } from "fs";
13
+ import { join as join6 } from "path";
14
+ import { execFileSync } from "child_process";
15
+ import { existsSync as existsSync7 } from "fs";
16
+ import { join as join7 } from "path";
17
+ import { existsSync as existsSync8 } from "fs";
18
+ import { join as join8 } from "path";
19
+ function detectRuntime(projectDir) {
20
+ if (existsSync(join(projectDir, "package.json"))) {
21
+ return { id: "node", name: "Node.js", confidence: "high", signal: "package.json" };
22
+ }
23
+ if (existsSync(join(projectDir, "requirements.txt")) || existsSync(join(projectDir, "pyproject.toml")) || existsSync(join(projectDir, "setup.py"))) {
24
+ const signal = existsSync(join(projectDir, "requirements.txt")) ? "requirements.txt" : existsSync(join(projectDir, "pyproject.toml")) ? "pyproject.toml" : "setup.py";
25
+ return { id: "python", name: "Python", confidence: "high", signal };
26
+ }
27
+ if (existsSync(join(projectDir, "go.mod"))) {
28
+ return { id: "go", name: "Go", confidence: "high", signal: "go.mod" };
29
+ }
30
+ if (existsSync(join(projectDir, "Cargo.toml"))) {
31
+ return { id: "rust", name: "Rust", confidence: "high", signal: "Cargo.toml" };
32
+ }
33
+ if (existsSync(join(projectDir, "composer.json"))) {
34
+ return { id: "php", name: "PHP", confidence: "high", signal: "composer.json" };
35
+ }
36
+ if (existsSync(join(projectDir, "Gemfile"))) {
37
+ return { id: "ruby", name: "Ruby", confidence: "high", signal: "Gemfile" };
38
+ }
39
+ if (existsSync(join(projectDir, "pubspec.yaml"))) {
40
+ return { id: "dart", name: "Dart", confidence: "high", signal: "pubspec.yaml" };
41
+ }
42
+ if (existsSync(join(projectDir, "pom.xml")) || existsSync(join(projectDir, "build.gradle")) || existsSync(join(projectDir, "build.gradle.kts"))) {
43
+ const signal = existsSync(join(projectDir, "pom.xml")) ? "pom.xml" : existsSync(join(projectDir, "build.gradle")) ? "build.gradle" : "build.gradle.kts";
44
+ return { id: "java", name: "Java", confidence: "high", signal };
45
+ }
46
+ try {
47
+ const entries = readdirSync(projectDir);
48
+ const csharpFile = entries.find((e) => e.endsWith(".csproj") || e.endsWith(".sln"));
49
+ if (csharpFile) {
50
+ return { id: "csharp", name: "C#", confidence: "high", signal: csharpFile };
51
+ }
52
+ } catch {
53
+ }
54
+ if (existsSync(join(projectDir, "mix.exs"))) {
55
+ return { id: "elixir", name: "Elixir", confidence: "high", signal: "mix.exs" };
56
+ }
57
+ return null;
58
+ }
59
+ function detectLanguage(projectDir) {
60
+ if (existsSync(join(projectDir, "tsconfig.json")) && existsSync(join(projectDir, "package.json"))) {
61
+ return { id: "typescript", name: "TypeScript", confidence: "high", signal: "tsconfig.json" };
62
+ }
63
+ if (existsSync(join(projectDir, "package.json"))) {
64
+ return { id: "javascript", name: "JavaScript", confidence: "high", signal: "package.json" };
65
+ }
66
+ if (existsSync(join(projectDir, "requirements.txt")) || existsSync(join(projectDir, "pyproject.toml")) || existsSync(join(projectDir, "setup.py"))) {
67
+ return { id: "python", name: "Python", confidence: "high", signal: "python project files" };
68
+ }
69
+ if (existsSync(join(projectDir, "go.mod"))) {
70
+ return { id: "go", name: "Go", confidence: "high", signal: "go.mod" };
71
+ }
72
+ if (existsSync(join(projectDir, "Cargo.toml"))) {
73
+ return { id: "rust", name: "Rust", confidence: "high", signal: "Cargo.toml" };
74
+ }
75
+ if (existsSync(join(projectDir, "composer.json"))) {
76
+ return { id: "php", name: "PHP", confidence: "high", signal: "composer.json" };
77
+ }
78
+ if (existsSync(join(projectDir, "Gemfile"))) {
79
+ return { id: "ruby", name: "Ruby", confidence: "high", signal: "Gemfile" };
80
+ }
81
+ if (existsSync(join(projectDir, "pubspec.yaml"))) {
82
+ return { id: "dart", name: "Dart", confidence: "high", signal: "pubspec.yaml" };
83
+ }
84
+ if (existsSync(join(projectDir, "mix.exs"))) {
85
+ return { id: "elixir", name: "Elixir", confidence: "high", signal: "mix.exs" };
86
+ }
87
+ return null;
88
+ }
89
+ var MAX_CONFIG_SIZE = 5e6;
90
+ function isReadable(filePath) {
91
+ try {
92
+ if (!existsSync2(filePath)) return false;
93
+ const stat = statSync(filePath);
94
+ if (stat.size > MAX_CONFIG_SIZE) return false;
95
+ return true;
96
+ } catch {
97
+ return false;
98
+ }
99
+ }
100
+ function readJsonFile(filePath) {
101
+ try {
102
+ if (!isReadable(filePath)) return null;
103
+ const raw = readFileSync(filePath, "utf-8");
104
+ return JSON.parse(raw);
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+ function readTextFile(filePath) {
110
+ try {
111
+ if (!isReadable(filePath)) return null;
112
+ return readFileSync(filePath, "utf-8");
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+ function getNodeDependencies(projectDir) {
118
+ const pkg = readJsonFile(join2(projectDir, "package.json"));
119
+ if (!pkg) return /* @__PURE__ */ new Set();
120
+ const deps = /* @__PURE__ */ new Set();
121
+ for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
122
+ const section = pkg[field];
123
+ if (section && typeof section === "object") {
124
+ for (const name of Object.keys(section)) {
125
+ deps.add(name);
126
+ }
127
+ }
128
+ }
129
+ return deps;
130
+ }
131
+ function getPythonDependencies(projectDir) {
132
+ const deps = /* @__PURE__ */ new Set();
133
+ const requirements = readTextFile(join2(projectDir, "requirements.txt"));
134
+ if (requirements) {
135
+ for (const line of requirements.split("\n")) {
136
+ const trimmed = line.trim();
137
+ if (trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("-")) {
138
+ const match = trimmed.match(/^([a-zA-Z0-9_-]+)/);
139
+ if (match) deps.add(match[1].toLowerCase());
140
+ }
141
+ }
142
+ }
143
+ return deps;
144
+ }
145
+ function getComposerDependencies(projectDir) {
146
+ const composer = readJsonFile(join2(projectDir, "composer.json"));
147
+ if (!composer) return /* @__PURE__ */ new Set();
148
+ const deps = /* @__PURE__ */ new Set();
149
+ for (const field of ["require", "require-dev"]) {
150
+ const section = composer[field];
151
+ if (section && typeof section === "object") {
152
+ for (const name of Object.keys(section)) {
153
+ deps.add(name);
154
+ }
155
+ }
156
+ }
157
+ return deps;
158
+ }
159
+ function getGoDependencies(projectDir) {
160
+ const gomod = readTextFile(join2(projectDir, "go.mod"));
161
+ if (!gomod) return /* @__PURE__ */ new Set();
162
+ const deps = /* @__PURE__ */ new Set();
163
+ const requireBlockRegex = /require\s*\(([\s\S]*?)\)/g;
164
+ let match;
165
+ while ((match = requireBlockRegex.exec(gomod)) !== null) {
166
+ const block = match[1];
167
+ for (const line of block.split("\n")) {
168
+ const trimmed = line.trim();
169
+ if (trimmed && !trimmed.startsWith("//")) {
170
+ const parts = trimmed.split(/\s+/);
171
+ if (parts[0]) deps.add(parts[0]);
172
+ }
173
+ }
174
+ }
175
+ const singleRegex = /^require\s+(\S+)\s+/gm;
176
+ while ((match = singleRegex.exec(gomod)) !== null) {
177
+ if (match[1]) deps.add(match[1]);
178
+ }
179
+ return deps;
180
+ }
181
+ function getCargoDependencies(projectDir) {
182
+ const cargo = readTextFile(join2(projectDir, "Cargo.toml"));
183
+ if (!cargo) return /* @__PURE__ */ new Set();
184
+ const deps = /* @__PURE__ */ new Set();
185
+ const sectionRegex = /\[((?:dev-|build-)?dependencies)\]([\s\S]*?)(?=\n\[|$)/g;
186
+ let match;
187
+ while ((match = sectionRegex.exec(cargo)) !== null) {
188
+ const block = match[2];
189
+ for (const line of block.split("\n")) {
190
+ const trimmed = line.trim();
191
+ if (trimmed && !trimmed.startsWith("#")) {
192
+ const keyMatch = trimmed.match(/^([a-zA-Z0-9_-]+)\s*=/);
193
+ if (keyMatch && keyMatch[1]) deps.add(keyMatch[1]);
194
+ }
195
+ }
196
+ }
197
+ return deps;
198
+ }
199
+ function getGemDependencies(projectDir) {
200
+ const gemfile = readTextFile(join2(projectDir, "Gemfile"));
201
+ if (!gemfile) return /* @__PURE__ */ new Set();
202
+ const deps = /* @__PURE__ */ new Set();
203
+ const gemRegex = /^\s*gem\s+['"]([^'"]+)['"]/gm;
204
+ let match;
205
+ while ((match = gemRegex.exec(gemfile)) !== null) {
206
+ if (match[1]) deps.add(match[1]);
207
+ }
208
+ return deps;
209
+ }
210
+ function getMixDependencies(projectDir) {
211
+ const mixExs = readTextFile(join2(projectDir, "mix.exs"));
212
+ if (!mixExs) return /* @__PURE__ */ new Set();
213
+ const deps = /* @__PURE__ */ new Set();
214
+ const depRegex = /\{:([a-z_]+)/g;
215
+ let match;
216
+ while ((match = depRegex.exec(mixExs)) !== null) {
217
+ if (match[1]) deps.add(match[1]);
218
+ }
219
+ return deps;
220
+ }
221
+ var NODE_FRAMEWORKS = [
222
+ { pkg: "next", id: "nextjs", name: "Next.js" },
223
+ { pkg: "express", id: "express", name: "Express" },
224
+ { pkg: "@nestjs/core", id: "nestjs", name: "NestJS" },
225
+ { pkg: "react-native", id: "react-native", name: "React Native" },
226
+ { pkg: "astro", id: "astro", name: "Astro" },
227
+ { pkg: "svelte", id: "svelte", name: "Svelte" }
228
+ ];
229
+ var PYTHON_FRAMEWORKS = [
230
+ { pkg: "django", id: "django", name: "Django" },
231
+ { pkg: "flask", id: "flask", name: "Flask" },
232
+ { pkg: "fastapi", id: "fastapi", name: "FastAPI" }
233
+ ];
234
+ var PHP_FRAMEWORKS = [
235
+ { pkg: "laravel/framework", id: "laravel", name: "Laravel" },
236
+ { pkg: "symfony/framework-bundle", id: "symfony", name: "Symfony" }
237
+ ];
238
+ var GO_FRAMEWORKS = [
239
+ { pkg: "github.com/gin-gonic/gin", id: "gin", name: "Gin" },
240
+ { pkg: "github.com/labstack/echo", id: "echo", name: "Echo" }
241
+ ];
242
+ var RUST_FRAMEWORKS = [
243
+ { pkg: "actix-web", id: "actix", name: "Actix" },
244
+ { pkg: "axum", id: "axum", name: "Axum" }
245
+ ];
246
+ function findInSet(deps, mappings, signalSource) {
247
+ for (const mapping of mappings) {
248
+ if (deps.has(mapping.pkg)) {
249
+ return {
250
+ id: mapping.id,
251
+ name: mapping.name,
252
+ confidence: "high",
253
+ signal: `${mapping.pkg} in ${signalSource}`
254
+ };
255
+ }
256
+ }
257
+ return null;
258
+ }
259
+ function findInGoDeps(deps, mappings) {
260
+ for (const mapping of mappings) {
261
+ for (const dep of deps) {
262
+ if (dep === mapping.pkg || dep.startsWith(mapping.pkg)) {
263
+ return {
264
+ id: mapping.id,
265
+ name: mapping.name,
266
+ confidence: "high",
267
+ signal: `${mapping.pkg} in go.mod`
268
+ };
269
+ }
270
+ }
271
+ }
272
+ return null;
273
+ }
274
+ function detectFramework(projectDir) {
275
+ const nodeDeps = getNodeDependencies(projectDir);
276
+ if (nodeDeps.size > 0) {
277
+ const result = findInSet(nodeDeps, NODE_FRAMEWORKS, "package.json");
278
+ if (result) return result;
279
+ }
280
+ const pyDeps = getPythonDependencies(projectDir);
281
+ if (pyDeps.size > 0) {
282
+ const result = findInSet(pyDeps, PYTHON_FRAMEWORKS, "requirements.txt");
283
+ if (result) return result;
284
+ }
285
+ const composerDeps = getComposerDependencies(projectDir);
286
+ if (composerDeps.size > 0) {
287
+ const result = findInSet(composerDeps, PHP_FRAMEWORKS, "composer.json");
288
+ if (result) return result;
289
+ }
290
+ const goDeps = getGoDependencies(projectDir);
291
+ if (goDeps.size > 0) {
292
+ const result = findInGoDeps(goDeps, GO_FRAMEWORKS);
293
+ if (result) return result;
294
+ }
295
+ const cargoDeps = getCargoDependencies(projectDir);
296
+ if (cargoDeps.size > 0) {
297
+ const result = findInSet(cargoDeps, RUST_FRAMEWORKS, "Cargo.toml");
298
+ if (result) return result;
299
+ }
300
+ const gemDeps = getGemDependencies(projectDir);
301
+ if (gemDeps.has("rails")) {
302
+ return { id: "rails", name: "Rails", confidence: "high", signal: "rails in Gemfile" };
303
+ }
304
+ const mixDeps = getMixDependencies(projectDir);
305
+ if (mixDeps.has("phoenix")) {
306
+ return { id: "phoenix", name: "Phoenix", confidence: "high", signal: "phoenix in mix.exs" };
307
+ }
308
+ return null;
309
+ }
310
+ var LOCK_FILES = [
311
+ { file: "pnpm-lock.yaml", id: "pnpm", name: "pnpm" },
312
+ { file: "yarn.lock", id: "yarn", name: "yarn" },
313
+ { file: "package-lock.json", id: "npm", name: "npm" },
314
+ { file: "bun.lockb", id: "bun", name: "bun" },
315
+ { file: "bun.lock", id: "bun", name: "bun" },
316
+ { file: "poetry.lock", id: "poetry", name: "poetry" },
317
+ { file: "uv.lock", id: "uv", name: "uv" },
318
+ { file: "Pipfile.lock", id: "pipenv", name: "pipenv" },
319
+ { file: "go.sum", id: "gomod", name: "go mod" },
320
+ { file: "Cargo.lock", id: "cargo", name: "cargo" },
321
+ { file: "composer.lock", id: "composer", name: "composer" },
322
+ { file: "Gemfile.lock", id: "bundler", name: "bundler" }
323
+ ];
324
+ function detectPackageManager(projectDir) {
325
+ for (const mapping of LOCK_FILES) {
326
+ if (existsSync3(join3(projectDir, mapping.file))) {
327
+ return {
328
+ id: mapping.id,
329
+ name: mapping.name,
330
+ confidence: "high",
331
+ signal: mapping.file
332
+ };
333
+ }
334
+ }
335
+ return null;
336
+ }
337
+ function detectFormatter(projectDir) {
338
+ const prettierConfigs = [
339
+ ".prettierrc",
340
+ ".prettierrc.json",
341
+ ".prettierrc.yml",
342
+ ".prettierrc.yaml",
343
+ ".prettierrc.js",
344
+ ".prettierrc.cjs",
345
+ ".prettierrc.mjs",
346
+ ".prettierrc.toml",
347
+ "prettier.config.js",
348
+ "prettier.config.cjs",
349
+ "prettier.config.mjs"
350
+ ];
351
+ for (const config of prettierConfigs) {
352
+ if (existsSync4(join4(projectDir, config))) {
353
+ return { id: "prettier", name: "Prettier", confidence: "high", signal: config };
354
+ }
355
+ }
356
+ const pkg = readJsonFile(join4(projectDir, "package.json"));
357
+ if (pkg && "prettier" in pkg) {
358
+ return {
359
+ id: "prettier",
360
+ name: "Prettier",
361
+ confidence: "high",
362
+ signal: "prettier key in package.json"
363
+ };
364
+ }
365
+ if (existsSync4(join4(projectDir, "biome.json"))) {
366
+ return { id: "biome", name: "Biome", confidence: "high", signal: "biome.json" };
367
+ }
368
+ if (existsSync4(join4(projectDir, "biome.jsonc"))) {
369
+ return { id: "biome", name: "Biome", confidence: "high", signal: "biome.jsonc" };
370
+ }
371
+ const pyproject = readTextFile(join4(projectDir, "pyproject.toml"));
372
+ if (pyproject) {
373
+ if (pyproject.includes("[tool.black]")) {
374
+ return {
375
+ id: "black",
376
+ name: "Black",
377
+ confidence: "high",
378
+ signal: "[tool.black] in pyproject.toml"
379
+ };
380
+ }
381
+ if (pyproject.includes("[tool.ruff.format]")) {
382
+ return {
383
+ id: "ruff-format",
384
+ name: "Ruff format",
385
+ confidence: "high",
386
+ signal: "[tool.ruff.format] in pyproject.toml"
387
+ };
388
+ }
389
+ }
390
+ if (existsSync4(join4(projectDir, "rustfmt.toml")) || existsSync4(join4(projectDir, ".rustfmt.toml"))) {
391
+ const signal = existsSync4(join4(projectDir, "rustfmt.toml")) ? "rustfmt.toml" : ".rustfmt.toml";
392
+ return { id: "rustfmt", name: "rustfmt", confidence: "high", signal };
393
+ }
394
+ if (existsSync4(join4(projectDir, ".editorconfig")) && existsSync4(join4(projectDir, "go.mod"))) {
395
+ return {
396
+ id: "gofmt",
397
+ name: "gofmt",
398
+ confidence: "medium",
399
+ signal: ".editorconfig with Go project"
400
+ };
401
+ }
402
+ return null;
403
+ }
404
+ function hasFileMatching(projectDir, prefix) {
405
+ try {
406
+ const entries = readdirSync2(projectDir);
407
+ const match = entries.find((e) => e.startsWith(prefix));
408
+ return match ?? null;
409
+ } catch {
410
+ return null;
411
+ }
412
+ }
413
+ function detectLinter(projectDir) {
414
+ const eslintFlatConfigs = [
415
+ "eslint.config.js",
416
+ "eslint.config.mjs",
417
+ "eslint.config.cjs",
418
+ "eslint.config.ts",
419
+ "eslint.config.mts",
420
+ "eslint.config.cts"
421
+ ];
422
+ for (const config of eslintFlatConfigs) {
423
+ if (existsSync5(join5(projectDir, config))) {
424
+ return { id: "eslint", name: "ESLint", confidence: "high", signal: config };
425
+ }
426
+ }
427
+ const eslintLegacy = hasFileMatching(projectDir, ".eslintrc");
428
+ if (eslintLegacy) {
429
+ return { id: "eslint", name: "ESLint", confidence: "high", signal: eslintLegacy };
430
+ }
431
+ const pkg = readJsonFile(join5(projectDir, "package.json"));
432
+ if (pkg && "eslintConfig" in pkg) {
433
+ return {
434
+ id: "eslint",
435
+ name: "ESLint",
436
+ confidence: "high",
437
+ signal: "eslintConfig in package.json"
438
+ };
439
+ }
440
+ if (existsSync5(join5(projectDir, "biome.json")) || existsSync5(join5(projectDir, "biome.jsonc"))) {
441
+ const signal = existsSync5(join5(projectDir, "biome.json")) ? "biome.json" : "biome.jsonc";
442
+ return { id: "biome-lint", name: "Biome lint", confidence: "high", signal };
443
+ }
444
+ if (existsSync5(join5(projectDir, "tsconfig.json"))) {
445
+ return {
446
+ id: "typescript",
447
+ name: "TypeScript",
448
+ confidence: "high",
449
+ signal: "tsconfig.json"
450
+ };
451
+ }
452
+ const pyproject = readTextFile(join5(projectDir, "pyproject.toml"));
453
+ if (pyproject && pyproject.includes("[tool.ruff]")) {
454
+ return {
455
+ id: "ruff",
456
+ name: "Ruff",
457
+ confidence: "high",
458
+ signal: "[tool.ruff] in pyproject.toml"
459
+ };
460
+ }
461
+ if (existsSync5(join5(projectDir, ".flake8"))) {
462
+ return { id: "flake8", name: "Flake8", confidence: "medium", signal: ".flake8" };
463
+ }
464
+ const setupCfg = readTextFile(join5(projectDir, "setup.cfg"));
465
+ if (setupCfg && setupCfg.includes("[flake8]")) {
466
+ return {
467
+ id: "flake8",
468
+ name: "Flake8",
469
+ confidence: "medium",
470
+ signal: "[flake8] in setup.cfg"
471
+ };
472
+ }
473
+ if (existsSync5(join5(projectDir, ".golangci.yml")) || existsSync5(join5(projectDir, ".golangci.yaml"))) {
474
+ const signal = existsSync5(join5(projectDir, ".golangci.yml")) ? ".golangci.yml" : ".golangci.yaml";
475
+ return { id: "golangci-lint", name: "golangci-lint", confidence: "high", signal };
476
+ }
477
+ if (existsSync5(join5(projectDir, "Cargo.toml"))) {
478
+ return {
479
+ id: "clippy",
480
+ name: "Clippy",
481
+ confidence: "medium",
482
+ signal: "Rust project (Cargo.toml)"
483
+ };
484
+ }
485
+ const phpstanFile = hasFileMatching(projectDir, "phpstan.neon");
486
+ if (phpstanFile) {
487
+ return { id: "phpstan", name: "PHPStan", confidence: "high", signal: phpstanFile };
488
+ }
489
+ return null;
490
+ }
491
+ function hasFileMatching2(projectDir, prefix) {
492
+ try {
493
+ const entries = readdirSync3(projectDir);
494
+ const match = entries.find((e) => e.startsWith(prefix));
495
+ return match ?? null;
496
+ } catch {
497
+ return null;
498
+ }
499
+ }
500
+ function hasTestGoFiles(projectDir) {
501
+ try {
502
+ const entries = readdirSync3(projectDir);
503
+ return entries.some((e) => e.endsWith("_test.go"));
504
+ } catch {
505
+ return false;
506
+ }
507
+ }
508
+ function detectTestRunner(projectDir) {
509
+ const vitestConfigs = [
510
+ "vitest.config.ts",
511
+ "vitest.config.js",
512
+ "vitest.config.mts",
513
+ "vitest.config.mjs"
514
+ ];
515
+ for (const config of vitestConfigs) {
516
+ if (existsSync6(join6(projectDir, config))) {
517
+ return { id: "vitest", name: "Vitest", confidence: "high", signal: config };
518
+ }
519
+ }
520
+ const nodeDeps = getNodeDependencies(projectDir);
521
+ if (nodeDeps.has("vitest")) {
522
+ return {
523
+ id: "vitest",
524
+ name: "Vitest",
525
+ confidence: "high",
526
+ signal: "vitest in package.json"
527
+ };
528
+ }
529
+ const jestConfigs = [
530
+ "jest.config.ts",
531
+ "jest.config.js",
532
+ "jest.config.mjs",
533
+ "jest.config.cjs"
534
+ ];
535
+ for (const config of jestConfigs) {
536
+ if (existsSync6(join6(projectDir, config))) {
537
+ return { id: "jest", name: "Jest", confidence: "high", signal: config };
538
+ }
539
+ }
540
+ const pkg = readJsonFile(join6(projectDir, "package.json"));
541
+ if (pkg && "jest" in pkg) {
542
+ return { id: "jest", name: "Jest", confidence: "high", signal: "jest key in package.json" };
543
+ }
544
+ if (nodeDeps.has("jest")) {
545
+ return { id: "jest", name: "Jest", confidence: "high", signal: "jest in package.json" };
546
+ }
547
+ if (existsSync6(join6(projectDir, "pytest.ini"))) {
548
+ return { id: "pytest", name: "pytest", confidence: "high", signal: "pytest.ini" };
549
+ }
550
+ if (existsSync6(join6(projectDir, "pyproject.toml"))) {
551
+ const pyproject = readTextFile(join6(projectDir, "pyproject.toml"));
552
+ if (pyproject && pyproject.includes("[tool.pytest")) {
553
+ return {
554
+ id: "pytest",
555
+ name: "pytest",
556
+ confidence: "high",
557
+ signal: "[tool.pytest] in pyproject.toml"
558
+ };
559
+ }
560
+ }
561
+ if (existsSync6(join6(projectDir, "go.mod")) && hasTestGoFiles(projectDir)) {
562
+ return {
563
+ id: "gotest",
564
+ name: "go test",
565
+ confidence: "medium",
566
+ signal: "*_test.go files in project"
567
+ };
568
+ }
569
+ if (existsSync6(join6(projectDir, "Cargo.toml"))) {
570
+ return {
571
+ id: "cargo-test",
572
+ name: "cargo test",
573
+ confidence: "medium",
574
+ signal: "Rust project (Cargo.toml)"
575
+ };
576
+ }
577
+ const phpunitFile = hasFileMatching2(projectDir, "phpunit.xml");
578
+ if (phpunitFile) {
579
+ return { id: "phpunit", name: "PHPUnit", confidence: "high", signal: phpunitFile };
580
+ }
581
+ const composerDeps = getComposerDependencies(projectDir);
582
+ if (composerDeps.has("pestphp/pest")) {
583
+ return {
584
+ id: "pest",
585
+ name: "Pest",
586
+ confidence: "high",
587
+ signal: "pestphp/pest in composer.json"
588
+ };
589
+ }
590
+ return null;
591
+ }
592
+ function getGitBranches(projectDir) {
593
+ try {
594
+ const output = execFileSync("git", ["branch", "-a"], {
595
+ cwd: projectDir,
596
+ encoding: "utf-8",
597
+ stdio: ["pipe", "pipe", "pipe"]
598
+ }).toString();
599
+ return output.split("\n").map((b) => b.trim().replace(/^\*\s*/, "").replace(/^remotes\/origin\//, "")).filter(Boolean);
600
+ } catch {
601
+ return [];
602
+ }
603
+ }
604
+ function getRecentCommitMessages(projectDir, count = 20) {
605
+ try {
606
+ const output = execFileSync("git", ["log", "--oneline", `-${count}`, "--format=%s"], {
607
+ cwd: projectDir,
608
+ encoding: "utf-8",
609
+ stdio: ["pipe", "pipe", "pipe"]
610
+ }).toString();
611
+ return output.split("\n").filter(Boolean);
612
+ } catch {
613
+ return [];
614
+ }
615
+ }
616
+ function detectGitWorkflow(projectDir) {
617
+ const branches = getGitBranches(projectDir);
618
+ if (existsSync7(join7(projectDir, "pnpm-workspace.yaml")) || existsSync7(join7(projectDir, "lerna.json")) || existsSync7(join7(projectDir, "nx.json"))) {
619
+ const signal = existsSync7(join7(projectDir, "pnpm-workspace.yaml")) ? "pnpm-workspace.yaml" : existsSync7(join7(projectDir, "lerna.json")) ? "lerna.json" : "nx.json";
620
+ return { id: "monorepo", name: "Monorepo", confidence: "high", signal };
621
+ }
622
+ if (existsSync7(join7(projectDir, ".gitmodules"))) {
623
+ return {
624
+ id: "git-submodules",
625
+ name: "Git Submodules",
626
+ confidence: "high",
627
+ signal: ".gitmodules"
628
+ };
629
+ }
630
+ if (branches.length === 0) {
631
+ return null;
632
+ }
633
+ const hasDevelop = branches.some((b) => b === "develop" || b === "development");
634
+ const hasFeatureBranches = branches.some((b) => b.startsWith("feature/"));
635
+ const hasReleaseBranches = branches.some((b) => b.startsWith("release/"));
636
+ const hasHotfixBranches = branches.some((b) => b.startsWith("hotfix/"));
637
+ if (hasDevelop && (hasFeatureBranches || hasReleaseBranches || hasHotfixBranches)) {
638
+ return {
639
+ id: "git-flow",
640
+ name: "Git Flow",
641
+ confidence: "high",
642
+ signal: "develop branch with feature/release/hotfix branches"
643
+ };
644
+ }
645
+ if (hasFeatureBranches && !hasDevelop) {
646
+ return {
647
+ id: "github-flow",
648
+ name: "GitHub Flow",
649
+ confidence: "medium",
650
+ signal: "feature/* branches without develop"
651
+ };
652
+ }
653
+ const messages = getRecentCommitMessages(projectDir);
654
+ if (messages.length > 0) {
655
+ const conventionalPattern = /^(feat|fix|chore|docs|style|refactor|perf|test|build|ci|revert)(\(.+\))?:/;
656
+ const conventionalCount = messages.filter((m) => conventionalPattern.test(m)).length;
657
+ if (conventionalCount >= messages.length * 0.5 && conventionalCount >= 3) {
658
+ return {
659
+ id: "conventional-commits",
660
+ name: "Conventional Commits",
661
+ confidence: "medium",
662
+ signal: `${conventionalCount}/${messages.length} commits match conventional pattern`
663
+ };
664
+ }
665
+ }
666
+ if (branches.length > 0) {
667
+ return { id: "simple", name: "Simple", confidence: "low", signal: "no clear workflow pattern" };
668
+ }
669
+ return null;
670
+ }
671
+ function detectOrm(projectDir) {
672
+ const nodeDeps = getNodeDependencies(projectDir);
673
+ if (existsSync8(join8(projectDir, "prisma", "schema.prisma"))) {
674
+ return {
675
+ id: "prisma",
676
+ name: "Prisma",
677
+ confidence: "high",
678
+ signal: "prisma/schema.prisma"
679
+ };
680
+ }
681
+ if (nodeDeps.has("prisma") || nodeDeps.has("@prisma/client")) {
682
+ return {
683
+ id: "prisma",
684
+ name: "Prisma",
685
+ confidence: "high",
686
+ signal: "prisma in package.json"
687
+ };
688
+ }
689
+ const drizzleConfigs = [
690
+ "drizzle.config.ts",
691
+ "drizzle.config.js",
692
+ "drizzle.config.mjs",
693
+ "drizzle.config.mts"
694
+ ];
695
+ for (const config of drizzleConfigs) {
696
+ if (existsSync8(join8(projectDir, config))) {
697
+ return { id: "drizzle", name: "Drizzle", confidence: "high", signal: config };
698
+ }
699
+ }
700
+ if (nodeDeps.has("drizzle-orm")) {
701
+ return {
702
+ id: "drizzle",
703
+ name: "Drizzle",
704
+ confidence: "high",
705
+ signal: "drizzle-orm in package.json"
706
+ };
707
+ }
708
+ if (nodeDeps.has("typeorm")) {
709
+ return {
710
+ id: "typeorm",
711
+ name: "TypeORM",
712
+ confidence: "high",
713
+ signal: "typeorm in package.json"
714
+ };
715
+ }
716
+ if (nodeDeps.has("sequelize")) {
717
+ return {
718
+ id: "sequelize",
719
+ name: "Sequelize",
720
+ confidence: "high",
721
+ signal: "sequelize in package.json"
722
+ };
723
+ }
724
+ const pyDeps = getPythonDependencies(projectDir);
725
+ if (pyDeps.has("django")) {
726
+ return {
727
+ id: "django-orm",
728
+ name: "Django ORM",
729
+ confidence: "high",
730
+ signal: "django in Python dependencies"
731
+ };
732
+ }
733
+ if (pyDeps.has("sqlalchemy")) {
734
+ return {
735
+ id: "sqlalchemy",
736
+ name: "SQLAlchemy",
737
+ confidence: "high",
738
+ signal: "sqlalchemy in requirements"
739
+ };
740
+ }
741
+ const gemDeps = getGemDependencies(projectDir);
742
+ if (gemDeps.has("activerecord") || gemDeps.has("rails")) {
743
+ const signal = gemDeps.has("activerecord") ? "activerecord in Gemfile" : "rails in Gemfile";
744
+ return { id: "activerecord", name: "ActiveRecord", confidence: "high", signal };
745
+ }
746
+ return null;
747
+ }
748
+ function safeDetect(name, fn) {
749
+ try {
750
+ return fn();
751
+ } catch (err) {
752
+ const message = err instanceof Error ? err.message : String(err);
753
+ console.error(`[ulpi] Warning: ${name} detector failed: ${message}`);
754
+ return null;
755
+ }
756
+ }
757
+ function detectStack(projectDir) {
758
+ const runtime = safeDetect("runtime", () => detectRuntime(projectDir));
759
+ const language = safeDetect("language", () => detectLanguage(projectDir));
760
+ const framework = safeDetect("framework", () => detectFramework(projectDir));
761
+ const packageManager = safeDetect("packageManager", () => detectPackageManager(projectDir));
762
+ const formatter = safeDetect("formatter", () => detectFormatter(projectDir));
763
+ const linter = safeDetect("linter", () => detectLinter(projectDir));
764
+ const testRunner = safeDetect("testRunner", () => detectTestRunner(projectDir));
765
+ const gitWorkflow = safeDetect("gitWorkflow", () => detectGitWorkflow(projectDir));
766
+ const orm = safeDetect("orm", () => detectOrm(projectDir));
767
+ const features = [];
768
+ if (language?.id === "typescript") features.push("typescript");
769
+ if (orm) features.push("database");
770
+ if (testRunner) features.push("testing");
771
+ if (formatter) features.push("formatting");
772
+ if (linter) features.push("linting");
773
+ if (gitWorkflow?.id === "monorepo") features.push("monorepo");
774
+ if (gitWorkflow?.id === "conventional-commits") features.push("conventional-commits");
775
+ return {
776
+ runtime,
777
+ language,
778
+ framework,
779
+ packageManager,
780
+ formatter,
781
+ linter,
782
+ testRunner,
783
+ orm,
784
+ gitWorkflow,
785
+ deployment: null,
786
+ // Reserved for future deployment detector
787
+ features
788
+ };
789
+ }
790
+
791
+ export {
792
+ detectStack
793
+ };