@geoql/doctor-core 1.3.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -70,6 +70,7 @@ type Capability = string;
70
70
  type MonorepoKind = 'pnpm' | 'yarn' | 'npm' | 'turbo' | null;
71
71
  interface ProjectInfo {
72
72
  readonly framework: Framework;
73
+ readonly frameworkDetected: boolean;
73
74
  readonly rootDirectory: string;
74
75
  readonly packageJsonPath: string | null;
75
76
  readonly vueVersion: string | null;
@@ -94,6 +95,7 @@ interface ProjectInfo {
94
95
  //#region src/reporters/types.d.ts
95
96
  interface ProjectInfoLite {
96
97
  readonly framework: Framework;
98
+ readonly frameworkDetected: boolean;
97
99
  readonly vueVersion: string | null;
98
100
  readonly nuxtVersion: string | null;
99
101
  readonly capabilities: readonly string[];
@@ -108,6 +110,8 @@ interface ReporterInput {
108
110
  readonly diagnostics: readonly Diagnostic[];
109
111
  readonly score: ScoreResult;
110
112
  readonly projectInfo: ProjectInfoLite;
113
+ readonly incomplete?: boolean;
114
+ readonly skippedCheckReasons?: readonly SkippedCheckReason[];
111
115
  }
112
116
  interface ReporterOptions {
113
117
  readonly color?: boolean;
@@ -143,6 +147,13 @@ interface AuditConfig {
143
147
  respectInlineDisables?: boolean;
144
148
  fix?: boolean;
145
149
  fixExcludes?: string[];
150
+ maxDurationMs?: number;
151
+ }
152
+ interface SkippedCheckReason {
153
+ kind: 'time-budget-exhausted';
154
+ deadlineMs: number;
155
+ elapsedMs: number;
156
+ skippedPasses: readonly string[];
146
157
  }
147
158
  interface AuditTimings {
148
159
  template: number;
@@ -165,6 +176,8 @@ interface AuditReport {
165
176
  elapsedMs: number;
166
177
  timings: AuditTimings;
167
178
  ruleCounts: Record<string, number>;
179
+ incomplete: boolean;
180
+ skippedCheckReasons?: readonly SkippedCheckReason[];
168
181
  }
169
182
  //#endregion
170
183
  //#region src/annotations.d.ts
@@ -392,6 +405,34 @@ declare function renderDetectSummary(project: ProjectInfo, sfcCount: number): st
392
405
  declare function detectSummary(dir: string): Promise<string>;
393
406
  declare function planInit(options: InitOptions): Promise<InitPlan>;
394
407
  //#endregion
408
+ //#region src/ci/provider-detect.d.ts
409
+ type CiProvider = 'github' | 'gitlab' | 'unknown';
410
+ /**
411
+ * Detects the CI provider from an explicit env snapshot. Takes the env as an
412
+ * argument (instead of reading process.env) so provider detection stays a
413
+ * deterministic, fully-testable pure function.
414
+ */
415
+ declare function detectCiProvider(env: Readonly<Record<string, string | undefined>>): CiProvider;
416
+ //#endregion
417
+ //#region src/ci/scaffold-workflow.d.ts
418
+ interface CiScaffoldOptions {
419
+ bin: 'vue-doctor' | 'nuxt-doctor';
420
+ framework: 'vue' | 'nuxt';
421
+ dir: string;
422
+ provider?: 'github' | 'gitlab' | 'auto';
423
+ pr?: boolean;
424
+ noComments?: boolean;
425
+ force?: boolean;
426
+ env?: Readonly<Record<string, string | undefined>>;
427
+ }
428
+ interface CiScaffoldPlan {
429
+ writes: InitFileWrite[];
430
+ conflict: boolean;
431
+ conflictPath: string | null;
432
+ provider: 'github' | 'gitlab';
433
+ }
434
+ declare function scaffoldCiWorkflow(opts: CiScaffoldOptions): Promise<CiScaffoldPlan>;
435
+ //#endregion
395
436
  //#region src/project-info/find-monorepo-root.d.ts
396
437
  interface MonorepoResult {
397
438
  root: string;
@@ -519,6 +560,7 @@ interface DoctorReport {
519
560
  };
520
561
  projectInfo: {
521
562
  framework: string;
563
+ frameworkDetected: boolean;
522
564
  vueVersion: string | null;
523
565
  nuxtVersion: string | null;
524
566
  capabilities: string[];
@@ -547,6 +589,8 @@ interface DoctorReport {
547
589
  elapsedMs: number;
548
590
  analyzedFileCount: number;
549
591
  };
592
+ incomplete?: boolean;
593
+ skippedCheckReasons?: SkippedCheckReason[];
550
594
  }
551
595
  declare function buildDoctorReport(input: ReporterInput): DoctorReport;
552
596
  declare function jsonReport(input: ReporterInput): string;
@@ -705,5 +749,5 @@ declare function buildPushPayload(input: PushPayloadInput): PushPayload;
705
749
  */
706
750
  declare function pushFindings(opts: PushOptions): Promise<PushResult>;
707
751
  //#endregion
708
- export { type ApplyInlineDisablesOptions, type AuditConfig, type AuditReport, type AuditTimings, BUILT_IN_RECOMMENDED, type Capability, type CliOverrides, ConfigCycleError, ConfigFileNotFoundError, type ConfigSource, DOCTOR_REPORT_SCHEMA_VERSION, DeadCodeImportFailed, DeadCodeTimeoutError, type Diagnostic, type DiagnosticSource, type DimensionScore, type DirectiveLine, type DirectiveRange, type DirectiveSet, type DoctorReport, type DoctorUserConfig, type DoctorUserConfigInput, DoctorUserConfigSchema, type Framework, type GitScopeMode, type InitConfigFormat, type InitFileWrite, type InitOptions, type InitPlan, type InitPreset, InvalidConfigError, LevelSchema, type ListRulesFilter, type LoadRuleDocOptions, type MonorepoKind, type MonorepoResult, OxlintOutputTooLarge, OxlintSpawnFailed, type ProjectInfo, type ProjectInfoLite, type PushOptions, type PushPayload, type PushPayloadInput, type PushResult, type PushedFinding, RULE_REGISTRY, type RawInitAnswers, type RegisteredRule, type ReporterFormat, type ReporterInput, type ReporterOptions, type ResolvedDoctorConfig, type ResolvedInitAnswers, type RuleCategory, type RuleDoc, RuleEntrySchema, type RuleSource, SCORE_DIMENSIONS, type ScoreBreakdownEntry, type ScoreConfig, type ScoreDimension, type ScoreResult, type Severity, type VerboseTraceOptions, type WorkspacePackage, 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 };
752
+ export { type ApplyInlineDisablesOptions, type AuditConfig, type AuditReport, type AuditTimings, BUILT_IN_RECOMMENDED, type Capability, type CiProvider, type CiScaffoldOptions, type CiScaffoldPlan, type CliOverrides, ConfigCycleError, ConfigFileNotFoundError, type ConfigSource, DOCTOR_REPORT_SCHEMA_VERSION, DeadCodeImportFailed, DeadCodeTimeoutError, type Diagnostic, type DiagnosticSource, type DimensionScore, type DirectiveLine, type DirectiveRange, type DirectiveSet, type DoctorReport, type DoctorUserConfig, type DoctorUserConfigInput, DoctorUserConfigSchema, type Framework, type GitScopeMode, type InitConfigFormat, type InitFileWrite, type InitOptions, type InitPlan, type InitPreset, InvalidConfigError, LevelSchema, type ListRulesFilter, type LoadRuleDocOptions, type MonorepoKind, type MonorepoResult, OxlintOutputTooLarge, OxlintSpawnFailed, type ProjectInfo, type ProjectInfoLite, type PushOptions, type PushPayload, type PushPayloadInput, type PushResult, type PushedFinding, RULE_REGISTRY, type RawInitAnswers, type RegisteredRule, type ReporterFormat, type ReporterInput, type ReporterOptions, type ResolvedDoctorConfig, type ResolvedInitAnswers, type RuleCategory, type RuleDoc, RuleEntrySchema, type RuleSource, SCORE_DIMENSIONS, type ScoreBreakdownEntry, type ScoreConfig, type ScoreDimension, type ScoreResult, type Severity, type SkippedCheckReason, type VerboseTraceOptions, type WorkspacePackage, 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 };
709
753
  //# sourceMappingURL=index.d.ts.map
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;
@@ -1462,6 +1462,7 @@ async function detectProject(rootDirectory) {
1462
1462
  });
1463
1463
  return {
1464
1464
  framework,
1465
+ frameworkDetected: framework === "vue" || framework === "nuxt",
1465
1466
  rootDirectory,
1466
1467
  packageJsonPath,
1467
1468
  vueVersion,
@@ -1819,12 +1820,24 @@ async function runOxlint(opts) {
1819
1820
  let timer;
1820
1821
  const timeoutMs = opts.timeoutMs ?? 6e4;
1821
1822
  const maxOutputBytes = opts.maxOutputBytes ?? 33554432;
1823
+ const onAbort = () => {
1824
+ child.kill("SIGKILL");
1825
+ finish(() => reject(/* @__PURE__ */ new Error("oxlint subprocess aborted")));
1826
+ };
1822
1827
  const finish = (fn) => {
1823
1828
  if (settled) return;
1824
1829
  settled = true;
1825
1830
  if (timer) clearTimeout(timer);
1831
+ opts.signal?.removeEventListener("abort", onAbort);
1826
1832
  fn();
1827
1833
  };
1834
+ if (opts.signal) {
1835
+ if (opts.signal.aborted) {
1836
+ onAbort();
1837
+ return;
1838
+ }
1839
+ opts.signal.addEventListener("abort", onAbort, { once: true });
1840
+ }
1828
1841
  if (timeoutMs > 0) timer = setTimeout(() => {
1829
1842
  child.kill("SIGKILL");
1830
1843
  finish(() => reject(/* @__PURE__ */ new Error(`oxlint subprocess timed out after ${timeoutMs}ms`)));
@@ -1906,7 +1919,8 @@ async function runScriptPass(opts) {
1906
1919
  configPath,
1907
1920
  oxlintBin,
1908
1921
  timeoutMs: opts.timeoutMs,
1909
- fix: opts.fix
1922
+ fix: opts.fix,
1923
+ signal: opts.signal
1910
1924
  });
1911
1925
  return {
1912
1926
  diagnostics: toCanonicalDiagnostics(raw.diagnostics, opts.rootDir),
@@ -4200,24 +4214,36 @@ async function audit(config = {}) {
4200
4214
  exclude
4201
4215
  });
4202
4216
  const overallStart = performance.now();
4217
+ const maxDurationMs = config.maxDurationMs;
4218
+ const deadlineMs = maxDurationMs ?? 0;
4219
+ const budgetExhausted = () => maxDurationMs !== void 0 && performance.now() - overallStart >= maxDurationMs;
4220
+ const skippedPasses = [];
4221
+ const deadlineController = new AbortController();
4222
+ let deadlineTimer;
4223
+ if (maxDurationMs !== void 0) deadlineTimer = setTimeout(() => deadlineController.abort(), maxDurationMs);
4203
4224
  const project = await detectProject(rootDir);
4204
4225
  const templateStart = performance.now();
4205
- const templateDiagnostics = lintEnabled ? await runTemplatePass({
4226
+ let templateDiagnostics = [];
4227
+ if (lintEnabled) if (budgetExhausted()) skippedPasses.push("template");
4228
+ else templateDiagnostics = await runTemplatePass({
4206
4229
  files,
4207
4230
  ruleOverrides: config.rules
4208
- }) : [];
4231
+ });
4209
4232
  const templateElapsed = performance.now() - templateStart;
4210
4233
  const sfcStart = performance.now();
4211
- const sfcDiagnostics = lintEnabled ? await runSfcPass({
4234
+ let sfcDiagnostics = [];
4235
+ if (lintEnabled) if (budgetExhausted()) skippedPasses.push("sfc");
4236
+ else sfcDiagnostics = await runSfcPass({
4212
4237
  files,
4213
4238
  ruleOverrides: config.rules,
4214
4239
  projectInfo: project
4215
- }) : [];
4240
+ });
4216
4241
  const sfcElapsed = performance.now() - sfcStart;
4217
4242
  let scriptDiagnostics = [];
4218
4243
  let oxlintStderr = "";
4219
4244
  const scriptStart = performance.now();
4220
- if (lintEnabled) try {
4245
+ if (lintEnabled) if (budgetExhausted()) skippedPasses.push("lint");
4246
+ else try {
4221
4247
  const result = await runScriptPass({
4222
4248
  rootDir,
4223
4249
  targetPath: rootDir,
@@ -4225,18 +4251,26 @@ async function audit(config = {}) {
4225
4251
  framework: project.framework === "nuxt" ? "nuxt" : "vue",
4226
4252
  fix: config.fix === true && config.scopeFiles === void 0,
4227
4253
  fixExcludes: config.fixExcludes,
4228
- exclude
4254
+ exclude,
4255
+ signal: deadlineController.signal
4229
4256
  });
4230
4257
  scriptDiagnostics = result.diagnostics;
4231
4258
  oxlintStderr = result.stderr;
4232
4259
  } 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`);
4260
+ if (deadlineController.signal.aborted) {
4261
+ skippedPasses.push("lint");
4262
+ oxlintStderr = "aborted: time-budget exhausted";
4263
+ } else {
4264
+ oxlintStderr = err instanceof Error ? err.message : String(err);
4265
+ if (process.env.DOCTOR_DEBUG) process.stderr.write(`[doctor-core] script pass failed: ${oxlintStderr}\n`);
4266
+ }
4235
4267
  }
4236
4268
  const scriptElapsed = performance.now() - scriptStart;
4237
4269
  let deadCodeDiagnostics = [];
4238
4270
  let deadCodeElapsed = 0;
4239
- if (config.deadCode !== false) {
4271
+ const deadCodeEnabled = config.deadCode !== false && !budgetExhausted();
4272
+ if (config.deadCode !== false && !deadCodeEnabled) skippedPasses.push("dead-code");
4273
+ if (deadCodeEnabled) {
4240
4274
  const deadCodeStart = performance.now();
4241
4275
  try {
4242
4276
  const { loadDoctorConfig } = await Promise.resolve().then(() => load_exports);
@@ -4255,48 +4289,54 @@ async function audit(config = {}) {
4255
4289
  deadCodeElapsed = performance.now() - deadCodeStart;
4256
4290
  }
4257
4291
  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
4292
  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
4293
  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
4294
  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 {}
4295
+ if (budgetExhausted()) skippedPasses.push("project");
4296
+ else {
4297
+ try {
4298
+ buildQualityDiagnostics = (await checkBuildQuality(project)).filter((d) => config.rules?.[d.ruleId] !== "off").map((d) => {
4299
+ const override = config.rules?.[d.ruleId];
4300
+ return override ? {
4301
+ ...d,
4302
+ severity: override
4303
+ } : d;
4304
+ });
4305
+ } catch {}
4306
+ try {
4307
+ depsDiagnostics = (await checkDeps(project)).filter((d) => config.rules?.[d.ruleId] !== "off").map((d) => {
4308
+ const override = config.rules?.[d.ruleId];
4309
+ return override ? {
4310
+ ...d,
4311
+ severity: override
4312
+ } : d;
4313
+ });
4314
+ } catch {}
4315
+ if (project.framework === "nuxt") {
4316
+ try {
4317
+ nuxtProjectDiagnostics = (await checkNuxtProject(project)).filter((d) => config.rules?.[d.ruleId] !== "off").map((d) => {
4318
+ const override = config.rules?.[d.ruleId];
4319
+ return override ? {
4320
+ ...d,
4321
+ severity: override
4322
+ } : d;
4323
+ });
4324
+ } catch {}
4325
+ try {
4326
+ crossFileDiagnostics = (await runCrossFilePass({
4327
+ files,
4328
+ projectInfo: project
4329
+ })).filter((d) => config.rules?.[d.ruleId] !== "off").map((d) => {
4330
+ const override = config.rules?.[d.ruleId];
4331
+ return override ? {
4332
+ ...d,
4333
+ severity: override
4334
+ } : d;
4335
+ });
4336
+ } catch {}
4337
+ }
4338
+ }
4339
+ if (deadlineTimer) clearTimeout(deadlineTimer);
4300
4340
  const elapsedMs = performance.now() - overallStart;
4301
4341
  const timings = {
4302
4342
  template: templateElapsed,
@@ -4315,6 +4355,7 @@ async function audit(config = {}) {
4315
4355
  const ruleCounts = countRuleCounts(diagnostics);
4316
4356
  const projectInfo = {
4317
4357
  framework: project.framework,
4358
+ frameworkDetected: project.frameworkDetected,
4318
4359
  vueVersion: project.vueVersion,
4319
4360
  nuxtVersion: project.nuxtVersion,
4320
4361
  capabilities: [...project.capabilities].sort(),
@@ -4325,6 +4366,13 @@ async function audit(config = {}) {
4325
4366
  else if (failOn !== "none") {
4326
4367
  if ((failOn === "warn" ? scored.errorCount + scored.warnCount : scored.errorCount) > 0) exitCode = 1;
4327
4368
  }
4369
+ const incomplete = skippedPasses.length > 0;
4370
+ const skippedCheckReasons = incomplete ? [{
4371
+ kind: "time-budget-exhausted",
4372
+ deadlineMs,
4373
+ elapsedMs,
4374
+ skippedPasses
4375
+ }] : void 0;
4328
4376
  return {
4329
4377
  rootDir,
4330
4378
  filesScanned: files.length,
@@ -4338,7 +4386,9 @@ async function audit(config = {}) {
4338
4386
  projectInfo,
4339
4387
  elapsedMs,
4340
4388
  timings,
4341
- ruleCounts
4389
+ ruleCounts,
4390
+ incomplete,
4391
+ ...skippedCheckReasons ? { skippedCheckReasons } : {}
4342
4392
  };
4343
4393
  }
4344
4394
  //#endregion
@@ -4719,6 +4769,146 @@ async function planInit(options) {
4719
4769
  };
4720
4770
  }
4721
4771
  //#endregion
4772
+ //#region src/ci/provider-detect.ts
4773
+ /**
4774
+ * Detects the CI provider from an explicit env snapshot. Takes the env as an
4775
+ * argument (instead of reading process.env) so provider detection stays a
4776
+ * deterministic, fully-testable pure function.
4777
+ */
4778
+ function detectCiProvider(env) {
4779
+ if (env.GITHUB_ACTIONS === "true") return "github";
4780
+ if (env.GITLAB_CI === "true") return "gitlab";
4781
+ return "unknown";
4782
+ }
4783
+ //#endregion
4784
+ //#region src/ci/scaffold-workflow.ts
4785
+ function renderGithubWorkflow(opts) {
4786
+ return `name: doctor
4787
+
4788
+ on:
4789
+ push:
4790
+ branches: [main]
4791
+ pull_request:
4792
+
4793
+ permissions:
4794
+ contents: read
4795
+ pull-requests: write
4796
+
4797
+ jobs:
4798
+ audit:
4799
+ runs-on: ubuntu-latest
4800
+ steps:
4801
+ - uses: actions/checkout@v6
4802
+
4803
+ - name: Run doctor
4804
+ uses: geoql/doctor-action@v2
4805
+ with:
4806
+ framework: ${opts.framework}
4807
+ preset: recommended
4808
+ threshold: '0'
4809
+ fail-on: error
4810
+ pr-comment: '${opts.prComment}'
4811
+ api-key: \${{ secrets.DOCTOR_API_KEY }}
4812
+ project: \${{ github.repository }}
4813
+ `;
4814
+ }
4815
+ function renderGithubPrWorkflow(opts) {
4816
+ return `name: doctor-pr
4817
+
4818
+ on:
4819
+ pull_request:
4820
+
4821
+ permissions:
4822
+ contents: read
4823
+ pull-requests: write
4824
+
4825
+ jobs:
4826
+ review:
4827
+ runs-on: ubuntu-latest
4828
+ steps:
4829
+ - uses: actions/checkout@v6
4830
+ with:
4831
+ fetch-depth: 0
4832
+
4833
+ - name: Doctor PR review
4834
+ uses: geoql/doctor-action@v2
4835
+ with:
4836
+ framework: ${opts.framework}
4837
+ preset: recommended
4838
+ fail-on: error
4839
+ diff: 'true'
4840
+ pr-comment: 'true'
4841
+ comment-mode: both
4842
+ api-key: \${{ secrets.DOCTOR_API_KEY }}
4843
+ project: \${{ github.repository }}
4844
+ `;
4845
+ }
4846
+ function renderGitlabPipeline(opts) {
4847
+ return `# Doctor quality gate — https://docs.the-doctor.report
4848
+ doctor:
4849
+ image: node:24
4850
+ stage: test
4851
+ script:
4852
+ - npx -y @geoql/${opts.bin}@latest --fail-on error
4853
+ rules:
4854
+ - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
4855
+ - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
4856
+ `;
4857
+ }
4858
+ async function scaffoldCiWorkflow(opts) {
4859
+ const requested = opts.provider ?? "auto";
4860
+ let provider;
4861
+ if (requested === "auto") provider = detectCiProvider(opts.env ?? process$1.env) === "gitlab" ? "gitlab" : "github";
4862
+ else provider = requested;
4863
+ const writes = [];
4864
+ let conflict = false;
4865
+ let conflictPath = null;
4866
+ if (provider === "gitlab") {
4867
+ const pipelinePath = join(opts.dir, ".gitlab-ci.yml");
4868
+ if (!opts.force && await pathExists(pipelinePath)) {
4869
+ conflict = true;
4870
+ conflictPath = pipelinePath;
4871
+ } else writes.push({
4872
+ path: pipelinePath,
4873
+ content: renderGitlabPipeline({ bin: opts.bin })
4874
+ });
4875
+ return {
4876
+ writes,
4877
+ conflict,
4878
+ conflictPath,
4879
+ provider
4880
+ };
4881
+ }
4882
+ const workflowPath = join(opts.dir, ".github", "workflows", "doctor.yml");
4883
+ if (!opts.force && await pathExists(workflowPath)) {
4884
+ conflict = true;
4885
+ conflictPath = workflowPath;
4886
+ return {
4887
+ writes,
4888
+ conflict,
4889
+ conflictPath,
4890
+ provider
4891
+ };
4892
+ }
4893
+ writes.push({
4894
+ path: workflowPath,
4895
+ content: renderGithubWorkflow({
4896
+ framework: opts.framework,
4897
+ prComment: opts.noComments !== true
4898
+ })
4899
+ });
4900
+ if (opts.pr === true) writes.push({
4901
+ path: join(opts.dir, ".github", "workflows", "doctor-pr.yml"),
4902
+ content: renderGithubPrWorkflow({ framework: opts.framework })
4903
+ });
4904
+ return {
4905
+ writes,
4906
+ conflict,
4907
+ conflictPath,
4908
+ provider
4909
+ };
4910
+ }
4911
+ //#endregion
4722
4912
  //#region src/git-scope.ts
4723
4913
  const execFileAsync = promisify(execFile);
4724
4914
  const SOURCE_EXTENSIONS = [
@@ -5119,6 +5309,7 @@ function buildDoctorReport(input) {
5119
5309
  },
5120
5310
  projectInfo: {
5121
5311
  framework: input.projectInfo.framework,
5312
+ frameworkDetected: input.projectInfo.frameworkDetected,
5122
5313
  vueVersion: input.projectInfo.vueVersion,
5123
5314
  nuxtVersion: input.projectInfo.nuxtVersion,
5124
5315
  capabilities: [...input.projectInfo.capabilities].sort(),
@@ -5162,7 +5353,16 @@ function buildDoctorReport(input) {
5162
5353
  timing: {
5163
5354
  elapsedMs: input.elapsedMs,
5164
5355
  analyzedFileCount: input.analyzedFileCount
5165
- }
5356
+ },
5357
+ ...input.incomplete !== void 0 && input.incomplete ? {
5358
+ incomplete: input.incomplete,
5359
+ skippedCheckReasons: input.skippedCheckReasons?.map((r) => ({
5360
+ kind: r.kind,
5361
+ deadlineMs: r.deadlineMs,
5362
+ elapsedMs: r.elapsedMs,
5363
+ skippedPasses: [...r.skippedPasses]
5364
+ }))
5365
+ } : {}
5166
5366
  };
5167
5367
  }
5168
5368
  function jsonReport(input) {
@@ -5643,6 +5843,6 @@ async function pushFindings(opts) {
5643
5843
  }
5644
5844
  }
5645
5845
  //#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 };
5846
+ 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
5847
 
5648
5848
  //# sourceMappingURL=index.js.map