popeye-cli 1.10.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +114 -0
- package/CONTRIBUTING.md +38 -3
- package/README.md +104 -18
- package/dist/adapters/gemini.js +3 -3
- package/dist/adapters/openai.js +2 -2
- package/dist/adapters/openai.js.map +1 -1
- package/dist/auth/gemini.js +1 -1
- package/dist/cli/commands/create.d.ts.map +1 -1
- package/dist/cli/commands/create.js +11 -5
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/commands/resume.d.ts.map +1 -1
- package/dist/cli/commands/resume.js +9 -1
- package/dist/cli/commands/resume.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +29 -3
- package/dist/cli/interactive.js.map +1 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +7 -2
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/index.d.ts +1 -7
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/popeye-md.d.ts +32 -0
- package/dist/config/popeye-md.d.ts.map +1 -0
- package/dist/config/popeye-md.js +111 -0
- package/dist/config/popeye-md.js.map +1 -0
- package/dist/config/schema.d.ts +3 -21
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +21 -8
- package/dist/config/schema.js.map +1 -1
- package/dist/pipeline/artifact-manager.d.ts +47 -0
- package/dist/pipeline/artifact-manager.d.ts.map +1 -0
- package/dist/pipeline/artifact-manager.js +251 -0
- package/dist/pipeline/artifact-manager.js.map +1 -0
- package/dist/pipeline/artifact-validators.d.ts +29 -0
- package/dist/pipeline/artifact-validators.d.ts.map +1 -0
- package/dist/pipeline/artifact-validators.js +173 -0
- package/dist/pipeline/artifact-validators.js.map +1 -0
- package/dist/pipeline/bridges/review-bridge.d.ts +70 -0
- package/dist/pipeline/bridges/review-bridge.d.ts.map +1 -0
- package/dist/pipeline/bridges/review-bridge.js +266 -0
- package/dist/pipeline/bridges/review-bridge.js.map +1 -0
- package/dist/pipeline/change-request.d.ts +47 -0
- package/dist/pipeline/change-request.d.ts.map +1 -0
- package/dist/pipeline/change-request.js +91 -0
- package/dist/pipeline/change-request.js.map +1 -0
- package/dist/pipeline/check-runner.d.ts +47 -0
- package/dist/pipeline/check-runner.d.ts.map +1 -0
- package/dist/pipeline/check-runner.js +417 -0
- package/dist/pipeline/check-runner.js.map +1 -0
- package/dist/pipeline/command-resolver.d.ts +9 -0
- package/dist/pipeline/command-resolver.d.ts.map +1 -0
- package/dist/pipeline/command-resolver.js +140 -0
- package/dist/pipeline/command-resolver.js.map +1 -0
- package/dist/pipeline/consensus/consensus-runner.d.ts +44 -0
- package/dist/pipeline/consensus/consensus-runner.d.ts.map +1 -0
- package/dist/pipeline/consensus/consensus-runner.js +212 -0
- package/dist/pipeline/consensus/consensus-runner.js.map +1 -0
- package/dist/pipeline/constitution.d.ts +45 -0
- package/dist/pipeline/constitution.d.ts.map +1 -0
- package/dist/pipeline/constitution.js +82 -0
- package/dist/pipeline/constitution.js.map +1 -0
- package/dist/pipeline/gate-engine.d.ts +55 -0
- package/dist/pipeline/gate-engine.d.ts.map +1 -0
- package/dist/pipeline/gate-engine.js +270 -0
- package/dist/pipeline/gate-engine.js.map +1 -0
- package/dist/pipeline/index.d.ts +26 -0
- package/dist/pipeline/index.d.ts.map +1 -0
- package/dist/pipeline/index.js +35 -0
- package/dist/pipeline/index.js.map +1 -0
- package/dist/pipeline/migration.d.ts +15 -0
- package/dist/pipeline/migration.d.ts.map +1 -0
- package/dist/pipeline/migration.js +76 -0
- package/dist/pipeline/migration.js.map +1 -0
- package/dist/pipeline/orchestrator.d.ts +30 -0
- package/dist/pipeline/orchestrator.d.ts.map +1 -0
- package/dist/pipeline/orchestrator.js +242 -0
- package/dist/pipeline/orchestrator.js.map +1 -0
- package/dist/pipeline/packets/audit-report-builder.d.ts +11 -0
- package/dist/pipeline/packets/audit-report-builder.d.ts.map +1 -0
- package/dist/pipeline/packets/audit-report-builder.js +32 -0
- package/dist/pipeline/packets/audit-report-builder.js.map +1 -0
- package/dist/pipeline/packets/consensus-packet-builder.d.ts +35 -0
- package/dist/pipeline/packets/consensus-packet-builder.d.ts.map +1 -0
- package/dist/pipeline/packets/consensus-packet-builder.js +80 -0
- package/dist/pipeline/packets/consensus-packet-builder.js.map +1 -0
- package/dist/pipeline/packets/index.d.ts +12 -0
- package/dist/pipeline/packets/index.d.ts.map +1 -0
- package/dist/pipeline/packets/index.js +8 -0
- package/dist/pipeline/packets/index.js.map +1 -0
- package/dist/pipeline/packets/plan-packet-builder.d.ts +21 -0
- package/dist/pipeline/packets/plan-packet-builder.d.ts.map +1 -0
- package/dist/pipeline/packets/plan-packet-builder.js +27 -0
- package/dist/pipeline/packets/plan-packet-builder.js.map +1 -0
- package/dist/pipeline/packets/rca-packet-builder.d.ts +19 -0
- package/dist/pipeline/packets/rca-packet-builder.d.ts.map +1 -0
- package/dist/pipeline/packets/rca-packet-builder.js +22 -0
- package/dist/pipeline/packets/rca-packet-builder.js.map +1 -0
- package/dist/pipeline/phases/architecture.d.ts +7 -0
- package/dist/pipeline/phases/architecture.d.ts.map +1 -0
- package/dist/pipeline/phases/architecture.js +60 -0
- package/dist/pipeline/phases/architecture.js.map +1 -0
- package/dist/pipeline/phases/audit.d.ts +8 -0
- package/dist/pipeline/phases/audit.d.ts.map +1 -0
- package/dist/pipeline/phases/audit.js +144 -0
- package/dist/pipeline/phases/audit.js.map +1 -0
- package/dist/pipeline/phases/consensus-architecture.d.ts +7 -0
- package/dist/pipeline/phases/consensus-architecture.d.ts.map +1 -0
- package/dist/pipeline/phases/consensus-architecture.js +84 -0
- package/dist/pipeline/phases/consensus-architecture.js.map +1 -0
- package/dist/pipeline/phases/consensus-master-plan.d.ts +7 -0
- package/dist/pipeline/phases/consensus-master-plan.d.ts.map +1 -0
- package/dist/pipeline/phases/consensus-master-plan.js +81 -0
- package/dist/pipeline/phases/consensus-master-plan.js.map +1 -0
- package/dist/pipeline/phases/consensus-role-plans.d.ts +7 -0
- package/dist/pipeline/phases/consensus-role-plans.d.ts.map +1 -0
- package/dist/pipeline/phases/consensus-role-plans.js +85 -0
- package/dist/pipeline/phases/consensus-role-plans.js.map +1 -0
- package/dist/pipeline/phases/done.d.ts +7 -0
- package/dist/pipeline/phases/done.d.ts.map +1 -0
- package/dist/pipeline/phases/done.js +45 -0
- package/dist/pipeline/phases/done.js.map +1 -0
- package/dist/pipeline/phases/implementation.d.ts +8 -0
- package/dist/pipeline/phases/implementation.d.ts.map +1 -0
- package/dist/pipeline/phases/implementation.js +45 -0
- package/dist/pipeline/phases/implementation.js.map +1 -0
- package/dist/pipeline/phases/index.d.ts +20 -0
- package/dist/pipeline/phases/index.d.ts.map +1 -0
- package/dist/pipeline/phases/index.js +19 -0
- package/dist/pipeline/phases/index.js.map +1 -0
- package/dist/pipeline/phases/intake.d.ts +8 -0
- package/dist/pipeline/phases/intake.d.ts.map +1 -0
- package/dist/pipeline/phases/intake.js +49 -0
- package/dist/pipeline/phases/intake.js.map +1 -0
- package/dist/pipeline/phases/phase-context.d.ts +30 -0
- package/dist/pipeline/phases/phase-context.d.ts.map +1 -0
- package/dist/pipeline/phases/phase-context.js +33 -0
- package/dist/pipeline/phases/phase-context.js.map +1 -0
- package/dist/pipeline/phases/production-gate.d.ts +8 -0
- package/dist/pipeline/phases/production-gate.d.ts.map +1 -0
- package/dist/pipeline/phases/production-gate.js +84 -0
- package/dist/pipeline/phases/production-gate.js.map +1 -0
- package/dist/pipeline/phases/qa-validation.d.ts +7 -0
- package/dist/pipeline/phases/qa-validation.d.ts.map +1 -0
- package/dist/pipeline/phases/qa-validation.js +50 -0
- package/dist/pipeline/phases/qa-validation.js.map +1 -0
- package/dist/pipeline/phases/recovery-loop.d.ts +7 -0
- package/dist/pipeline/phases/recovery-loop.d.ts.map +1 -0
- package/dist/pipeline/phases/recovery-loop.js +93 -0
- package/dist/pipeline/phases/recovery-loop.js.map +1 -0
- package/dist/pipeline/phases/review.d.ts +8 -0
- package/dist/pipeline/phases/review.d.ts.map +1 -0
- package/dist/pipeline/phases/review.js +127 -0
- package/dist/pipeline/phases/review.js.map +1 -0
- package/dist/pipeline/phases/role-planning.d.ts +7 -0
- package/dist/pipeline/phases/role-planning.d.ts.map +1 -0
- package/dist/pipeline/phases/role-planning.js +75 -0
- package/dist/pipeline/phases/role-planning.js.map +1 -0
- package/dist/pipeline/phases/stuck.d.ts +7 -0
- package/dist/pipeline/phases/stuck.d.ts.map +1 -0
- package/dist/pipeline/phases/stuck.js +51 -0
- package/dist/pipeline/phases/stuck.js.map +1 -0
- package/dist/pipeline/repo-snapshot.d.ts +24 -0
- package/dist/pipeline/repo-snapshot.d.ts.map +1 -0
- package/dist/pipeline/repo-snapshot.js +343 -0
- package/dist/pipeline/repo-snapshot.js.map +1 -0
- package/dist/pipeline/role-execution-adapter.d.ts +59 -0
- package/dist/pipeline/role-execution-adapter.d.ts.map +1 -0
- package/dist/pipeline/role-execution-adapter.js +159 -0
- package/dist/pipeline/role-execution-adapter.js.map +1 -0
- package/dist/pipeline/skill-loader.d.ts +34 -0
- package/dist/pipeline/skill-loader.d.ts.map +1 -0
- package/dist/pipeline/skill-loader.js +156 -0
- package/dist/pipeline/skill-loader.js.map +1 -0
- package/dist/pipeline/skills/defaults.d.ts +16 -0
- package/dist/pipeline/skills/defaults.d.ts.map +1 -0
- package/dist/pipeline/skills/defaults.js +189 -0
- package/dist/pipeline/skills/defaults.js.map +1 -0
- package/dist/pipeline/type-defs/artifacts.d.ts +207 -0
- package/dist/pipeline/type-defs/artifacts.d.ts.map +1 -0
- package/dist/pipeline/type-defs/artifacts.js +67 -0
- package/dist/pipeline/type-defs/artifacts.js.map +1 -0
- package/dist/pipeline/type-defs/audit.d.ts +259 -0
- package/dist/pipeline/type-defs/audit.d.ts.map +1 -0
- package/dist/pipeline/type-defs/audit.js +54 -0
- package/dist/pipeline/type-defs/audit.js.map +1 -0
- package/dist/pipeline/type-defs/checks.d.ts +82 -0
- package/dist/pipeline/type-defs/checks.d.ts.map +1 -0
- package/dist/pipeline/type-defs/checks.js +38 -0
- package/dist/pipeline/type-defs/checks.js.map +1 -0
- package/dist/pipeline/type-defs/enums.d.ts +43 -0
- package/dist/pipeline/type-defs/enums.d.ts.map +1 -0
- package/dist/pipeline/type-defs/enums.js +55 -0
- package/dist/pipeline/type-defs/enums.js.map +1 -0
- package/dist/pipeline/type-defs/index.d.ts +12 -0
- package/dist/pipeline/type-defs/index.d.ts.map +1 -0
- package/dist/pipeline/type-defs/index.js +12 -0
- package/dist/pipeline/type-defs/index.js.map +1 -0
- package/dist/pipeline/type-defs/packets.d.ts +821 -0
- package/dist/pipeline/type-defs/packets.d.ts.map +1 -0
- package/dist/pipeline/type-defs/packets.js +109 -0
- package/dist/pipeline/type-defs/packets.js.map +1 -0
- package/dist/pipeline/type-defs/snapshot.d.ts +52 -0
- package/dist/pipeline/type-defs/snapshot.d.ts.map +1 -0
- package/dist/pipeline/type-defs/snapshot.js +35 -0
- package/dist/pipeline/type-defs/snapshot.js.map +1 -0
- package/dist/pipeline/type-defs/state.d.ts +455 -0
- package/dist/pipeline/type-defs/state.d.ts.map +1 -0
- package/dist/pipeline/type-defs/state.js +90 -0
- package/dist/pipeline/type-defs/state.js.map +1 -0
- package/dist/pipeline/types.d.ts +16 -0
- package/dist/pipeline/types.d.ts.map +1 -0
- package/dist/pipeline/types.js +16 -0
- package/dist/pipeline/types.js.map +1 -0
- package/dist/types/audit.d.ts +6 -6
- package/dist/types/consensus.d.ts +5 -1
- package/dist/types/consensus.d.ts.map +1 -1
- package/dist/types/consensus.js +15 -4
- package/dist/types/consensus.js.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/project.d.ts +1 -1
- package/dist/types/project.d.ts.map +1 -1
- package/dist/types/project.js +39 -10
- package/dist/types/project.js.map +1 -1
- package/dist/types/workflow.d.ts +1 -7
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +1 -1
- package/dist/types/workflow.js.map +1 -1
- package/dist/upgrade/handlers.js +5 -5
- package/dist/upgrade/handlers.js.map +1 -1
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +52 -0
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/website-strategy.js +1 -1
- package/dist/workflow/website-strategy.js.map +1 -1
- package/package.json +1 -1
- package/skills/PHASE_GATE_ENGINE_SPEC.md +113 -20
- package/skills/POPEYE_FULL_AUTONOMY_PIPELINE.md +66 -13
- package/src/adapters/gemini.ts +3 -3
- package/src/adapters/openai.ts +2 -2
- package/src/auth/gemini.ts +1 -1
- package/src/cli/commands/create.ts +12 -6
- package/src/cli/commands/resume.ts +9 -1
- package/src/cli/interactive.ts +32 -3
- package/src/config/defaults.ts +7 -2
- package/src/config/popeye-md.ts +139 -0
- package/src/config/schema.ts +21 -8
- package/src/pipeline/artifact-manager.ts +339 -0
- package/src/pipeline/artifact-validators.ts +224 -0
- package/src/pipeline/bridges/review-bridge.ts +371 -0
- package/src/pipeline/change-request.ts +119 -0
- package/src/pipeline/check-runner.ts +504 -0
- package/src/pipeline/command-resolver.ts +168 -0
- package/src/pipeline/consensus/consensus-runner.ts +317 -0
- package/src/pipeline/constitution.ts +109 -0
- package/src/pipeline/gate-engine.ts +347 -0
- package/src/pipeline/index.ts +82 -0
- package/src/pipeline/migration.ts +91 -0
- package/src/pipeline/orchestrator.ts +322 -0
- package/src/pipeline/packets/audit-report-builder.ts +47 -0
- package/src/pipeline/packets/consensus-packet-builder.ts +112 -0
- package/src/pipeline/packets/index.ts +15 -0
- package/src/pipeline/packets/plan-packet-builder.ts +52 -0
- package/src/pipeline/packets/rca-packet-builder.ts +38 -0
- package/src/pipeline/phases/architecture.ts +73 -0
- package/src/pipeline/phases/audit.ts +193 -0
- package/src/pipeline/phases/consensus-architecture.ts +104 -0
- package/src/pipeline/phases/consensus-master-plan.ts +100 -0
- package/src/pipeline/phases/consensus-role-plans.ts +105 -0
- package/src/pipeline/phases/done.ts +68 -0
- package/src/pipeline/phases/implementation.ts +52 -0
- package/src/pipeline/phases/index.ts +21 -0
- package/src/pipeline/phases/intake.ts +68 -0
- package/src/pipeline/phases/phase-context.ts +86 -0
- package/src/pipeline/phases/production-gate.ts +113 -0
- package/src/pipeline/phases/qa-validation.ts +63 -0
- package/src/pipeline/phases/recovery-loop.ts +120 -0
- package/src/pipeline/phases/review.ts +149 -0
- package/src/pipeline/phases/role-planning.ts +92 -0
- package/src/pipeline/phases/stuck.ts +62 -0
- package/src/pipeline/repo-snapshot.ts +395 -0
- package/src/pipeline/role-execution-adapter.ts +238 -0
- package/src/pipeline/skill-loader.ts +192 -0
- package/src/pipeline/skills/defaults.ts +215 -0
- package/src/pipeline/type-defs/artifacts.ts +82 -0
- package/src/pipeline/type-defs/audit.ts +67 -0
- package/src/pipeline/type-defs/checks.ts +47 -0
- package/src/pipeline/type-defs/enums.ts +62 -0
- package/src/pipeline/type-defs/index.ts +12 -0
- package/src/pipeline/type-defs/packets.ts +131 -0
- package/src/pipeline/type-defs/snapshot.ts +55 -0
- package/src/pipeline/type-defs/state.ts +167 -0
- package/src/pipeline/types.ts +16 -0
- package/src/types/consensus.ts +16 -4
- package/src/types/index.ts +1 -0
- package/src/types/project.ts +39 -10
- package/src/types/workflow.ts +1 -1
- package/src/upgrade/handlers.ts +5 -5
- package/src/workflow/index.ts +52 -0
- package/src/workflow/website-strategy.ts +1 -1
- package/tests/cli/model-command.test.ts +19 -9
- package/tests/config/config.test.ts +3 -3
- package/tests/config/popeye-md.test.ts +168 -0
- package/tests/pipeline/artifact-manager.test.ts +183 -0
- package/tests/pipeline/artifact-validators.test.ts +207 -0
- package/tests/pipeline/bridges/review-bridge.test.ts +243 -0
- package/tests/pipeline/change-request.test.ts +180 -0
- package/tests/pipeline/check-runner.test.ts +157 -0
- package/tests/pipeline/command-resolver.test.ts +159 -0
- package/tests/pipeline/consensus-runner.test.ts +206 -0
- package/tests/pipeline/consensus-scoring.test.ts +163 -0
- package/tests/pipeline/constitution.test.ts +122 -0
- package/tests/pipeline/gate-engine.test.ts +195 -0
- package/tests/pipeline/migration.test.ts +133 -0
- package/tests/pipeline/orchestrator.test.ts +614 -0
- package/tests/pipeline/packets/builders.test.ts +347 -0
- package/tests/pipeline/repo-snapshot.test.ts +189 -0
- package/tests/pipeline/role-execution-adapter.test.ts +299 -0
- package/tests/pipeline/session-guidance.test.ts +205 -0
- package/tests/pipeline/skill-loader.test.ts +186 -0
- package/tests/pipeline/start-env-checks.test.ts +123 -0
- package/tests/pipeline/types.test.ts +156 -0
- package/tests/types/consensus.test.ts +1 -1
- package/tests/workflow/pipeline-bootstrap.test.ts +162 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check Runner — executes build/test/lint/typecheck commands
|
|
3
|
+
* and produces GateCheckResult artifacts.
|
|
4
|
+
*
|
|
5
|
+
* Safety: command sanitization, cwd enforcement, stream caps,
|
|
6
|
+
* configurable timeouts (P2-G).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { exec } from 'node:child_process';
|
|
10
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
11
|
+
import { join, extname } from 'node:path';
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
GateCheckResult,
|
|
15
|
+
GateCheckType,
|
|
16
|
+
ResolvedCommands,
|
|
17
|
+
RepoSnapshot,
|
|
18
|
+
ArtifactEntry,
|
|
19
|
+
PipelinePhase,
|
|
20
|
+
} from './types.js';
|
|
21
|
+
import { ArtifactManager } from './artifact-manager.js';
|
|
22
|
+
|
|
23
|
+
// ─── Constants ───────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/** Default timeout per check type in milliseconds */
|
|
26
|
+
const DEFAULT_TIMEOUTS: Record<string, number> = {
|
|
27
|
+
build: 20 * 60 * 1000, // 20 minutes
|
|
28
|
+
test: 10 * 60 * 1000, // 10 minutes
|
|
29
|
+
lint: 5 * 60 * 1000, // 5 minutes
|
|
30
|
+
typecheck: 5 * 60 * 1000, // 5 minutes
|
|
31
|
+
migration: 5 * 60 * 1000, // 5 minutes
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Max stdout/stderr capture in bytes */
|
|
35
|
+
const MAX_OUTPUT_SIZE = 1024 * 1024; // 1 MB
|
|
36
|
+
|
|
37
|
+
/** Dangerous command patterns to reject */
|
|
38
|
+
const DANGEROUS_PATTERNS = [
|
|
39
|
+
/rm\s+-rf\s+\//,
|
|
40
|
+
/sudo\s+/,
|
|
41
|
+
/>\s*\/dev\//,
|
|
42
|
+
/>\s*\/etc\//,
|
|
43
|
+
/>\s*\/usr\//,
|
|
44
|
+
/;\s*rm\s/,
|
|
45
|
+
/&&\s*rm\s/,
|
|
46
|
+
/\|\s*sh$/,
|
|
47
|
+
/\|\s*bash$/,
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
/** Placeholder patterns for scanning */
|
|
51
|
+
const PLACEHOLDER_PATTERNS = [
|
|
52
|
+
/\bTODO\b/i,
|
|
53
|
+
/\bFIXME\b/i,
|
|
54
|
+
/\bHACK\b/i,
|
|
55
|
+
/\bXXX\b/i,
|
|
56
|
+
/placeholder/i,
|
|
57
|
+
/\bmock\b(?!\.)/i, // 'mock' but not 'mock.' (import paths)
|
|
58
|
+
/\btemp\b(?!late)/i, // 'temp' but not 'template'
|
|
59
|
+
/lorem ipsum/i,
|
|
60
|
+
/example\.com/i,
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
// ─── Command Sanitization ────────────────────────────────
|
|
64
|
+
|
|
65
|
+
function sanitizeCommand(command: string): { safe: boolean; reason?: string } {
|
|
66
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
67
|
+
if (pattern.test(command)) {
|
|
68
|
+
return { safe: false, reason: `Matches dangerous pattern: ${pattern.source}` };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { safe: true };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── Check Execution ─────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
/** Execute a single check command */
|
|
77
|
+
export async function runCheck(
|
|
78
|
+
checkType: GateCheckType,
|
|
79
|
+
command: string,
|
|
80
|
+
projectDir: string,
|
|
81
|
+
timeoutOverride?: number,
|
|
82
|
+
): Promise<GateCheckResult> {
|
|
83
|
+
const startTime = Date.now();
|
|
84
|
+
|
|
85
|
+
// Sanitize command
|
|
86
|
+
const { safe, reason } = sanitizeCommand(command);
|
|
87
|
+
if (!safe) {
|
|
88
|
+
return {
|
|
89
|
+
check_type: checkType,
|
|
90
|
+
status: 'fail',
|
|
91
|
+
command,
|
|
92
|
+
exit_code: -1,
|
|
93
|
+
stderr_summary: `Command rejected: ${reason}`,
|
|
94
|
+
duration_ms: 0,
|
|
95
|
+
timestamp: new Date().toISOString(),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const timeout = timeoutOverride ?? DEFAULT_TIMEOUTS[checkType] ?? 5 * 60 * 1000;
|
|
100
|
+
|
|
101
|
+
return new Promise<GateCheckResult>((resolve) => {
|
|
102
|
+
const proc = exec(command, {
|
|
103
|
+
cwd: projectDir,
|
|
104
|
+
timeout,
|
|
105
|
+
maxBuffer: MAX_OUTPUT_SIZE,
|
|
106
|
+
env: {
|
|
107
|
+
...process.env,
|
|
108
|
+
NODE_ENV: 'test',
|
|
109
|
+
CI: 'true',
|
|
110
|
+
},
|
|
111
|
+
}, (error, _stdout, stderr) => {
|
|
112
|
+
const duration = Date.now() - startTime;
|
|
113
|
+
const exitCode = error ? (error as NodeJS.ErrnoException & { code?: number }).code ?? 1 : 0;
|
|
114
|
+
|
|
115
|
+
// Truncate output for summary
|
|
116
|
+
const stderrSummary = stderr
|
|
117
|
+
? stderr.slice(0, 2000) + (stderr.length > 2000 ? '\n... (truncated)' : '')
|
|
118
|
+
: undefined;
|
|
119
|
+
|
|
120
|
+
resolve({
|
|
121
|
+
check_type: checkType,
|
|
122
|
+
status: exitCode === 0 ? 'pass' : 'fail',
|
|
123
|
+
command,
|
|
124
|
+
exit_code: typeof exitCode === 'number' ? exitCode : 1,
|
|
125
|
+
stdout_artifact: undefined, // Filled by storeCheckResults if needed
|
|
126
|
+
stderr_summary: stderrSummary,
|
|
127
|
+
duration_ms: duration,
|
|
128
|
+
timestamp: new Date().toISOString(),
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Safety: kill after timeout (backup for exec timeout)
|
|
133
|
+
setTimeout(() => {
|
|
134
|
+
try { proc.kill('SIGTERM'); } catch { /* already dead */ }
|
|
135
|
+
}, timeout + 5000);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Run all applicable checks based on resolved commands */
|
|
140
|
+
export async function runAllChecks(
|
|
141
|
+
resolvedCommands: ResolvedCommands,
|
|
142
|
+
projectDir: string,
|
|
143
|
+
): Promise<GateCheckResult[]> {
|
|
144
|
+
const results: GateCheckResult[] = [];
|
|
145
|
+
const checkMap: [GateCheckType, string | undefined][] = [
|
|
146
|
+
['build', resolvedCommands.build],
|
|
147
|
+
['test', resolvedCommands.test],
|
|
148
|
+
['lint', resolvedCommands.lint],
|
|
149
|
+
['typecheck', resolvedCommands.typecheck],
|
|
150
|
+
['migration', resolvedCommands.migrations],
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
for (const [checkType, command] of checkMap) {
|
|
154
|
+
if (!command) {
|
|
155
|
+
results.push({
|
|
156
|
+
check_type: checkType,
|
|
157
|
+
status: 'skip',
|
|
158
|
+
command: '',
|
|
159
|
+
exit_code: 0,
|
|
160
|
+
duration_ms: 0,
|
|
161
|
+
timestamp: new Date().toISOString(),
|
|
162
|
+
});
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const result = await runCheck(checkType, command, projectDir);
|
|
167
|
+
results.push(result);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return results;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Store check results as artifacts */
|
|
174
|
+
export function storeCheckResults(
|
|
175
|
+
results: GateCheckResult[],
|
|
176
|
+
artifactManager: ArtifactManager,
|
|
177
|
+
phase: PipelinePhase,
|
|
178
|
+
): ArtifactEntry[] {
|
|
179
|
+
const artifacts: ArtifactEntry[] = [];
|
|
180
|
+
|
|
181
|
+
for (const result of results) {
|
|
182
|
+
if (result.status === 'skip') continue;
|
|
183
|
+
|
|
184
|
+
// Only store meaningful output
|
|
185
|
+
const content = JSON.stringify(result, null, 2);
|
|
186
|
+
if (content.length > 100) {
|
|
187
|
+
const entry = artifactManager.createAndStoreJson(
|
|
188
|
+
mapCheckTypeToArtifactType(result.check_type),
|
|
189
|
+
result,
|
|
190
|
+
phase,
|
|
191
|
+
);
|
|
192
|
+
artifacts.push(entry);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return artifacts;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function mapCheckTypeToArtifactType(
|
|
200
|
+
checkType: GateCheckType,
|
|
201
|
+
): 'build_check' | 'test_check' | 'lint_check' | 'typecheck_check' | 'placeholder_scan' {
|
|
202
|
+
switch (checkType) {
|
|
203
|
+
case 'build': return 'build_check';
|
|
204
|
+
case 'test': return 'test_check';
|
|
205
|
+
case 'lint': return 'lint_check';
|
|
206
|
+
case 'typecheck': return 'typecheck_check';
|
|
207
|
+
case 'placeholder_scan': return 'placeholder_scan';
|
|
208
|
+
default: return 'build_check';
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ─── Placeholder Scanner (P2-2) ──────────────────────────
|
|
213
|
+
|
|
214
|
+
/** Scan project for placeholder/TODO/mock content */
|
|
215
|
+
export function runPlaceholderScan(
|
|
216
|
+
projectDir: string,
|
|
217
|
+
allowlistPath?: string,
|
|
218
|
+
): GateCheckResult {
|
|
219
|
+
const startTime = Date.now();
|
|
220
|
+
const findings: string[] = [];
|
|
221
|
+
|
|
222
|
+
// Load allowlist if present
|
|
223
|
+
const allowlist = loadAllowlist(
|
|
224
|
+
allowlistPath ?? join(projectDir, '.popeye-placeholder-allowlist'),
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// Scan source directories
|
|
228
|
+
const scanDirs = ['src', 'app', 'pages', 'components', 'lib', 'server', 'api'];
|
|
229
|
+
|
|
230
|
+
for (const dir of scanDirs) {
|
|
231
|
+
const fullDir = join(projectDir, dir);
|
|
232
|
+
if (!existsSync(fullDir)) continue;
|
|
233
|
+
scanDirForPlaceholders(fullDir, projectDir, allowlist, findings);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const duration = Date.now() - startTime;
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
check_type: 'placeholder_scan',
|
|
240
|
+
status: findings.length > 0 ? 'fail' : 'pass',
|
|
241
|
+
command: 'placeholder-scan',
|
|
242
|
+
exit_code: findings.length > 0 ? 1 : 0,
|
|
243
|
+
stderr_summary: findings.length > 0
|
|
244
|
+
? `Found ${findings.length} placeholder(s):\n${findings.slice(0, 20).join('\n')}`
|
|
245
|
+
: undefined,
|
|
246
|
+
duration_ms: duration,
|
|
247
|
+
timestamp: new Date().toISOString(),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function scanDirForPlaceholders(
|
|
252
|
+
dir: string,
|
|
253
|
+
projectDir: string,
|
|
254
|
+
allowlist: Set<string>,
|
|
255
|
+
findings: string[],
|
|
256
|
+
): void {
|
|
257
|
+
const codeExts = new Set(['.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.rs']);
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
261
|
+
for (const entry of entries) {
|
|
262
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
263
|
+
|
|
264
|
+
const fullPath = join(dir, entry.name);
|
|
265
|
+
|
|
266
|
+
if (entry.isDirectory()) {
|
|
267
|
+
scanDirForPlaceholders(fullPath, projectDir, allowlist, findings);
|
|
268
|
+
} else if (codeExts.has(extname(entry.name))) {
|
|
269
|
+
const relativePath = fullPath.replace(projectDir + '/', '');
|
|
270
|
+
if (allowlist.has(relativePath)) continue;
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
274
|
+
const lines = content.split('\n');
|
|
275
|
+
|
|
276
|
+
for (let i = 0; i < lines.length; i++) {
|
|
277
|
+
for (const pattern of PLACEHOLDER_PATTERNS) {
|
|
278
|
+
if (pattern.test(lines[i])) {
|
|
279
|
+
findings.push(`${relativePath}:${i + 1}: ${lines[i].trim().slice(0, 80)}`);
|
|
280
|
+
break; // One finding per line
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
// Skip unreadable files
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
} catch {
|
|
290
|
+
// Skip unreadable directories
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function loadAllowlist(path: string): Set<string> {
|
|
295
|
+
if (!existsSync(path)) return new Set();
|
|
296
|
+
try {
|
|
297
|
+
const content = readFileSync(path, 'utf-8');
|
|
298
|
+
return new Set(
|
|
299
|
+
content.split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#')),
|
|
300
|
+
);
|
|
301
|
+
} catch {
|
|
302
|
+
return new Set();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ─── Start Check (v1.1 Gap #5) ──────────────────────────
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Attempt to start the application and verify it does not crash immediately.
|
|
310
|
+
* Optionally checks a health endpoint if a port is detected.
|
|
311
|
+
*
|
|
312
|
+
* Args:
|
|
313
|
+
* startCommand: The command to start the app (e.g., "npm run start").
|
|
314
|
+
* projectDir: Project root directory.
|
|
315
|
+
* options: Optional port, health path, and timeout.
|
|
316
|
+
*
|
|
317
|
+
* Returns:
|
|
318
|
+
* GateCheckResult with pass/fail status.
|
|
319
|
+
*/
|
|
320
|
+
export async function runStartCheck(
|
|
321
|
+
startCommand: string,
|
|
322
|
+
projectDir: string,
|
|
323
|
+
options?: { port?: number; healthPath?: string; timeoutMs?: number },
|
|
324
|
+
): Promise<GateCheckResult> {
|
|
325
|
+
const startTime = Date.now();
|
|
326
|
+
const timeout = options?.timeoutMs ?? 15000;
|
|
327
|
+
|
|
328
|
+
// Sanitize command
|
|
329
|
+
const { safe, reason } = sanitizeCommand(startCommand);
|
|
330
|
+
if (!safe) {
|
|
331
|
+
return {
|
|
332
|
+
check_type: 'start',
|
|
333
|
+
status: 'fail',
|
|
334
|
+
command: startCommand,
|
|
335
|
+
exit_code: -1,
|
|
336
|
+
stderr_summary: `Command rejected: ${reason}`,
|
|
337
|
+
duration_ms: 0,
|
|
338
|
+
timestamp: new Date().toISOString(),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return new Promise<GateCheckResult>((resolve) => {
|
|
343
|
+
let stderr = '';
|
|
344
|
+
let resolved = false;
|
|
345
|
+
|
|
346
|
+
const proc = exec(startCommand, {
|
|
347
|
+
cwd: projectDir,
|
|
348
|
+
timeout: timeout + 5000,
|
|
349
|
+
maxBuffer: MAX_OUTPUT_SIZE,
|
|
350
|
+
env: { ...process.env, NODE_ENV: 'production' },
|
|
351
|
+
}, (error, _stdout, stderrOutput) => {
|
|
352
|
+
if (resolved) return;
|
|
353
|
+
resolved = true;
|
|
354
|
+
|
|
355
|
+
const duration = Date.now() - startTime;
|
|
356
|
+
stderr = stderrOutput ?? '';
|
|
357
|
+
|
|
358
|
+
// Process exited — if it exited within timeout, it crashed
|
|
359
|
+
resolve({
|
|
360
|
+
check_type: 'start',
|
|
361
|
+
status: 'fail',
|
|
362
|
+
command: startCommand,
|
|
363
|
+
exit_code: error ? (typeof (error as NodeJS.ErrnoException & { code?: number }).code === 'number'
|
|
364
|
+
? (error as NodeJS.ErrnoException & { code?: number }).code!
|
|
365
|
+
: 1) : 0,
|
|
366
|
+
stderr_summary: stderr ? stderr.slice(0, 2000) : 'Process exited prematurely',
|
|
367
|
+
duration_ms: duration,
|
|
368
|
+
timestamp: new Date().toISOString(),
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// If process survives for the timeout period, consider it passing
|
|
373
|
+
setTimeout(() => {
|
|
374
|
+
if (resolved) return;
|
|
375
|
+
resolved = true;
|
|
376
|
+
|
|
377
|
+
// Kill the process
|
|
378
|
+
try { proc.kill('SIGTERM'); } catch { /* already dead */ }
|
|
379
|
+
|
|
380
|
+
const duration = Date.now() - startTime;
|
|
381
|
+
resolve({
|
|
382
|
+
check_type: 'start',
|
|
383
|
+
status: 'pass',
|
|
384
|
+
command: startCommand,
|
|
385
|
+
exit_code: 0,
|
|
386
|
+
stderr_summary: stderr ? stderr.slice(0, 500) : undefined,
|
|
387
|
+
duration_ms: duration,
|
|
388
|
+
timestamp: new Date().toISOString(),
|
|
389
|
+
});
|
|
390
|
+
}, timeout);
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ─── Env Check (v1.1 Gap #5) ────────────────────────────
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Validate that required environment variables exist.
|
|
398
|
+
* Reads .env.example for required var names and checks .env has them set.
|
|
399
|
+
*
|
|
400
|
+
* Args:
|
|
401
|
+
* projectDir: Project root directory.
|
|
402
|
+
* _snapshot: Repo snapshot (for future use).
|
|
403
|
+
*
|
|
404
|
+
* Returns:
|
|
405
|
+
* GateCheckResult with pass/fail status.
|
|
406
|
+
*/
|
|
407
|
+
export function runEnvCheck(
|
|
408
|
+
projectDir: string,
|
|
409
|
+
_snapshot?: RepoSnapshot,
|
|
410
|
+
): GateCheckResult {
|
|
411
|
+
const startTime = Date.now();
|
|
412
|
+
const examplePath = join(projectDir, '.env.example');
|
|
413
|
+
const envPath = join(projectDir, '.env');
|
|
414
|
+
const missingVars: string[] = [];
|
|
415
|
+
const emptyVars: string[] = [];
|
|
416
|
+
|
|
417
|
+
// If no .env.example, skip check
|
|
418
|
+
if (!existsSync(examplePath)) {
|
|
419
|
+
return {
|
|
420
|
+
check_type: 'env_check',
|
|
421
|
+
status: 'pass',
|
|
422
|
+
command: 'env-check',
|
|
423
|
+
exit_code: 0,
|
|
424
|
+
stderr_summary: 'No .env.example found — skipping env validation',
|
|
425
|
+
duration_ms: Date.now() - startTime,
|
|
426
|
+
timestamp: new Date().toISOString(),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Parse .env.example for required var names
|
|
431
|
+
const exampleContent = readFileSync(examplePath, 'utf-8');
|
|
432
|
+
const requiredVars = parseEnvVarNames(exampleContent);
|
|
433
|
+
|
|
434
|
+
// Check .env exists
|
|
435
|
+
if (!existsSync(envPath)) {
|
|
436
|
+
return {
|
|
437
|
+
check_type: 'env_check',
|
|
438
|
+
status: 'fail',
|
|
439
|
+
command: 'env-check',
|
|
440
|
+
exit_code: 1,
|
|
441
|
+
stderr_summary: `.env file not found. Required vars from .env.example: ${requiredVars.join(', ')}`,
|
|
442
|
+
duration_ms: Date.now() - startTime,
|
|
443
|
+
timestamp: new Date().toISOString(),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Parse .env and check all required vars are present and non-empty
|
|
448
|
+
const envContent = readFileSync(envPath, 'utf-8');
|
|
449
|
+
const envVars = parseEnvVarValues(envContent);
|
|
450
|
+
|
|
451
|
+
for (const varName of requiredVars) {
|
|
452
|
+
if (!(varName in envVars)) {
|
|
453
|
+
missingVars.push(varName);
|
|
454
|
+
} else if (!envVars[varName]) {
|
|
455
|
+
emptyVars.push(varName);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const duration = Date.now() - startTime;
|
|
460
|
+
const hasFail = missingVars.length > 0;
|
|
461
|
+
const summaryParts: string[] = [];
|
|
462
|
+
|
|
463
|
+
if (missingVars.length > 0) {
|
|
464
|
+
summaryParts.push(`Missing vars: ${missingVars.join(', ')}`);
|
|
465
|
+
}
|
|
466
|
+
if (emptyVars.length > 0) {
|
|
467
|
+
summaryParts.push(`Empty vars (warning): ${emptyVars.join(', ')}`);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
check_type: 'env_check',
|
|
472
|
+
status: hasFail ? 'fail' : 'pass',
|
|
473
|
+
command: 'env-check',
|
|
474
|
+
exit_code: hasFail ? 1 : 0,
|
|
475
|
+
stderr_summary: summaryParts.length > 0 ? summaryParts.join('; ') : undefined,
|
|
476
|
+
duration_ms: duration,
|
|
477
|
+
timestamp: new Date().toISOString(),
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Parse env var names from .env.example (lines like KEY=value or KEY=) */
|
|
482
|
+
function parseEnvVarNames(content: string): string[] {
|
|
483
|
+
return content
|
|
484
|
+
.split('\n')
|
|
485
|
+
.map((line) => line.trim())
|
|
486
|
+
.filter((line) => line && !line.startsWith('#'))
|
|
487
|
+
.map((line) => line.split('=')[0].trim())
|
|
488
|
+
.filter((name) => name.length > 0);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Parse env vars into key-value map from .env content */
|
|
492
|
+
function parseEnvVarValues(content: string): Record<string, string> {
|
|
493
|
+
const vars: Record<string, string> = {};
|
|
494
|
+
for (const line of content.split('\n')) {
|
|
495
|
+
const trimmed = line.trim();
|
|
496
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
497
|
+
const eqIndex = trimmed.indexOf('=');
|
|
498
|
+
if (eqIndex === -1) continue;
|
|
499
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
500
|
+
const value = trimmed.slice(eqIndex + 1).trim().replace(/^["']|["']$/g, '');
|
|
501
|
+
vars[key] = value;
|
|
502
|
+
}
|
|
503
|
+
return vars;
|
|
504
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Resolver — detects project-type-specific build/test/lint/typecheck
|
|
3
|
+
* commands from a RepoSnapshot. Used by CheckRunner and ProductionGate.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { RepoSnapshot, ResolvedCommands } from './types.js';
|
|
7
|
+
|
|
8
|
+
// ─── Project Type Detection ──────────────────────────────
|
|
9
|
+
|
|
10
|
+
export type ProjectType = 'node' | 'python' | 'mixed' | 'unknown';
|
|
11
|
+
|
|
12
|
+
export function detectProjectType(snapshot: RepoSnapshot): ProjectType {
|
|
13
|
+
const hasNode = snapshot.config_files.some((c) => c.type === 'package.json');
|
|
14
|
+
const hasPython = snapshot.config_files.some(
|
|
15
|
+
(c) => c.type === 'pyproject.toml' || c.type === 'requirements.txt' || c.type === 'setup.py',
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
if (hasNode && hasPython) return 'mixed';
|
|
19
|
+
if (hasNode) return 'node';
|
|
20
|
+
if (hasPython) return 'python';
|
|
21
|
+
return 'unknown';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ─── Command Resolution ──────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export function resolveCommands(
|
|
27
|
+
snapshot: RepoSnapshot,
|
|
28
|
+
overrides?: Partial<ResolvedCommands>,
|
|
29
|
+
): ResolvedCommands {
|
|
30
|
+
const projectType = detectProjectType(snapshot);
|
|
31
|
+
const pm = snapshot.package_manager ?? 'npm';
|
|
32
|
+
const scripts = snapshot.scripts;
|
|
33
|
+
|
|
34
|
+
let resolved: ResolvedCommands;
|
|
35
|
+
|
|
36
|
+
switch (projectType) {
|
|
37
|
+
case 'node':
|
|
38
|
+
resolved = resolveNodeCommands(pm, scripts, snapshot);
|
|
39
|
+
break;
|
|
40
|
+
case 'python':
|
|
41
|
+
resolved = resolvePythonCommands(snapshot);
|
|
42
|
+
break;
|
|
43
|
+
case 'mixed':
|
|
44
|
+
// Prefer Node commands, augment with Python where Node is missing
|
|
45
|
+
resolved = resolveNodeCommands(pm, scripts, snapshot);
|
|
46
|
+
if (!resolved.test) {
|
|
47
|
+
const pyResolved = resolvePythonCommands(snapshot);
|
|
48
|
+
resolved.test = pyResolved.test;
|
|
49
|
+
}
|
|
50
|
+
break;
|
|
51
|
+
default:
|
|
52
|
+
resolved = { resolved_from: 'none' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Apply overrides
|
|
56
|
+
if (overrides) {
|
|
57
|
+
if (overrides.build) resolved.build = overrides.build;
|
|
58
|
+
if (overrides.test) resolved.test = overrides.test;
|
|
59
|
+
if (overrides.lint) resolved.lint = overrides.lint;
|
|
60
|
+
if (overrides.typecheck) resolved.typecheck = overrides.typecheck;
|
|
61
|
+
if (overrides.migrations) resolved.migrations = overrides.migrations;
|
|
62
|
+
if (overrides.start) resolved.start = overrides.start;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return resolved;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── Node Resolution ─────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
function resolveNodeCommands(
|
|
71
|
+
pm: string,
|
|
72
|
+
scripts: Record<string, string>,
|
|
73
|
+
snapshot: RepoSnapshot,
|
|
74
|
+
): ResolvedCommands {
|
|
75
|
+
const run = pm === 'yarn' ? 'yarn' : pm === 'pnpm' ? 'pnpm' : `${pm} run`;
|
|
76
|
+
const npx = pm === 'pnpm' ? 'pnpm exec' : pm === 'yarn' ? 'yarn' : 'npx';
|
|
77
|
+
|
|
78
|
+
const resolved: ResolvedCommands = {
|
|
79
|
+
resolved_from: 'package.json',
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Build
|
|
83
|
+
if (scripts.build) {
|
|
84
|
+
resolved.build = `${run} build`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Test
|
|
88
|
+
if (scripts.test) {
|
|
89
|
+
resolved.test = `${run} test`;
|
|
90
|
+
} else if (snapshot.test_framework === 'vitest') {
|
|
91
|
+
resolved.test = `${npx} vitest run`;
|
|
92
|
+
} else if (snapshot.test_framework === 'jest') {
|
|
93
|
+
resolved.test = `${npx} jest`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Lint
|
|
97
|
+
if (scripts.lint) {
|
|
98
|
+
resolved.lint = `${run} lint`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Typecheck
|
|
102
|
+
if (scripts.typecheck) {
|
|
103
|
+
resolved.typecheck = `${run} typecheck`;
|
|
104
|
+
} else if (snapshot.languages_detected.includes('typescript')) {
|
|
105
|
+
resolved.typecheck = `${npx} tsc --noEmit`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Migrations
|
|
109
|
+
const hasPrisma = snapshot.config_files.some(
|
|
110
|
+
(c) => c.type === 'prisma/schema.prisma',
|
|
111
|
+
);
|
|
112
|
+
if (hasPrisma) {
|
|
113
|
+
resolved.migrations = `${npx} prisma migrate deploy`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Start
|
|
117
|
+
if (scripts.start) {
|
|
118
|
+
resolved.start = `${run} start`;
|
|
119
|
+
} else if (scripts.dev) {
|
|
120
|
+
resolved.start = `${run} dev`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return resolved;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── Python Resolution ───────────────────────────────────
|
|
127
|
+
|
|
128
|
+
function resolvePythonCommands(snapshot: RepoSnapshot): ResolvedCommands {
|
|
129
|
+
const resolved: ResolvedCommands = {
|
|
130
|
+
resolved_from: snapshot.config_files
|
|
131
|
+
.find((c) => c.type === 'pyproject.toml' || c.type === 'requirements.txt')
|
|
132
|
+
?.path ?? 'python-defaults',
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Test
|
|
136
|
+
if (snapshot.test_framework === 'pytest') {
|
|
137
|
+
resolved.test = 'pytest tests/';
|
|
138
|
+
} else {
|
|
139
|
+
resolved.test = 'pytest tests/'; // default for Python
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Lint
|
|
143
|
+
const hasPyproject = snapshot.config_files.some((c) => c.type === 'pyproject.toml');
|
|
144
|
+
if (hasPyproject) {
|
|
145
|
+
resolved.lint = 'ruff check .';
|
|
146
|
+
} else {
|
|
147
|
+
resolved.lint = 'flake8 src/';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Typecheck
|
|
151
|
+
if (snapshot.languages_detected.includes('python')) {
|
|
152
|
+
resolved.typecheck = 'mypy src/';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Build
|
|
156
|
+
resolved.build = 'python -m build';
|
|
157
|
+
|
|
158
|
+
// Migrations
|
|
159
|
+
const hasAlembic = snapshot.config_files.some((c) => c.type === 'alembic.ini');
|
|
160
|
+
if (hasAlembic) {
|
|
161
|
+
resolved.migrations = 'alembic upgrade head';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Start
|
|
165
|
+
resolved.start = 'uvicorn main:app --host 0.0.0.0 --port 8000';
|
|
166
|
+
|
|
167
|
+
return resolved;
|
|
168
|
+
}
|