Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
steps:
- name: Release
id: release
uses: google-github-actions/release-please-action@v4
uses: googleapis/release-please-action@v4
with:
config-file: release-please-config.json
manifest-file: release-please-manifest.json
Expand Down
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@ primer init

## Commands

### `primer analyze` — Inspect Repository Structure

Detects languages, frameworks, monorepo/workspace structure, and area mappings:

```bash
primer analyze # terminal summary
primer analyze --json # machine-readable analysis
primer analyze --output analysis.json # save JSON report
primer analyze --output analysis.md # save Markdown report
primer analyze --output analysis.json --force # overwrite existing report
```

### `primer readiness` — Assess AI Readiness

Score a repo across 9 pillars grouped into **Repo Health** and **AI Setup**:
Expand All @@ -54,7 +66,10 @@ Score a repo across 9 pillars grouped into **Repo Health** and **AI Setup**:
primer readiness # terminal summary
primer readiness --visual # GitHub-themed HTML report
primer readiness --per-area # include per-area breakdown
primer readiness --policy ./strict.json # apply a custom policy
primer readiness --output readiness.json # save JSON report
primer readiness --output readiness.md # save Markdown report
primer readiness --output readiness.html # save HTML report
primer readiness --policy ./examples/policies/strict.json # apply a custom policy
primer readiness --json # machine-readable JSON
primer readiness --fail-level 3 # CI gate: exit 1 if below level 3
```
Expand Down Expand Up @@ -133,8 +148,8 @@ All commands support `--json` (structured JSON to stdout) and `--quiet` (suppres
Policies customize scoring criteria, override metadata, and tune thresholds:

```bash
primer readiness --policy ./strict.json
primer readiness --policy ./base.json,./strict.json # chain multiple
primer readiness --policy ./examples/policies/strict.json
primer readiness --policy ./examples/policies/strict.json,./my-overrides.json # chain multiple
```

```json
Expand Down
26 changes: 26 additions & 0 deletions examples/policies/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Example Policies

Readiness policies customize which criteria are evaluated and how they're scored.

## Usage

Pass a policy file with `--policy` using a relative `./` path:

```sh
primer readiness --policy ./examples/policies/ai-only.json
primer readiness --policy ./examples/policies/strict.json
```

Multiple policies can be chained (comma-separated):

```sh
primer readiness --policy ./examples/policies/ai-only.json,./my-overrides.json
```

## Included Policies

