@payconductor/houndsman 0.2.0 → 0.3.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/package.json +1 -1
- package/src/cli/arg-parser.ts +8 -0
- package/src/cli/commands/command.ts +23 -0
- package/src/cli/commands/context-command.ts +38 -0
- package/src/cli/commands/doctor-command.ts +55 -0
- package/src/cli/commands/findings-command.ts +28 -0
- package/src/cli/commands/help-command.ts +13 -0
- package/src/cli/commands/registry.ts +34 -0
- package/src/cli/commands/review-command.ts +22 -0
- package/src/cli/commands/server-command.ts +37 -0
- package/src/cli/commands/setup-command.ts +34 -0
- package/src/cli/commands/watchdog-command.ts +34 -0
- package/src/cli/help.ts +43 -0
- package/src/cli/index.ts +58 -266
- package/src/core/arg-parser.ts +48 -0
- package/src/core/commands/findings.ts +34 -144
- package/src/core/commands/review.ts +18 -35
- package/src/core/commands/scaffold.ts +174 -64
- package/src/core/commands/setup.ts +16 -11
- package/src/core/commands/update.ts +7 -7
- package/src/core/commands/watchdog.ts +8 -80
- package/src/core/config.ts +9 -19
- package/src/core/defaults.ts +20 -0
- package/src/core/diff-source.ts +1 -13
- package/src/core/doc-discoverer.ts +25 -15
- package/src/core/domain/budget.ts +42 -0
- package/src/core/domain/classification.ts +94 -0
- package/src/core/domain/diff.ts +53 -0
- package/src/core/domain/watchdog.ts +96 -0
- package/src/core/engine/carryover.ts +67 -0
- package/src/core/engine/houndsman-engine.ts +51 -0
- package/src/core/engine/review-scope.ts +95 -0
- package/src/core/engine/review-session.ts +51 -0
- package/src/core/file-classifier.ts +11 -67
- package/src/core/findings-state.ts +170 -38
- package/src/core/git-revision.ts +14 -40
- package/src/core/git-runner.ts +46 -0
- package/src/core/github-review.ts +4 -33
- package/src/core/orchestrator.ts +116 -337
- package/src/core/paths.ts +9 -0
- package/src/core/pr-context.ts +28 -54
- package/src/core/project-config.ts +57 -0
- package/src/core/renderers/batch-presenter.ts +40 -0
- package/src/core/renderers/brief-renderer.ts +23 -0
- package/src/core/renderers/findings-presenter.ts +74 -0
- package/src/core/renderers/manifest-writer.ts +112 -0
- package/src/core/renderers/report-renderer.ts +66 -0
- package/src/core/renderers/verify-presenter.ts +50 -0
- package/src/core/renderers/watchdog-presenter.ts +100 -0
- package/src/core/report-builder.ts +12 -7
- package/src/core/run-layout.ts +108 -0
- package/src/core/run-status.ts +6 -11
- package/src/core/services/clock.ts +12 -0
- package/src/core/services/file-classifier.ts +34 -0
- package/src/core/services/fix-batch-builder.ts +13 -0
- package/src/core/services/gh-client.ts +54 -0
- package/src/core/services/git-repository.ts +123 -0
- package/src/core/services/project-files.ts +79 -0
- package/src/core/services/verifier.ts +58 -0
- package/src/core/verify.ts +2 -1
- package/src/core/watchdog-registry.ts +58 -16
- package/src/index.ts +31 -1
- package/src/server/index.ts +64 -234
- package/src/server/run-repository.ts +291 -0
- package/src/tests/__snapshots__/baseline.test.ts.snap +814 -0
- package/src/tests/baseline.test.ts +513 -493
- package/src/tests/domain.test.ts +328 -0
- package/src/tests/infrastructure.test.ts +210 -0
|
@@ -27,7 +27,6 @@ import {
|
|
|
27
27
|
type FindingsFilter,
|
|
28
28
|
type FindingsStateFile,
|
|
29
29
|
FindingsStateStore,
|
|
30
|
-
type FindingsSummary,
|
|
31
30
|
type FindingUpdate,
|
|
32
31
|
fileReader,
|
|
33
32
|
filterFindings,
|
|
@@ -35,7 +34,6 @@ import {
|
|
|
35
34
|
summarize,
|
|
36
35
|
syncState,
|
|
37
36
|
} from "../findings-state";
|
|
38
|
-
import { buildFixBatch, type FixBatchResult } from "../fix-batch";
|
|
39
37
|
import {
|
|
40
38
|
buildSubmitPreview,
|
|
41
39
|
defaultGhRunner,
|
|
@@ -47,11 +45,13 @@ import {
|
|
|
47
45
|
} from "../github-review";
|
|
48
46
|
import { relFromRoot, VERIFY_FILE } from "../paths";
|
|
49
47
|
import { currentBranch } from "../pr-context";
|
|
48
|
+
import { BatchPresenter } from "../renderers/batch-presenter";
|
|
49
|
+
import { FindingsPresenter } from "../renderers/findings-presenter";
|
|
50
|
+
import { VerifyPresenter } from "../renderers/verify-presenter";
|
|
50
51
|
import { pingFindingsStatus } from "../run-status";
|
|
51
|
-
import {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
import type { VerifyEntry, VerifyStatus, VerifySummary } from "../verify";
|
|
52
|
+
import { FixBatchBuilder } from "../services/fix-batch-builder";
|
|
53
|
+
import { Verifier } from "../services/verifier";
|
|
54
|
+
import { SEVERITY_ORDER, type Severity } from "../types";
|
|
55
55
|
|
|
56
56
|
export type FindingsCommandResult = {
|
|
57
57
|
output: string;
|
|
@@ -134,90 +134,16 @@ function loadState(config: HoundsmanConfig, parsed: ParsedArgs): FindingsStateFi
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
// ---------------------------------------------------------------------------
|
|
137
|
-
//
|
|
137
|
+
// Apresentação
|
|
138
138
|
// ---------------------------------------------------------------------------
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
function renderFindingLine(finding: FindingState): string[] {
|
|
146
|
-
const emoji = SEVERITY_EMOJI[finding.severity];
|
|
147
|
-
const location = finding.line ? `${finding.file}:${finding.line}` : finding.file;
|
|
148
|
-
const flags = `${decisionLabel(finding.decision)}/${finding.execState}`;
|
|
149
|
-
const count = finding.count > 1 ? ` ×${finding.count}` : "";
|
|
150
|
-
const lines = [
|
|
151
|
-
`- \`${finding.id}\` ${emoji} **${finding.severity}** \`${location}\` — ${finding.rule}${count}`,
|
|
152
|
-
` ${finding.message}`,
|
|
153
|
-
` _${flags}_ · watchdogs: ${finding.watchdogIds.join(", ") || "—"}`,
|
|
154
|
-
];
|
|
155
|
-
if (finding.comment) lines.push(` **Comentário:** ${finding.comment}`);
|
|
156
|
-
return lines;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function renderList(state: FindingsStateFile, findings: FindingState[]): string {
|
|
160
|
-
if (findings.length === 0) return "Nenhum finding para o filtro informado.";
|
|
161
|
-
const lines = [`# Findings — ${state.branch} (${findings.length})`, ""];
|
|
162
|
-
for (const finding of findings) lines.push(...renderFindingLine(finding));
|
|
163
|
-
return lines.join("\n");
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function renderSummary(state: FindingsStateFile, summary: FindingsSummary): string {
|
|
167
|
-
const decisions = FINDING_DECISIONS.map((d) => `${d} ${summary.byDecision[d]}`).join(
|
|
168
|
-
" · ",
|
|
169
|
-
);
|
|
170
|
-
const exec = FINDING_EXEC_STATES.map((e) => `${e} ${summary.byExecState[e]}`).join(
|
|
171
|
-
" · ",
|
|
172
|
-
);
|
|
173
|
-
const severities = SEVERITY_ORDER.map((s) => `${s} ${summary.bySeverity[s]}`).join(
|
|
174
|
-
" · ",
|
|
175
|
-
);
|
|
176
|
-
return [
|
|
177
|
-
`# Triagem — ${state.branch}`,
|
|
178
|
-
"",
|
|
179
|
-
`Total: ${summary.total} (presentes: ${summary.present})`,
|
|
180
|
-
`Decisões: ${decisions}`,
|
|
181
|
-
`Execução: ${exec}`,
|
|
182
|
-
`Severidade: ${severities}`,
|
|
183
|
-
].join("\n");
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/** Mix de severidade compacto (ex.: `❌1 ⚠️2`). */
|
|
187
|
-
function severityBreakdown(bySeverity: Record<Severity, number>): string {
|
|
188
|
-
return SEVERITY_ORDER.filter((severity) => bySeverity[severity] > 0)
|
|
189
|
-
.map((severity) => `${SEVERITY_EMOJI[severity]}${bySeverity[severity]}`)
|
|
190
|
-
.join(" ");
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** Render do lote de correção: grupo atual paginado + outros grupos. */
|
|
194
|
-
function renderBatch(result: FixBatchResult): string {
|
|
195
|
-
const { dimension, current, others, totals } = result;
|
|
196
|
-
if (totals.queue === 0) return "Nenhum finding pendente de correção.";
|
|
140
|
+
const presenters = {
|
|
141
|
+
findings: new FindingsPresenter(),
|
|
142
|
+
batch: new BatchPresenter(),
|
|
143
|
+
verify: new VerifyPresenter(),
|
|
144
|
+
};
|
|
197
145
|
|
|
198
|
-
|
|
199
|
-
`# Lote de correção — agrupado por ${dimension} (${totals.groups} grupo(s))`,
|
|
200
|
-
"",
|
|
201
|
-
`## Grupo atual — ${current.key} (${current.total} finding(s), mostrando ${current.returned})`,
|
|
202
|
-
];
|
|
203
|
-
for (const finding of current.findings) lines.push(...renderFindingLine(finding));
|
|
204
|
-
if (current.remaining > 0) {
|
|
205
|
-
lines.push("", `_… +${current.remaining} neste grupo (use \`--offset\`)._`);
|
|
206
|
-
}
|
|
207
|
-
if (others.length > 0) {
|
|
208
|
-
lines.push("", "## Outros grupos disponíveis");
|
|
209
|
-
for (const group of others) {
|
|
210
|
-
lines.push(
|
|
211
|
-
`- ${group.key} — ${group.count} (${severityBreakdown(group.bySeverity)})`,
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
lines.push(
|
|
216
|
-
"",
|
|
217
|
-
`fila: ${totals.queue} · retornados: ${totals.returned} · grupos: ${totals.groups}`,
|
|
218
|
-
);
|
|
219
|
-
return lines.join("\n");
|
|
220
|
-
}
|
|
146
|
+
const fixBatchBuilder = new FixBatchBuilder();
|
|
221
147
|
|
|
222
148
|
// ---------------------------------------------------------------------------
|
|
223
149
|
// Execução
|
|
@@ -332,7 +258,7 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
332
258
|
return {
|
|
333
259
|
output: parsed.bools.has("--json")
|
|
334
260
|
? JSON.stringify(findings, null, 2)
|
|
335
|
-
:
|
|
261
|
+
: presenters.findings.list(state, findings),
|
|
336
262
|
exitCode: 0,
|
|
337
263
|
};
|
|
338
264
|
}
|
|
@@ -345,7 +271,7 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
345
271
|
return {
|
|
346
272
|
output: parsed.bools.has("--json")
|
|
347
273
|
? JSON.stringify(finding, null, 2)
|
|
348
|
-
:
|
|
274
|
+
: presenters.findings.list(state, [finding]),
|
|
349
275
|
exitCode: 0,
|
|
350
276
|
};
|
|
351
277
|
}
|
|
@@ -417,7 +343,7 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
417
343
|
return {
|
|
418
344
|
output: parsed.bools.has("--json")
|
|
419
345
|
? JSON.stringify(summary, null, 2)
|
|
420
|
-
:
|
|
346
|
+
: presenters.findings.summary(state, summary),
|
|
421
347
|
exitCode: 0,
|
|
422
348
|
};
|
|
423
349
|
}
|
|
@@ -430,7 +356,7 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
430
356
|
const state = loadState(config, parsed);
|
|
431
357
|
const severity = parsed.values["--severity"];
|
|
432
358
|
const offset = Number(parsed.values["--offset"]);
|
|
433
|
-
const result =
|
|
359
|
+
const result = fixBatchBuilder.build(state, {
|
|
434
360
|
severity: severity ? (severity as Severity) : undefined,
|
|
435
361
|
includeDone: parsed.bools.has("--include-done"),
|
|
436
362
|
offset: Number.isFinite(offset) ? offset : undefined,
|
|
@@ -439,7 +365,7 @@ function execute(config: HoundsmanConfig, parsed: ParsedArgs): FindingsCommandRe
|
|
|
439
365
|
return {
|
|
440
366
|
output: parsed.bools.has("--json")
|
|
441
367
|
? JSON.stringify(result, null, 2)
|
|
442
|
-
:
|
|
368
|
+
: presenters.batch.render(result),
|
|
443
369
|
exitCode: 0,
|
|
444
370
|
};
|
|
445
371
|
}
|
|
@@ -479,43 +405,6 @@ function requireIds(parsed: ParsedArgs, sub: string): string[] {
|
|
|
479
405
|
// verify — as violações foram resolvidas? (sem re-revisar a PR)
|
|
480
406
|
// ---------------------------------------------------------------------------
|
|
481
407
|
|
|
482
|
-
/** Rótulo curto de cada status de verificação. */
|
|
483
|
-
const VERIFY_LABEL: Record<VerifyStatus, string> = {
|
|
484
|
-
resolved: "✅ resolvido",
|
|
485
|
-
persisting: "❌ persiste",
|
|
486
|
-
uncertain: "⚠️ incerto",
|
|
487
|
-
"file-missing": "🗑️ arquivo ausente",
|
|
488
|
-
};
|
|
489
|
-
|
|
490
|
-
/** Render da verificação: contagem por status + uma linha por finding. */
|
|
491
|
-
function renderVerify(
|
|
492
|
-
state: FindingsStateFile,
|
|
493
|
-
entries: VerifyEntry[],
|
|
494
|
-
summary: VerifySummary,
|
|
495
|
-
meta: { jevChecked: number; jevReason?: string },
|
|
496
|
-
): string {
|
|
497
|
-
if (entries.length === 0) return "Nenhum finding para verificar.";
|
|
498
|
-
const lines = [
|
|
499
|
-
`# Verificação — ${state.branch} (${entries.length})`,
|
|
500
|
-
"",
|
|
501
|
-
`Resolvidos: ${summary.resolved} · Persistem: ${summary.persisting} · Incertos: ${summary.uncertain} · Arquivo ausente: ${summary["file-missing"]}`,
|
|
502
|
-
];
|
|
503
|
-
if (meta.jevChecked > 0) lines.push(`Confirmados por Jev: ${meta.jevChecked}.`);
|
|
504
|
-
else if (meta.jevReason) lines.push(`Jev: pulado (${meta.jevReason}).`);
|
|
505
|
-
lines.push("");
|
|
506
|
-
for (const entry of entries) {
|
|
507
|
-
const location = entry.line ? `${entry.file}:${entry.line}` : entry.file;
|
|
508
|
-
const via =
|
|
509
|
-
entry.basis === "jev" ? ` · Jev ${(entry.confidence ?? 0).toFixed(2)}` : "";
|
|
510
|
-
lines.push(
|
|
511
|
-
`- \`${entry.id}\` ${VERIFY_LABEL[entry.status]} — **${entry.severity}** \`${location}\` — ${entry.rule}${via}`,
|
|
512
|
-
` ${entry.message}`,
|
|
513
|
-
);
|
|
514
|
-
if (entry.detail) lines.push(` _${entry.detail}_`);
|
|
515
|
-
}
|
|
516
|
-
return lines.join("\n");
|
|
517
|
-
}
|
|
518
|
-
|
|
519
408
|
/**
|
|
520
409
|
* Subcomando `findings verify` — checa se as violações foram resolvidas, sem
|
|
521
410
|
* re-revisar a PR. Tier local (grátis) + confirmação Jev dos casos ambíguos
|
|
@@ -544,28 +433,26 @@ async function executeVerify(
|
|
|
544
433
|
const store = FindingsStateStore.at(dir, branch);
|
|
545
434
|
const state = loadState(config, parsed);
|
|
546
435
|
|
|
547
|
-
// Lazy: `../
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
{ JevCandidatesStore, JevClient, JevVerifier },
|
|
551
|
-
] = await Promise.all([import("../verify"), import("../jev")]);
|
|
436
|
+
// Lazy: `../jev` (axios) só entra no caminho do verify.
|
|
437
|
+
const verifier = new Verifier();
|
|
438
|
+
const { JevCandidatesStore, JevClient, JevVerifier } = await import("../jev");
|
|
552
439
|
|
|
553
|
-
const findings =
|
|
554
|
-
const entries = verifyLocal(findings, fileReader(process.cwd()));
|
|
440
|
+
const findings = verifier.select(state, filterFrom(parsed));
|
|
441
|
+
const entries = verifier.verifyLocal(findings, fileReader(process.cwd()));
|
|
555
442
|
|
|
556
443
|
// Tier 1 — Jev só nos ambíguos (fail-open: sem chave/erro mantém o provisório).
|
|
557
444
|
let jevChecked = 0;
|
|
558
445
|
let jevReason: string | undefined;
|
|
559
446
|
const ambiguous = entries.filter((entry) => entry.ambiguous);
|
|
560
447
|
if (ambiguous.length > 0) {
|
|
561
|
-
const
|
|
448
|
+
const jevVerifier = new JevVerifier(new JevClient());
|
|
562
449
|
if (parsed.bools.has("--no-jev")) {
|
|
563
450
|
jevReason = "desativado (--no-jev)";
|
|
564
|
-
} else if (!
|
|
451
|
+
} else if (!jevVerifier.isConfigured()) {
|
|
565
452
|
jevReason = "TYPESAFE_API_KEY não configurada";
|
|
566
453
|
} else {
|
|
567
454
|
const byId = new Map(findings.map((finding) => [finding.id, finding]));
|
|
568
|
-
const outcome = await
|
|
455
|
+
const outcome = await jevVerifier.verify(
|
|
569
456
|
ambiguous.map((entry) => ({
|
|
570
457
|
id: entry.id,
|
|
571
458
|
file: entry.file,
|
|
@@ -577,23 +464,23 @@ async function executeVerify(
|
|
|
577
464
|
concurrency: resolveJevConcurrency(config),
|
|
578
465
|
},
|
|
579
466
|
);
|
|
580
|
-
applyJev(entries, outcome.confidence, JevCandidatesStore.THRESHOLD);
|
|
467
|
+
verifier.applyJev(entries, outcome.confidence, JevCandidatesStore.THRESHOLD);
|
|
581
468
|
jevChecked = outcome.checked;
|
|
582
469
|
jevReason = outcome.reason;
|
|
583
470
|
}
|
|
584
471
|
}
|
|
585
472
|
|
|
586
|
-
const summary =
|
|
473
|
+
const summary = verifier.summarize(entries);
|
|
587
474
|
const reportPath = join(runDir(dir, branch), VERIFY_FILE);
|
|
588
475
|
writeFileSync(
|
|
589
476
|
reportPath,
|
|
590
|
-
`${
|
|
477
|
+
`${presenters.verify.render(state, entries, summary, { jevChecked, jevReason })}\n`,
|
|
591
478
|
);
|
|
592
479
|
|
|
593
480
|
let applied: string[] = [];
|
|
594
481
|
if (parsed.bools.has("--apply")) {
|
|
595
482
|
store.update((current) => {
|
|
596
|
-
applied = applyResolutions(current, entries);
|
|
483
|
+
applied = verifier.applyResolutions(current, entries);
|
|
597
484
|
});
|
|
598
485
|
pingStatus(config, parsed);
|
|
599
486
|
}
|
|
@@ -605,7 +492,10 @@ async function executeVerify(
|
|
|
605
492
|
2,
|
|
606
493
|
)
|
|
607
494
|
: [
|
|
608
|
-
|
|
495
|
+
presenters.verify.render(state, entries, summary, {
|
|
496
|
+
jevChecked,
|
|
497
|
+
jevReason,
|
|
498
|
+
}),
|
|
609
499
|
"",
|
|
610
500
|
`Relatório: ${relFromRoot(reportPath)}`,
|
|
611
501
|
parsed.bools.has("--apply")
|
|
@@ -4,49 +4,32 @@
|
|
|
4
4
|
* Biblioteca: NÃO usa `process.argv`/`process.exit` (o CLI decide o exit code).
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { ArgParser } from "../arg-parser";
|
|
7
8
|
import { defaultOptions, type ReviewOptions } from "../orchestrator";
|
|
8
9
|
import { ARTIFACT_DIR } from "../paths";
|
|
9
10
|
|
|
10
|
-
const VALUE_FLAGS = new Set(["--base", "--head", "--branch", "--dir"]);
|
|
11
|
-
const BOOLEAN_FLAGS = new Set([
|
|
12
|
-
"--finalize",
|
|
13
|
-
"--report-only",
|
|
14
|
-
"--fail-on-blocking",
|
|
15
|
-
"--clean",
|
|
16
|
-
"--no-jev",
|
|
17
|
-
"--full",
|
|
18
|
-
]);
|
|
19
|
-
|
|
20
|
-
function applyValue(opts: ReviewOptions, flag: string, value: string | undefined): void {
|
|
21
|
-
if (!value) return;
|
|
22
|
-
if (flag === "--base") opts.base = value;
|
|
23
|
-
else if (flag === "--head") opts.head = value;
|
|
24
|
-
else if (flag === "--branch") opts.branch = value;
|
|
25
|
-
else if (flag === "--dir") opts.dir = value;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function applyBoolean(opts: ReviewOptions, flag: string): void {
|
|
29
|
-
if (flag === "--finalize" || flag === "--report-only") opts.finalize = true;
|
|
30
|
-
else if (flag === "--fail-on-blocking") opts.failOnBlocking = true;
|
|
31
|
-
else if (flag === "--clean") opts.clean = true;
|
|
32
|
-
else if (flag === "--no-jev") opts.noJev = true;
|
|
33
|
-
else if (flag === "--full") opts.full = true;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
11
|
/** Converte argv (sem o subcomando) nas opções de prepare/finalize. */
|
|
37
12
|
export function parseReviewArgs(
|
|
38
13
|
args: string[],
|
|
39
14
|
artifactDir: string = ARTIFACT_DIR,
|
|
40
15
|
): ReviewOptions {
|
|
16
|
+
const parser = ArgParser.from(args);
|
|
41
17
|
const opts = defaultOptions(artifactDir);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
18
|
+
|
|
19
|
+
const base = parser.value("--base");
|
|
20
|
+
const head = parser.value("--head");
|
|
21
|
+
const branch = parser.value("--branch");
|
|
22
|
+
const dir = parser.value("--dir");
|
|
23
|
+
if (base) opts.base = base;
|
|
24
|
+
if (head) opts.head = head;
|
|
25
|
+
if (branch) opts.branch = branch;
|
|
26
|
+
if (dir) opts.dir = dir;
|
|
27
|
+
|
|
28
|
+
if (parser.has("--finalize") || parser.has("--report-only")) opts.finalize = true;
|
|
29
|
+
if (parser.has("--fail-on-blocking")) opts.failOnBlocking = true;
|
|
30
|
+
if (parser.has("--clean")) opts.clean = true;
|
|
31
|
+
if (parser.has("--no-jev")) opts.noJev = true;
|
|
32
|
+
if (parser.has("--full")) opts.full = true;
|
|
33
|
+
|
|
51
34
|
return opts;
|
|
52
35
|
}
|
|
@@ -280,83 +280,193 @@ core/
|
|
|
280
280
|
`;
|
|
281
281
|
|
|
282
282
|
// ---------------------------------------------------------------------------
|
|
283
|
-
//
|
|
283
|
+
// Templates (arquivo → conteúdo)
|
|
284
284
|
// ---------------------------------------------------------------------------
|
|
285
285
|
|
|
286
|
-
/**
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
286
|
+
/** Contexto de render de um template. */
|
|
287
|
+
type TemplateContext = { projectName: string };
|
|
288
|
+
|
|
289
|
+
/** Um arquivo da camada (caminho relativo + conteúdo). */
|
|
290
|
+
interface ScaffoldTemplate {
|
|
291
|
+
readonly path: string;
|
|
292
|
+
render(context: TemplateContext): string;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
class ConfigTemplate implements ScaffoldTemplate {
|
|
296
|
+
readonly path = "houndsman.config.ts";
|
|
297
|
+
|
|
298
|
+
render(context: TemplateContext): string {
|
|
299
|
+
return CONFIG_TEMPLATE(context.projectName);
|
|
296
300
|
}
|
|
297
|
-
mkdirSync(join(full, ".."), { recursive: true });
|
|
298
|
-
writeFileSync(full, content);
|
|
299
|
-
ctx.created.push(relativePath);
|
|
300
301
|
}
|
|
301
302
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const full = join(ctx.cwd, relativePath);
|
|
309
|
-
mkdirSync(join(full, ".."), { recursive: true });
|
|
310
|
-
writeFileSync(full, content);
|
|
311
|
-
ctx.generated.push(relativePath);
|
|
303
|
+
class WatchdogExampleTemplate implements ScaffoldTemplate {
|
|
304
|
+
readonly path = join(PROJECT_DIR, "watchdogs", "example.ts");
|
|
305
|
+
|
|
306
|
+
render(): string {
|
|
307
|
+
return WATCHDOG_EXAMPLE;
|
|
308
|
+
}
|
|
312
309
|
}
|
|
313
310
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
// ---------------------------------------------------------------------------
|
|
311
|
+
class WatchdogsIndexTemplate implements ScaffoldTemplate {
|
|
312
|
+
readonly path = join(PROJECT_DIR, "watchdogs", "index.ts");
|
|
317
313
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
writeUserFile(ctx, join(PROJECT_DIR, "watchdogs", "example.ts"), WATCHDOG_EXAMPLE);
|
|
322
|
-
writeUserFile(ctx, join(PROJECT_DIR, "watchdogs", "index.ts"), WATCHDOGS_INDEX);
|
|
323
|
-
writeUserFile(ctx, join(PROJECT_DIR, "exclusions.ts"), EXCLUSIONS_TEMPLATE);
|
|
324
|
-
writeUserFile(ctx, join(PROJECT_DIR, "doc-mappings.ts"), DOC_MAPPINGS_TEMPLATE);
|
|
314
|
+
render(): string {
|
|
315
|
+
return WATCHDOGS_INDEX;
|
|
316
|
+
}
|
|
325
317
|
}
|
|
326
318
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
319
|
+
class ExclusionsTemplate implements ScaffoldTemplate {
|
|
320
|
+
readonly path = join(PROJECT_DIR, "exclusions.ts");
|
|
321
|
+
|
|
322
|
+
render(): string {
|
|
323
|
+
return EXCLUSIONS_TEMPLATE;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
class DocMappingsTemplate implements ScaffoldTemplate {
|
|
328
|
+
readonly path = join(PROJECT_DIR, "doc-mappings.ts");
|
|
329
|
+
|
|
330
|
+
render(): string {
|
|
331
|
+
return DOC_MAPPINGS_TEMPLATE;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
class TypesTemplate implements ScaffoldTemplate {
|
|
336
|
+
readonly path = join(CORE_DIR, "types.ts");
|
|
337
|
+
|
|
338
|
+
render(): string {
|
|
339
|
+
return TYPES_TEMPLATE;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
class GitignoreTemplate implements ScaffoldTemplate {
|
|
344
|
+
readonly path = join(PROJECT_DIR, ".gitignore");
|
|
345
|
+
|
|
346
|
+
render(): string {
|
|
347
|
+
return PROJECT_GITIGNORE;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
class SkillDocTemplate implements ScaffoldTemplate {
|
|
352
|
+
private readonly fileName: string;
|
|
353
|
+
|
|
354
|
+
constructor(fileName: string) {
|
|
355
|
+
this.fileName = fileName;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
get path(): string {
|
|
359
|
+
return join(CORE_SKILL_DIR, this.fileName);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
render(context: TemplateContext): string {
|
|
363
|
+
return readFileSync(join(SKILL_DIR, this.fileName), "utf8").replaceAll(
|
|
364
|
+
"{{projectName}}",
|
|
365
|
+
context.projectName,
|
|
345
366
|
);
|
|
346
367
|
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
class HarnessPointerTemplate implements ScaffoldTemplate {
|
|
371
|
+
private readonly harness: Harness;
|
|
347
372
|
|
|
348
|
-
|
|
373
|
+
constructor(harness: Harness) {
|
|
374
|
+
this.harness = harness;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
get path(): string {
|
|
378
|
+
return join(`.${this.harness}`, "skills", "houndsman", SKILL_FILE);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
render(context: TemplateContext): string {
|
|
382
|
+
return readFileSync(join(SKILL_DIR, SKILL_FILE), "utf8").replaceAll(
|
|
383
|
+
"{{projectName}}",
|
|
384
|
+
context.projectName,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
349
387
|
}
|
|
350
388
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
// Scaffolder
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
|
|
393
|
+
/** Escreve as camadas do projeto (`setup`) e sincroniza os auto-gerados (`update`). */
|
|
394
|
+
export class ProjectScaffolder {
|
|
395
|
+
private readonly ctx: ScaffoldContext;
|
|
396
|
+
|
|
397
|
+
constructor(ctx: ScaffoldContext) {
|
|
398
|
+
this.ctx = ctx;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Arquivos do usuário: config, watchdogs, exclusões e mapeamentos. */
|
|
402
|
+
writeUserLayer(): void {
|
|
403
|
+
const context = this.context();
|
|
404
|
+
const templates: ScaffoldTemplate[] = [
|
|
405
|
+
new ConfigTemplate(),
|
|
406
|
+
new WatchdogExampleTemplate(),
|
|
407
|
+
new WatchdogsIndexTemplate(),
|
|
408
|
+
new ExclusionsTemplate(),
|
|
409
|
+
new DocMappingsTemplate(),
|
|
410
|
+
];
|
|
411
|
+
for (const template of templates) this.writeUserFile(template, context);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Arquivos auto-gerados (sempre sobrescritos). */
|
|
415
|
+
syncGenerated(): void {
|
|
416
|
+
const context = this.context();
|
|
417
|
+
this.writeGeneratedFile(new TypesTemplate(), context);
|
|
418
|
+
this.writeGeneratedFile(new GitignoreTemplate(), context);
|
|
419
|
+
|
|
420
|
+
// Docs do skill vão para `.houndsman/core/skill/`. O `SKILL.md` é o entry
|
|
421
|
+
// point dos harnesses, não um doc da camada.
|
|
422
|
+
for (const name of readdirSync(SKILL_DIR)) {
|
|
423
|
+
if (!name.endsWith(".md") || name === SKILL_FILE) continue;
|
|
424
|
+
this.writeGeneratedFile(new SkillDocTemplate(name), context);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
for (const harness of this.ctx.harnesses) {
|
|
428
|
+
this.writeGeneratedFile(new HarnessPointerTemplate(harness), context);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private context(): TemplateContext {
|
|
433
|
+
return { projectName: this.ctx.projectName };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Usuário: só escreve se ausente (ou `force`). */
|
|
437
|
+
private writeUserFile(template: ScaffoldTemplate, context: TemplateContext): void {
|
|
438
|
+
const full = join(this.ctx.cwd, template.path);
|
|
439
|
+
if (existsSync(full) && !this.ctx.force) {
|
|
440
|
+
this.ctx.skipped.push(template.path);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
mkdirSync(join(full, ".."), { recursive: true });
|
|
444
|
+
writeFileSync(full, template.render(context));
|
|
445
|
+
this.ctx.created.push(template.path);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** Auto-gerado: sempre sobrescreve. */
|
|
449
|
+
private writeGeneratedFile(
|
|
450
|
+
template: ScaffoldTemplate,
|
|
451
|
+
context: TemplateContext,
|
|
452
|
+
): void {
|
|
453
|
+
const full = join(this.ctx.cwd, template.path);
|
|
454
|
+
mkdirSync(join(full, ".."), { recursive: true });
|
|
455
|
+
writeFileSync(full, template.render(context));
|
|
456
|
+
this.ctx.generated.push(template.path);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ---------------------------------------------------------------------------
|
|
461
|
+
// Fachada (compatibilidade)
|
|
462
|
+
// ---------------------------------------------------------------------------
|
|
463
|
+
|
|
464
|
+
/** Arquivos do usuário (setup). */
|
|
465
|
+
export function writeUserLayer(ctx: ScaffoldContext): void {
|
|
466
|
+
new ProjectScaffolder(ctx).writeUserLayer();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Arquivos auto-gerados (setup/update). */
|
|
470
|
+
export function syncGeneratedFiles(ctx: ScaffoldContext): void {
|
|
471
|
+
new ProjectScaffolder(ctx).syncGenerated();
|
|
362
472
|
}
|
|
@@ -49,27 +49,32 @@ function ensurePackageScript(ctx: ScaffoldContext): void {
|
|
|
49
49
|
ctx.created.push("package.json#scripts.houndsman");
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/** Cria a configuração inicial do projeto. */
|
|
53
|
-
export async function runSetupCommand(options: SetupOptions): Promise<
|
|
52
|
+
/** Cria a configuração inicial do projeto. Devolve as mensagens do CLI. */
|
|
53
|
+
export async function runSetupCommand(options: SetupOptions): Promise<string[]> {
|
|
54
54
|
const ctx = createScaffoldContext(
|
|
55
55
|
options.cwd,
|
|
56
56
|
options.args,
|
|
57
57
|
options.args.includes("--force"),
|
|
58
58
|
);
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
const messages = [
|
|
61
|
+
`→ Configurando houndsman em ${ctx.cwd} (projeto: ${ctx.projectName})`,
|
|
62
|
+
];
|
|
61
63
|
|
|
62
64
|
writeUserLayer(ctx);
|
|
63
65
|
syncGeneratedFiles(ctx);
|
|
64
66
|
ensurePackageScript(ctx);
|
|
65
67
|
|
|
66
|
-
for (const path of ctx.created)
|
|
67
|
-
for (const path of ctx.skipped)
|
|
68
|
-
for (const path of ctx.generated)
|
|
68
|
+
for (const path of ctx.created) messages.push(` + ${path}`);
|
|
69
|
+
for (const path of ctx.skipped) messages.push(` = ${path} (mantido)`);
|
|
70
|
+
for (const path of ctx.generated) messages.push(` ~ ${path} (auto)`);
|
|
69
71
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
messages.push(
|
|
73
|
+
"\nPróximos passos:",
|
|
74
|
+
" 1. Ajuste `houndsman.config.ts` e `.houndsman/watchdogs/`.",
|
|
75
|
+
" 2. Valide: bun run houndsman doctor",
|
|
76
|
+
" 3. Prepare a revisão: bun run houndsman",
|
|
77
|
+
" · Sincronizar arquivos auto-gerados: bun run houndsman update",
|
|
78
|
+
);
|
|
79
|
+
return messages;
|
|
75
80
|
}
|