ai-project-maintainer 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/LICENSE +21 -0
- package/README.md +175 -0
- package/ai-project-maintainer/SKILL.md +62 -0
- package/ai-project-maintainer/agents/openai.yaml +6 -0
- package/ai-project-maintainer/references/ci-guardrails.md +55 -0
- package/ai-project-maintainer/references/database.md +60 -0
- package/ai-project-maintainer/references/electron-desktop.md +43 -0
- package/ai-project-maintainer/references/incident-response.md +52 -0
- package/ai-project-maintainer/references/local-gate.md +117 -0
- package/ai-project-maintainer/references/security.md +48 -0
- package/ai-project-maintainer/references/tool-router.md +53 -0
- package/ai-project-maintainer/scripts/audit-plan.mjs +155 -0
- package/ai-project-maintainer/scripts/bootstrap-local-tools.ps1 +109 -0
- package/ai-project-maintainer/scripts/check-syntax.mjs +41 -0
- package/ai-project-maintainer/scripts/ci-smoke-gate.mjs +26 -0
- package/ai-project-maintainer/scripts/cli.mjs +165 -0
- package/ai-project-maintainer/scripts/doctor.mjs +80 -0
- package/ai-project-maintainer/scripts/init-audit.mjs +105 -0
- package/ai-project-maintainer/scripts/init-project.mjs +229 -0
- package/ai-project-maintainer/scripts/lib/check-registry.mjs +68 -0
- package/ai-project-maintainer/scripts/lib/checks.mjs +337 -0
- package/ai-project-maintainer/scripts/lib/command-runner.mjs +130 -0
- package/ai-project-maintainer/scripts/lib/intake.mjs +172 -0
- package/ai-project-maintainer/scripts/lib/policy.mjs +150 -0
- package/ai-project-maintainer/scripts/lib/project-detect.mjs +111 -0
- package/ai-project-maintainer/scripts/lib/report.mjs +227 -0
- package/ai-project-maintainer/scripts/probe-project.mjs +218 -0
- package/ai-project-maintainer/scripts/report-summary.mjs +25 -0
- package/ai-project-maintainer/scripts/run-local-gate.mjs +147 -0
- package/docs/CI-GITHUB-ACTIONS.zh-CN.md +83 -0
- package/docs/DEMO.md +81 -0
- package/docs/DEMO.zh-CN.md +81 -0
- package/docs/GITHUB-LAUNCH-CHECKLIST.md +77 -0
- package/docs/INSTALL.zh-CN.md +112 -0
- package/docs/INTAKE-SCHEMA.zh-CN.md +105 -0
- package/docs/POLICY-AND-EXCEPTIONS.zh-CN.md +96 -0
- package/docs/PRODUCTION-AUDIT.zh-CN.md +89 -0
- package/docs/PROMOTION.md +116 -0
- package/docs/UPGRADE-ROADMAP.zh-CN.md +47 -0
- package/docs/demo-output/security-report.md +57 -0
- package/docs/superpowers/plans/2026-06-29-ci-dogfooding.md +200 -0
- package/package.json +21 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Tool Router
|
|
2
|
+
|
|
3
|
+
Use this reference to choose checks dynamically. Run only tools that match the repository and the user's scope.
|
|
4
|
+
|
|
5
|
+
## Selection Order
|
|
6
|
+
|
|
7
|
+
1. Inspect the project with `scripts/probe-project.mjs`.
|
|
8
|
+
2. Use installed Codex skills for security review when available.
|
|
9
|
+
3. Use local CLI tools that are already installed.
|
|
10
|
+
4. Use package-manager or containerized tools only when the repo already uses them or the user approves.
|
|
11
|
+
5. Fall back to manual review with explicit coverage gaps.
|
|
12
|
+
|
|
13
|
+
## Core Routing Table
|
|
14
|
+
|
|
15
|
+
| Surface | Signals | Primary tools | Notes |
|
|
16
|
+
| --- | --- | --- | --- |
|
|
17
|
+
| Code security | Auth, API routes, serializers, file upload, HTTP clients, SQL construction, crypto | `codex-security`, Semgrep, CodeQL, Open Code Review | Prioritize changed files and dataflow into sinks. |
|
|
18
|
+
| Secrets | `.env`, private keys, token-like config, CI variables, committed credentials | Gitleaks, Trivy secret scan | Do not print secret values. Report only location and type. |
|
|
19
|
+
| Dependencies | Lockfiles, Docker images, SBOM, package manifests | Trivy, OSV-Scanner, native package audit | Separate reachable production risk from dev-only noise. |
|
|
20
|
+
| Database migrations | `migrations/`, `db/migrate`, Prisma, Drizzle, Alembic, Flyway, Liquibase, raw SQL | Squawk, Atlas, Bytebase, pgroll, strong_migrations, gh-ost, pt-online-schema-change | Review lock behavior and rollback before correctness polish. |
|
|
21
|
+
| IaC and cloud | Terraform, CloudFormation, Pulumi, Helm, Kubernetes YAML, Docker Compose | Checkov, Trivy config, Conftest/OPA, Kubescape | Focus on public exposure, IAM, network policy, privileged runtime, and secret mounts. |
|
|
22
|
+
| Containers | Dockerfile, compose, image references, deploy manifests | Trivy image/config, Hadolint, Docker build checks | Check root user, privileged mode, writable filesystem, and unpinned base images. |
|
|
23
|
+
| Kubernetes runtime | K8s manifests, `kubectl` context, incident request | k8sgpt, HolmesGPT, Kubescape, Falco, Cilium/Tetragon | Default to read-only commands. |
|
|
24
|
+
| Web/API DAST | Staging URL explicitly provided by user | OWASP ZAP, Nuclei | Require explicit scope for active scans. Never infer internet targets. |
|
|
25
|
+
| Incident response | Alerts, logs, metrics, traces, rollout history, deploys, migrations | HolmesGPT, OpenSRE, k8sgpt, kubectl, observability CLIs/APIs | Build a timeline before proposing fixes. |
|
|
26
|
+
|
|
27
|
+
## Command Patterns
|
|
28
|
+
|
|
29
|
+
Use these as patterns, adapting paths and package managers to the repo.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
semgrep scan --config auto
|
|
33
|
+
trivy fs --scanners vuln,secret,misconfig .
|
|
34
|
+
gitleaks detect --source . --redact
|
|
35
|
+
checkov -d .
|
|
36
|
+
squawk path/to/migration.sql
|
|
37
|
+
atlas migrate lint --dir "file://migrations"
|
|
38
|
+
kubescape scan
|
|
39
|
+
k8sgpt analyze
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Use CodeQL when a CodeQL database or GitHub code scanning setup already exists, or when the user asks for deeper security analysis.
|
|
43
|
+
|
|
44
|
+
## Missing Tool Handling
|
|
45
|
+
|
|
46
|
+
When a useful tool is unavailable, write a short gap:
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
Unavailable: squawk. Coverage gap: Postgres migration lock-risk linting was not run.
|
|
50
|
+
Smallest next guardrail: add squawk to CI for changed `.sql` migration files.
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Do not turn the review into an installation project unless the user asks for setup.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { detectProject } from "./lib/project-detect.mjs";
|
|
6
|
+
import { hasConfiguredEvidence, loadIntake } from "./lib/intake.mjs";
|
|
7
|
+
|
|
8
|
+
function item(id, title, status, summary, recommendation = "") {
|
|
9
|
+
return { id, title, status, summary, recommendation };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function realBusinessFlows(intake) {
|
|
13
|
+
return (intake.businessFlows.business_flows || []).filter((flow) => flow && !flow.template && flow.id);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function flowTests(flows) {
|
|
17
|
+
return flows.flatMap((flow) => (Array.isArray(flow.tests) ? flow.tests : []));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function buildAuditPlan(project, intake = loadIntake(project.root, project)) {
|
|
21
|
+
const profile = {
|
|
22
|
+
projectType: intake.profile.derived.projectType,
|
|
23
|
+
hasDatabase: intake.profile.derived.hasDatabase,
|
|
24
|
+
hasCi: intake.profile.derived.hasCi,
|
|
25
|
+
hasElectron: intake.profile.derived.hasElectron,
|
|
26
|
+
hasDeployment: intake.profile.derived.hasDeployment,
|
|
27
|
+
handlesAuth: Boolean(intake.profile.risk?.handles_auth),
|
|
28
|
+
handlesSensitiveData: Boolean(intake.profile.risk?.handles_sensitive_data),
|
|
29
|
+
handlesFinancialData: Boolean(intake.profile.risk?.handles_financial_data),
|
|
30
|
+
};
|
|
31
|
+
const evidence = intake.evidence.evidence || {};
|
|
32
|
+
const deployment = evidence.deployment || {};
|
|
33
|
+
const observability = evidence.observability || {};
|
|
34
|
+
const database = evidence.database || {};
|
|
35
|
+
const flows = realBusinessFlows(intake);
|
|
36
|
+
const tests = flowTests(flows);
|
|
37
|
+
const plan = [];
|
|
38
|
+
|
|
39
|
+
plan.push(
|
|
40
|
+
intake.initialized
|
|
41
|
+
? item("intake-config", "Production audit intake", "PASS", "Project profile and evidence source templates are present.")
|
|
42
|
+
: item("intake-config", "Production audit intake", "GAP", "Production audit intake has not been initialized.", "Run init-audit and fill the generated templates."),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
plan.push(
|
|
46
|
+
flows.length
|
|
47
|
+
? item("business-critical-flows", "Critical business flows", "PASS", `${flows.length} critical flow(s) declared.`)
|
|
48
|
+
: item("business-critical-flows", "Critical business flows", "USER_DECISION", "The maintainer must declare the business flows that must not break.", "Fill .ai-maintainer/business-flows.yml with real flows."),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
plan.push(
|
|
52
|
+
flows.length && tests.length === 0
|
|
53
|
+
? item("business-flow-tests", "Business flow tests", "GAP", "Critical flows are declared without linked automated tests.", "Add tests for each critical flow and list them in business-flows.yml.")
|
|
54
|
+
: item("business-flow-tests", "Business flow tests", flows.length ? "PASS" : "USER_DECISION", flows.length ? `${tests.length} test reference(s) declared.` : "Declare flows before test coverage can be judged."),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
plan.push(
|
|
58
|
+
profile.hasElectron
|
|
59
|
+
? item("electron-security", "Electron security review", "PASS", "Electron surface detected; IPC, preload, file permission, and update trust checks are enabled.")
|
|
60
|
+
: item("electron-security", "Electron security review", "N/A", "No Electron surface detected."),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
plan.push(
|
|
64
|
+
profile.hasCi
|
|
65
|
+
? item("ci-security", "CI security review", "PASS", "CI workflow evidence detected; actionlint and zizmor checks are applicable.")
|
|
66
|
+
: item("ci-security", "CI security review", "GAP", "No GitHub Actions workflow evidence detected.", "Add CI or document another release gate in evidence-sources.yml."),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
plan.push(
|
|
70
|
+
profile.hasDeployment || deployment.has_production
|
|
71
|
+
? item("release-approval", "Production release approval", deployment.production_requires_approval ? "PASS" : "GAP", deployment.production_requires_approval ? "Production approval evidence declared." : "Production deployment exists without approval evidence.", "Use GitHub Environments or document the approval gate.")
|
|
72
|
+
: item("release-approval", "Production release approval", "GAP", "No production deployment evidence declared.", "Declare deployment provider and approval status in evidence-sources.yml."),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
for (const [id, title, key] of [
|
|
76
|
+
["observability-errors", "Error monitoring", "errors"],
|
|
77
|
+
["observability-logs", "Production logs", "logs"],
|
|
78
|
+
["observability-metrics", "Production metrics", "metrics"],
|
|
79
|
+
["observability-alerts", "Production alerts", "alerts"],
|
|
80
|
+
]) {
|
|
81
|
+
plan.push(
|
|
82
|
+
hasConfiguredEvidence(observability[key])
|
|
83
|
+
? item(id, title, "PASS", `${title} evidence declared.`)
|
|
84
|
+
: item(id, title, "GAP", `${title} evidence is missing.`, `Declare ${key} evidence in evidence-sources.yml.`),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (profile.hasDatabase) {
|
|
89
|
+
plan.push(item("database-migrations", "Database migration review", hasConfiguredEvidence(database.review_tool) ? "PASS" : "GAP", hasConfiguredEvidence(database.review_tool) ? "Database review tool evidence declared." : "Database surface detected without migration review tool evidence.", "Use Atlas, Bytebase, Squawk, or document manual migration review."));
|
|
90
|
+
plan.push(item("database-backup", "Database backup evidence", hasConfiguredEvidence(database.backup_policy) ? "PASS" : "GAP", hasConfiguredEvidence(database.backup_policy) ? "Backup policy evidence declared." : "Database backup evidence is missing.", "Document backup policy before production migration."));
|
|
91
|
+
plan.push(item("database-rollback", "Database rollback or forward-fix plan", hasConfiguredEvidence(database.rollback_plan) ? "PASS" : "GAP", hasConfiguredEvidence(database.rollback_plan) ? "Rollback or forward-fix evidence declared." : "Database rollback or forward-fix evidence is missing.", "Document rollback or forward-fix strategy."));
|
|
92
|
+
} else {
|
|
93
|
+
plan.push(item("database-migrations", "Database migration review", "N/A", "No database surface detected or declared."));
|
|
94
|
+
plan.push(item("database-backup", "Database backup evidence", "N/A", "No database surface detected or declared."));
|
|
95
|
+
plan.push(item("database-rollback", "Database rollback or forward-fix plan", "N/A", "No database surface detected or declared."));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (intake.parseErrors.length) {
|
|
99
|
+
for (const error of intake.parseErrors) {
|
|
100
|
+
plan.push(item(`intake-parse-${error.path}`, "Intake YAML parse error", "GAP", `${error.path}: ${error.reason}`, "Fix malformed YAML so the audit can use maintainer-supplied evidence."));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
schemaVersion: 1,
|
|
106
|
+
generatedAt: new Date().toISOString(),
|
|
107
|
+
root: project.root,
|
|
108
|
+
profile,
|
|
109
|
+
plan,
|
|
110
|
+
evidence: plan.filter((entry) => entry.status === "PASS"),
|
|
111
|
+
coverageGaps: plan.filter((entry) => entry.status === "GAP"),
|
|
112
|
+
userDecisions: plan.filter((entry) => entry.status === "USER_DECISION"),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function resolveOutputPath(root, outputPath) {
|
|
117
|
+
if (!outputPath) return null;
|
|
118
|
+
return path.isAbsolute(outputPath) ? outputPath : path.resolve(root, outputPath);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function runAuditPlan(projectRoot, options = {}) {
|
|
122
|
+
const root = path.resolve(projectRoot || process.cwd());
|
|
123
|
+
const project = detectProject(root);
|
|
124
|
+
const audit = buildAuditPlan(project, loadIntake(root, project));
|
|
125
|
+
const outputPath = resolveOutputPath(root, options.outputPath);
|
|
126
|
+
if (outputPath) {
|
|
127
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
128
|
+
fs.writeFileSync(outputPath, JSON.stringify(audit, null, 2));
|
|
129
|
+
}
|
|
130
|
+
return audit;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parseArgs(args) {
|
|
134
|
+
const positional = [];
|
|
135
|
+
let outputPath = null;
|
|
136
|
+
let jsonOnly = false;
|
|
137
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
138
|
+
const arg = args[i];
|
|
139
|
+
if (arg === "--output") outputPath = args[++i];
|
|
140
|
+
else if (arg.startsWith("--output=")) outputPath = arg.slice("--output=".length);
|
|
141
|
+
else if (arg === "--json") jsonOnly = true;
|
|
142
|
+
else if (!arg.startsWith("--")) positional.push(arg);
|
|
143
|
+
}
|
|
144
|
+
return { projectRoot: positional[0] || process.cwd(), outputPath, jsonOnly };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function main() {
|
|
148
|
+
const args = parseArgs(process.argv.slice(2));
|
|
149
|
+
const audit = runAuditPlan(args.projectRoot, { outputPath: args.outputPath });
|
|
150
|
+
console.log(JSON.stringify(audit, null, 2));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
154
|
+
main();
|
|
155
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[string[]]$Tools = @("gitleaks", "trivy", "squawk"),
|
|
3
|
+
[string]$InstallDir = "$env:USERPROFILE\.codex\security-tools",
|
|
4
|
+
[switch]$AddToPath
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
$ErrorActionPreference = "Stop"
|
|
8
|
+
$binDir = Join-Path $InstallDir "bin"
|
|
9
|
+
New-Item -ItemType Directory -Force -Path $binDir | Out-Null
|
|
10
|
+
$requestedTools = @()
|
|
11
|
+
foreach ($item in $Tools) {
|
|
12
|
+
$requestedTools += ($item -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function Write-Step($Message) {
|
|
16
|
+
Write-Host "==> $Message"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function Test-Command($Name) {
|
|
20
|
+
$null -ne (Get-Command $Name -ErrorAction SilentlyContinue)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function Get-Asset($Repo, [string[]]$Patterns) {
|
|
24
|
+
$release = Invoke-RestMethod -Headers @{ "User-Agent" = "ai-project-maintainer" } -Uri "https://api.github.com/repos/$Repo/releases/latest"
|
|
25
|
+
foreach ($pattern in $Patterns) {
|
|
26
|
+
$asset = $release.assets | Where-Object { $_.name -match $pattern -and $_.name -match '\.zip$' } | Select-Object -First 1
|
|
27
|
+
if ($asset) { return $asset }
|
|
28
|
+
}
|
|
29
|
+
throw "No Windows zip asset matched $($Patterns -join ', ') for $Repo"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function Install-FromGitHubZip($Name, $Repo, [string[]]$Patterns, $ExeName) {
|
|
33
|
+
if (Test-Command $ExeName) {
|
|
34
|
+
Write-Step "$Name already available on PATH"
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
$asset = Get-Asset -Repo $Repo -Patterns $Patterns
|
|
39
|
+
$tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("$Name-" + [guid]::NewGuid())
|
|
40
|
+
$zip = Join-Path $tmp $asset.name
|
|
41
|
+
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
|
|
42
|
+
|
|
43
|
+
Write-Step "Downloading $Name from $Repo ($($asset.name))"
|
|
44
|
+
Invoke-WebRequest -Headers @{ "User-Agent" = "ai-project-maintainer" } -Uri $asset.browser_download_url -OutFile $zip
|
|
45
|
+
Expand-Archive -LiteralPath $zip -DestinationPath $tmp -Force
|
|
46
|
+
|
|
47
|
+
$exe = Get-ChildItem -Path $tmp -Recurse -File -Filter "$ExeName.exe" | Select-Object -First 1
|
|
48
|
+
if (-not $exe) { throw "Downloaded $Name but could not find $ExeName.exe in archive" }
|
|
49
|
+
|
|
50
|
+
Copy-Item -LiteralPath $exe.FullName -Destination (Join-Path $binDir "$ExeName.exe") -Force
|
|
51
|
+
Write-Step "Installed $Name to $binDir"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function Install-WithUvTool($Name, $Package, $ExeName) {
|
|
55
|
+
if (Test-Command $ExeName) {
|
|
56
|
+
Write-Step "$Name already available on PATH"
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
if (-not (Test-Command "uv")) {
|
|
60
|
+
Write-Warning "uv is not installed; cannot install $Name locally. Install uv, Python, or Docker and rerun."
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
Write-Step "Installing $Name with uv tool install $Package"
|
|
64
|
+
& uv tool install $Package
|
|
65
|
+
if ($LASTEXITCODE -ne 0) {
|
|
66
|
+
throw "uv tool install $Package failed with exit code $LASTEXITCODE"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
foreach ($tool in $requestedTools) {
|
|
71
|
+
try {
|
|
72
|
+
switch ($tool.ToLowerInvariant()) {
|
|
73
|
+
"gitleaks" {
|
|
74
|
+
Install-FromGitHubZip -Name "gitleaks" -Repo "gitleaks/gitleaks" -Patterns @("windows.*x64", "windows.*64") -ExeName "gitleaks"
|
|
75
|
+
}
|
|
76
|
+
"trivy" {
|
|
77
|
+
Install-FromGitHubZip -Name "trivy" -Repo "aquasecurity/trivy" -Patterns @("windows-64bit", "windows.*64") -ExeName "trivy"
|
|
78
|
+
}
|
|
79
|
+
"squawk" {
|
|
80
|
+
Install-FromGitHubZip -Name "squawk" -Repo "sbdchd/squawk" -Patterns @("windows.*x86_64", "windows.*amd64", "windows.*64") -ExeName "squawk"
|
|
81
|
+
}
|
|
82
|
+
"semgrep" {
|
|
83
|
+
Install-WithUvTool -Name "semgrep" -Package "semgrep" -ExeName "semgrep"
|
|
84
|
+
}
|
|
85
|
+
"checkov" {
|
|
86
|
+
Install-WithUvTool -Name "checkov" -Package "checkov" -ExeName "checkov"
|
|
87
|
+
}
|
|
88
|
+
default {
|
|
89
|
+
Write-Warning "Unknown local bootstrap tool '$tool'. Supported: gitleaks, trivy, squawk, semgrep, checkov."
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
Write-Warning "Could not install ${tool}: $($_.Exception.Message)"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if ($AddToPath) {
|
|
98
|
+
$current = [Environment]::GetEnvironmentVariable("Path", "User")
|
|
99
|
+
$parts = $current -split ';' | Where-Object { $_ }
|
|
100
|
+
if ($parts -notcontains $binDir) {
|
|
101
|
+
[Environment]::SetEnvironmentVariable("Path", ($parts + $binDir -join ';'), "User")
|
|
102
|
+
Write-Step "Added $binDir to the user PATH. Restart terminals/Codex to pick it up."
|
|
103
|
+
} else {
|
|
104
|
+
Write-Step "$binDir is already in the user PATH"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
Write-Step "Installed tools:"
|
|
109
|
+
Get-ChildItem -File -LiteralPath $binDir | Select-Object Name,Length,LastWriteTime
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
8
|
+
const targetDirs = [
|
|
9
|
+
path.join(repoRoot, "ai-project-maintainer", "scripts"),
|
|
10
|
+
path.join(repoRoot, "test"),
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
function collectMjsFiles(dir, out = []) {
|
|
14
|
+
if (!fs.existsSync(dir)) return out;
|
|
15
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
16
|
+
const full = path.join(dir, entry.name);
|
|
17
|
+
if (entry.isDirectory()) collectMjsFiles(full, out);
|
|
18
|
+
else if (entry.isFile() && entry.name.endsWith(".mjs")) out.push(full);
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const files = targetDirs.flatMap((dir) => collectMjsFiles(dir));
|
|
24
|
+
let failed = false;
|
|
25
|
+
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
const result = spawnSync(process.execPath, ["--check", file], {
|
|
28
|
+
cwd: repoRoot,
|
|
29
|
+
encoding: "utf8",
|
|
30
|
+
});
|
|
31
|
+
if (result.status !== 0) {
|
|
32
|
+
failed = true;
|
|
33
|
+
process.stderr.write(result.stderr || result.stdout);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!failed) {
|
|
38
|
+
console.log(`Syntax check passed for ${files.length} files.`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
process.exit(failed ? 1 : 0);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { runLocalGate } from "./run-local-gate.mjs";
|
|
4
|
+
|
|
5
|
+
const projectRoot = path.resolve(process.argv[2] || process.cwd());
|
|
6
|
+
const outputPath = process.argv[3] || "reports/security-report.json";
|
|
7
|
+
|
|
8
|
+
const report = runLocalGate(projectRoot, {
|
|
9
|
+
noTests: true,
|
|
10
|
+
outputPath,
|
|
11
|
+
writeReports: true,
|
|
12
|
+
runnerOptions: {
|
|
13
|
+
envPath: "",
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const summary = {
|
|
18
|
+
status: report.passed ? "PASS" : "FAIL",
|
|
19
|
+
blockers: report.summary?.blockers ?? 0,
|
|
20
|
+
warnings: report.summary?.warnings ?? 0,
|
|
21
|
+
coverageGaps: report.summary?.coverageGaps ?? 0,
|
|
22
|
+
outputPath,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
26
|
+
process.exit(report.passed ? 0 : 1);
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { runAuditPlan } from "./audit-plan.mjs";
|
|
5
|
+
import { runDoctor } from "./doctor.mjs";
|
|
6
|
+
import { initAudit } from "./init-audit.mjs";
|
|
7
|
+
import { initProject } from "./init-project.mjs";
|
|
8
|
+
import { runLocalGate } from "./run-local-gate.mjs";
|
|
9
|
+
import { summarizeReport } from "./report-summary.mjs";
|
|
10
|
+
import { toMarkdown } from "./lib/report.mjs";
|
|
11
|
+
|
|
12
|
+
function readOption(args, name, fallback = null) {
|
|
13
|
+
const index = args.indexOf(name);
|
|
14
|
+
if (index === -1) {
|
|
15
|
+
const inline = args.find((arg) => arg.startsWith(`${name}=`));
|
|
16
|
+
return inline ? inline.slice(name.length + 1) : fallback;
|
|
17
|
+
}
|
|
18
|
+
return args[index + 1] || fallback;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function parseCliArgs(argv) {
|
|
22
|
+
const [command = "help", ...rest] = argv;
|
|
23
|
+
|
|
24
|
+
if (command === "doctor") {
|
|
25
|
+
return {
|
|
26
|
+
command,
|
|
27
|
+
args: {
|
|
28
|
+
jsonOnly: rest.includes("--json"),
|
|
29
|
+
checkTrivyDb: !rest.includes("--no-trivy-db"),
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (command === "init") {
|
|
35
|
+
return {
|
|
36
|
+
command,
|
|
37
|
+
args: {
|
|
38
|
+
projectRoot: rest.find((arg) => !arg.startsWith("--")) || process.cwd(),
|
|
39
|
+
profile: readOption(rest, "--profile", "oss"),
|
|
40
|
+
ci: readOption(rest, "--ci", "github"),
|
|
41
|
+
preCommit: rest.includes("--pre-commit"),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (command === "gate") {
|
|
47
|
+
return {
|
|
48
|
+
command,
|
|
49
|
+
args: {
|
|
50
|
+
projectRoot: rest.find((arg) => !arg.startsWith("--")) || process.cwd(),
|
|
51
|
+
strict: rest.includes("--strict"),
|
|
52
|
+
release: rest.includes("--release"),
|
|
53
|
+
noTests: rest.includes("--no-tests"),
|
|
54
|
+
jsonOnly: rest.includes("--json"),
|
|
55
|
+
production: rest.includes("--production"),
|
|
56
|
+
outputPath: readOption(rest, "--output", null),
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (command === "init-audit") {
|
|
62
|
+
return {
|
|
63
|
+
command,
|
|
64
|
+
args: {
|
|
65
|
+
projectRoot: rest.find((arg) => !arg.startsWith("--")) || process.cwd(),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (command === "audit-plan") {
|
|
71
|
+
return {
|
|
72
|
+
command,
|
|
73
|
+
args: {
|
|
74
|
+
projectRoot: rest.find((arg) => !arg.startsWith("--")) || process.cwd(),
|
|
75
|
+
outputPath: readOption(rest, "--output", null),
|
|
76
|
+
jsonOnly: rest.includes("--json"),
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (command === "summary") {
|
|
82
|
+
return {
|
|
83
|
+
command,
|
|
84
|
+
args: {
|
|
85
|
+
reportPath: rest.find((arg) => !arg.startsWith("--")),
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { command: "help", args: {} };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function doctorToMarkdown(report) {
|
|
94
|
+
const lines = [];
|
|
95
|
+
lines.push(`# AI Project Maintainer Doctor: ${report.passed ? "PASS" : "WARN"}`);
|
|
96
|
+
lines.push("");
|
|
97
|
+
lines.push(`- node: ${report.node.version}`);
|
|
98
|
+
lines.push("");
|
|
99
|
+
lines.push("## Required Scanners");
|
|
100
|
+
for (const tool of report.required) lines.push(`- ${tool.name}: ${tool.status}${tool.version ? ` (${tool.version})` : ""}`);
|
|
101
|
+
lines.push("");
|
|
102
|
+
lines.push("## Optional Scanners");
|
|
103
|
+
for (const tool of report.optional) lines.push(`- ${tool.name}: ${tool.status}${tool.version ? ` (${tool.version})` : ""}`);
|
|
104
|
+
lines.push("");
|
|
105
|
+
lines.push("## Trivy DB");
|
|
106
|
+
lines.push(`- ${report.trivyDb.status}: ${report.trivyDb.summary}`);
|
|
107
|
+
return lines.join("\n");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function runCli(argv = process.argv.slice(2), io = { stdout: process.stdout, stderr: process.stderr }) {
|
|
111
|
+
const parsed = parseCliArgs(argv);
|
|
112
|
+
|
|
113
|
+
if (parsed.command === "doctor") {
|
|
114
|
+
const report = runDoctor({ checkTrivyDb: parsed.args.checkTrivyDb });
|
|
115
|
+
io.stdout.write(`${parsed.args.jsonOnly ? JSON.stringify(report, null, 2) : doctorToMarkdown(report)}\n`);
|
|
116
|
+
return report.passed ? 0 : 1;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (parsed.command === "init") {
|
|
120
|
+
const result = initProject(parsed.args.projectRoot, {
|
|
121
|
+
profile: parsed.args.profile,
|
|
122
|
+
ci: parsed.args.ci,
|
|
123
|
+
preCommit: parsed.args.preCommit,
|
|
124
|
+
});
|
|
125
|
+
io.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (parsed.command === "gate") {
|
|
130
|
+
const report = runLocalGate(parsed.args.projectRoot, {
|
|
131
|
+
strict: parsed.args.strict,
|
|
132
|
+
release: parsed.args.release,
|
|
133
|
+
noTests: parsed.args.noTests,
|
|
134
|
+
production: parsed.args.production,
|
|
135
|
+
outputPath: parsed.args.outputPath,
|
|
136
|
+
writeReports: true,
|
|
137
|
+
});
|
|
138
|
+
io.stdout.write(`${parsed.args.jsonOnly ? JSON.stringify(report, null, 2) : toMarkdown(report)}\n`);
|
|
139
|
+
return report.passed ? 0 : 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (parsed.command === "init-audit") {
|
|
143
|
+
const result = initAudit(parsed.args.projectRoot);
|
|
144
|
+
io.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (parsed.command === "audit-plan") {
|
|
149
|
+
const audit = runAuditPlan(parsed.args.projectRoot, { outputPath: parsed.args.outputPath });
|
|
150
|
+
io.stdout.write(`${JSON.stringify(audit, null, 2)}\n`);
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (parsed.command === "summary" && parsed.args.reportPath) {
|
|
155
|
+
io.stdout.write(`${summarizeReport(parsed.args.reportPath)}\n`);
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
io.stderr.write("Usage: ai-project-maintainer <doctor|init|init-audit|audit-plan|gate|summary> [options]\n");
|
|
160
|
+
return 2;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
164
|
+
process.exit(runCli());
|
|
165
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { commandPath, runCommand } from "./lib/command-runner.mjs";
|
|
5
|
+
|
|
6
|
+
const coreTools = ["git", "npm", "pnpm", "yarn", "bun"];
|
|
7
|
+
const requiredScanners = ["gitleaks", "trivy", "semgrep"];
|
|
8
|
+
const optionalScanners = ["osv-scanner", "actionlint", "zizmor", "syft", "grype", "checkov", "squawk", "scorecard", "pre-commit", "mega-linter-runner"];
|
|
9
|
+
|
|
10
|
+
function inspectTool(name, options) {
|
|
11
|
+
const resolved = commandPath(name, options.runnerOptions || {});
|
|
12
|
+
if (!resolved) return { name, status: "missing", path: null, version: null };
|
|
13
|
+
const result = runCommand(name, ["--version"], { ...(options.runnerOptions || {}), timeoutMs: 30 * 1000 });
|
|
14
|
+
return {
|
|
15
|
+
name,
|
|
16
|
+
status: result.status === "pass" ? "available" : "error",
|
|
17
|
+
path: resolved,
|
|
18
|
+
version: `${result.stdout}\n${result.stderr}`.trim().split(/\r?\n/).find(Boolean) || `exit ${result.code}`,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function runDoctor(options = {}) {
|
|
23
|
+
const required = requiredScanners.map((tool) => inspectTool(tool, options));
|
|
24
|
+
const optional = optionalScanners.map((tool) => inspectTool(tool, options));
|
|
25
|
+
const core = coreTools.map((tool) => inspectTool(tool, options));
|
|
26
|
+
const trivyAvailable = required.find((tool) => tool.name === "trivy")?.status === "available";
|
|
27
|
+
let trivyDb = { status: "skipped", summary: trivyAvailable ? "Trivy DB check disabled by option." : "trivy is not available" };
|
|
28
|
+
|
|
29
|
+
if (trivyAvailable && options.checkTrivyDb !== false) {
|
|
30
|
+
const result = runCommand("trivy", ["image", "--download-db-only", "--timeout", "30s"], {
|
|
31
|
+
...(options.runnerOptions || {}),
|
|
32
|
+
timeoutMs: 60 * 1000,
|
|
33
|
+
});
|
|
34
|
+
trivyDb = {
|
|
35
|
+
status: result.status === "pass" ? "available" : "error",
|
|
36
|
+
summary: result.status === "pass" ? "Trivy vulnerability DB can be downloaded." : `${result.stderr}\n${result.stdout}`.trim(),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
node: { status: "available", version: process.version },
|
|
42
|
+
core,
|
|
43
|
+
required,
|
|
44
|
+
optional,
|
|
45
|
+
trivyDb,
|
|
46
|
+
passed: required.every((tool) => tool.status === "available") && (trivyDb.status === "available" || trivyDb.status === "skipped"),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function toMarkdown(report) {
|
|
51
|
+
const lines = [];
|
|
52
|
+
lines.push(`# AI Project Maintainer Doctor: ${report.passed ? "PASS" : "WARN"}`);
|
|
53
|
+
lines.push("");
|
|
54
|
+
lines.push(`- node: ${report.node.version}`);
|
|
55
|
+
lines.push("");
|
|
56
|
+
lines.push("## Core");
|
|
57
|
+
for (const tool of report.core) lines.push(`- ${tool.name}: ${tool.status}${tool.version ? ` (${tool.version})` : ""}`);
|
|
58
|
+
lines.push("");
|
|
59
|
+
lines.push("## Required Scanners");
|
|
60
|
+
for (const tool of report.required) lines.push(`- ${tool.name}: ${tool.status}${tool.version ? ` (${tool.version})` : ""}`);
|
|
61
|
+
lines.push("");
|
|
62
|
+
lines.push("## Optional Scanners");
|
|
63
|
+
for (const tool of report.optional) lines.push(`- ${tool.name}: ${tool.status}${tool.version ? ` (${tool.version})` : ""}`);
|
|
64
|
+
lines.push("");
|
|
65
|
+
lines.push("## Trivy DB");
|
|
66
|
+
lines.push(`- ${report.trivyDb.status}: ${report.trivyDb.summary}`);
|
|
67
|
+
return lines.join("\n");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function main() {
|
|
71
|
+
const jsonOnly = process.argv.includes("--json");
|
|
72
|
+
const noTrivyDb = process.argv.includes("--no-trivy-db");
|
|
73
|
+
const report = runDoctor({ checkTrivyDb: !noTrivyDb });
|
|
74
|
+
console.log(jsonOnly ? JSON.stringify(report, null, 2) : toMarkdown(report));
|
|
75
|
+
process.exit(report.passed ? 0 : 1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
79
|
+
main();
|
|
80
|
+
}
|