| File | Purpose |
| ----------------------- | ------------------------------------------------------------------------------------ |
| `ai-only.json` | Disables all repo-health criteria, focusing only on AI tooling readiness |
| `repo-health-only.json` | Disables all AI-tooling criteria and the `agents-doc` extra, focusing on repo health |
| `strict.json` | Sets 100% pass-rate threshold and elevates several criteria to high impact |
25 changes: 25 additions & 0 deletions examples/policies/ai-only.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "ai-only",
"criteria": {
"disable": [
"lint-config",
"typecheck-config",
"build-script",
"ci-config",
"test-script",
"readme",
"contributing",
"lockfile",
"env-example",
"format-config",
"codeowners",
"license",
"security-policy",
"dependabot",
"observability",
"area-readme",
"area-build-script",
"area-test-script"
]
}
}
15 changes: 15 additions & 0 deletions examples/policies/repo-health-only.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "repo-health-only",
"criteria": {
"disable": [
"custom-instructions",
"mcp-config",
"custom-agents",
"copilot-skills",
"area-instructions"
]
},
"extras": {
"disable": ["agents-doc"]
}
}
13 changes: 13 additions & 0 deletions examples/policies/strict.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "strict",
"thresholds": {
"passRate": 1.0
},
"criteria": {
"override": {
"env-example": { "impact": "high" },
"format-config": { "impact": "high" },
"contributing": { "impact": "high" }
}
}
}
4 changes: 3 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export function runCli(argv: string[]): void {
.command("analyze")
.description("Detect languages, frameworks, monorepo structure, and areas")
.argument("[path]", "Path to a local repository")
.option("--output <path>", "Write report to file (.json or .md)")
.option("--force", "Overwrite existing output file")
.action(withGlobalOpts(analyzeCommand));

program
Expand Down Expand Up @@ -120,7 +122,7 @@ export function runCli(argv: string[]): void {
.command("readiness")
.description("AI readiness assessment across 9 maturity pillars")
.argument("[path]", "Path to a local repository")
.option("--output <path>", "Write report to file (.json or .html)")
.option("--output <path>", "Write report to file (.json, .md, or .html)")
.option("--force", "Overwrite existing output file")
.option("--visual", "Generate visual HTML report")
.option("--per-area", "Show per-area readiness breakdown")
Expand Down
98 changes: 97 additions & 1 deletion src/commands/analyze.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import path from "path";

import chalk from "chalk";

import type { RepoAnalysis } from "../services/analyzer";
import { analyzeRepo } from "../services/analyzer";
import { safeWriteFile } from "../utils/fs";
import { prettyPrintSummary } from "../utils/logger";
import type { CommandResult } from "../utils/output";
import { outputResult, outputError, shouldLog } from "../utils/output";
import { prettyPrintSummary } from "../utils/logger";

type AnalyzeOptions = {
json?: boolean;
quiet?: boolean;
output?: string;
force?: boolean;
};

export async function analyzeCommand(
Expand All @@ -19,6 +25,41 @@ export async function analyzeCommand(
try {
const analysis = await analyzeRepo(repoPath);

// Write to file when --output is specified
if (options.output) {
const outputPath = path.resolve(options.output);
const ext = path.extname(outputPath).toLowerCase();
if (ext !== ".json" && ext !== ".md") {
outputError(
`Unsupported output format: ${ext || "(no extension)"}. Use .json or .md`,
Boolean(options.json)
);
return;
}
const content =
ext === ".md" ? formatAnalysisMarkdown(analysis) : JSON.stringify(analysis, null, 2);

const { wrote, reason } = await safeWriteFile(outputPath, content, Boolean(options.force));
if (!wrote) {
const why = reason === "symlink" ? "path is a symlink" : "file exists (use --force)";
outputError(`Skipped ${outputPath}: ${why}`, Boolean(options.json));
return;
}
if (options.json) {
const result: CommandResult<typeof analysis> = {
ok: true,
status: "success",
data: analysis
};
outputResult(result, true);
return;
}
if (shouldLog(options)) {
process.stderr.write(chalk.green(`✓ Report saved: ${outputPath}`) + "\n");
}
return;
}

if (options.json) {
const result: CommandResult<typeof analysis> = {
ok: true,
Expand All @@ -33,3 +74,58 @@ export async function analyzeCommand(
outputError(error instanceof Error ? error.message : String(error), Boolean(options.json));
}
}

export function formatAnalysisMarkdown(analysis: RepoAnalysis): string {
const lines: string[] = [];
const repoName = path.basename(analysis.path);

lines.push(`# Repository Analysis: ${repoName}`);
lines.push("");
lines.push("## Overview");
lines.push("");
lines.push(`| Property | Value |`);
lines.push(`| --- | --- |`);
lines.push(`| Path | \`${analysis.path}\` |`);
lines.push(`| Git repository | ${analysis.isGitRepo ? "Yes" : "No"} |`);
lines.push(`| Languages | ${analysis.languages.join(", ") || "Unknown"} |`);
lines.push(`| Frameworks | ${analysis.frameworks.join(", ") || "None detected"} |`);
lines.push(`| Package manager | ${analysis.packageManager ?? "Unknown"} |`);
if (analysis.isMonorepo) {
lines.push(
`| Monorepo | ${analysis.workspaceType ?? "yes"} (${analysis.apps?.length ?? 0} apps) |`
);
}

if (analysis.apps && analysis.apps.length > 0) {
lines.push("");
lines.push("## Applications");
lines.push("");
lines.push("| Name | Ecosystem | TypeScript | Path |");
lines.push("| --- | --- | --- | --- |");
for (const app of analysis.apps) {
const rel = path.relative(analysis.path, app.path).replace(/\\/gu, "/") || ".";
lines.push(
`| ${app.name} | ${app.ecosystem ?? "—"} | ${app.hasTsConfig ? "Yes" : "No"} | \`${rel}\` |`
);
}
}

if (analysis.areas && analysis.areas.length > 0) {
lines.push("");
lines.push("## Areas");
lines.push("");
lines.push("| Name | Source | Pattern |");
lines.push("| --- | --- | --- |");
for (const area of analysis.areas) {
const pattern = Array.isArray(area.applyTo) ? area.applyTo.join(", ") : area.applyTo;
lines.push(`| ${area.name} | ${area.source} | \`${pattern}\` |`);
}
}

lines.push("");
lines.push(`---`);
lines.push(`*Generated by [Primer](https://github.com/digitarald/primer)*`);
lines.push("");

return lines.join("\n");
}
2 changes: 1 addition & 1 deletion src/commands/batchReadiness.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import React from "react";

import { getGitHubToken } from "../services/github";
import { parsePolicySources } from "../services/policy";
import { outputError } from "../utils/output";
import { BatchReadinessTui } from "../ui/BatchReadinessTui";
import { outputError } from "../utils/output";

type BatchReadinessOptions = {
output?: string;
Expand Down
Loading
Loading