@h-rig/dependency-graph-plugin 0.0.6-alpha.135 → 0.0.6-alpha.138
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/src/index.js +266 -14
- package/dist/src/plugin.d.ts +42 -3
- package/dist/src/plugin.js +266 -14
- package/package.json +5 -4
package/dist/src/index.js
CHANGED
|
@@ -1,28 +1,280 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// packages/dependency-graph-plugin/src/plugin.ts
|
|
3
3
|
import { definePlugin } from "@rig/core";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
classifyWorkspaceBlockers,
|
|
6
|
+
formatDependencyGraphDot,
|
|
7
|
+
getWorkspaceDependencyGraph,
|
|
8
|
+
getWorkspaceRollups,
|
|
9
|
+
listRuns,
|
|
10
|
+
listTasks
|
|
11
|
+
} from "@rig/client";
|
|
5
12
|
var DEPENDENCY_GRAPH_PLUGIN_NAME = "@rig/dependency-graph-plugin";
|
|
6
|
-
|
|
7
|
-
|
|
13
|
+
var DEPENDENCY_GRAPH_CLI_ID = "dependency-graph.graph";
|
|
14
|
+
var WORKSPACE_STATUS_CLI_ID = "dependency-graph.status";
|
|
15
|
+
var WORKSPACE_SUMMARY_CLI_ID = "dependency-graph.summary";
|
|
16
|
+
var DEPENDENCY_GRAPH_PANEL_ID = "dependency-graph";
|
|
17
|
+
var EPICS_PANEL_ID = "epics";
|
|
18
|
+
var PEOPLE_PANEL_ID = "people";
|
|
19
|
+
function isRecord(value) {
|
|
20
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
21
|
}
|
|
22
|
+
function panelProjectRoot(context) {
|
|
23
|
+
return isRecord(context) && typeof context.projectRoot === "string" && context.projectRoot.length > 0 ? context.projectRoot : null;
|
|
24
|
+
}
|
|
25
|
+
function toDependencyGraphPanelPayload(model) {
|
|
26
|
+
const layoutByTaskId = new Map(model.layout.nodes.map((node) => [node.taskId, node]));
|
|
27
|
+
return {
|
|
28
|
+
nodes: model.nodes.map((node) => {
|
|
29
|
+
const layout = layoutByTaskId.get(node.taskId);
|
|
30
|
+
return {
|
|
31
|
+
id: node.taskId,
|
|
32
|
+
title: node.title,
|
|
33
|
+
status: node.status,
|
|
34
|
+
epicKey: node.epicKey,
|
|
35
|
+
assignee: node.assignee,
|
|
36
|
+
blockerClass: node.blockerClass,
|
|
37
|
+
blockingDepth: node.blockingDepth,
|
|
38
|
+
layout: layout ? { x: layout.x, y: layout.y, width: layout.width, height: layout.height } : undefined
|
|
39
|
+
};
|
|
40
|
+
}),
|
|
41
|
+
edges: model.edges.map((edge) => ({
|
|
42
|
+
id: `${edge.fromTaskId}:${edge.toTaskId}:${edge.type}`,
|
|
43
|
+
from: edge.fromTaskId,
|
|
44
|
+
to: edge.toTaskId,
|
|
45
|
+
type: edge.type
|
|
46
|
+
})),
|
|
47
|
+
cycles: model.cycles,
|
|
48
|
+
unresolvedRefs: model.unresolvedRefs,
|
|
49
|
+
degraded: model.degraded ? "dependency graph projection degraded" : null
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function toWorkspaceRollupsPanelPayload(rollups) {
|
|
53
|
+
return {
|
|
54
|
+
epics: rollups.epics.map((epic) => ({
|
|
55
|
+
epicKey: epic.epicKey,
|
|
56
|
+
title: epic.epicKey,
|
|
57
|
+
total: epic.total,
|
|
58
|
+
percentComplete: epic.percentComplete,
|
|
59
|
+
blockedCount: epic.blockedCount,
|
|
60
|
+
inFlightCount: epic.inFlightCount,
|
|
61
|
+
humanBlockedCount: epic.humanBlockedCount
|
|
62
|
+
})),
|
|
63
|
+
people: rollups.assignees.map((assignee) => ({
|
|
64
|
+
assignee: assignee.assignee,
|
|
65
|
+
openCount: assignee.openTaskCount,
|
|
66
|
+
inFlightCount: assignee.inFlightRunCount,
|
|
67
|
+
prReviewCount: assignee.prsAwaitingReview,
|
|
68
|
+
humanBlockedCount: assignee.blockers.length,
|
|
69
|
+
activeRunIds: []
|
|
70
|
+
}))
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async function produceDependencyGraphPanel(context) {
|
|
74
|
+
const projectRoot = panelProjectRoot(context);
|
|
75
|
+
if (!projectRoot)
|
|
76
|
+
return;
|
|
77
|
+
const model = await getWorkspaceDependencyGraph(projectRoot, {
|
|
78
|
+
listTasks,
|
|
79
|
+
listRuns,
|
|
80
|
+
classifyBlockers: (root) => classifyWorkspaceBlockers(root, { listTasks, listRuns })
|
|
81
|
+
});
|
|
82
|
+
return toDependencyGraphPanelPayload(model);
|
|
83
|
+
}
|
|
84
|
+
async function produceWorkspaceRollupsPanel(context) {
|
|
85
|
+
const projectRoot = panelProjectRoot(context);
|
|
86
|
+
if (!projectRoot)
|
|
87
|
+
return;
|
|
88
|
+
const rollups = await getWorkspaceRollups(projectRoot, {
|
|
89
|
+
listTasks,
|
|
90
|
+
listRuns,
|
|
91
|
+
classifyBlockers: (root) => classifyWorkspaceBlockers(root, { listTasks, listRuns })
|
|
92
|
+
});
|
|
93
|
+
return toWorkspaceRollupsPanelPayload(rollups);
|
|
94
|
+
}
|
|
95
|
+
function printJson(value) {
|
|
96
|
+
console.log(JSON.stringify(value, null, 2));
|
|
97
|
+
}
|
|
98
|
+
function takeFlag(args, flag) {
|
|
99
|
+
const rest = [...args];
|
|
100
|
+
const index = rest.indexOf(flag);
|
|
101
|
+
if (index < 0)
|
|
102
|
+
return { value: false, rest };
|
|
103
|
+
rest.splice(index, 1);
|
|
104
|
+
return { value: true, rest };
|
|
105
|
+
}
|
|
106
|
+
function takeOption(args, flag) {
|
|
107
|
+
const rest = [...args];
|
|
108
|
+
const index = rest.indexOf(flag);
|
|
109
|
+
if (index < 0)
|
|
110
|
+
return { rest };
|
|
111
|
+
const value = rest[index + 1];
|
|
112
|
+
if (!value || value.startsWith("-"))
|
|
113
|
+
throw new Error(`${flag} requires a value.`);
|
|
114
|
+
rest.splice(index, 2);
|
|
115
|
+
return { value, rest };
|
|
116
|
+
}
|
|
117
|
+
function requireNoExtraArgs(args, usage) {
|
|
118
|
+
if (args.length > 0)
|
|
119
|
+
throw new Error(`Unexpected argument: ${args[0]}
|
|
120
|
+
Usage: ${usage}`);
|
|
121
|
+
}
|
|
122
|
+
function graphSummary(model) {
|
|
123
|
+
const blocked = model.nodes.filter((node) => node.blockedBy.length > 0 || node.blockerClass === "task-blocked").length;
|
|
124
|
+
return [
|
|
125
|
+
`graph ${model.graphId}`,
|
|
126
|
+
`tasks: ${model.nodes.length}`,
|
|
127
|
+
`edges: ${model.edges.length}`,
|
|
128
|
+
`blocked tasks: ${blocked}`,
|
|
129
|
+
`human blockers: ${model.nodes.filter((node) => node.blockerClass && node.blockerClass !== "not-blocked" && node.blockerClass !== "task-blocked").length}`,
|
|
130
|
+
`cycles: ${model.cycles.length}`,
|
|
131
|
+
`unresolved refs: ${model.unresolvedRefs.length}`
|
|
132
|
+
].join(`
|
|
133
|
+
`);
|
|
134
|
+
}
|
|
135
|
+
async function executeGraph(context, args) {
|
|
136
|
+
const dot = takeFlag(args, "--dot");
|
|
137
|
+
const json = takeFlag(dot.rest, "--json");
|
|
138
|
+
requireNoExtraArgs(json.rest, "rig graph [--json|--dot]");
|
|
139
|
+
if (dot.value && json.value)
|
|
140
|
+
throw new Error("Pass only one of --json or --dot.");
|
|
141
|
+
const model = await getWorkspaceDependencyGraph(context.projectRoot, {
|
|
142
|
+
listTasks,
|
|
143
|
+
listRuns,
|
|
144
|
+
classifyBlockers: (projectRoot) => classifyWorkspaceBlockers(projectRoot, { listTasks, listRuns })
|
|
145
|
+
});
|
|
146
|
+
const dotText = formatDependencyGraphDot(model);
|
|
147
|
+
if (context.outputMode === "text") {
|
|
148
|
+
if (dot.value)
|
|
149
|
+
console.log(dotText);
|
|
150
|
+
else if (json.value)
|
|
151
|
+
printJson(model);
|
|
152
|
+
else
|
|
153
|
+
console.log(graphSummary(model));
|
|
154
|
+
}
|
|
155
|
+
return { ok: true, group: "graph", command: dot.value ? "dot" : "show", details: { graph: model, dot: dotText } };
|
|
156
|
+
}
|
|
157
|
+
async function executeStatus(context, args) {
|
|
158
|
+
const json = takeFlag(args, "--json");
|
|
159
|
+
const epic = takeOption(json.rest, "--epic");
|
|
160
|
+
requireNoExtraArgs(epic.rest, "rig status [--epic <key>] [--json]");
|
|
161
|
+
const [rollups, blockers, runs] = await Promise.all([
|
|
162
|
+
getWorkspaceRollups(context.projectRoot, { listTasks, listRuns }),
|
|
163
|
+
classifyWorkspaceBlockers(context.projectRoot, { listTasks, listRuns }),
|
|
164
|
+
listRuns(context.projectRoot)
|
|
165
|
+
]);
|
|
166
|
+
const filteredEpics = epic.value ? rollups.epics.filter((entry) => entry.epicKey === epic.value) : rollups.epics;
|
|
167
|
+
const details = {
|
|
168
|
+
epic: epic.value ?? null,
|
|
169
|
+
epicRollups: filteredEpics,
|
|
170
|
+
epics: filteredEpics.length,
|
|
171
|
+
assignees: rollups.assignees.length,
|
|
172
|
+
humanBlockers: blockers.human.length,
|
|
173
|
+
machineBlockers: blockers.machine.length,
|
|
174
|
+
runs: runs.length,
|
|
175
|
+
activeRuns: runs.filter((run) => run.live && !run.stale).length,
|
|
176
|
+
needsAttention: runs.filter((run) => run.status === "needs-attention" || run.pendingApprovals > 0 || run.pendingInputs > 0).length,
|
|
177
|
+
generatedAt: rollups.generatedAt
|
|
178
|
+
};
|
|
179
|
+
if (context.outputMode === "text") {
|
|
180
|
+
if (json.value)
|
|
181
|
+
printJson(details);
|
|
182
|
+
else
|
|
183
|
+
console.log([`epics: ${details.epics}`, `assignees: ${details.assignees}`, `blockers: ${details.humanBlockers} human, ${details.machineBlockers} machine`, `runs: ${details.runs} total, ${details.activeRuns} active, ${details.needsAttention} need attention`].join(`
|
|
184
|
+
`));
|
|
185
|
+
}
|
|
186
|
+
return { ok: true, group: "status", command: "show", details };
|
|
187
|
+
}
|
|
188
|
+
async function executeSummary(context, args) {
|
|
189
|
+
const json = takeFlag(args, "--json");
|
|
190
|
+
const epicOpt = takeOption(json.rest, "--epic");
|
|
191
|
+
requireNoExtraArgs(epicOpt.rest, "rig summary [--epic <key>] [--json]");
|
|
192
|
+
const rollups = await getWorkspaceRollups(context.projectRoot, { listTasks, listRuns });
|
|
193
|
+
const details = epicOpt.value ? { ...rollups, epics: rollups.epics.filter((epic) => epic.epicKey === epicOpt.value) } : rollups;
|
|
194
|
+
if (context.outputMode === "text") {
|
|
195
|
+
if (json.value)
|
|
196
|
+
printJson(details);
|
|
197
|
+
else
|
|
198
|
+
console.log([
|
|
199
|
+
...details.epics.map((epic) => `epic ${epic.epicKey}: ${epic.percentComplete}% complete, ${epic.blockedCount} blocked`),
|
|
200
|
+
...details.assignees.map((assignee) => `${assignee.assignee}: ${assignee.openTaskCount} open, ${assignee.inFlightRunCount} in flight`)
|
|
201
|
+
].join(`
|
|
202
|
+
`) || "No rollups.");
|
|
203
|
+
}
|
|
204
|
+
return { ok: true, group: "summary", command: "show", details };
|
|
205
|
+
}
|
|
206
|
+
var dependencyGraphCliCommands = [
|
|
207
|
+
{
|
|
208
|
+
id: DEPENDENCY_GRAPH_CLI_ID,
|
|
209
|
+
family: "graph",
|
|
210
|
+
command: "rig graph [--json|--dot]",
|
|
211
|
+
description: "Render task dependency graph from the active task source.",
|
|
212
|
+
usage: "rig graph [--json|--dot]",
|
|
213
|
+
projectRequired: true,
|
|
214
|
+
run: executeGraph
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: WORKSPACE_STATUS_CLI_ID,
|
|
218
|
+
family: "status",
|
|
219
|
+
command: "rig status [--epic <key>] [--json]",
|
|
220
|
+
description: "Summarize workspace epics, blockers, and active runs.",
|
|
221
|
+
usage: "rig status [--epic <key>] [--json]",
|
|
222
|
+
projectRequired: true,
|
|
223
|
+
run: executeStatus
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: WORKSPACE_SUMMARY_CLI_ID,
|
|
227
|
+
family: "summary",
|
|
228
|
+
command: "rig summary [--epic <key>] [--json]",
|
|
229
|
+
description: "Print workspace rollups by epic and assignee.",
|
|
230
|
+
usage: "rig summary [--epic <key>] [--json]",
|
|
231
|
+
projectRequired: true,
|
|
232
|
+
run: executeSummary
|
|
233
|
+
}
|
|
234
|
+
];
|
|
9
235
|
var dependencyGraphPlugin = definePlugin({
|
|
10
236
|
name: DEPENDENCY_GRAPH_PLUGIN_NAME,
|
|
11
237
|
version: "0.0.0-alpha.1",
|
|
12
|
-
provides: [],
|
|
13
|
-
requires: [],
|
|
14
238
|
contributes: {
|
|
15
|
-
|
|
16
|
-
{
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
239
|
+
capabilities: [
|
|
240
|
+
{ id: "workspace.dependency-graph", title: "Workspace dependency graph", commandId: DEPENDENCY_GRAPH_CLI_ID, panelId: DEPENDENCY_GRAPH_PANEL_ID },
|
|
241
|
+
{ id: "workspace.status", title: "Workspace status", commandId: WORKSPACE_STATUS_CLI_ID },
|
|
242
|
+
{ id: "workspace.rollups", title: "Workspace rollups", commandId: WORKSPACE_SUMMARY_CLI_ID, panelId: EPICS_PANEL_ID }
|
|
243
|
+
],
|
|
244
|
+
panels: [
|
|
245
|
+
{ id: DEPENDENCY_GRAPH_PANEL_ID, slot: "capability", title: "Dependency graph", capabilityId: "workspace.dependency-graph" },
|
|
246
|
+
{ id: EPICS_PANEL_ID, slot: "capability", title: "Epics", capabilityId: "workspace.rollups" },
|
|
247
|
+
{ id: PEOPLE_PANEL_ID, slot: "capability", title: "People", capabilityId: "workspace.rollups" }
|
|
248
|
+
],
|
|
249
|
+
cliCommands: dependencyGraphCliCommands.map(({ run: _run, ...metadata }) => metadata)
|
|
22
250
|
}
|
|
251
|
+
}, {
|
|
252
|
+
featureCapabilities: [
|
|
253
|
+
{ id: "workspace.dependency-graph", title: "Workspace dependency graph", commandId: DEPENDENCY_GRAPH_CLI_ID, panelId: DEPENDENCY_GRAPH_PANEL_ID },
|
|
254
|
+
{ id: "workspace.status", title: "Workspace status", commandId: WORKSPACE_STATUS_CLI_ID },
|
|
255
|
+
{ id: "workspace.rollups", title: "Workspace rollups", commandId: WORKSPACE_SUMMARY_CLI_ID, panelId: EPICS_PANEL_ID }
|
|
256
|
+
],
|
|
257
|
+
panels: [
|
|
258
|
+
{ id: DEPENDENCY_GRAPH_PANEL_ID, slot: "capability", title: "Dependency graph", capabilityId: "workspace.dependency-graph", produce: produceDependencyGraphPanel },
|
|
259
|
+
{ id: EPICS_PANEL_ID, slot: "capability", title: "Epics", capabilityId: "workspace.rollups", produce: produceWorkspaceRollupsPanel }
|
|
260
|
+
],
|
|
261
|
+
cliCommands: dependencyGraphCliCommands
|
|
23
262
|
});
|
|
263
|
+
function createDependencyGraphPlugin() {
|
|
264
|
+
return dependencyGraphPlugin;
|
|
265
|
+
}
|
|
24
266
|
export {
|
|
25
|
-
|
|
267
|
+
executeSummary,
|
|
268
|
+
executeStatus,
|
|
269
|
+
executeGraph,
|
|
26
270
|
dependencyGraphPlugin,
|
|
27
|
-
|
|
271
|
+
dependencyGraphCliCommands,
|
|
272
|
+
createDependencyGraphPlugin,
|
|
273
|
+
WORKSPACE_SUMMARY_CLI_ID,
|
|
274
|
+
WORKSPACE_STATUS_CLI_ID,
|
|
275
|
+
PEOPLE_PANEL_ID,
|
|
276
|
+
EPICS_PANEL_ID,
|
|
277
|
+
DEPENDENCY_GRAPH_PLUGIN_NAME,
|
|
278
|
+
DEPENDENCY_GRAPH_PANEL_ID,
|
|
279
|
+
DEPENDENCY_GRAPH_CLI_ID
|
|
28
280
|
};
|
package/dist/src/plugin.d.ts
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { type BuildDependencyGraphModelOptions, type DependencyGraphModel } from "@rig/core/dependency-graph";
|
|
1
|
+
import { type RuntimeCliContext } from "@rig/core";
|
|
3
2
|
export declare const DEPENDENCY_GRAPH_PLUGIN_NAME = "@rig/dependency-graph-plugin";
|
|
4
|
-
export declare
|
|
3
|
+
export declare const DEPENDENCY_GRAPH_CLI_ID = "dependency-graph.graph";
|
|
4
|
+
export declare const WORKSPACE_STATUS_CLI_ID = "dependency-graph.status";
|
|
5
|
+
export declare const WORKSPACE_SUMMARY_CLI_ID = "dependency-graph.summary";
|
|
6
|
+
export declare const DEPENDENCY_GRAPH_PANEL_ID = "dependency-graph";
|
|
7
|
+
export declare const EPICS_PANEL_ID = "epics";
|
|
8
|
+
export declare const PEOPLE_PANEL_ID = "people";
|
|
9
|
+
type CommandOutcome = {
|
|
10
|
+
readonly ok: boolean;
|
|
11
|
+
readonly group: string;
|
|
12
|
+
readonly command: string;
|
|
13
|
+
readonly details?: Record<string, unknown>;
|
|
14
|
+
};
|
|
15
|
+
export declare function executeGraph(context: RuntimeCliContext, args: readonly string[]): Promise<CommandOutcome>;
|
|
16
|
+
export declare function executeStatus(context: RuntimeCliContext, args: readonly string[]): Promise<CommandOutcome>;
|
|
17
|
+
export declare function executeSummary(context: RuntimeCliContext, args: readonly string[]): Promise<CommandOutcome>;
|
|
18
|
+
export declare const dependencyGraphCliCommands: readonly [{
|
|
19
|
+
readonly id: "dependency-graph.graph";
|
|
20
|
+
readonly family: "graph";
|
|
21
|
+
readonly command: "rig graph [--json|--dot]";
|
|
22
|
+
readonly description: "Render task dependency graph from the active task source.";
|
|
23
|
+
readonly usage: "rig graph [--json|--dot]";
|
|
24
|
+
readonly projectRequired: true;
|
|
25
|
+
readonly run: typeof executeGraph;
|
|
26
|
+
}, {
|
|
27
|
+
readonly id: "dependency-graph.status";
|
|
28
|
+
readonly family: "status";
|
|
29
|
+
readonly command: "rig status [--epic <key>] [--json]";
|
|
30
|
+
readonly description: "Summarize workspace epics, blockers, and active runs.";
|
|
31
|
+
readonly usage: "rig status [--epic <key>] [--json]";
|
|
32
|
+
readonly projectRequired: true;
|
|
33
|
+
readonly run: typeof executeStatus;
|
|
34
|
+
}, {
|
|
35
|
+
readonly id: "dependency-graph.summary";
|
|
36
|
+
readonly family: "summary";
|
|
37
|
+
readonly command: "rig summary [--epic <key>] [--json]";
|
|
38
|
+
readonly description: "Print workspace rollups by epic and assignee.";
|
|
39
|
+
readonly usage: "rig summary [--epic <key>] [--json]";
|
|
40
|
+
readonly projectRequired: true;
|
|
41
|
+
readonly run: typeof executeSummary;
|
|
42
|
+
}];
|
|
5
43
|
export declare const dependencyGraphPlugin: import("@rig/core").RigPluginWithRuntime;
|
|
44
|
+
export declare function createDependencyGraphPlugin(): import("@rig/core").RigPluginWithRuntime;
|
|
6
45
|
export default dependencyGraphPlugin;
|
package/dist/src/plugin.js
CHANGED
|
@@ -1,30 +1,282 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// packages/dependency-graph-plugin/src/plugin.ts
|
|
3
3
|
import { definePlugin } from "@rig/core";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
classifyWorkspaceBlockers,
|
|
6
|
+
formatDependencyGraphDot,
|
|
7
|
+
getWorkspaceDependencyGraph,
|
|
8
|
+
getWorkspaceRollups,
|
|
9
|
+
listRuns,
|
|
10
|
+
listTasks
|
|
11
|
+
} from "@rig/client";
|
|
5
12
|
var DEPENDENCY_GRAPH_PLUGIN_NAME = "@rig/dependency-graph-plugin";
|
|
6
|
-
|
|
7
|
-
|
|
13
|
+
var DEPENDENCY_GRAPH_CLI_ID = "dependency-graph.graph";
|
|
14
|
+
var WORKSPACE_STATUS_CLI_ID = "dependency-graph.status";
|
|
15
|
+
var WORKSPACE_SUMMARY_CLI_ID = "dependency-graph.summary";
|
|
16
|
+
var DEPENDENCY_GRAPH_PANEL_ID = "dependency-graph";
|
|
17
|
+
var EPICS_PANEL_ID = "epics";
|
|
18
|
+
var PEOPLE_PANEL_ID = "people";
|
|
19
|
+
function isRecord(value) {
|
|
20
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
21
|
}
|
|
22
|
+
function panelProjectRoot(context) {
|
|
23
|
+
return isRecord(context) && typeof context.projectRoot === "string" && context.projectRoot.length > 0 ? context.projectRoot : null;
|
|
24
|
+
}
|
|
25
|
+
function toDependencyGraphPanelPayload(model) {
|
|
26
|
+
const layoutByTaskId = new Map(model.layout.nodes.map((node) => [node.taskId, node]));
|
|
27
|
+
return {
|
|
28
|
+
nodes: model.nodes.map((node) => {
|
|
29
|
+
const layout = layoutByTaskId.get(node.taskId);
|
|
30
|
+
return {
|
|
31
|
+
id: node.taskId,
|
|
32
|
+
title: node.title,
|
|
33
|
+
status: node.status,
|
|
34
|
+
epicKey: node.epicKey,
|
|
35
|
+
assignee: node.assignee,
|
|
36
|
+
blockerClass: node.blockerClass,
|
|
37
|
+
blockingDepth: node.blockingDepth,
|
|
38
|
+
layout: layout ? { x: layout.x, y: layout.y, width: layout.width, height: layout.height } : undefined
|
|
39
|
+
};
|
|
40
|
+
}),
|
|
41
|
+
edges: model.edges.map((edge) => ({
|
|
42
|
+
id: `${edge.fromTaskId}:${edge.toTaskId}:${edge.type}`,
|
|
43
|
+
from: edge.fromTaskId,
|
|
44
|
+
to: edge.toTaskId,
|
|
45
|
+
type: edge.type
|
|
46
|
+
})),
|
|
47
|
+
cycles: model.cycles,
|
|
48
|
+
unresolvedRefs: model.unresolvedRefs,
|
|
49
|
+
degraded: model.degraded ? "dependency graph projection degraded" : null
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function toWorkspaceRollupsPanelPayload(rollups) {
|
|
53
|
+
return {
|
|
54
|
+
epics: rollups.epics.map((epic) => ({
|
|
55
|
+
epicKey: epic.epicKey,
|
|
56
|
+
title: epic.epicKey,
|
|
57
|
+
total: epic.total,
|
|
58
|
+
percentComplete: epic.percentComplete,
|
|
59
|
+
blockedCount: epic.blockedCount,
|
|
60
|
+
inFlightCount: epic.inFlightCount,
|
|
61
|
+
humanBlockedCount: epic.humanBlockedCount
|
|
62
|
+
})),
|
|
63
|
+
people: rollups.assignees.map((assignee) => ({
|
|
64
|
+
assignee: assignee.assignee,
|
|
65
|
+
openCount: assignee.openTaskCount,
|
|
66
|
+
inFlightCount: assignee.inFlightRunCount,
|
|
67
|
+
prReviewCount: assignee.prsAwaitingReview,
|
|
68
|
+
humanBlockedCount: assignee.blockers.length,
|
|
69
|
+
activeRunIds: []
|
|
70
|
+
}))
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async function produceDependencyGraphPanel(context) {
|
|
74
|
+
const projectRoot = panelProjectRoot(context);
|
|
75
|
+
if (!projectRoot)
|
|
76
|
+
return;
|
|
77
|
+
const model = await getWorkspaceDependencyGraph(projectRoot, {
|
|
78
|
+
listTasks,
|
|
79
|
+
listRuns,
|
|
80
|
+
classifyBlockers: (root) => classifyWorkspaceBlockers(root, { listTasks, listRuns })
|
|
81
|
+
});
|
|
82
|
+
return toDependencyGraphPanelPayload(model);
|
|
83
|
+
}
|
|
84
|
+
async function produceWorkspaceRollupsPanel(context) {
|
|
85
|
+
const projectRoot = panelProjectRoot(context);
|
|
86
|
+
if (!projectRoot)
|
|
87
|
+
return;
|
|
88
|
+
const rollups = await getWorkspaceRollups(projectRoot, {
|
|
89
|
+
listTasks,
|
|
90
|
+
listRuns,
|
|
91
|
+
classifyBlockers: (root) => classifyWorkspaceBlockers(root, { listTasks, listRuns })
|
|
92
|
+
});
|
|
93
|
+
return toWorkspaceRollupsPanelPayload(rollups);
|
|
94
|
+
}
|
|
95
|
+
function printJson(value) {
|
|
96
|
+
console.log(JSON.stringify(value, null, 2));
|
|
97
|
+
}
|
|
98
|
+
function takeFlag(args, flag) {
|
|
99
|
+
const rest = [...args];
|
|
100
|
+
const index = rest.indexOf(flag);
|
|
101
|
+
if (index < 0)
|
|
102
|
+
return { value: false, rest };
|
|
103
|
+
rest.splice(index, 1);
|
|
104
|
+
return { value: true, rest };
|
|
105
|
+
}
|
|
106
|
+
function takeOption(args, flag) {
|
|
107
|
+
const rest = [...args];
|
|
108
|
+
const index = rest.indexOf(flag);
|
|
109
|
+
if (index < 0)
|
|
110
|
+
return { rest };
|
|
111
|
+
const value = rest[index + 1];
|
|
112
|
+
if (!value || value.startsWith("-"))
|
|
113
|
+
throw new Error(`${flag} requires a value.`);
|
|
114
|
+
rest.splice(index, 2);
|
|
115
|
+
return { value, rest };
|
|
116
|
+
}
|
|
117
|
+
function requireNoExtraArgs(args, usage) {
|
|
118
|
+
if (args.length > 0)
|
|
119
|
+
throw new Error(`Unexpected argument: ${args[0]}
|
|
120
|
+
Usage: ${usage}`);
|
|
121
|
+
}
|
|
122
|
+
function graphSummary(model) {
|
|
123
|
+
const blocked = model.nodes.filter((node) => node.blockedBy.length > 0 || node.blockerClass === "task-blocked").length;
|
|
124
|
+
return [
|
|
125
|
+
`graph ${model.graphId}`,
|
|
126
|
+
`tasks: ${model.nodes.length}`,
|
|
127
|
+
`edges: ${model.edges.length}`,
|
|
128
|
+
`blocked tasks: ${blocked}`,
|
|
129
|
+
`human blockers: ${model.nodes.filter((node) => node.blockerClass && node.blockerClass !== "not-blocked" && node.blockerClass !== "task-blocked").length}`,
|
|
130
|
+
`cycles: ${model.cycles.length}`,
|
|
131
|
+
`unresolved refs: ${model.unresolvedRefs.length}`
|
|
132
|
+
].join(`
|
|
133
|
+
`);
|
|
134
|
+
}
|
|
135
|
+
async function executeGraph(context, args) {
|
|
136
|
+
const dot = takeFlag(args, "--dot");
|
|
137
|
+
const json = takeFlag(dot.rest, "--json");
|
|
138
|
+
requireNoExtraArgs(json.rest, "rig graph [--json|--dot]");
|
|
139
|
+
if (dot.value && json.value)
|
|
140
|
+
throw new Error("Pass only one of --json or --dot.");
|
|
141
|
+
const model = await getWorkspaceDependencyGraph(context.projectRoot, {
|
|
142
|
+
listTasks,
|
|
143
|
+
listRuns,
|
|
144
|
+
classifyBlockers: (projectRoot) => classifyWorkspaceBlockers(projectRoot, { listTasks, listRuns })
|
|
145
|
+
});
|
|
146
|
+
const dotText = formatDependencyGraphDot(model);
|
|
147
|
+
if (context.outputMode === "text") {
|
|
148
|
+
if (dot.value)
|
|
149
|
+
console.log(dotText);
|
|
150
|
+
else if (json.value)
|
|
151
|
+
printJson(model);
|
|
152
|
+
else
|
|
153
|
+
console.log(graphSummary(model));
|
|
154
|
+
}
|
|
155
|
+
return { ok: true, group: "graph", command: dot.value ? "dot" : "show", details: { graph: model, dot: dotText } };
|
|
156
|
+
}
|
|
157
|
+
async function executeStatus(context, args) {
|
|
158
|
+
const json = takeFlag(args, "--json");
|
|
159
|
+
const epic = takeOption(json.rest, "--epic");
|
|
160
|
+
requireNoExtraArgs(epic.rest, "rig status [--epic <key>] [--json]");
|
|
161
|
+
const [rollups, blockers, runs] = await Promise.all([
|
|
162
|
+
getWorkspaceRollups(context.projectRoot, { listTasks, listRuns }),
|
|
163
|
+
classifyWorkspaceBlockers(context.projectRoot, { listTasks, listRuns }),
|
|
164
|
+
listRuns(context.projectRoot)
|
|
165
|
+
]);
|
|
166
|
+
const filteredEpics = epic.value ? rollups.epics.filter((entry) => entry.epicKey === epic.value) : rollups.epics;
|
|
167
|
+
const details = {
|
|
168
|
+
epic: epic.value ?? null,
|
|
169
|
+
epicRollups: filteredEpics,
|
|
170
|
+
epics: filteredEpics.length,
|
|
171
|
+
assignees: rollups.assignees.length,
|
|
172
|
+
humanBlockers: blockers.human.length,
|
|
173
|
+
machineBlockers: blockers.machine.length,
|
|
174
|
+
runs: runs.length,
|
|
175
|
+
activeRuns: runs.filter((run) => run.live && !run.stale).length,
|
|
176
|
+
needsAttention: runs.filter((run) => run.status === "needs-attention" || run.pendingApprovals > 0 || run.pendingInputs > 0).length,
|
|
177
|
+
generatedAt: rollups.generatedAt
|
|
178
|
+
};
|
|
179
|
+
if (context.outputMode === "text") {
|
|
180
|
+
if (json.value)
|
|
181
|
+
printJson(details);
|
|
182
|
+
else
|
|
183
|
+
console.log([`epics: ${details.epics}`, `assignees: ${details.assignees}`, `blockers: ${details.humanBlockers} human, ${details.machineBlockers} machine`, `runs: ${details.runs} total, ${details.activeRuns} active, ${details.needsAttention} need attention`].join(`
|
|
184
|
+
`));
|
|
185
|
+
}
|
|
186
|
+
return { ok: true, group: "status", command: "show", details };
|
|
187
|
+
}
|
|
188
|
+
async function executeSummary(context, args) {
|
|
189
|
+
const json = takeFlag(args, "--json");
|
|
190
|
+
const epicOpt = takeOption(json.rest, "--epic");
|
|
191
|
+
requireNoExtraArgs(epicOpt.rest, "rig summary [--epic <key>] [--json]");
|
|
192
|
+
const rollups = await getWorkspaceRollups(context.projectRoot, { listTasks, listRuns });
|
|
193
|
+
const details = epicOpt.value ? { ...rollups, epics: rollups.epics.filter((epic) => epic.epicKey === epicOpt.value) } : rollups;
|
|
194
|
+
if (context.outputMode === "text") {
|
|
195
|
+
if (json.value)
|
|
196
|
+
printJson(details);
|
|
197
|
+
else
|
|
198
|
+
console.log([
|
|
199
|
+
...details.epics.map((epic) => `epic ${epic.epicKey}: ${epic.percentComplete}% complete, ${epic.blockedCount} blocked`),
|
|
200
|
+
...details.assignees.map((assignee) => `${assignee.assignee}: ${assignee.openTaskCount} open, ${assignee.inFlightRunCount} in flight`)
|
|
201
|
+
].join(`
|
|
202
|
+
`) || "No rollups.");
|
|
203
|
+
}
|
|
204
|
+
return { ok: true, group: "summary", command: "show", details };
|
|
205
|
+
}
|
|
206
|
+
var dependencyGraphCliCommands = [
|
|
207
|
+
{
|
|
208
|
+
id: DEPENDENCY_GRAPH_CLI_ID,
|
|
209
|
+
family: "graph",
|
|
210
|
+
command: "rig graph [--json|--dot]",
|
|
211
|
+
description: "Render task dependency graph from the active task source.",
|
|
212
|
+
usage: "rig graph [--json|--dot]",
|
|
213
|
+
projectRequired: true,
|
|
214
|
+
run: executeGraph
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: WORKSPACE_STATUS_CLI_ID,
|
|
218
|
+
family: "status",
|
|
219
|
+
command: "rig status [--epic <key>] [--json]",
|
|
220
|
+
description: "Summarize workspace epics, blockers, and active runs.",
|
|
221
|
+
usage: "rig status [--epic <key>] [--json]",
|
|
222
|
+
projectRequired: true,
|
|
223
|
+
run: executeStatus
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: WORKSPACE_SUMMARY_CLI_ID,
|
|
227
|
+
family: "summary",
|
|
228
|
+
command: "rig summary [--epic <key>] [--json]",
|
|
229
|
+
description: "Print workspace rollups by epic and assignee.",
|
|
230
|
+
usage: "rig summary [--epic <key>] [--json]",
|
|
231
|
+
projectRequired: true,
|
|
232
|
+
run: executeSummary
|
|
233
|
+
}
|
|
234
|
+
];
|
|
9
235
|
var dependencyGraphPlugin = definePlugin({
|
|
10
236
|
name: DEPENDENCY_GRAPH_PLUGIN_NAME,
|
|
11
237
|
version: "0.0.0-alpha.1",
|
|
12
|
-
provides: [],
|
|
13
|
-
requires: [],
|
|
14
238
|
contributes: {
|
|
15
|
-
|
|
16
|
-
{
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
239
|
+
capabilities: [
|
|
240
|
+
{ id: "workspace.dependency-graph", title: "Workspace dependency graph", commandId: DEPENDENCY_GRAPH_CLI_ID, panelId: DEPENDENCY_GRAPH_PANEL_ID },
|
|
241
|
+
{ id: "workspace.status", title: "Workspace status", commandId: WORKSPACE_STATUS_CLI_ID },
|
|
242
|
+
{ id: "workspace.rollups", title: "Workspace rollups", commandId: WORKSPACE_SUMMARY_CLI_ID, panelId: EPICS_PANEL_ID }
|
|
243
|
+
],
|
|
244
|
+
panels: [
|
|
245
|
+
{ id: DEPENDENCY_GRAPH_PANEL_ID, slot: "capability", title: "Dependency graph", capabilityId: "workspace.dependency-graph" },
|
|
246
|
+
{ id: EPICS_PANEL_ID, slot: "capability", title: "Epics", capabilityId: "workspace.rollups" },
|
|
247
|
+
{ id: PEOPLE_PANEL_ID, slot: "capability", title: "People", capabilityId: "workspace.rollups" }
|
|
248
|
+
],
|
|
249
|
+
cliCommands: dependencyGraphCliCommands.map(({ run: _run, ...metadata }) => metadata)
|
|
22
250
|
}
|
|
251
|
+
}, {
|
|
252
|
+
featureCapabilities: [
|
|
253
|
+
{ id: "workspace.dependency-graph", title: "Workspace dependency graph", commandId: DEPENDENCY_GRAPH_CLI_ID, panelId: DEPENDENCY_GRAPH_PANEL_ID },
|
|
254
|
+
{ id: "workspace.status", title: "Workspace status", commandId: WORKSPACE_STATUS_CLI_ID },
|
|
255
|
+
{ id: "workspace.rollups", title: "Workspace rollups", commandId: WORKSPACE_SUMMARY_CLI_ID, panelId: EPICS_PANEL_ID }
|
|
256
|
+
],
|
|
257
|
+
panels: [
|
|
258
|
+
{ id: DEPENDENCY_GRAPH_PANEL_ID, slot: "capability", title: "Dependency graph", capabilityId: "workspace.dependency-graph", produce: produceDependencyGraphPanel },
|
|
259
|
+
{ id: EPICS_PANEL_ID, slot: "capability", title: "Epics", capabilityId: "workspace.rollups", produce: produceWorkspaceRollupsPanel }
|
|
260
|
+
],
|
|
261
|
+
cliCommands: dependencyGraphCliCommands
|
|
23
262
|
});
|
|
263
|
+
function createDependencyGraphPlugin() {
|
|
264
|
+
return dependencyGraphPlugin;
|
|
265
|
+
}
|
|
24
266
|
var plugin_default = dependencyGraphPlugin;
|
|
25
267
|
export {
|
|
26
|
-
|
|
268
|
+
executeSummary,
|
|
269
|
+
executeStatus,
|
|
270
|
+
executeGraph,
|
|
27
271
|
dependencyGraphPlugin,
|
|
272
|
+
dependencyGraphCliCommands,
|
|
28
273
|
plugin_default as default,
|
|
29
|
-
|
|
274
|
+
createDependencyGraphPlugin,
|
|
275
|
+
WORKSPACE_SUMMARY_CLI_ID,
|
|
276
|
+
WORKSPACE_STATUS_CLI_ID,
|
|
277
|
+
PEOPLE_PANEL_ID,
|
|
278
|
+
EPICS_PANEL_ID,
|
|
279
|
+
DEPENDENCY_GRAPH_PLUGIN_NAME,
|
|
280
|
+
DEPENDENCY_GRAPH_PANEL_ID,
|
|
281
|
+
DEPENDENCY_GRAPH_CLI_ID
|
|
30
282
|
};
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@h-rig/dependency-graph-plugin",
|
|
3
|
-
"version": "0.0.6-alpha.
|
|
3
|
+
"version": "0.0.6-alpha.138",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "First-party dependency graph
|
|
5
|
+
"description": "First-party dependency graph, workspace status, and rollup capability plugin for Rig.",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"files": [
|
|
8
8
|
"dist",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"module": "./dist/src/index.js",
|
|
26
26
|
"types": "./dist/src/index.d.ts",
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@rig/
|
|
29
|
-
"@rig/
|
|
28
|
+
"@rig/client": "npm:@h-rig/client@0.0.6-alpha.138",
|
|
29
|
+
"@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.138",
|
|
30
|
+
"@rig/core": "npm:@h-rig/core@0.0.6-alpha.138"
|
|
30
31
|
}
|
|
31
32
|
}
|