@geoql/doctor-core 1.3.3 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -12,8 +12,8 @@ import { fileURLToPath } from "node:url";
12
12
  import { babelParse, parse } from "@vue/compiler-sfc";
13
13
  import { loadConfig } from "c12";
14
14
  import { z } from "zod";
15
- import { promisify } from "node:util";
16
15
  import process$1 from "node:process";
16
+ import { promisify } from "node:util";
17
17
  import pc from "picocolors";
18
18
  //#region \0rolldown/runtime.js
19
19
  var __defProp = Object.defineProperty;
@@ -337,7 +337,7 @@ async function buildKnipConfig(projectInfo, doctorConfig) {
337
337
  "!**/knip.config.mjs"
338
338
  ],
339
339
  ignoreFiles: [...doctorConfig.exclude, "knip.config.mjs"],
340
- ignoreDependencies: [...new Set([
340
+ ignoreDependencies: [.../* @__PURE__ */ new Set([
341
341
  "vite-plus",
342
342
  "@geoql/vue-doctor",
343
343
  "@geoql/nuxt-doctor",
@@ -808,9 +808,10 @@ const RULE_ID$17 = "nuxt-doctor/nitro/compatibilityDate-set";
808
808
  function checkCompatibilityDateSet(projectInfo, nuxtConfig) {
809
809
  if (projectInfo.packageJsonPath === null) return [];
810
810
  if (nuxtConfig?.compatibilityDate !== void 0) return [];
811
+ const file = projectInfo.nuxtConfigPath ?? projectInfo.packageJsonPath;
811
812
  return [{
812
813
  ruleId: RULE_ID$17,
813
- file: projectInfo.nuxtConfigPath ?? projectInfo.packageJsonPath,
814
+ file,
814
815
  line: 1,
815
816
  column: 1,
816
817
  severity: "error",
@@ -824,9 +825,10 @@ const RULE_ID$16 = "nuxt-doctor/seo/lang-on-html";
824
825
  function checkLangOnHtml(projectInfo, nuxtConfig) {
825
826
  if (projectInfo.packageJsonPath === null) return [];
826
827
  if (nuxtConfig?.htmlLang !== void 0) return [];
828
+ const file = projectInfo.nuxtConfigPath ?? projectInfo.packageJsonPath;
827
829
  return [{
828
830
  ruleId: RULE_ID$16,
829
- file: projectInfo.nuxtConfigPath ?? projectInfo.packageJsonPath,
831
+ file,
830
832
  line: 1,
831
833
  column: 1,
832
834
  severity: "warn",
@@ -837,7 +839,7 @@ function checkLangOnHtml(projectInfo, nuxtConfig) {
837
839
  //#endregion
838
840
  //#region src/nuxt/post-checks/nitro-cloudflare-preset.ts
839
841
  const RULE_ID$15 = "nuxt-doctor/cloudflare/nitro-cloudflare-preset";
840
- const CLOUDFLARE_PRESETS = new Set([
842
+ const CLOUDFLARE_PRESETS = /* @__PURE__ */ new Set([
841
843
  "cloudflare-pages",
842
844
  "cloudflare-module",
843
845
  "cloudflare"
@@ -847,9 +849,10 @@ function checkNitroCloudflarePreset(projectInfo) {
847
849
  if (!projectInfo.hasWranglerConfig) return [];
848
850
  if (projectInfo.nitroPreset === null) return [];
849
851
  if (CLOUDFLARE_PRESETS.has(projectInfo.nitroPreset)) return [];
852
+ const file = projectInfo.nuxtConfigPath ?? projectInfo.packageJsonPath;
850
853
  return [{
851
854
  ruleId: RULE_ID$15,
852
- file: projectInfo.nuxtConfigPath ?? projectInfo.packageJsonPath,
855
+ file,
853
856
  line: 1,
854
857
  column: 1,
855
858
  severity: "warn",
@@ -860,7 +863,7 @@ function checkNitroCloudflarePreset(projectInfo) {
860
863
  //#endregion
861
864
  //#region src/nuxt/post-checks/no-modules-incompatible-with-nuxt-4.ts
862
865
  const RULE_ID$14 = "nuxt-doctor/modules-deps/no-modules-incompatible-with-nuxt-4";
863
- const INCOMPATIBLE_MODULES = new Set([
866
+ const INCOMPATIBLE_MODULES = /* @__PURE__ */ new Set([
864
867
  "@nuxtjs/composition-api",
865
868
  "@nuxt/bridge",
866
869
  "nuxt-property-decorator"
@@ -884,7 +887,7 @@ async function checkNoModulesIncompatibleWithNuxt4(projectInfo) {
884
887
  //#endregion
885
888
  //#region src/nuxt/post-checks/no-node-only-modules.ts
886
889
  const RULE_ID$13 = "nuxt-doctor/cloudflare/no-node-only-modules";
887
- const NODE_ONLY_MODULES = new Set(["fs-extra", "sharp"]);
890
+ const NODE_ONLY_MODULES = /* @__PURE__ */ new Set(["fs-extra", "sharp"]);
888
891
  async function checkNoNodeOnlyModules(projectInfo) {
889
892
  if (projectInfo.packageJsonPath === null) return [];
890
893
  if (!projectInfo.hasWranglerConfig) return [];
@@ -1030,7 +1033,7 @@ async function checkOgImageViaSatori(projectInfo) {
1030
1033
  if (!projectInfo.hasWranglerConfig) return [];
1031
1034
  const deps = await readDeps(projectInfo.packageJsonPath);
1032
1035
  if (deps === null) return [];
1033
- const installed = new Set([...Object.keys(deps.dependencies), ...Object.keys(deps.devDependencies)]);
1036
+ const installed = /* @__PURE__ */ new Set([...Object.keys(deps.dependencies), ...Object.keys(deps.devDependencies)]);
1034
1037
  if (OG_IMAGE_MODULES.some((name) => installed.has(name))) return [];
1035
1038
  return [{
1036
1039
  ruleId: RULE_ID$10,
@@ -1049,9 +1052,10 @@ function checkPresetDefinedForDeployTarget(projectInfo) {
1049
1052
  if (projectInfo.packageJsonPath === null) return [];
1050
1053
  if (!projectInfo.hasWranglerConfig) return [];
1051
1054
  if (projectInfo.nitroPreset !== null) return [];
1055
+ const file = projectInfo.nuxtConfigPath ?? projectInfo.packageJsonPath;
1052
1056
  return [{
1053
1057
  ruleId: RULE_ID$9,
1054
- file: projectInfo.nuxtConfigPath ?? projectInfo.packageJsonPath,
1058
+ file,
1055
1059
  line: 1,
1056
1060
  column: 1,
1057
1061
  severity: "warn",
@@ -1080,7 +1084,7 @@ async function checkRecommendedModulesInstalled(projectInfo) {
1080
1084
  if (projectInfo.packageJsonPath === null) return [];
1081
1085
  const deps = await readDeps(projectInfo.packageJsonPath);
1082
1086
  if (deps === null) return [];
1083
- const installed = new Set([...Object.keys(deps.dependencies), ...Object.keys(deps.devDependencies)]);
1087
+ const installed = /* @__PURE__ */ new Set([...Object.keys(deps.dependencies), ...Object.keys(deps.devDependencies)]);
1084
1088
  return RECOMMENDED_GROUPS.filter((group) => !group.candidates.some((name) => installed.has(name))).map((group, index) => ({
1085
1089
  ruleId: RULE_ID$8,
1086
1090
  file: projectInfo.packageJsonPath,
@@ -1462,6 +1466,7 @@ async function detectProject(rootDirectory) {
1462
1466
  });
1463
1467
  return {
1464
1468
  framework,
1469
+ frameworkDetected: framework === "vue" || framework === "nuxt",
1465
1470
  rootDirectory,
1466
1471
  packageJsonPath,
1467
1472
  vueVersion,
@@ -1592,7 +1597,7 @@ const NUXT_PLUGIN_RULES = {
1592
1597
  "nuxt-doctor/hydration/clientOnly-for-browser-apis": "error",
1593
1598
  "nuxt-doctor/security/no-user-input-in-fetch-url": "warn"
1594
1599
  };
1595
- const VUE_OXLINT_RULE_IDS = new Set([
1600
+ const VUE_OXLINT_RULE_IDS = /* @__PURE__ */ new Set([
1596
1601
  "vue/no-export-in-script-setup",
1597
1602
  "vue/require-typed-ref",
1598
1603
  "vue/no-arrow-functions-in-watch",
@@ -1640,7 +1645,7 @@ const VUE_OXLINT_RULE_IDS = new Set([
1640
1645
  const NUXT_OXLINT_RULE_IDS = new Set(Object.keys(NUXT_PLUGIN_RULES));
1641
1646
  function oxlintRuleAllowlist(framework) {
1642
1647
  if (framework !== "nuxt") return VUE_OXLINT_RULE_IDS;
1643
- return new Set([...VUE_OXLINT_RULE_IDS, ...NUXT_OXLINT_RULE_IDS]);
1648
+ return /* @__PURE__ */ new Set([...VUE_OXLINT_RULE_IDS, ...NUXT_OXLINT_RULE_IDS]);
1644
1649
  }
1645
1650
  function toOxlintSeverity(s) {
1646
1651
  if (s === "error") return "error";
@@ -1819,12 +1824,24 @@ async function runOxlint(opts) {
1819
1824
  let timer;
1820
1825
  const timeoutMs = opts.timeoutMs ?? 6e4;
1821
1826
  const maxOutputBytes = opts.maxOutputBytes ?? 33554432;
1827
+ const onAbort = () => {
1828
+ child.kill("SIGKILL");
1829
+ finish(() => reject(/* @__PURE__ */ new Error("oxlint subprocess aborted")));
1830
+ };
1822
1831
  const finish = (fn) => {
1823
1832
  if (settled) return;
1824
1833
  settled = true;
1825
1834
  if (timer) clearTimeout(timer);
1835
+ opts.signal?.removeEventListener("abort", onAbort);
1826
1836
  fn();
1827
1837
  };
1838
+ if (opts.signal) {
1839
+ if (opts.signal.aborted) {
1840
+ onAbort();
1841
+ return;
1842
+ }
1843
+ opts.signal.addEventListener("abort", onAbort, { once: true });
1844
+ }
1828
1845
  if (timeoutMs > 0) timer = setTimeout(() => {
1829
1846
  child.kill("SIGKILL");
1830
1847
  finish(() => reject(/* @__PURE__ */ new Error(`oxlint subprocess timed out after ${timeoutMs}ms`)));
@@ -1906,7 +1923,8 @@ async function runScriptPass(opts) {
1906
1923
  configPath,
1907
1924
  oxlintBin,
1908
1925
  timeoutMs: opts.timeoutMs,
1909
- fix: opts.fix
1926
+ fix: opts.fix,
1927
+ signal: opts.signal
1910
1928
  });
1911
1929
  return {
1912
1930
  diagnostics: toCanonicalDiagnostics(raw.diagnostics, opts.rootDir),
@@ -2819,7 +2837,7 @@ async function parseSfcDescriptor(absPath) {
2819
2837
  const RULE_ID$5 = "vue-doctor/sfc/no-mixed-options-and-composition-api";
2820
2838
  const MESSAGE$5 = "Mixed Options API in a <script setup> SFC. Move data/methods/computed/watch/lifecycle into <script setup> Composition API; keep <script> only for options like name/inheritAttrs. See https://vuejs.org/api/sfc-script-setup.html#usage-alongside-normal-script";
2821
2839
  const RECOMMENDATION$5 = "Move this option into the <script setup> block using the Composition API, or keep <script> only for options-only config such as name or inheritAttrs.";
2822
- const DISALLOWED = new Set([
2840
+ const DISALLOWED = /* @__PURE__ */ new Set([
2823
2841
  "data",
2824
2842
  "methods",
2825
2843
  "computed",
@@ -3000,7 +3018,7 @@ function check$20(ctx) {
3000
3018
  const RULE_ID$3 = "nuxt-doctor/seo/og-image-defined";
3001
3019
  const MESSAGE$3 = "Page uses SEO meta but has no og:image property. Open Graph images improve social sharing previews. Add an og:image value or install @nuxtjs/og-image for automatic OG images.";
3002
3020
  const RECOMMENDATION$3 = "Add ogImage: \"/path/to/image.png\" to useSeoMeta / useHead, or install @nuxtjs/og-image.";
3003
- const SEO_PRIMITIVES$1 = new Set(["useSeoMeta", "useHead"]);
3021
+ const SEO_PRIMITIVES$1 = /* @__PURE__ */ new Set(["useSeoMeta", "useHead"]);
3004
3022
  function isSeoWrapperName$1(name) {
3005
3023
  if (SEO_PRIMITIVES$1.has(name)) return false;
3006
3024
  return /^use[a-z]*seo/i.test(name);
@@ -3066,7 +3084,7 @@ function check$19(ctx) {
3066
3084
  const RULE_ID$2 = "nuxt-doctor/seo/useSeoMeta-on-public-page";
3067
3085
  const MESSAGE$2 = "Public page component is missing SEO metadata. Add useSeoMeta, useHead, or definePageMeta with a title so search engines can index it properly.";
3068
3086
  const RECOMMENDATION$2 = "Call useSeoMeta({ title: \"...\" }) in<script setup> to define page title and meta tags for search engines and social previews.";
3069
- const SEO_PRIMITIVES = new Set([
3087
+ const SEO_PRIMITIVES = /* @__PURE__ */ new Set([
3070
3088
  "useSeoMeta",
3071
3089
  "useHead",
3072
3090
  "definePageMeta"
@@ -3259,7 +3277,7 @@ function ruleNoSharedKeyAcrossPages(pages) {
3259
3277
  }
3260
3278
  return diags;
3261
3279
  }
3262
- const BROWSER_GLOBALS = new Set([
3280
+ const BROWSER_GLOBALS = /* @__PURE__ */ new Set([
3263
3281
  "window",
3264
3282
  "document",
3265
3283
  "navigator",
@@ -3303,7 +3321,7 @@ function ruleSsrSafeOnMountedOnlyForClient(pages) {
3303
3321
  }
3304
3322
  return diags;
3305
3323
  }
3306
- const CLIENT_SAFE_WRAPPERS = new Set([
3324
+ const CLIENT_SAFE_WRAPPERS = /* @__PURE__ */ new Set([
3307
3325
  "onMounted",
3308
3326
  "watchEffect",
3309
3327
  "watch"
@@ -3739,7 +3757,7 @@ function check$8(ctx) {
3739
3757
  //#endregion
3740
3758
  //#region src/template/rules/no-javascript-uri.ts
3741
3759
  const NODE_ATTRIBUTE$1 = 6;
3742
- const URI_SINKS = new Set([
3760
+ const URI_SINKS = /* @__PURE__ */ new Set([
3743
3761
  "href",
3744
3762
  "src",
3745
3763
  "srcset",
@@ -3972,7 +3990,7 @@ function check$2(ctx) {
3972
3990
  }
3973
3991
  //#endregion
3974
3992
  //#region src/template/rules/no-missing-alt.ts
3975
- const IMAGE_TAGS = new Set([
3993
+ const IMAGE_TAGS = /* @__PURE__ */ new Set([
3976
3994
  "img",
3977
3995
  "NuxtImg",
3978
3996
  "nuxt-img"
@@ -4200,24 +4218,36 @@ async function audit(config = {}) {
4200
4218
  exclude
4201
4219
  });
4202
4220
  const overallStart = performance.now();
4221
+ const maxDurationMs = config.maxDurationMs;
4222
+ const deadlineMs = maxDurationMs ?? 0;
4223
+ const budgetExhausted = () => maxDurationMs !== void 0 && performance.now() - overallStart >= maxDurationMs;
4224
+ const skippedPasses = [];
4225
+ const deadlineController = new AbortController();
4226
+ let deadlineTimer;
4227
+ if (maxDurationMs !== void 0) deadlineTimer = setTimeout(() => deadlineController.abort(), maxDurationMs);
4203
4228
  const project = await detectProject(rootDir);
4204
4229
  const templateStart = performance.now();
4205
- const templateDiagnostics = lintEnabled ? await runTemplatePass({
4230
+ let templateDiagnostics = [];
4231
+ if (lintEnabled) if (budgetExhausted()) skippedPasses.push("template");
4232
+ else templateDiagnostics = await runTemplatePass({
4206
4233
  files,
4207
4234
  ruleOverrides: config.rules
4208
- }) : [];
4235
+ });
4209
4236
  const templateElapsed = performance.now() - templateStart;
4210
4237
  const sfcStart = performance.now();
4211
- const sfcDiagnostics = lintEnabled ? await runSfcPass({
4238
+ let sfcDiagnostics = [];
4239
+ if (lintEnabled) if (budgetExhausted()) skippedPasses.push("sfc");
4240
+ else sfcDiagnostics = await runSfcPass({
4212
4241
  files,
4213
4242
  ruleOverrides: config.rules,
4214
4243
  projectInfo: project
4215
- }) : [];
4244
+ });
4216
4245
  const sfcElapsed = performance.now() - sfcStart;
4217
4246
  let scriptDiagnostics = [];
4218
4247
  let oxlintStderr = "";
4219
4248
  const scriptStart = performance.now();
4220
- if (lintEnabled) try {
4249
+ if (lintEnabled) if (budgetExhausted()) skippedPasses.push("lint");
4250
+ else try {
4221
4251
  const result = await runScriptPass({
4222
4252
  rootDir,
4223
4253
  targetPath: rootDir,
@@ -4225,18 +4255,26 @@ async function audit(config = {}) {
4225
4255
  framework: project.framework === "nuxt" ? "nuxt" : "vue",
4226
4256
  fix: config.fix === true && config.scopeFiles === void 0,
4227
4257
  fixExcludes: config.fixExcludes,
4228
- exclude
4258
+ exclude,
4259
+ signal: deadlineController.signal
4229
4260
  });
4230
4261
  scriptDiagnostics = result.diagnostics;
4231
4262
  oxlintStderr = result.stderr;
4232
4263
  } catch (err) {
4233
- oxlintStderr = err instanceof Error ? err.message : String(err);
4234
- if (process.env.DOCTOR_DEBUG) process.stderr.write(`[doctor-core] script pass failed: ${oxlintStderr}\n`);
4264
+ if (deadlineController.signal.aborted) {
4265
+ skippedPasses.push("lint");
4266
+ oxlintStderr = "aborted: time-budget exhausted";
4267
+ } else {
4268
+ oxlintStderr = err instanceof Error ? err.message : String(err);
4269
+ if (process.env.DOCTOR_DEBUG) process.stderr.write(`[doctor-core] script pass failed: ${oxlintStderr}\n`);
4270
+ }
4235
4271
  }
4236
4272
  const scriptElapsed = performance.now() - scriptStart;
4237
4273
  let deadCodeDiagnostics = [];
4238
4274
  let deadCodeElapsed = 0;
4239
- if (config.deadCode !== false) {
4275
+ const deadCodeEnabled = config.deadCode !== false && !budgetExhausted();
4276
+ if (config.deadCode !== false && !deadCodeEnabled) skippedPasses.push("dead-code");
4277
+ if (deadCodeEnabled) {
4240
4278
  const deadCodeStart = performance.now();
4241
4279
  try {
4242
4280
  const { loadDoctorConfig } = await Promise.resolve().then(() => load_exports);
@@ -4255,48 +4293,54 @@ async function audit(config = {}) {
4255
4293
  deadCodeElapsed = performance.now() - deadCodeStart;
4256
4294
  }
4257
4295
  let buildQualityDiagnostics = [];
4258
- try {
4259
- buildQualityDiagnostics = (await checkBuildQuality(project)).filter((d) => config.rules?.[d.ruleId] !== "off").map((d) => {
4260
- const override = config.rules?.[d.ruleId];
4261
- return override ? {
4262
- ...d,
4263
- severity: override
4264
- } : d;
4265
- });
4266
- } catch {}
4267
4296
  let depsDiagnostics = [];
4268
- try {
4269
- depsDiagnostics = (await checkDeps(project)).filter((d) => config.rules?.[d.ruleId] !== "off").map((d) => {
4270
- const override = config.rules?.[d.ruleId];
4271
- return override ? {
4272
- ...d,
4273
- severity: override
4274
- } : d;
4275
- });
4276
- } catch {}
4277
4297
  let nuxtProjectDiagnostics = [];
4278
- if (project.framework === "nuxt") try {
4279
- nuxtProjectDiagnostics = (await checkNuxtProject(project)).filter((d) => config.rules?.[d.ruleId] !== "off").map((d) => {
4280
- const override = config.rules?.[d.ruleId];
4281
- return override ? {
4282
- ...d,
4283
- severity: override
4284
- } : d;
4285
- });
4286
- } catch {}
4287
4298
  let crossFileDiagnostics = [];
4288
- if (project.framework === "nuxt") try {
4289
- crossFileDiagnostics = (await runCrossFilePass({
4290
- files,
4291
- projectInfo: project
4292
- })).filter((d) => config.rules?.[d.ruleId] !== "off").map((d) => {
4293
- const override = config.rules?.[d.ruleId];
4294
- return override ? {
4295
- ...d,
4296
- severity: override
4297
- } : d;
4298
- });
4299
- } catch {}
4299
+ if (budgetExhausted()) skippedPasses.push("project");
4300
+ else {
4301
+ try {
4302
+ buildQualityDiagnostics = (await checkBuildQuality(project)).filter((d) => config.rules?.[d.ruleId] !== "off").map((d) => {
4303
+ const override = config.rules?.[d.ruleId];
4304
+ return override ? {
4305
+ ...d,
4306
+ severity: override
4307
+ } : d;
4308
+ });
4309
+ } catch {}
4310
+ try {
4311
+ depsDiagnostics = (await checkDeps(project)).filter((d) => config.rules?.[d.ruleId] !== "off").map((d) => {
4312
+ const override = config.rules?.[d.ruleId];
4313
+ return override ? {
4314
+ ...d,
4315
+ severity: override
4316
+ } : d;
4317
+ });
4318
+ } catch {}
4319
+ if (project.framework === "nuxt") {
4320
+ try {
4321
+ nuxtProjectDiagnostics = (await checkNuxtProject(project)).filter((d) => config.rules?.[d.ruleId] !== "off").map((d) => {
4322
+ const override = config.rules?.[d.ruleId];
4323
+ return override ? {
4324
+ ...d,
4325
+ severity: override
4326
+ } : d;
4327
+ });
4328
+ } catch {}
4329
+ try {
4330
+ crossFileDiagnostics = (await runCrossFilePass({
4331
+ files,
4332
+ projectInfo: project
4333
+ })).filter((d) => config.rules?.[d.ruleId] !== "off").map((d) => {
4334
+ const override = config.rules?.[d.ruleId];
4335
+ return override ? {
4336
+ ...d,
4337
+ severity: override
4338
+ } : d;
4339
+ });
4340
+ } catch {}
4341
+ }
4342
+ }
4343
+ if (deadlineTimer) clearTimeout(deadlineTimer);
4300
4344
  const elapsedMs = performance.now() - overallStart;
4301
4345
  const timings = {
4302
4346
  template: templateElapsed,
@@ -4315,6 +4359,7 @@ async function audit(config = {}) {
4315
4359
  const ruleCounts = countRuleCounts(diagnostics);
4316
4360
  const projectInfo = {
4317
4361
  framework: project.framework,
4362
+ frameworkDetected: project.frameworkDetected,
4318
4363
  vueVersion: project.vueVersion,
4319
4364
  nuxtVersion: project.nuxtVersion,
4320
4365
  capabilities: [...project.capabilities].sort(),
@@ -4325,6 +4370,13 @@ async function audit(config = {}) {
4325
4370
  else if (failOn !== "none") {
4326
4371
  if ((failOn === "warn" ? scored.errorCount + scored.warnCount : scored.errorCount) > 0) exitCode = 1;
4327
4372
  }
4373
+ const incomplete = skippedPasses.length > 0;
4374
+ const skippedCheckReasons = incomplete ? [{
4375
+ kind: "time-budget-exhausted",
4376
+ deadlineMs,
4377
+ elapsedMs,
4378
+ skippedPasses
4379
+ }] : void 0;
4328
4380
  return {
4329
4381
  rootDir,
4330
4382
  filesScanned: files.length,
@@ -4338,7 +4390,9 @@ async function audit(config = {}) {
4338
4390
  projectInfo,
4339
4391
  elapsedMs,
4340
4392
  timings,
4341
- ruleCounts
4393
+ ruleCounts,
4394
+ incomplete,
4395
+ ...skippedCheckReasons ? { skippedCheckReasons } : {}
4342
4396
  };
4343
4397
  }
4344
4398
  //#endregion
@@ -4564,7 +4618,8 @@ async function loadDoctorConfig(rootDir, explicitPathOrOptions) {
4564
4618
  source = "flag";
4565
4619
  configFile = resolve(rootDir, explicitPath);
4566
4620
  } else if (result._configFile) {
4567
- source = SOURCE_MAP[extname(result._configFile).slice(1)];
4621
+ const ext = extname(result._configFile).slice(1);
4622
+ source = SOURCE_MAP[ext];
4568
4623
  configFile = result._configFile;
4569
4624
  } else if (Object.keys(raw).length > 0) {
4570
4625
  source = "package.json";
@@ -4719,6 +4774,146 @@ async function planInit(options) {
4719
4774
  };
4720
4775
  }
4721
4776
  //#endregion
4777
+ //#region src/ci/provider-detect.ts
4778
+ /**
4779
+ * Detects the CI provider from an explicit env snapshot. Takes the env as an
4780
+ * argument (instead of reading process.env) so provider detection stays a
4781
+ * deterministic, fully-testable pure function.
4782
+ */
4783
+ function detectCiProvider(env) {
4784
+ if (env.GITHUB_ACTIONS === "true") return "github";
4785
+ if (env.GITLAB_CI === "true") return "gitlab";
4786
+ return "unknown";
4787
+ }
4788
+ //#endregion
4789
+ //#region src/ci/scaffold-workflow.ts
4790
+ function renderGithubWorkflow(opts) {
4791
+ return `name: doctor
4792
+
4793
+ on:
4794
+ push:
4795
+ branches: [main]
4796
+ pull_request:
4797
+
4798
+ permissions:
4799
+ contents: read
4800
+ pull-requests: write
4801
+
4802
+ jobs:
4803
+ audit:
4804
+ runs-on: ubuntu-latest
4805
+ steps:
4806
+ - uses: actions/checkout@v6
4807
+
4808
+ - name: Run doctor
4809
+ uses: geoql/doctor-action@v2
4810
+ with:
4811
+ framework: ${opts.framework}
4812
+ preset: recommended
4813
+ threshold: '0'
4814
+ fail-on: error
4815
+ pr-comment: '${opts.prComment}'
4816
+ api-key: \${{ secrets.DOCTOR_API_KEY }}
4817
+ project: \${{ github.repository }}
4818
+ `;
4819
+ }
4820
+ function renderGithubPrWorkflow(opts) {
4821
+ return `name: doctor-pr
4822
+
4823
+ on:
4824
+ pull_request:
4825
+
4826
+ permissions:
4827
+ contents: read
4828
+ pull-requests: write
4829
+
4830
+ jobs:
4831
+ review:
4832
+ runs-on: ubuntu-latest
4833
+ steps:
4834
+ - uses: actions/checkout@v6
4835
+ with:
4836
+ fetch-depth: 0
4837
+
4838
+ - name: Doctor PR review
4839
+ uses: geoql/doctor-action@v2
4840
+ with:
4841
+ framework: ${opts.framework}
4842
+ preset: recommended
4843
+ fail-on: error
4844
+ diff: 'true'
4845
+ pr-comment: 'true'
4846
+ comment-mode: both
4847
+ api-key: \${{ secrets.DOCTOR_API_KEY }}
4848
+ project: \${{ github.repository }}
4849
+ `;
4850
+ }
4851
+ function renderGitlabPipeline(opts) {
4852
+ return `# Doctor quality gate — https://docs.the-doctor.report
4853
+ doctor:
4854
+ image: node:24
4855
+ stage: test
4856
+ script:
4857
+ - npx -y @geoql/${opts.bin}@latest --fail-on error
4858
+ rules:
4859
+ - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
4860
+ - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
4861
+ `;
4862
+ }
4863
+ async function scaffoldCiWorkflow(opts) {
4864
+ const requested = opts.provider ?? "auto";
4865
+ let provider;
4866
+ if (requested === "auto") provider = detectCiProvider(opts.env ?? process$1.env) === "gitlab" ? "gitlab" : "github";
4867
+ else provider = requested;
4868
+ const writes = [];
4869
+ let conflict = false;
4870
+ let conflictPath = null;
4871
+ if (provider === "gitlab") {
4872
+ const pipelinePath = join(opts.dir, ".gitlab-ci.yml");
4873
+ if (!opts.force && await pathExists(pipelinePath)) {
4874
+ conflict = true;
4875
+ conflictPath = pipelinePath;
4876
+ } else writes.push({
4877
+ path: pipelinePath,
4878
+ content: renderGitlabPipeline({ bin: opts.bin })
4879
+ });
4880
+ return {
4881
+ writes,
4882
+ conflict,
4883
+ conflictPath,
4884
+ provider
4885
+ };
4886
+ }
4887
+ const workflowPath = join(opts.dir, ".github", "workflows", "doctor.yml");
4888
+ if (!opts.force && await pathExists(workflowPath)) {
4889
+ conflict = true;
4890
+ conflictPath = workflowPath;
4891
+ return {
4892
+ writes,
4893
+ conflict,
4894
+ conflictPath,
4895
+ provider
4896
+ };
4897
+ }
4898
+ writes.push({
4899
+ path: workflowPath,
4900
+ content: renderGithubWorkflow({
4901
+ framework: opts.framework,
4902
+ prComment: opts.noComments !== true
4903
+ })
4904
+ });
4905
+ if (opts.pr === true) writes.push({
4906
+ path: join(opts.dir, ".github", "workflows", "doctor-pr.yml"),
4907
+ content: renderGithubPrWorkflow({ framework: opts.framework })
4908
+ });
4909
+ return {
4910
+ writes,
4911
+ conflict,
4912
+ conflictPath,
4913
+ provider
4914
+ };
4915
+ }
4916
+ //#endregion
4722
4917
  //#region src/git-scope.ts
4723
4918
  const execFileAsync = promisify(execFile);
4724
4919
  const SOURCE_EXTENSIONS = [
@@ -5119,6 +5314,7 @@ function buildDoctorReport(input) {
5119
5314
  },
5120
5315
  projectInfo: {
5121
5316
  framework: input.projectInfo.framework,
5317
+ frameworkDetected: input.projectInfo.frameworkDetected,
5122
5318
  vueVersion: input.projectInfo.vueVersion,
5123
5319
  nuxtVersion: input.projectInfo.nuxtVersion,
5124
5320
  capabilities: [...input.projectInfo.capabilities].sort(),
@@ -5162,7 +5358,16 @@ function buildDoctorReport(input) {
5162
5358
  timing: {
5163
5359
  elapsedMs: input.elapsedMs,
5164
5360
  analyzedFileCount: input.analyzedFileCount
5165
- }
5361
+ },
5362
+ ...input.incomplete !== void 0 && input.incomplete ? {
5363
+ incomplete: input.incomplete,
5364
+ skippedCheckReasons: input.skippedCheckReasons?.map((r) => ({
5365
+ kind: r.kind,
5366
+ deadlineMs: r.deadlineMs,
5367
+ elapsedMs: r.elapsedMs,
5368
+ skippedPasses: [...r.skippedPasses]
5369
+ }))
5370
+ } : {}
5166
5371
  };
5167
5372
  }
5168
5373
  function jsonReport(input) {
@@ -5643,6 +5848,6 @@ async function pushFindings(opts) {
5643
5848
  }
5644
5849
  }
5645
5850
  //#endregion
5646
- export { BUILT_IN_RECOMMENDED, ConfigCycleError, ConfigFileNotFoundError, DOCTOR_REPORT_SCHEMA_VERSION, DeadCodeImportFailed, DeadCodeTimeoutError, DoctorUserConfigSchema, InvalidConfigError, LevelSchema, OxlintOutputTooLarge, OxlintSpawnFailed, RULE_REGISTRY, RuleEntrySchema, SCORE_DIMENSIONS, agentReport, applyInlineDisables, audit, buildDoctorReport, buildJsonSchema, buildPushPayload, categoriesForDimension, checkBuildQuality, checkDeadCode, checkDeps, checkNuxtProject, dedupeDeadCodeAgainstLint, defineConfig, detectProject, detectSummary, dimensionForCategory, docsUrl, encodeAnnotation, encodeAnnotations, filterReportByRules, filterRuleIdsByCategory, findMonorepoRoot, format, isNuxtLayoutFile, isNuxtPageFile, isNuxtServerFile, isScoreDimension, jsonCompactReport, jsonReport, listChangedFiles, listRules, listWorkspacePackages, loadAllRuleDocs, loadDoctorConfig, loadRuleDoc, mergeCliOverrides, normalizeInitAnswers, parseDirectives, parseExcludeList, planInit, prCommentReport, prettyReport, pushFindings, renderAgentPlaybook, renderDetectSummary, renderRulePrompt, renderVerboseTrace, resolveCategoryScope, runCrossFilePass, sarifReport, scoreDiagnostics, stripFindings, validateConfig };
5851
+ export { BUILT_IN_RECOMMENDED, ConfigCycleError, ConfigFileNotFoundError, DOCTOR_REPORT_SCHEMA_VERSION, DeadCodeImportFailed, DeadCodeTimeoutError, DoctorUserConfigSchema, InvalidConfigError, LevelSchema, OxlintOutputTooLarge, OxlintSpawnFailed, RULE_REGISTRY, RuleEntrySchema, SCORE_DIMENSIONS, agentReport, applyInlineDisables, audit, buildDoctorReport, buildJsonSchema, buildPushPayload, categoriesForDimension, checkBuildQuality, checkDeadCode, checkDeps, checkNuxtProject, dedupeDeadCodeAgainstLint, defineConfig, detectCiProvider, detectProject, detectSummary, dimensionForCategory, docsUrl, encodeAnnotation, encodeAnnotations, filterReportByRules, filterRuleIdsByCategory, findMonorepoRoot, format, isNuxtLayoutFile, isNuxtPageFile, isNuxtServerFile, isScoreDimension, jsonCompactReport, jsonReport, listChangedFiles, listRules, listWorkspacePackages, loadAllRuleDocs, loadDoctorConfig, loadRuleDoc, mergeCliOverrides, normalizeInitAnswers, parseDirectives, parseExcludeList, planInit, prCommentReport, prettyReport, pushFindings, renderAgentPlaybook, renderDetectSummary, renderRulePrompt, renderVerboseTrace, resolveCategoryScope, runCrossFilePass, sarifReport, scaffoldCiWorkflow, scoreDiagnostics, stripFindings, validateConfig };
5647
5852
 
5648
5853
  //# sourceMappingURL=index.js.map