edsger 0.56.1 → 0.56.2
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.
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI command: `edsger quality-benchmark <productId>`
|
|
3
3
|
*
|
|
4
|
-
* Runs the quality-benchmark phase against
|
|
4
|
+
* Runs the quality-benchmark phase against the product's GitHub repo and
|
|
5
5
|
* writes the resulting JSON report to disk. Persistence to the
|
|
6
6
|
* `quality_reports` table is the caller's responsibility — desktop-app
|
|
7
7
|
* picks the JSON up via stdout / file and writes via its supabase
|
|
8
8
|
* client, exactly the same pattern used by other CLI commands.
|
|
9
9
|
*
|
|
10
|
+
* Default invocation (desktop): no --repo, no --branch — the CLI fetches
|
|
11
|
+
* the product's GitHub config via MCP, clones (or reuses) the repo into
|
|
12
|
+
* `~/edsger/quality-<owner>-<repo>`, and analyses its default branch.
|
|
13
|
+
*
|
|
10
14
|
* Usage:
|
|
11
15
|
* edsger quality-benchmark <productId>
|
|
12
|
-
* --repo <path> (
|
|
13
|
-
*
|
|
16
|
+
* --repo <path> (optional) override the auto-clone with a
|
|
17
|
+
* local checkout (used by tests / power users)
|
|
18
|
+
* --branch <name> (optional) override the detected default
|
|
19
|
+
* branch on the report envelope
|
|
14
20
|
* --pkg-manager <name> (optional) npm|pnpm|yarn (auto-detected if absent)
|
|
15
21
|
* --no-install refuse to install missing tools
|
|
16
22
|
* --output <path> where to write the JSON report
|
|
@@ -18,7 +24,8 @@
|
|
|
18
24
|
* --verbose print every progress event
|
|
19
25
|
*/
|
|
20
26
|
export interface QualityBenchmarkCliOptions {
|
|
21
|
-
repo
|
|
27
|
+
/** Optional local-checkout override; when absent the CLI clones the product's repo. */
|
|
28
|
+
repo?: string;
|
|
22
29
|
branch?: string;
|
|
23
30
|
pkgManager?: string;
|
|
24
31
|
install?: boolean;
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI command: `edsger quality-benchmark <productId>`
|
|
3
3
|
*
|
|
4
|
-
* Runs the quality-benchmark phase against
|
|
4
|
+
* Runs the quality-benchmark phase against the product's GitHub repo and
|
|
5
5
|
* writes the resulting JSON report to disk. Persistence to the
|
|
6
6
|
* `quality_reports` table is the caller's responsibility — desktop-app
|
|
7
7
|
* picks the JSON up via stdout / file and writes via its supabase
|
|
8
8
|
* client, exactly the same pattern used by other CLI commands.
|
|
9
9
|
*
|
|
10
|
+
* Default invocation (desktop): no --repo, no --branch — the CLI fetches
|
|
11
|
+
* the product's GitHub config via MCP, clones (or reuses) the repo into
|
|
12
|
+
* `~/edsger/quality-<owner>-<repo>`, and analyses its default branch.
|
|
13
|
+
*
|
|
10
14
|
* Usage:
|
|
11
15
|
* edsger quality-benchmark <productId>
|
|
12
|
-
* --repo <path> (
|
|
13
|
-
*
|
|
16
|
+
* --repo <path> (optional) override the auto-clone with a
|
|
17
|
+
* local checkout (used by tests / power users)
|
|
18
|
+
* --branch <name> (optional) override the detected default
|
|
19
|
+
* branch on the report envelope
|
|
14
20
|
* --pkg-manager <name> (optional) npm|pnpm|yarn (auto-detected if absent)
|
|
15
21
|
* --no-install refuse to install missing tools
|
|
16
22
|
* --output <path> where to write the JSON report
|
|
@@ -19,15 +25,39 @@
|
|
|
19
25
|
*/
|
|
20
26
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
21
27
|
import { dirname, resolve } from 'node:path';
|
|
28
|
+
import { getGitHubConfigByProduct } from '../../api/github.js';
|
|
22
29
|
import { callMcpEndpoint } from '../../api/mcp-client.js';
|
|
23
30
|
import { fetchProductBasics } from '../../phases/find-shared/mcp.js';
|
|
24
31
|
import { runQualityBenchmark, } from '../../phases/quality-benchmark/index.js';
|
|
32
|
+
import { prepareQualityWorkspace } from '../../phases/quality-benchmark/workspace.js';
|
|
25
33
|
import { logError, logInfo, logSuccess, logWarning, } from '../../utils/logger.js';
|
|
26
34
|
export async function runQualityBenchmarkCli(productId, options) {
|
|
27
|
-
const repoRoot = resolve(options.repo);
|
|
28
35
|
const installEnabled = options.install !== false;
|
|
29
36
|
logInfo(`Starting quality benchmark for product ${productId}`);
|
|
30
|
-
|
|
37
|
+
let repoRoot;
|
|
38
|
+
let resolvedBranch;
|
|
39
|
+
if (options.repo) {
|
|
40
|
+
repoRoot = resolve(options.repo);
|
|
41
|
+
resolvedBranch = options.branch;
|
|
42
|
+
logInfo(`Repo (override): ${repoRoot}`);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const gh = await getGitHubConfigByProduct(productId, options.verbose);
|
|
46
|
+
if (!gh.configured || !gh.token || !gh.owner || !gh.repo) {
|
|
47
|
+
logError(`Cannot run quality benchmark: ${gh.message ??
|
|
48
|
+
'GitHub is not configured for this product. Connect a repo in product settings.'}`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
const ws = prepareQualityWorkspace({
|
|
52
|
+
owner: gh.owner,
|
|
53
|
+
repo: gh.repo,
|
|
54
|
+
token: gh.token,
|
|
55
|
+
verbose: options.verbose,
|
|
56
|
+
});
|
|
57
|
+
repoRoot = ws.repoPath;
|
|
58
|
+
resolvedBranch = options.branch ?? ws.branch;
|
|
59
|
+
logInfo(`Repo: ${repoRoot} (branch: ${resolvedBranch})`);
|
|
60
|
+
}
|
|
31
61
|
if (!installEnabled) {
|
|
32
62
|
logWarning('Install consent NOT granted (--no-install). Missing tools will be marked unmeasured.');
|
|
33
63
|
}
|
|
@@ -52,7 +82,7 @@ export async function runQualityBenchmarkCli(productId, options) {
|
|
|
52
82
|
productId,
|
|
53
83
|
productName,
|
|
54
84
|
repoRoot,
|
|
55
|
-
branch:
|
|
85
|
+
branch: resolvedBranch,
|
|
56
86
|
packageManager: options.pkgManager,
|
|
57
87
|
installEnabled,
|
|
58
88
|
onProgress,
|
|
@@ -72,7 +102,7 @@ export async function runQualityBenchmarkCli(productId, options) {
|
|
|
72
102
|
run_id: outcome.runId,
|
|
73
103
|
product_id: productId,
|
|
74
104
|
commit_sha: outcome.commitSha,
|
|
75
|
-
branch:
|
|
105
|
+
branch: resolvedBranch ?? null,
|
|
76
106
|
started_at: outcome.startedAt,
|
|
77
107
|
completed_at: outcome.completedAt,
|
|
78
108
|
duration_seconds: outcome.durationSeconds,
|
|
@@ -88,7 +118,7 @@ export async function runQualityBenchmarkCli(productId, options) {
|
|
|
88
118
|
product_id: productId,
|
|
89
119
|
commit_sha: outcome.commitSha,
|
|
90
120
|
rubric_version: outcome.report.rubric_version,
|
|
91
|
-
branch:
|
|
121
|
+
branch: resolvedBranch ?? null,
|
|
92
122
|
repo_root: repoRoot,
|
|
93
123
|
detected_context: outcome.report.detected_context,
|
|
94
124
|
tool_versions: outcome.report.tool_versions,
|
package/dist/index.js
CHANGED
|
@@ -491,9 +491,9 @@ program
|
|
|
491
491
|
// ============================================================
|
|
492
492
|
program
|
|
493
493
|
.command('quality-benchmark <productId>')
|
|
494
|
-
.description(
|
|
495
|
-
.
|
|
496
|
-
.option('--branch <name>', '
|
|
494
|
+
.description("Run an industrial-grade code quality benchmark against the product's GitHub repo")
|
|
495
|
+
.option('--repo <path>', "Override the auto-clone with a local checkout (default: clone the product's repo into ~/edsger/quality-<owner>-<repo>)")
|
|
496
|
+
.option('--branch <name>', 'Override the detected default branch on the report envelope')
|
|
497
497
|
.option('--pkg-manager <name>', 'npm | pnpm | yarn (auto-detected if absent)')
|
|
498
498
|
.option('--no-install', 'Refuse to install missing tools; mark them unmeasured')
|
|
499
499
|
.option('--output <path>', 'Where to write the JSON report (default: ./quality-report-<commit>.json)')
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git workspace prep for `edsger quality-benchmark`.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the convention in `phases/pr-resolve/workspace.ts`:
|
|
5
|
+
* - Clones into `~/edsger/quality-<owner>-<repo>`
|
|
6
|
+
* - Reuses + fetches + hard-resets to the remote default branch when the
|
|
7
|
+
* dir already has a `.git`
|
|
8
|
+
* - Authenticates via `buildCredentialArgs(token)` so the GitHub App
|
|
9
|
+
* installation token never lands in the remote URL or `ps`
|
|
10
|
+
*
|
|
11
|
+
* Quality runs are read-only — no per-commit / per-run subdirectory, one
|
|
12
|
+
* checkout per (owner, repo) is enough and lets repeated runs skip the
|
|
13
|
+
* full clone cost.
|
|
14
|
+
*/
|
|
15
|
+
export interface PrepareQualityWorkspaceOptions {
|
|
16
|
+
owner: string;
|
|
17
|
+
repo: string;
|
|
18
|
+
token: string;
|
|
19
|
+
verbose?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface QualityWorkspace {
|
|
22
|
+
/** Absolute path to the checkout. */
|
|
23
|
+
repoPath: string;
|
|
24
|
+
/** The default branch we ended up on (e.g. "main"). */
|
|
25
|
+
branch: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function getQualityWorkspacePath(owner: string, repo: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Clone (or reuse) the product's repo and check out its default branch.
|
|
30
|
+
*/
|
|
31
|
+
export declare function prepareQualityWorkspace(options: PrepareQualityWorkspaceOptions): QualityWorkspace;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git workspace prep for `edsger quality-benchmark`.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the convention in `phases/pr-resolve/workspace.ts`:
|
|
5
|
+
* - Clones into `~/edsger/quality-<owner>-<repo>`
|
|
6
|
+
* - Reuses + fetches + hard-resets to the remote default branch when the
|
|
7
|
+
* dir already has a `.git`
|
|
8
|
+
* - Authenticates via `buildCredentialArgs(token)` so the GitHub App
|
|
9
|
+
* installation token never lands in the remote URL or `ps`
|
|
10
|
+
*
|
|
11
|
+
* Quality runs are read-only — no per-commit / per-run subdirectory, one
|
|
12
|
+
* checkout per (owner, repo) is enough and lets repeated runs skip the
|
|
13
|
+
* full clone cost.
|
|
14
|
+
*/
|
|
15
|
+
import { execFileSync, execSync } from 'child_process';
|
|
16
|
+
import { existsSync } from 'fs';
|
|
17
|
+
import { homedir } from 'os';
|
|
18
|
+
import { join } from 'path';
|
|
19
|
+
import { logError, logInfo, logSuccess } from '../../utils/logger.js';
|
|
20
|
+
import { buildCredentialArgs } from '../pr-resolve/workspace.js';
|
|
21
|
+
export function getQualityWorkspacePath(owner, repo) {
|
|
22
|
+
return join(homedir(), 'edsger', `quality-${owner}-${repo}`);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Clone (or reuse) the product's repo and check out its default branch.
|
|
26
|
+
*/
|
|
27
|
+
export function prepareQualityWorkspace(options) {
|
|
28
|
+
const { owner, repo, token, verbose } = options;
|
|
29
|
+
const repoPath = getQualityWorkspacePath(owner, repo);
|
|
30
|
+
const repoUrl = `https://github.com/${owner}/${repo}.git`;
|
|
31
|
+
const credArgs = buildCredentialArgs(token);
|
|
32
|
+
if (existsSync(join(repoPath, '.git'))) {
|
|
33
|
+
logInfo(`Reusing existing quality workspace at ${repoPath}`);
|
|
34
|
+
try {
|
|
35
|
+
execSync(`git remote set-url origin "${repoUrl}"`, {
|
|
36
|
+
cwd: repoPath,
|
|
37
|
+
stdio: 'pipe',
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Non-fatal — the URL may already be correct.
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
execFileSync('git', [...credArgs, 'fetch', 'origin', '--prune'], {
|
|
45
|
+
cwd: repoPath,
|
|
46
|
+
stdio: 'pipe',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
logError('Could not fetch latest changes; analysis will use cached state');
|
|
51
|
+
}
|
|
52
|
+
const branch = detectDefaultBranch(repoPath, credArgs);
|
|
53
|
+
try {
|
|
54
|
+
execFileSync('git', ['checkout', '-B', branch, `origin/${branch}`], {
|
|
55
|
+
cwd: repoPath,
|
|
56
|
+
stdio: 'pipe',
|
|
57
|
+
});
|
|
58
|
+
execFileSync('git', ['reset', '--hard', `origin/${branch}`], {
|
|
59
|
+
cwd: repoPath,
|
|
60
|
+
stdio: 'pipe',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
throw new Error(`Failed to update workspace to origin/${branch}: ${error instanceof Error ? error.message : String(error)}`);
|
|
65
|
+
}
|
|
66
|
+
if (verbose) {
|
|
67
|
+
logInfo(`Workspace synced to origin/${branch}`);
|
|
68
|
+
}
|
|
69
|
+
return { repoPath, branch };
|
|
70
|
+
}
|
|
71
|
+
logInfo(`Cloning ${owner}/${repo} for quality benchmark...`);
|
|
72
|
+
try {
|
|
73
|
+
execFileSync('git', [...credArgs, 'clone', repoUrl, repoPath], {
|
|
74
|
+
stdio: 'pipe',
|
|
75
|
+
});
|
|
76
|
+
logSuccess(`Cloned to ${repoPath}`);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
throw new Error(`Failed to clone ${owner}/${repo}: ${error instanceof Error ? error.message : String(error)}`);
|
|
80
|
+
}
|
|
81
|
+
const branch = detectDefaultBranch(repoPath, credArgs);
|
|
82
|
+
return { repoPath, branch };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Return the remote's default branch (the one `origin/HEAD` points at).
|
|
86
|
+
* Falls back to `main` if the symbolic ref isn't set — `git remote set-head`
|
|
87
|
+
* is used to (re)populate it before reading.
|
|
88
|
+
*/
|
|
89
|
+
function detectDefaultBranch(repoPath, credArgs) {
|
|
90
|
+
try {
|
|
91
|
+
execFileSync('git', [...credArgs, 'remote', 'set-head', 'origin', '-a'], {
|
|
92
|
+
cwd: repoPath,
|
|
93
|
+
stdio: 'pipe',
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Non-fatal; the symbolic ref may already be set from a fresh clone.
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const out = execFileSync('git', ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], { cwd: repoPath, stdio: ['ignore', 'pipe', 'pipe'] }).toString();
|
|
101
|
+
const ref = out.trim();
|
|
102
|
+
if (ref.startsWith('origin/')) {
|
|
103
|
+
return ref.slice('origin/'.length);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// fall through
|
|
108
|
+
}
|
|
109
|
+
return 'main';
|
|
110
|
+
}
|