edsger 0.68.0 → 0.70.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/github.d.ts +23 -0
- package/dist/api/github.js +61 -0
- package/dist/commands/architecture-diagram/index.d.ts +8 -0
- package/dist/commands/architecture-diagram/index.js +10 -0
- package/dist/commands/class-diagram/index.d.ts +7 -0
- package/dist/commands/class-diagram/index.js +9 -0
- package/dist/commands/data-flow/index.d.ts +8 -6
- package/dist/commands/data-flow/index.js +21 -12
- package/dist/commands/diagram-shared/index.d.ts +21 -0
- package/dist/commands/diagram-shared/index.js +37 -0
- package/dist/commands/er-diagram/index.d.ts +19 -0
- package/dist/commands/er-diagram/index.js +55 -0
- package/dist/commands/flowchart/index.d.ts +8 -0
- package/dist/commands/flowchart/index.js +10 -0
- package/dist/commands/quality-benchmark/index.js +43 -9
- package/dist/commands/recipes/index.d.ts +3 -1
- package/dist/commands/recipes/index.js +10 -4
- package/dist/commands/screen-flow/index.d.ts +8 -6
- package/dist/commands/screen-flow/index.js +21 -12
- package/dist/commands/sequence-diagram/index.d.ts +19 -0
- package/dist/commands/sequence-diagram/index.js +55 -0
- package/dist/commands/state-diagram/index.d.ts +7 -0
- package/dist/commands/state-diagram/index.js +9 -0
- package/dist/index.js +144 -14
- package/dist/phases/architecture-diagram/index.d.ts +15 -0
- package/dist/phases/architecture-diagram/index.js +51 -0
- package/dist/phases/class-diagram/index.d.ts +14 -0
- package/dist/phases/class-diagram/index.js +76 -0
- package/dist/phases/data-flow/index.d.ts +6 -3
- package/dist/phases/data-flow/index.js +59 -38
- package/dist/phases/data-flow/mcp-server.d.ts +1 -1
- package/dist/phases/data-flow/mcp-server.js +2 -2
- package/dist/phases/data-flow/types.d.ts +1 -1
- package/dist/phases/data-flow/types.js +1 -1
- package/dist/phases/diagram-shared/clone-repos.d.ts +63 -0
- package/dist/phases/diagram-shared/clone-repos.js +153 -0
- package/dist/phases/diagram-shared/generate.d.ts +42 -0
- package/dist/phases/diagram-shared/generate.js +162 -0
- package/dist/phases/diagram-shared/graph.d.ts +62 -0
- package/dist/phases/diagram-shared/graph.js +169 -0
- package/dist/phases/diagram-shared/mcp.d.ts +35 -0
- package/dist/phases/diagram-shared/mcp.js +68 -0
- package/dist/phases/diagram-shared/prompts.d.ts +23 -0
- package/dist/phases/diagram-shared/prompts.js +35 -0
- package/dist/phases/er-diagram/index.d.ts +28 -0
- package/dist/phases/er-diagram/index.js +290 -0
- package/dist/phases/er-diagram/mcp-server.d.ts +77 -0
- package/dist/phases/er-diagram/mcp-server.js +144 -0
- package/dist/phases/er-diagram/prompts.d.ts +14 -0
- package/dist/phases/er-diagram/prompts.js +36 -0
- package/dist/phases/er-diagram/types.d.ts +76 -0
- package/dist/phases/er-diagram/types.js +84 -0
- package/dist/phases/flow-shared/clone-repos.d.ts +8 -2
- package/dist/phases/flow-shared/clone-repos.js +36 -18
- package/dist/phases/flowchart/index.d.ts +15 -0
- package/dist/phases/flowchart/index.js +50 -0
- package/dist/phases/output-contracts.js +178 -2
- package/dist/phases/recipes/index.d.ts +8 -1
- package/dist/phases/recipes/index.js +74 -17
- package/dist/phases/recipes/mcp-server.d.ts +4 -1
- package/dist/phases/recipes/mcp-server.js +43 -18
- package/dist/phases/screen-flow/index.d.ts +7 -4
- package/dist/phases/screen-flow/index.js +66 -45
- package/dist/phases/screen-flow/mcp-server.js +2 -2
- package/dist/phases/sequence-diagram/index.d.ts +30 -0
- package/dist/phases/sequence-diagram/index.js +290 -0
- package/dist/phases/sequence-diagram/mcp-server.d.ts +64 -0
- package/dist/phases/sequence-diagram/mcp-server.js +134 -0
- package/dist/phases/sequence-diagram/prompts.d.ts +14 -0
- package/dist/phases/sequence-diagram/prompts.js +36 -0
- package/dist/phases/sequence-diagram/types.d.ts +52 -0
- package/dist/phases/sequence-diagram/types.js +93 -0
- package/dist/phases/state-diagram/index.d.ts +15 -0
- package/dist/phases/state-diagram/index.js +53 -0
- package/dist/skills/phase/architecture-diagram/SKILL.md +41 -0
- package/dist/skills/phase/class-diagram/SKILL.md +44 -0
- package/dist/skills/phase/er-diagram/SKILL.md +71 -0
- package/dist/skills/phase/flowchart/SKILL.md +38 -0
- package/dist/skills/phase/sequence-diagram/SKILL.md +67 -0
- package/dist/skills/phase/state-diagram/SKILL.md +38 -0
- package/dist/workspace/session-workspace.d.ts +2 -2
- package/dist/workspace/session-workspace.js +2 -2
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* data-flow phase: clone the product's repo, ask Claude to map every data
|
|
3
3
|
* node (source / dataset / transform / sink / queue / model) and the
|
|
4
4
|
* connections between them into a structured DataFlowExtraction, then
|
|
5
|
-
* persist the result to
|
|
5
|
+
* persist the result to diagrams / diagram_nodes / diagram_edges (rows tagged
|
|
6
6
|
* `type = 'data'`) via the Supabase SDK.
|
|
7
7
|
*
|
|
8
8
|
* Companion to screen-flow: same generation pattern (workspace clone +
|
|
@@ -10,12 +10,13 @@
|
|
|
10
10
|
* domain.
|
|
11
11
|
*/
|
|
12
12
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
13
|
+
import { getRepositoryBasics } from '../../api/github.js';
|
|
13
14
|
import { DEFAULT_MODEL } from '../../constants.js';
|
|
14
15
|
import { getSupabase } from '../../supabase/client.js';
|
|
15
16
|
import { logError, logInfo, logSuccess, logWarning } from '../../utils/logger.js';
|
|
16
17
|
import { cleanupIssueRepo } from '../../workspace/workspace-manager.js';
|
|
17
18
|
import { fetchProductBasics } from '../find-shared/mcp.js';
|
|
18
|
-
import {
|
|
19
|
+
import { cloneDiagramRepos, describeRepoScope } from '../diagram-shared/clone-repos.js';
|
|
19
20
|
import { createPromptGenerator, extractTextFromContent, tryExtractResult, } from '../pr-shared/agent-utils.js';
|
|
20
21
|
import { createDataFlowCaptureState, createDataFlowMcpServer, validateConsistency, } from './mcp-server.js';
|
|
21
22
|
import { createDataFlowSystemPrompt, createDataFlowUserPrompt, } from './prompts.js';
|
|
@@ -27,25 +28,34 @@ const COLUMN_WIDTH = 320;
|
|
|
27
28
|
const ROW_HEIGHT = 220;
|
|
28
29
|
const COLUMNS = 4;
|
|
29
30
|
export async function runDataFlowPhase(options) {
|
|
30
|
-
const { productId,
|
|
31
|
-
|
|
31
|
+
const { productId, repoId, diagramId, guidance, verbose } = options;
|
|
32
|
+
const repoOnly = !productId && Boolean(repoId);
|
|
33
|
+
if (productId) {
|
|
34
|
+
logInfo(`Starting data-flow generation for product ${productId}`);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
logInfo(`Starting data-flow generation for repository ${repoId}`);
|
|
38
|
+
}
|
|
32
39
|
const supabase = getSupabase();
|
|
33
|
-
await
|
|
34
|
-
const repositoryIds = await
|
|
35
|
-
const cloneResult = await
|
|
40
|
+
await markDiagramRunning(supabase, diagramId);
|
|
41
|
+
const repositoryIds = await getDiagramRepositoryIds(supabase, diagramId);
|
|
42
|
+
const cloneResult = await cloneDiagramRepos({
|
|
36
43
|
productId,
|
|
44
|
+
repoId,
|
|
37
45
|
repositoryIds,
|
|
38
46
|
workspaceKey: WORKSPACE_KEY,
|
|
39
47
|
verbose,
|
|
40
48
|
});
|
|
41
49
|
if (!cloneResult.ok) {
|
|
42
|
-
await
|
|
50
|
+
await markDiagramFailed(supabase, diagramId, cloneResult.message);
|
|
43
51
|
return { status: 'error', message: cloneResult.message };
|
|
44
52
|
}
|
|
45
53
|
const { projectDir, cleanupDir, repos } = cloneResult;
|
|
46
54
|
let succeeded = false;
|
|
47
55
|
try {
|
|
48
|
-
const product =
|
|
56
|
+
const product = repoOnly
|
|
57
|
+
? await resolveRepoBasics(repoId, repos)
|
|
58
|
+
: await fetchProductBasics(productId);
|
|
49
59
|
const systemPrompt = await createDataFlowSystemPrompt({
|
|
50
60
|
projectDir,
|
|
51
61
|
hasCodebase: true,
|
|
@@ -90,12 +100,12 @@ export async function runDataFlowPhase(options) {
|
|
|
90
100
|
}
|
|
91
101
|
if (!extraction) {
|
|
92
102
|
const msg = 'Data flow extraction failed: agent did not call submit_data_flow and no parseable data_flow block was found in the response';
|
|
93
|
-
await
|
|
103
|
+
await markDiagramFailed(supabase, diagramId, msg);
|
|
94
104
|
return { status: 'error', message: msg };
|
|
95
105
|
}
|
|
96
106
|
logInfo(`Extraction produced ${extraction.nodes.length} nodes / ${extraction.edges.length} connections`);
|
|
97
|
-
const { nodesCreated, edgesCreated } = await
|
|
98
|
-
await
|
|
107
|
+
const { nodesCreated, edgesCreated } = await persistDiagram(supabase, diagramId, extraction);
|
|
108
|
+
await markDiagramSuccess(supabase, diagramId, extraction.summary);
|
|
99
109
|
succeeded = true;
|
|
100
110
|
logSuccess(`Data flow generated: ${nodesCreated} nodes, ${edgesCreated} connections`);
|
|
101
111
|
return {
|
|
@@ -109,7 +119,7 @@ export async function runDataFlowPhase(options) {
|
|
|
109
119
|
catch (error) {
|
|
110
120
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
111
121
|
logError(`Data flow failed: ${errorMessage}`);
|
|
112
|
-
await
|
|
122
|
+
await markDiagramFailed(supabase, diagramId, errorMessage);
|
|
113
123
|
return { status: 'error', message: errorMessage };
|
|
114
124
|
}
|
|
115
125
|
finally {
|
|
@@ -118,12 +128,23 @@ export async function runDataFlowPhase(options) {
|
|
|
118
128
|
}
|
|
119
129
|
}
|
|
120
130
|
}
|
|
121
|
-
/**
|
|
122
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Build "product basics" for repo-only mode from the repositories row,
|
|
133
|
+
* falling back to the cloned repo's full name when the row has no name.
|
|
134
|
+
*/
|
|
135
|
+
async function resolveRepoBasics(repositoryId, repos) {
|
|
136
|
+
const basics = await getRepositoryBasics(repositoryId).catch(() => null);
|
|
137
|
+
return {
|
|
138
|
+
name: basics?.fullName ?? repos[0]?.fullName ?? repositoryId,
|
|
139
|
+
description: basics?.description ?? undefined,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/** Read the ordered repo set a diagram was scoped to (may be empty). */
|
|
143
|
+
async function getDiagramRepositoryIds(supabase, diagramId) {
|
|
123
144
|
const { data } = await supabase
|
|
124
|
-
.from('
|
|
145
|
+
.from('diagrams')
|
|
125
146
|
.select('repository_ids')
|
|
126
|
-
.eq('id',
|
|
147
|
+
.eq('id', diagramId)
|
|
127
148
|
.single();
|
|
128
149
|
return (data?.repository_ids ?? []).filter(Boolean);
|
|
129
150
|
}
|
|
@@ -176,45 +197,45 @@ function tryFallbackParse(resultMessage, assistantText) {
|
|
|
176
197
|
// ============================================================================
|
|
177
198
|
// Persistence
|
|
178
199
|
// ============================================================================
|
|
179
|
-
async function
|
|
200
|
+
async function markDiagramRunning(supabase, diagramId) {
|
|
180
201
|
const { error } = await supabase
|
|
181
|
-
.from('
|
|
202
|
+
.from('diagrams')
|
|
182
203
|
.update({ status: 'running', error: null })
|
|
183
|
-
.eq('id',
|
|
204
|
+
.eq('id', diagramId);
|
|
184
205
|
if (error) {
|
|
185
|
-
logWarning(`Could not mark
|
|
206
|
+
logWarning(`Could not mark diagram as running: ${error.message}`);
|
|
186
207
|
}
|
|
187
208
|
}
|
|
188
|
-
async function
|
|
209
|
+
async function markDiagramFailed(supabase, diagramId, errorMessage) {
|
|
189
210
|
await supabase
|
|
190
|
-
.from('
|
|
211
|
+
.from('diagrams')
|
|
191
212
|
.update({
|
|
192
213
|
status: 'failed',
|
|
193
214
|
error: errorMessage,
|
|
194
215
|
completed_at: new Date().toISOString(),
|
|
195
216
|
})
|
|
196
|
-
.eq('id',
|
|
217
|
+
.eq('id', diagramId);
|
|
197
218
|
}
|
|
198
|
-
async function
|
|
219
|
+
async function markDiagramSuccess(supabase, diagramId, summary) {
|
|
199
220
|
await supabase
|
|
200
|
-
.from('
|
|
221
|
+
.from('diagrams')
|
|
201
222
|
.update({
|
|
202
223
|
status: 'success',
|
|
203
224
|
summary,
|
|
204
225
|
error: null,
|
|
205
226
|
completed_at: new Date().toISOString(),
|
|
206
227
|
})
|
|
207
|
-
.eq('id',
|
|
228
|
+
.eq('id', diagramId);
|
|
208
229
|
}
|
|
209
|
-
async function
|
|
210
|
-
await supabase.from('
|
|
211
|
-
await supabase.from('
|
|
230
|
+
async function persistDiagram(supabase, diagramId, extraction) {
|
|
231
|
+
await supabase.from('diagram_edges').delete().eq('diagram_id', diagramId);
|
|
232
|
+
await supabase.from('diagram_nodes').delete().eq('diagram_id', diagramId);
|
|
212
233
|
if (extraction.nodes.length === 0) {
|
|
213
234
|
return { nodesCreated: 0, edgesCreated: 0 };
|
|
214
235
|
}
|
|
215
|
-
const nodeRows = extraction.nodes.map((n, i) => buildNodeRow(
|
|
236
|
+
const nodeRows = extraction.nodes.map((n, i) => buildNodeRow(diagramId, n, i));
|
|
216
237
|
const { data: insertedNodes, error: nodesError } = await supabase
|
|
217
|
-
.from('
|
|
238
|
+
.from('diagram_nodes')
|
|
218
239
|
.insert(nodeRows)
|
|
219
240
|
.select('id, slug');
|
|
220
241
|
if (nodesError) {
|
|
@@ -222,11 +243,11 @@ async function persistFlow(supabase, flowId, extraction) {
|
|
|
222
243
|
}
|
|
223
244
|
const slugToId = new Map((insertedNodes ?? []).map((n) => [n.slug, n.id]));
|
|
224
245
|
const edgeRows = extraction.edges
|
|
225
|
-
.map((e) => buildEdgeRow(
|
|
246
|
+
.map((e) => buildEdgeRow(diagramId, e, slugToId))
|
|
226
247
|
.filter((e) => e !== null);
|
|
227
248
|
if (edgeRows.length > 0) {
|
|
228
249
|
const { error: edgesError } = await supabase
|
|
229
|
-
.from('
|
|
250
|
+
.from('diagram_edges')
|
|
230
251
|
.insert(edgeRows);
|
|
231
252
|
if (edgesError) {
|
|
232
253
|
throw new Error(`Failed to insert edges: ${edgesError.message}`);
|
|
@@ -237,9 +258,9 @@ async function persistFlow(supabase, flowId, extraction) {
|
|
|
237
258
|
edgesCreated: edgeRows.length,
|
|
238
259
|
};
|
|
239
260
|
}
|
|
240
|
-
function buildNodeRow(
|
|
261
|
+
function buildNodeRow(diagramId, node, index) {
|
|
241
262
|
return {
|
|
242
|
-
|
|
263
|
+
diagram_id: diagramId,
|
|
243
264
|
slug: node.slug,
|
|
244
265
|
name: node.name,
|
|
245
266
|
kind: node.kind,
|
|
@@ -248,14 +269,14 @@ function buildNodeRow(flowId, node, index) {
|
|
|
248
269
|
position_y: Math.floor(index / COLUMNS) * ROW_HEIGHT,
|
|
249
270
|
};
|
|
250
271
|
}
|
|
251
|
-
function buildEdgeRow(
|
|
272
|
+
function buildEdgeRow(diagramId, edge, slugToId) {
|
|
252
273
|
const fromId = slugToId.get(edge.fromSlug);
|
|
253
274
|
const toId = slugToId.get(edge.toSlug);
|
|
254
275
|
if (!fromId || !toId) {
|
|
255
276
|
return null;
|
|
256
277
|
}
|
|
257
278
|
return {
|
|
258
|
-
|
|
279
|
+
diagram_id: diagramId,
|
|
259
280
|
from_node_id: fromId,
|
|
260
281
|
to_node_id: toId,
|
|
261
282
|
label: edge.label ?? null,
|
|
@@ -27,8 +27,8 @@ export declare function createSubmitDataFlowTool(state: DataFlowCaptureState): i
|
|
|
27
27
|
kind: z.ZodEnum<{
|
|
28
28
|
model: "model";
|
|
29
29
|
source: "source";
|
|
30
|
-
dataset: "dataset";
|
|
31
30
|
transform: "transform";
|
|
31
|
+
dataset: "dataset";
|
|
32
32
|
sink: "sink";
|
|
33
33
|
queue: "queue";
|
|
34
34
|
}>;
|
|
@@ -53,7 +53,7 @@ export function validateConsistency(extraction) {
|
|
|
53
53
|
for (const node of extraction.nodes) {
|
|
54
54
|
if (slugs.has(node.slug)) {
|
|
55
55
|
return {
|
|
56
|
-
error: `Duplicate node slug "${node.slug}". Each node.slug MUST be unique within the
|
|
56
|
+
error: `Duplicate node slug "${node.slug}". Each node.slug MUST be unique within the diagram. Re-call submit_data_flow with deduplicated nodes.`,
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
slugs.add(node.slug);
|
|
@@ -87,7 +87,7 @@ export function createSubmitDataFlowTool(state) {
|
|
|
87
87
|
.describe('1-3 sentence narrative of what this system does with data and the primary pipelines.'),
|
|
88
88
|
nodes: z
|
|
89
89
|
.array(dataNodeSchema)
|
|
90
|
-
.describe('Every data node: source / dataset / transform / sink / queue / model. node.slug MUST be unique within the
|
|
90
|
+
.describe('Every data node: source / dataset / transform / sink / queue / model. node.slug MUST be unique within the diagram.'),
|
|
91
91
|
edges: z
|
|
92
92
|
.array(dataEdgeSchema)
|
|
93
93
|
.describe('Connections. fromSlug = upstream, toSlug = downstream. Every fromSlug / toSlug MUST reference a slug present in nodes; drop edges whose endpoints you did not emit.'),
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* unified <DataNodePreview> component.
|
|
9
9
|
*
|
|
10
10
|
* Companion to ScreenSchema: same flow-graph shape (nodes + edges with a
|
|
11
|
-
* shared `
|
|
11
|
+
* shared `diagrams` table storing the JSONB schema), different domain. Data
|
|
12
12
|
* flow edges describe how data moves between nodes, not user navigation.
|
|
13
13
|
*/
|
|
14
14
|
export type DataNodeKind = 'source' | 'dataset' | 'transform' | 'sink' | 'queue' | 'model';
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* unified <DataNodePreview> component.
|
|
9
9
|
*
|
|
10
10
|
* Companion to ScreenSchema: same flow-graph shape (nodes + edges with a
|
|
11
|
-
* shared `
|
|
11
|
+
* shared `diagrams` table storing the JSONB schema), different domain. Data
|
|
12
12
|
* flow edges describe how data moves between nodes, not user navigation.
|
|
13
13
|
*/
|
|
14
14
|
// ============================================================================
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared multi-repo cloning for flow generation (screen-flow / data-flow).
|
|
3
|
+
*
|
|
4
|
+
* A flow can be scoped to one or several repositories (the user picks them in
|
|
5
|
+
* the desktop UI; the chosen set is stored on `diagrams.repository_ids`). This
|
|
6
|
+
* helper resolves that set, clones each repo into a per-flow parent workspace
|
|
7
|
+
* directory, and returns the directory the agent should run against:
|
|
8
|
+
* - single repo → the repo's own clone dir
|
|
9
|
+
* - many repos → the parent dir holding every clone as a subdirectory,
|
|
10
|
+
* so the agent can explore them all and produce one unified
|
|
11
|
+
* flow.
|
|
12
|
+
*
|
|
13
|
+
* Falls back to the product's primary repo when `repository_ids` is empty
|
|
14
|
+
* (older diagrams, or single-repo products).
|
|
15
|
+
*/
|
|
16
|
+
export interface ClonedRepo {
|
|
17
|
+
fullName: string;
|
|
18
|
+
owner: string;
|
|
19
|
+
repo: string;
|
|
20
|
+
dir: string;
|
|
21
|
+
}
|
|
22
|
+
export interface CloneFlowReposSuccess {
|
|
23
|
+
ok: true;
|
|
24
|
+
/** Directory to point the agent at (parent dir for multi-repo). */
|
|
25
|
+
projectDir: string;
|
|
26
|
+
/** Directory to clean up afterwards (always the per-flow parent). */
|
|
27
|
+
cleanupDir: string;
|
|
28
|
+
repos: ClonedRepo[];
|
|
29
|
+
}
|
|
30
|
+
export interface CloneFlowReposFailure {
|
|
31
|
+
ok: false;
|
|
32
|
+
message: string;
|
|
33
|
+
}
|
|
34
|
+
export declare function safeDirName(fullName: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the repositories a flow targets (by id, preserving the stored
|
|
37
|
+
* order), falling back to the product's primary repo.
|
|
38
|
+
*
|
|
39
|
+
* In repo-only mode there is no product, so no `fallback` is provided: the
|
|
40
|
+
* set is resolved purely from `repositoryIds`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveTargetRepos(productId: string | undefined, repositoryIds: string[], fallback?: {
|
|
43
|
+
owner: string;
|
|
44
|
+
repo: string;
|
|
45
|
+
}): Promise<{
|
|
46
|
+
fullName: string;
|
|
47
|
+
owner: string;
|
|
48
|
+
repo: string;
|
|
49
|
+
}[]>;
|
|
50
|
+
export declare function cloneDiagramRepos(opts: {
|
|
51
|
+
/** Product-scoped flow. Mutually exclusive with `repoId`. */
|
|
52
|
+
productId?: string;
|
|
53
|
+
/** Repo-only flow: a single repositories row, no product context. */
|
|
54
|
+
repoId?: string;
|
|
55
|
+
repositoryIds: string[];
|
|
56
|
+
workspaceKey: string;
|
|
57
|
+
verbose?: boolean;
|
|
58
|
+
}): Promise<CloneFlowReposSuccess | CloneFlowReposFailure>;
|
|
59
|
+
/**
|
|
60
|
+
* Build a short note describing the repo scope, appended to the agent's user
|
|
61
|
+
* prompt so it knows whether to map one repo or unify several.
|
|
62
|
+
*/
|
|
63
|
+
export declare function describeRepoScope(repos: ClonedRepo[]): string;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared multi-repo cloning for flow generation (screen-flow / data-flow).
|
|
3
|
+
*
|
|
4
|
+
* A flow can be scoped to one or several repositories (the user picks them in
|
|
5
|
+
* the desktop UI; the chosen set is stored on `diagrams.repository_ids`). This
|
|
6
|
+
* helper resolves that set, clones each repo into a per-flow parent workspace
|
|
7
|
+
* directory, and returns the directory the agent should run against:
|
|
8
|
+
* - single repo → the repo's own clone dir
|
|
9
|
+
* - many repos → the parent dir holding every clone as a subdirectory,
|
|
10
|
+
* so the agent can explore them all and produce one unified
|
|
11
|
+
* flow.
|
|
12
|
+
*
|
|
13
|
+
* Falls back to the product's primary repo when `repository_ids` is empty
|
|
14
|
+
* (older diagrams, or single-repo products).
|
|
15
|
+
*/
|
|
16
|
+
import { getGitHubConfigByProduct, getGitHubConfigByRepository, } from '../../api/github.js';
|
|
17
|
+
import { getSupabase } from '../../supabase/client.js';
|
|
18
|
+
import { logInfo, logWarning } from '../../utils/logger.js';
|
|
19
|
+
import { cloneIssueRepo, ensureWorkspaceDir, getIssueRepoPath, } from '../../workspace/workspace-manager.js';
|
|
20
|
+
export function safeDirName(fullName) {
|
|
21
|
+
return fullName.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the repositories a flow targets (by id, preserving the stored
|
|
25
|
+
* order), falling back to the product's primary repo.
|
|
26
|
+
*
|
|
27
|
+
* In repo-only mode there is no product, so no `fallback` is provided: the
|
|
28
|
+
* set is resolved purely from `repositoryIds`.
|
|
29
|
+
*/
|
|
30
|
+
export async function resolveTargetRepos(productId, repositoryIds, fallback) {
|
|
31
|
+
if (repositoryIds.length === 0) {
|
|
32
|
+
if (fallback) {
|
|
33
|
+
return [
|
|
34
|
+
{
|
|
35
|
+
fullName: `${fallback.owner}/${fallback.repo}`,
|
|
36
|
+
owner: fallback.owner,
|
|
37
|
+
repo: fallback.repo,
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
const supabase = getSupabase();
|
|
44
|
+
const { data } = await supabase
|
|
45
|
+
.from('repositories')
|
|
46
|
+
.select('id, full_name')
|
|
47
|
+
.in('id', repositoryIds);
|
|
48
|
+
const byId = new Map((data ?? []).map((r) => [r.id, r.full_name]));
|
|
49
|
+
// Preserve the caller's order (diagrams.repository_ids is ordered).
|
|
50
|
+
const resolved = [];
|
|
51
|
+
for (const id of repositoryIds) {
|
|
52
|
+
const fullName = byId.get(id);
|
|
53
|
+
if (!fullName) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const [owner, repo] = fullName.split('/');
|
|
57
|
+
if (!owner || !repo) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
resolved.push({ fullName, owner, repo });
|
|
61
|
+
}
|
|
62
|
+
// If none resolved (deleted repos / RLS), fall back to the primary repo so
|
|
63
|
+
// generation still produces something useful (product mode only).
|
|
64
|
+
if (resolved.length === 0 && fallback) {
|
|
65
|
+
return [
|
|
66
|
+
{
|
|
67
|
+
fullName: `${fallback.owner}/${fallback.repo}`,
|
|
68
|
+
owner: fallback.owner,
|
|
69
|
+
repo: fallback.repo,
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
return resolved;
|
|
74
|
+
}
|
|
75
|
+
export async function cloneDiagramRepos(opts) {
|
|
76
|
+
const { productId, repoId, repositoryIds, workspaceKey, verbose } = opts;
|
|
77
|
+
const repoOnly = !productId && Boolean(repoId);
|
|
78
|
+
// Resolve the auth token. Product mode reuses the product's installation /
|
|
79
|
+
// PAT for every repo; repo-only mode resolves it from the first (only) repo.
|
|
80
|
+
const gh = repoOnly
|
|
81
|
+
? await getGitHubConfigByRepository(repositoryIds[0] ?? repoId, verbose)
|
|
82
|
+
: await getGitHubConfigByProduct(productId, verbose);
|
|
83
|
+
if (!gh.configured || !gh.token || !gh.owner || !gh.repo) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
message: gh.message ||
|
|
87
|
+
(repoOnly
|
|
88
|
+
? 'GitHub repository not configured. Connect the repo first.'
|
|
89
|
+
: 'GitHub repository not configured for this product. Connect a repo first.'),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// In repo-only mode there is no product primary-repo fallback; targets come
|
|
93
|
+
// purely from repositoryIds.
|
|
94
|
+
const targets = await resolveTargetRepos(productId, repositoryIds, repoOnly ? undefined : { owner: gh.owner, repo: gh.repo });
|
|
95
|
+
if (targets.length === 0) {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
message: 'No repositories resolved for this flow.',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const workspaceRoot = ensureWorkspaceDir();
|
|
102
|
+
const parentDir = getIssueRepoPath(workspaceRoot, `${workspaceKey}-${repoOnly ? `repo-${repoId}` : productId}`);
|
|
103
|
+
const repos = [];
|
|
104
|
+
for (const target of targets) {
|
|
105
|
+
try {
|
|
106
|
+
// The product-level token (installation or user PAT/OAuth) is reused for
|
|
107
|
+
// every repo; if it can't access one, that clone fails and we skip it
|
|
108
|
+
// rather than aborting the whole generation.
|
|
109
|
+
const { repoPath } = cloneIssueRepo(parentDir, safeDirName(target.fullName), target.owner, target.repo, gh.token);
|
|
110
|
+
repos.push({
|
|
111
|
+
fullName: target.fullName,
|
|
112
|
+
owner: target.owner,
|
|
113
|
+
repo: target.repo,
|
|
114
|
+
dir: repoPath,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
logWarning(`Skipping ${target.fullName}: clone failed (${err instanceof Error ? err.message : String(err)})`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (repos.length === 0) {
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
message: 'Failed to clone any of the selected repositories.',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (repos.length > 1) {
|
|
128
|
+
logInfo(`Cloned ${repos.length} repos for ${workspaceKey}: ${repos.map((r) => r.fullName).join(', ')}`);
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
ok: true,
|
|
132
|
+
// Single repo: run directly in its dir. Multi-repo: run in the parent so
|
|
133
|
+
// the agent sees every clone as a subdirectory.
|
|
134
|
+
projectDir: repos.length === 1 ? repos[0].dir : parentDir,
|
|
135
|
+
cleanupDir: parentDir,
|
|
136
|
+
repos,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Build a short note describing the repo scope, appended to the agent's user
|
|
141
|
+
* prompt so it knows whether to map one repo or unify several.
|
|
142
|
+
*/
|
|
143
|
+
export function describeRepoScope(repos) {
|
|
144
|
+
if (repos.length <= 1) {
|
|
145
|
+
return '';
|
|
146
|
+
}
|
|
147
|
+
const list = repos.map((r) => `- ${r.fullName} (subdirectory: ${safeDirName(r.fullName)})`);
|
|
148
|
+
return [
|
|
149
|
+
`This product spans ${repos.length} repositories, each cloned into its own subdirectory of the working directory:`,
|
|
150
|
+
...list,
|
|
151
|
+
'Explore all of them and produce a single unified flow that spans the repositories.',
|
|
152
|
+
].join('\n');
|
|
153
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared diagram generation runner.
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates the common pipeline every node/edge diagram phase uses: clone
|
|
5
|
+
* the repo(s), resolve product/repo basics, build prompts, run the Claude
|
|
6
|
+
* Agent SDK loop with an in-process MCP capture server, fall back to a fenced
|
|
7
|
+
* block if the agent forgot the tool, then persist to the diagrams tables and
|
|
8
|
+
* flip status. Each lean phase just supplies its domain MCP config + prompts.
|
|
9
|
+
*/
|
|
10
|
+
import { type DiagramMcpConfig } from './mcp.js';
|
|
11
|
+
export interface GenerateDiagramOptions {
|
|
12
|
+
productId?: string;
|
|
13
|
+
repoId?: string;
|
|
14
|
+
diagramId: string;
|
|
15
|
+
guidance?: string;
|
|
16
|
+
verbose?: boolean;
|
|
17
|
+
/** Workspace clone key, e.g. 'state-diagram'. */
|
|
18
|
+
workspaceKey: string;
|
|
19
|
+
/** Fenced block name for the fallback parser, e.g. 'state_diagram'. */
|
|
20
|
+
fenceName: string;
|
|
21
|
+
/** Plural noun for log/result messages, e.g. 'states', 'classes'. */
|
|
22
|
+
nounPlural: string;
|
|
23
|
+
edgeNounPlural: string;
|
|
24
|
+
mcpConfig: DiagramMcpConfig;
|
|
25
|
+
buildSystemPrompt: (args: {
|
|
26
|
+
projectDir: string;
|
|
27
|
+
hasCodebase: boolean;
|
|
28
|
+
}) => Promise<string>;
|
|
29
|
+
buildUserPrompt: (args: {
|
|
30
|
+
productName: string;
|
|
31
|
+
productDescription?: string;
|
|
32
|
+
guidance?: string;
|
|
33
|
+
}) => string;
|
|
34
|
+
}
|
|
35
|
+
export interface DiagramPhaseResult {
|
|
36
|
+
status: 'success' | 'error';
|
|
37
|
+
message: string;
|
|
38
|
+
nodesCreated?: number;
|
|
39
|
+
edgesCreated?: number;
|
|
40
|
+
summary?: string;
|
|
41
|
+
}
|
|
42
|
+
export declare function generateDiagram(options: GenerateDiagramOptions): Promise<DiagramPhaseResult>;
|