edsger 0.73.0 → 0.75.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/dist/api/adr.d.ts +48 -0
- package/dist/api/adr.js +139 -0
- package/dist/commands/adr/index.d.ts +13 -0
- package/dist/commands/adr/index.js +31 -0
- package/dist/commands/features/index.d.ts +15 -0
- package/dist/commands/features/index.js +34 -0
- package/dist/commands/pr-resolve/index.d.ts +3 -1
- package/dist/commands/pr-resolve/index.js +12 -7
- package/dist/commands/pr-review/index.d.ts +3 -1
- package/dist/commands/pr-review/index.js +10 -6
- package/dist/commands/sync-github-pull-requests/index.d.ts +11 -0
- package/dist/commands/sync-github-pull-requests/index.js +42 -0
- package/dist/index.js +66 -4
- package/dist/phases/adr-generation/agent.d.ts +6 -0
- package/dist/phases/adr-generation/agent.js +69 -0
- package/dist/phases/adr-generation/index.d.ts +15 -0
- package/dist/phases/adr-generation/index.js +66 -0
- package/dist/phases/adr-generation/parse.d.ts +12 -0
- package/dist/phases/adr-generation/parse.js +123 -0
- package/dist/phases/adr-generation/prompts.d.ts +8 -0
- package/dist/phases/adr-generation/prompts.js +35 -0
- package/dist/phases/data-flow/mcp-server.d.ts +1 -1
- package/dist/phases/features/index.d.ts +65 -0
- package/dist/phases/features/index.js +292 -0
- package/dist/phases/features/mcp-server.d.ts +61 -0
- package/dist/phases/features/mcp-server.js +165 -0
- package/dist/phases/features/prompts.d.ts +32 -0
- package/dist/phases/features/prompts.js +92 -0
- package/dist/phases/features/types.d.ts +34 -0
- package/dist/phases/features/types.js +15 -0
- package/dist/phases/pr-resolve/index.d.ts +3 -1
- package/dist/phases/pr-resolve/index.js +12 -12
- package/dist/phases/pr-review/index.d.ts +3 -1
- package/dist/phases/pr-review/index.js +13 -16
- package/dist/phases/pr-shared/status.d.ts +18 -0
- package/dist/phases/pr-shared/status.js +37 -0
- package/dist/phases/quality-benchmark/parsers.js +79 -0
- package/dist/phases/quality-benchmark/rubric.md +125 -17
- package/dist/phases/quality-benchmark/tool-catalog.js +39 -0
- package/dist/phases/sync-github-pull-requests/index.d.ts +23 -0
- package/dist/phases/sync-github-pull-requests/index.js +210 -0
- package/dist/phases/sync-github-pull-requests/state.d.ts +24 -0
- package/dist/phases/sync-github-pull-requests/state.js +16 -0
- package/dist/phases/sync-github-pull-requests/types.d.ts +22 -0
- package/dist/phases/sync-github-pull-requests/types.js +1 -0
- package/dist/skills/phase/adr-generation/SKILL.md +51 -0
- package/package.json +1 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Architecture Decision Record API for the CLI.
|
|
3
|
+
*
|
|
4
|
+
* The desktop app creates the `adrs` draft row (the human types the decision
|
|
5
|
+
* topic + context); the CLI agent reads it here, fills `adr_options` with
|
|
6
|
+
* pros/cons, and flips `generation_status` to `ready`. All writes go through the
|
|
7
|
+
* user-scoped Supabase client (RLS gates on product/repository access), so the
|
|
8
|
+
* CLI must run with a synced desktop session.
|
|
9
|
+
*/
|
|
10
|
+
export interface AdrRow {
|
|
11
|
+
id: string;
|
|
12
|
+
product_id: string | null;
|
|
13
|
+
repository_id: string | null;
|
|
14
|
+
sequence: number;
|
|
15
|
+
title: string;
|
|
16
|
+
context: string | null;
|
|
17
|
+
generation_prompt: string | null;
|
|
18
|
+
}
|
|
19
|
+
export interface AdrOptionInput {
|
|
20
|
+
title: string;
|
|
21
|
+
summary?: string;
|
|
22
|
+
pros: string[];
|
|
23
|
+
cons: string[];
|
|
24
|
+
is_recommended?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** Context gathered for the agent prompt. */
|
|
27
|
+
export interface AdrContext {
|
|
28
|
+
scope: 'product' | 'repo';
|
|
29
|
+
name: string;
|
|
30
|
+
description: string | null;
|
|
31
|
+
/** Repo full names linked to the product, or the single repo's name. */
|
|
32
|
+
repos: string[];
|
|
33
|
+
/** Recent issue titles (product scope only) for situational awareness. */
|
|
34
|
+
issues: string[];
|
|
35
|
+
}
|
|
36
|
+
export declare function getAdr(adrId: string): Promise<AdrRow>;
|
|
37
|
+
/**
|
|
38
|
+
* Gather lightweight context for the decision: the product/repo identity, its
|
|
39
|
+
* linked repositories, and (for products) a sample of recent issue titles.
|
|
40
|
+
*/
|
|
41
|
+
export declare function loadAdrContext(adr: AdrRow): Promise<AdrContext>;
|
|
42
|
+
/**
|
|
43
|
+
* Replace the ADR's options with a freshly generated set and mark the run
|
|
44
|
+
* ready for the human to choose. Clears any prior options so re-runs are clean.
|
|
45
|
+
*/
|
|
46
|
+
export declare function saveAdrOptions(adrId: string, options: AdrOptionInput[]): Promise<void>;
|
|
47
|
+
export declare function markGenerationReady(adrId: string): Promise<void>;
|
|
48
|
+
export declare function markGenerationFailed(adrId: string, message: string): Promise<void>;
|
package/dist/api/adr.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Architecture Decision Record API for the CLI.
|
|
3
|
+
*
|
|
4
|
+
* The desktop app creates the `adrs` draft row (the human types the decision
|
|
5
|
+
* topic + context); the CLI agent reads it here, fills `adr_options` with
|
|
6
|
+
* pros/cons, and flips `generation_status` to `ready`. All writes go through the
|
|
7
|
+
* user-scoped Supabase client (RLS gates on product/repository access), so the
|
|
8
|
+
* CLI must run with a synced desktop session.
|
|
9
|
+
*/
|
|
10
|
+
import { ensureSupabaseSession, getSupabase } from '../supabase/client.js';
|
|
11
|
+
import { logInfo } from '../utils/logger.js';
|
|
12
|
+
async function requireSession() {
|
|
13
|
+
const ready = await ensureSupabaseSession();
|
|
14
|
+
if (!ready) {
|
|
15
|
+
throw new Error('Supabase session unavailable. Sign in to the Edsger desktop app to authorize the CLI.');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export async function getAdr(adrId) {
|
|
19
|
+
await requireSession();
|
|
20
|
+
const { data, error } = await getSupabase()
|
|
21
|
+
.from('adrs')
|
|
22
|
+
.select('id, product_id, repository_id, sequence, title, context, generation_prompt')
|
|
23
|
+
.eq('id', adrId)
|
|
24
|
+
.single();
|
|
25
|
+
if (error) {
|
|
26
|
+
throw new Error(`Failed to load ADR ${adrId}: ${error.message}`);
|
|
27
|
+
}
|
|
28
|
+
return data;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Gather lightweight context for the decision: the product/repo identity, its
|
|
32
|
+
* linked repositories, and (for products) a sample of recent issue titles.
|
|
33
|
+
*/
|
|
34
|
+
export async function loadAdrContext(adr) {
|
|
35
|
+
const sb = getSupabase();
|
|
36
|
+
if (adr.product_id) {
|
|
37
|
+
const { data: product } = await sb
|
|
38
|
+
.from('products')
|
|
39
|
+
.select('name, description, product_repositories(repositories(full_name))')
|
|
40
|
+
.eq('id', adr.product_id)
|
|
41
|
+
.single();
|
|
42
|
+
const { data: issues } = await sb
|
|
43
|
+
.from('issues')
|
|
44
|
+
.select('name')
|
|
45
|
+
.eq('product_id', adr.product_id)
|
|
46
|
+
.order('created_at', { ascending: false })
|
|
47
|
+
.limit(25);
|
|
48
|
+
const repoLinks = (product?.product_repositories ??
|
|
49
|
+
[]);
|
|
50
|
+
return {
|
|
51
|
+
scope: 'product',
|
|
52
|
+
name: product?.name ?? 'Unknown product',
|
|
53
|
+
description: product?.description ?? null,
|
|
54
|
+
repos: repoLinks
|
|
55
|
+
.flatMap((l) => Array.isArray(l.repositories)
|
|
56
|
+
? l.repositories
|
|
57
|
+
: l.repositories
|
|
58
|
+
? [l.repositories]
|
|
59
|
+
: [])
|
|
60
|
+
.map((r) => r.full_name)
|
|
61
|
+
.filter((n) => Boolean(n)),
|
|
62
|
+
issues: (issues ?? []).map((i) => i.name),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const { data: repo } = await sb
|
|
66
|
+
.from('repositories')
|
|
67
|
+
.select('full_name, description, language')
|
|
68
|
+
.eq('id', adr.repository_id)
|
|
69
|
+
.single();
|
|
70
|
+
return {
|
|
71
|
+
scope: 'repo',
|
|
72
|
+
name: repo?.full_name ?? 'Unknown repository',
|
|
73
|
+
description: repo?.description ?? null,
|
|
74
|
+
repos: repo?.full_name ? [repo.full_name] : [],
|
|
75
|
+
issues: [],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Replace the ADR's options with a freshly generated set and mark the run
|
|
80
|
+
* ready for the human to choose. Clears any prior options so re-runs are clean.
|
|
81
|
+
*/
|
|
82
|
+
export async function saveAdrOptions(adrId, options) {
|
|
83
|
+
await requireSession();
|
|
84
|
+
const sb = getSupabase();
|
|
85
|
+
// Detach the previously selected option (FK) before clearing options, so the
|
|
86
|
+
// delete can't trip the ON DELETE SET NULL ordering on a re-run.
|
|
87
|
+
await sb.from('adrs').update({ selected_option_id: null }).eq('id', adrId);
|
|
88
|
+
const { error: delError } = await sb
|
|
89
|
+
.from('adr_options')
|
|
90
|
+
.delete()
|
|
91
|
+
.eq('adr_id', adrId);
|
|
92
|
+
if (delError) {
|
|
93
|
+
throw new Error(`Failed to clear prior options: ${delError.message}`);
|
|
94
|
+
}
|
|
95
|
+
if (options.length > 0) {
|
|
96
|
+
const rows = options.map((o, i) => ({
|
|
97
|
+
adr_id: adrId,
|
|
98
|
+
position: i,
|
|
99
|
+
title: o.title,
|
|
100
|
+
summary: o.summary ?? null,
|
|
101
|
+
pros: o.pros ?? [],
|
|
102
|
+
cons: o.cons ?? [],
|
|
103
|
+
is_recommended: o.is_recommended ?? false,
|
|
104
|
+
source: 'ai_generated',
|
|
105
|
+
}));
|
|
106
|
+
const { error } = await sb.from('adr_options').insert(rows);
|
|
107
|
+
if (error) {
|
|
108
|
+
throw new Error(`Failed to save options: ${error.message}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
logInfo(`Saved ${options.length} decision options`);
|
|
112
|
+
}
|
|
113
|
+
export async function markGenerationReady(adrId) {
|
|
114
|
+
const { error } = await getSupabase()
|
|
115
|
+
.from('adrs')
|
|
116
|
+
.update({
|
|
117
|
+
generation_status: 'ready',
|
|
118
|
+
generation_error: null,
|
|
119
|
+
status: 'proposed',
|
|
120
|
+
})
|
|
121
|
+
.eq('id', adrId);
|
|
122
|
+
if (error) {
|
|
123
|
+
throw new Error(`Failed to mark ADR ready: ${error.message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export async function markGenerationFailed(adrId, message) {
|
|
127
|
+
try {
|
|
128
|
+
await getSupabase()
|
|
129
|
+
.from('adrs')
|
|
130
|
+
.update({
|
|
131
|
+
generation_status: 'failed',
|
|
132
|
+
generation_error: message.slice(0, 1000),
|
|
133
|
+
})
|
|
134
|
+
.eq('id', adrId);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Best-effort — the caller already logged the underlying error.
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface AdrCommandOptions {
|
|
2
|
+
adrId: string;
|
|
3
|
+
verbose?: boolean;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Generate decision options for a draft ADR. The desktop app creates the draft
|
|
7
|
+
* row (the human types the decision topic) and passes its id here; this run
|
|
8
|
+
* fills it with options + pros/cons for the human to choose from.
|
|
9
|
+
*
|
|
10
|
+
* The cli_sessions `command` column is keyed off EDSGER_PROCESS_KEY (set by the
|
|
11
|
+
* desktop runner to `adr:<scopeId>`), so the originating tab can replay logs.
|
|
12
|
+
*/
|
|
13
|
+
export declare const runAdr: (options: AdrCommandOptions) => Promise<void>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { generateAdr } from '../../phases/adr-generation/index.js';
|
|
2
|
+
import { deregisterSession, registerSession, } from '../../system/session-manager.js';
|
|
3
|
+
import { logError, logInfo } from '../../utils/logger.js';
|
|
4
|
+
import { validateConfiguration } from '../../utils/validation.js';
|
|
5
|
+
/**
|
|
6
|
+
* Generate decision options for a draft ADR. The desktop app creates the draft
|
|
7
|
+
* row (the human types the decision topic) and passes its id here; this run
|
|
8
|
+
* fills it with options + pros/cons for the human to choose from.
|
|
9
|
+
*
|
|
10
|
+
* The cli_sessions `command` column is keyed off EDSGER_PROCESS_KEY (set by the
|
|
11
|
+
* desktop runner to `adr:<scopeId>`), so the originating tab can replay logs.
|
|
12
|
+
*/
|
|
13
|
+
export const runAdr = async (options) => {
|
|
14
|
+
const { adrId } = options;
|
|
15
|
+
// Establish CLI config (validates auth/env) before doing any work.
|
|
16
|
+
validateConfiguration({ verbose: options.verbose });
|
|
17
|
+
await registerSession({ command: 'adr' });
|
|
18
|
+
logInfo(`Starting ADR generation for: ${adrId}`);
|
|
19
|
+
try {
|
|
20
|
+
const result = await generateAdr({ adrId, verbose: options.verbose });
|
|
21
|
+
if (result.status !== 'success') {
|
|
22
|
+
logError(`ADR generation failed: ${result.error || 'Unknown error'}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
logError(`ADR generation error: ${error instanceof Error ? error.message : String(error)}`);
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
await deregisterSession();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: edsger features <productId> --scan-id <id>
|
|
3
|
+
*
|
|
4
|
+
* Clones every repository linked to the product, asks Claude to catalogue
|
|
5
|
+
* the user-facing features it delivers, and persists them via the
|
|
6
|
+
* product_features table. The desktop UI creates a pending feature_scans
|
|
7
|
+
* row first then invokes the CLI with --scan-id; the CLI flips status
|
|
8
|
+
* running → success/failed and writes features via the MCP toolkit.
|
|
9
|
+
*/
|
|
10
|
+
export interface FeaturesCliOptions {
|
|
11
|
+
scanId: string;
|
|
12
|
+
guidance?: string;
|
|
13
|
+
verbose?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function runFeatures(productId: string, options: FeaturesCliOptions): Promise<void>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: edsger features <productId> --scan-id <id>
|
|
3
|
+
*
|
|
4
|
+
* Clones every repository linked to the product, asks Claude to catalogue
|
|
5
|
+
* the user-facing features it delivers, and persists them via the
|
|
6
|
+
* product_features table. The desktop UI creates a pending feature_scans
|
|
7
|
+
* row first then invokes the CLI with --scan-id; the CLI flips status
|
|
8
|
+
* running → success/failed and writes features via the MCP toolkit.
|
|
9
|
+
*/
|
|
10
|
+
import { runFeaturesPhase } from '../../phases/features/index.js';
|
|
11
|
+
import { logError, logInfo, logSuccess } from '../../utils/logger.js';
|
|
12
|
+
export async function runFeatures(productId, options) {
|
|
13
|
+
const { scanId, guidance, verbose } = options;
|
|
14
|
+
if (!productId) {
|
|
15
|
+
throw new Error('A product ID is required for features');
|
|
16
|
+
}
|
|
17
|
+
if (!scanId) {
|
|
18
|
+
throw new Error('--scan-id is required (the pending feature_scans row id)');
|
|
19
|
+
}
|
|
20
|
+
logInfo(`Starting features scan for product ${productId}`);
|
|
21
|
+
const result = await runFeaturesPhase({
|
|
22
|
+
productId,
|
|
23
|
+
scanId,
|
|
24
|
+
guidance,
|
|
25
|
+
verbose,
|
|
26
|
+
});
|
|
27
|
+
if (result.status === 'success') {
|
|
28
|
+
logSuccess(result.message);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
logError(result.message);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
export interface PRResolveCliOptions {
|
|
6
6
|
prUrl: string;
|
|
7
7
|
prId?: string;
|
|
8
|
+
/** Repo-only mode: resolve GitHub config from a repositories row. */
|
|
9
|
+
repoId?: string;
|
|
8
10
|
verbose?: boolean;
|
|
9
11
|
learn?: boolean;
|
|
10
12
|
}
|
|
11
|
-
export declare function runPRResolve(productId: string, options: PRResolveCliOptions): Promise<void>;
|
|
13
|
+
export declare function runPRResolve(productId: string | undefined, options: PRResolveCliOptions): Promise<void>;
|
|
@@ -2,31 +2,36 @@
|
|
|
2
2
|
* CLI command: edsger pr-resolve <productId> --pr-url <url>
|
|
3
3
|
* Resolves PR change requests: fixes what's genuinely better, explains what it won't change.
|
|
4
4
|
*/
|
|
5
|
-
import { getGitHubConfigByProduct } from '../../api/github.js';
|
|
5
|
+
import { getGitHubConfigByProduct, getGitHubConfigByRepository, } from '../../api/github.js';
|
|
6
6
|
import { resolveStandalonePR } from '../../phases/pr-resolve/index.js';
|
|
7
7
|
import { logError, logInfo, logSuccess } from '../../utils/logger.js';
|
|
8
8
|
export async function runPRResolve(productId, options) {
|
|
9
|
-
const { prUrl, prId, verbose } = options;
|
|
10
|
-
|
|
9
|
+
const { prUrl, prId, repoId, verbose } = options;
|
|
10
|
+
const scopeLabel = repoId ? `repository ${repoId}` : `product ${productId}`;
|
|
11
|
+
logInfo(`Starting PR resolve for ${scopeLabel}`);
|
|
11
12
|
logInfo(`PR URL: ${prUrl}`);
|
|
12
|
-
//
|
|
13
|
-
const githubConfig =
|
|
13
|
+
// Resolve GitHub config from the repository (repo-only mode) or the product.
|
|
14
|
+
const githubConfig = repoId
|
|
15
|
+
? await getGitHubConfigByRepository(repoId, verbose)
|
|
16
|
+
: await getGitHubConfigByProduct(productId ?? '', verbose);
|
|
14
17
|
if (!githubConfig.configured ||
|
|
15
18
|
!githubConfig.token ||
|
|
16
19
|
!githubConfig.owner ||
|
|
17
20
|
!githubConfig.repo) {
|
|
18
|
-
logError(`GitHub not configured for
|
|
21
|
+
logError(`GitHub not configured for ${scopeLabel}: ${githubConfig.message || 'No installation found'}`);
|
|
19
22
|
process.exit(1);
|
|
20
23
|
}
|
|
21
24
|
const result = await resolveStandalonePR({
|
|
22
25
|
productId,
|
|
26
|
+
repositoryId: repoId,
|
|
23
27
|
pullRequestUrl: prUrl,
|
|
24
28
|
githubToken: githubConfig.token,
|
|
25
29
|
owner: githubConfig.owner,
|
|
26
30
|
repo: githubConfig.repo,
|
|
27
31
|
prId,
|
|
28
32
|
verbose,
|
|
29
|
-
|
|
33
|
+
// Checklist learning is product-scoped; skip it in repo-only mode.
|
|
34
|
+
learn: repoId ? false : options.learn,
|
|
30
35
|
});
|
|
31
36
|
if (result.status === 'success') {
|
|
32
37
|
logSuccess(`PR resolve completed: ${result.message}`);
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
export interface PRReviewCliOptions {
|
|
6
6
|
prUrl: string;
|
|
7
7
|
prId?: string;
|
|
8
|
+
/** Repo-only mode: resolve GitHub config from a repositories row. */
|
|
9
|
+
repoId?: string;
|
|
8
10
|
verbose?: boolean;
|
|
9
11
|
}
|
|
10
|
-
export declare function runPRReview(productId: string, options: PRReviewCliOptions): Promise<void>;
|
|
12
|
+
export declare function runPRReview(productId: string | undefined, options: PRReviewCliOptions): Promise<void>;
|
|
@@ -2,24 +2,28 @@
|
|
|
2
2
|
* CLI command: edsger pr-review <productId> --pr-url <url>
|
|
3
3
|
* Reviews a standalone GitHub PR and posts comments.
|
|
4
4
|
*/
|
|
5
|
-
import { getGitHubConfigByProduct } from '../../api/github.js';
|
|
5
|
+
import { getGitHubConfigByProduct, getGitHubConfigByRepository, } from '../../api/github.js';
|
|
6
6
|
import { reviewStandalonePR } from '../../phases/pr-review/index.js';
|
|
7
7
|
import { logError, logInfo, logSuccess } from '../../utils/logger.js';
|
|
8
8
|
export async function runPRReview(productId, options) {
|
|
9
|
-
const { prUrl, prId, verbose } = options;
|
|
10
|
-
|
|
9
|
+
const { prUrl, prId, repoId, verbose } = options;
|
|
10
|
+
const scopeLabel = repoId ? `repository ${repoId}` : `product ${productId}`;
|
|
11
|
+
logInfo(`Starting PR review for ${scopeLabel}`);
|
|
11
12
|
logInfo(`PR URL: ${prUrl}`);
|
|
12
|
-
//
|
|
13
|
-
const githubConfig =
|
|
13
|
+
// Resolve GitHub config from the repository (repo-only mode) or the product.
|
|
14
|
+
const githubConfig = repoId
|
|
15
|
+
? await getGitHubConfigByRepository(repoId, verbose)
|
|
16
|
+
: await getGitHubConfigByProduct(productId ?? '', verbose);
|
|
14
17
|
if (!githubConfig.configured ||
|
|
15
18
|
!githubConfig.token ||
|
|
16
19
|
!githubConfig.owner ||
|
|
17
20
|
!githubConfig.repo) {
|
|
18
|
-
logError(`GitHub not configured for
|
|
21
|
+
logError(`GitHub not configured for ${scopeLabel}: ${githubConfig.message || 'No installation found'}`);
|
|
19
22
|
process.exit(1);
|
|
20
23
|
}
|
|
21
24
|
const result = await reviewStandalonePR({
|
|
22
25
|
productId,
|
|
26
|
+
repositoryId: repoId,
|
|
23
27
|
pullRequestUrl: prUrl,
|
|
24
28
|
githubToken: githubConfig.token,
|
|
25
29
|
owner: githubConfig.owner,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: `edsger sync-github-pull-requests <repoId>`
|
|
3
|
+
*
|
|
4
|
+
* Mirrors a connected GitHub repo's pull requests into the local
|
|
5
|
+
* `pull_requests` table (scoped by repository_id) for the repo detail page.
|
|
6
|
+
* Idempotent — already-synced PRs are refreshed, never duplicated.
|
|
7
|
+
*/
|
|
8
|
+
export interface SyncGithubPullRequestsCliOptions {
|
|
9
|
+
verbose?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function runSyncGithubPullRequests(repositoryId: string, options?: SyncGithubPullRequestsCliOptions): Promise<void>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: `edsger sync-github-pull-requests <repoId>`
|
|
3
|
+
*
|
|
4
|
+
* Mirrors a connected GitHub repo's pull requests into the local
|
|
5
|
+
* `pull_requests` table (scoped by repository_id) for the repo detail page.
|
|
6
|
+
* Idempotent — already-synced PRs are refreshed, never duplicated.
|
|
7
|
+
*/
|
|
8
|
+
import { getGitHubConfigByRepository } from '../../api/github.js';
|
|
9
|
+
import { syncGithubPullRequests } from '../../phases/sync-github-pull-requests/index.js';
|
|
10
|
+
import { logError, logInfo, logSuccess } from '../../utils/logger.js';
|
|
11
|
+
export async function runSyncGithubPullRequests(repositoryId, options = {}) {
|
|
12
|
+
const { verbose } = options;
|
|
13
|
+
logInfo(`Starting GitHub pull request sync for repository ${repositoryId}`);
|
|
14
|
+
const githubConfig = await getGitHubConfigByRepository(repositoryId, verbose);
|
|
15
|
+
if (!githubConfig.configured ||
|
|
16
|
+
!githubConfig.token ||
|
|
17
|
+
!githubConfig.owner ||
|
|
18
|
+
!githubConfig.repo) {
|
|
19
|
+
logError(`GitHub not configured for repository ${repositoryId}: ${githubConfig.message || 'No installation found'}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
const result = await syncGithubPullRequests({
|
|
23
|
+
repositoryId,
|
|
24
|
+
githubToken: githubConfig.token,
|
|
25
|
+
owner: githubConfig.owner,
|
|
26
|
+
repo: githubConfig.repo,
|
|
27
|
+
verbose,
|
|
28
|
+
});
|
|
29
|
+
if (result.status === 'success') {
|
|
30
|
+
logSuccess(`GitHub PR sync completed: ${result.message}`);
|
|
31
|
+
if (result.repository) {
|
|
32
|
+
logInfo(`Repository: ${result.repository}`);
|
|
33
|
+
}
|
|
34
|
+
if (result.fetchedCount !== undefined) {
|
|
35
|
+
logInfo(`Fetched ${result.fetchedCount} · created ${result.createdCount ?? 0} · updated ${result.updatedCount ?? 0}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
logError(`GitHub PR sync failed: ${result.message}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { dirname, join } from 'path';
|
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { applyEnvStore } from './auth/env-store.js';
|
|
8
8
|
import { runLogin, runLogout, runStatus } from './auth/login.js';
|
|
9
|
+
import { runAdr } from './commands/adr/index.js';
|
|
9
10
|
import { runAgentWorkflow } from './commands/agent-workflow/index.js';
|
|
10
11
|
import { runAnalyzeLogs } from './commands/analyze-logs/index.js';
|
|
11
12
|
import { runAppStoreGeneration } from './commands/app-store/index.js';
|
|
@@ -19,6 +20,7 @@ import { runConfigGet, runConfigList, runConfigSet, runConfigUnset, } from './co
|
|
|
19
20
|
import { runDataFlow } from './commands/data-flow/index.js';
|
|
20
21
|
import { runDiscover } from './commands/discover/index.js';
|
|
21
22
|
import { runErDiagram } from './commands/er-diagram/index.js';
|
|
23
|
+
import { runFeatures } from './commands/features/index.js';
|
|
22
24
|
import { runFinancingDeck } from './commands/financing-deck/index.js';
|
|
23
25
|
import { runFindArchitecture } from './commands/find-architecture/index.js';
|
|
24
26
|
import { runFindBugs } from './commands/find-bugs/index.js';
|
|
@@ -46,6 +48,7 @@ import { runStateDiagram } from './commands/state-diagram/index.js';
|
|
|
46
48
|
import { runSyncAws } from './commands/sync-aws/index.js';
|
|
47
49
|
import { runSyncDatadog } from './commands/sync-datadog/index.js';
|
|
48
50
|
import { runSyncGithubIssues } from './commands/sync-github-issues/index.js';
|
|
51
|
+
import { runSyncGithubPullRequests } from './commands/sync-github-pull-requests/index.js';
|
|
49
52
|
import { runSyncOrgRepos } from './commands/sync-org-repos/index.js';
|
|
50
53
|
import { runSyncSentryIssues } from './commands/sync-sentry-issues/index.js';
|
|
51
54
|
import { runSyncTerraform } from './commands/sync-terraform/index.js';
|
|
@@ -462,6 +465,21 @@ program
|
|
|
462
465
|
}
|
|
463
466
|
});
|
|
464
467
|
// ============================================================
|
|
468
|
+
// Subcommand: edsger adr <adrId>
|
|
469
|
+
// ============================================================
|
|
470
|
+
program
|
|
471
|
+
.command('adr <adrId>')
|
|
472
|
+
.description('Generate decision options (pros/cons) for a draft Architecture Decision Record')
|
|
473
|
+
.option('-v, --verbose', 'Verbose output')
|
|
474
|
+
.action(async (adrId, opts) => {
|
|
475
|
+
try {
|
|
476
|
+
await runAdr({ adrId, verbose: opts.verbose });
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
exitWithError(error);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
// ============================================================
|
|
465
483
|
// Subcommand: edsger app-store <productId>
|
|
466
484
|
// ============================================================
|
|
467
485
|
program
|
|
@@ -763,13 +781,17 @@ program
|
|
|
763
781
|
// Subcommand: edsger pr-review <productId>
|
|
764
782
|
// ============================================================
|
|
765
783
|
program
|
|
766
|
-
.command('pr-review
|
|
767
|
-
.description('AI-review a GitHub PR for a product')
|
|
784
|
+
.command('pr-review [productId]')
|
|
785
|
+
.description('AI-review a GitHub PR for a product (or standalone repository)')
|
|
768
786
|
.requiredOption('--pr-url <url>', 'GitHub PR URL')
|
|
769
787
|
.option('--pr-id <id>', 'Pull request record ID in database')
|
|
788
|
+
.option('--repo-id <id>', 'Run in repo-only mode against a single repositories row (no product context)')
|
|
770
789
|
.option('-v, --verbose', 'Verbose output')
|
|
771
790
|
.action(async (productId, opts) => {
|
|
772
791
|
try {
|
|
792
|
+
if (!productId && !opts.repoId) {
|
|
793
|
+
throw new Error('Provide a productId or --repo-id (repo-only mode) for pr-review');
|
|
794
|
+
}
|
|
773
795
|
await runPRReview(productId, opts);
|
|
774
796
|
}
|
|
775
797
|
catch (error) {
|
|
@@ -842,6 +864,21 @@ program
|
|
|
842
864
|
}
|
|
843
865
|
});
|
|
844
866
|
// ============================================================
|
|
867
|
+
// Subcommand: edsger sync-github-pull-requests <repoId>
|
|
868
|
+
// ============================================================
|
|
869
|
+
program
|
|
870
|
+
.command('sync-github-pull-requests <repoId>')
|
|
871
|
+
.description("Mirror a connected GitHub repo's pull requests into the local pull_requests list for the repo detail page (idempotent — refreshes, never duplicates)")
|
|
872
|
+
.option('-v, --verbose', 'Verbose output')
|
|
873
|
+
.action(async (repoId, opts) => {
|
|
874
|
+
try {
|
|
875
|
+
await runSyncGithubPullRequests(repoId, opts);
|
|
876
|
+
}
|
|
877
|
+
catch (error) {
|
|
878
|
+
exitWithError(error);
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
// ============================================================
|
|
845
882
|
// Subcommand: edsger sync-org-repos <teamId>
|
|
846
883
|
// ============================================================
|
|
847
884
|
program
|
|
@@ -1081,17 +1118,42 @@ program
|
|
|
1081
1118
|
}
|
|
1082
1119
|
});
|
|
1083
1120
|
// ============================================================
|
|
1121
|
+
// Subcommand: edsger features <productId>
|
|
1122
|
+
// ============================================================
|
|
1123
|
+
program
|
|
1124
|
+
.command('features <productId>')
|
|
1125
|
+
.description('Scan every repository linked to a product for the user-facing features it delivers and persist them via the product_features table. Manually defined features are enriched, never rewritten. Writes against the pending feature_scans row identified by --scan-id.')
|
|
1126
|
+
.requiredOption('--scan-id <id>', 'Pending feature_scans row id to drive (created by the desktop UI before invocation)')
|
|
1127
|
+
.option('-g, --guidance <text>', 'Human direction for the AI (focus areas, exclusions)')
|
|
1128
|
+
.option('-v, --verbose', 'Verbose output')
|
|
1129
|
+
.action(async (productId, opts) => {
|
|
1130
|
+
try {
|
|
1131
|
+
await runFeatures(productId, {
|
|
1132
|
+
scanId: opts.scanId,
|
|
1133
|
+
guidance: opts.guidance,
|
|
1134
|
+
verbose: opts.verbose,
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
catch (error) {
|
|
1138
|
+
exitWithError(error);
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
// ============================================================
|
|
1084
1142
|
// Subcommand: edsger pr-resolve <productId>
|
|
1085
1143
|
// ============================================================
|
|
1086
1144
|
program
|
|
1087
|
-
.command('pr-resolve
|
|
1088
|
-
.description('AI-resolve change requests on a GitHub PR')
|
|
1145
|
+
.command('pr-resolve [productId]')
|
|
1146
|
+
.description('AI-resolve change requests on a GitHub PR for a product (or standalone repository)')
|
|
1089
1147
|
.requiredOption('--pr-url <url>', 'GitHub PR URL')
|
|
1090
1148
|
.option('--pr-id <id>', 'Pull request record ID in database')
|
|
1149
|
+
.option('--repo-id <id>', 'Run in repo-only mode against a single repositories row (no product context)')
|
|
1091
1150
|
.option('--no-learn', 'Skip checklist learning after resolve')
|
|
1092
1151
|
.option('-v, --verbose', 'Verbose output')
|
|
1093
1152
|
.action(async (productId, opts) => {
|
|
1094
1153
|
try {
|
|
1154
|
+
if (!productId && !opts.repoId) {
|
|
1155
|
+
throw new Error('Provide a productId or --repo-id (repo-only mode) for pr-resolve');
|
|
1156
|
+
}
|
|
1095
1157
|
await runPRResolve(productId, opts);
|
|
1096
1158
|
}
|
|
1097
1159
|
catch (error) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type AdrOptionInput } from '../../api/adr.js';
|
|
2
|
+
/**
|
|
3
|
+
* Run the ADR agent. Returns the validated options, or null if the model
|
|
4
|
+
* produced nothing parseable.
|
|
5
|
+
*/
|
|
6
|
+
export declare function executeAdrQuery(userPrompt: string, systemPrompt: string, verbose?: boolean, cwd?: string): Promise<AdrOptionInput[] | null>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import { DEFAULT_MODEL } from '../../constants.js';
|
|
3
|
+
import { logDebug, logError, logInfo } from '../../utils/logger.js';
|
|
4
|
+
import { extractJSON, normalizeOptions } from './parse.js';
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
6
|
+
async function* promptStream(text) {
|
|
7
|
+
yield { type: 'user', message: { role: 'user', content: text } };
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Run the ADR agent. Returns the validated options, or null if the model
|
|
11
|
+
* produced nothing parseable.
|
|
12
|
+
*/
|
|
13
|
+
export async function executeAdrQuery(userPrompt, systemPrompt, verbose, cwd) {
|
|
14
|
+
let lastAssistant = '';
|
|
15
|
+
let result = null;
|
|
16
|
+
let turnCount = 0;
|
|
17
|
+
logInfo('Connecting to AI agent for decision analysis...');
|
|
18
|
+
for await (const message of query({
|
|
19
|
+
prompt: promptStream(userPrompt),
|
|
20
|
+
options: {
|
|
21
|
+
systemPrompt: {
|
|
22
|
+
type: 'preset',
|
|
23
|
+
preset: 'claude_code',
|
|
24
|
+
append: systemPrompt,
|
|
25
|
+
},
|
|
26
|
+
model: DEFAULT_MODEL,
|
|
27
|
+
maxTurns: 1000,
|
|
28
|
+
permissionMode: 'bypassPermissions',
|
|
29
|
+
...(cwd ? { cwd } : {}),
|
|
30
|
+
},
|
|
31
|
+
})) {
|
|
32
|
+
if (message.type === 'assistant' && message.message?.content) {
|
|
33
|
+
turnCount++;
|
|
34
|
+
for (const content of message.message.content) {
|
|
35
|
+
if (content.type === 'text') {
|
|
36
|
+
lastAssistant += `${content.text}\n`;
|
|
37
|
+
logDebug(`${content.text}`, verbose);
|
|
38
|
+
}
|
|
39
|
+
else if (content.type === 'tool_use') {
|
|
40
|
+
const input = (content.input ?? {});
|
|
41
|
+
const desc = input.description ?? input.command ?? 'Running...';
|
|
42
|
+
logInfo(`[Turn ${turnCount}] ${content.name}: ${typeof desc === 'string' ? desc.slice(0, 120) : 'Running...'}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (message.type === 'result') {
|
|
47
|
+
const text = message.subtype === 'success'
|
|
48
|
+
? message.result || lastAssistant
|
|
49
|
+
: lastAssistant;
|
|
50
|
+
if (message.subtype === 'success') {
|
|
51
|
+
logInfo(`\nAnalysis completed after ${turnCount} turns, parsing...`);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
logError(`\nAnalysis incomplete: ${message.subtype}`);
|
|
55
|
+
}
|
|
56
|
+
const parsed = extractJSON(text);
|
|
57
|
+
if (parsed) {
|
|
58
|
+
const options = normalizeOptions(parsed);
|
|
59
|
+
if (options.length > 0) {
|
|
60
|
+
result = options;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (!result) {
|
|
66
|
+
logError('Could not extract decision options from the agent response.');
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|