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 a local repo checkout and
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> (required) local repo checkout
13
- * --branch <name> (optional) branch name (informational)
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: string;
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 a local repo checkout and
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> (required) local repo checkout
13
- * --branch <name> (optional) branch name (informational)
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
- logInfo(`Repo: ${repoRoot}`);
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: options.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: options.branch ?? null,
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: options.branch ?? null,
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('Run an industrial-grade code quality benchmark against a local repo')
495
- .requiredOption('--repo <path>', 'Path to the local repo checkout')
496
- .option('--branch <name>', 'Branch name (informational; does not checkout)')
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edsger",
3
- "version": "0.56.1",
3
+ "version": "0.56.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "edsger": "dist/index.js"