@crafter/cli-tree 0.1.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/LICENSE +21 -0
- package/README.md +328 -0
- package/dist/archaeology/cache.d.ts +11 -0
- package/dist/archaeology/delegate.d.ts +43 -0
- package/dist/archaeology/index.d.ts +12 -0
- package/dist/archaeology/index.js +61 -0
- package/dist/archaeology/index.js.map +9 -0
- package/dist/archaeology/llm.d.ts +1 -0
- package/dist/archaeology/merge.d.ts +3 -0
- package/dist/archaeology/orchestrator.d.ts +25 -0
- package/dist/archaeology/prompts.d.ts +13 -0
- package/dist/archaeology/types.d.ts +101 -0
- package/dist/archaeology/validate.d.ts +18 -0
- package/dist/chunk-57gtsvhb.js +434 -0
- package/dist/chunk-57gtsvhb.js.map +16 -0
- package/dist/chunk-5aahbfr2.js +293 -0
- package/dist/chunk-5aahbfr2.js.map +10 -0
- package/dist/chunk-pkfpaae1.js +678 -0
- package/dist/chunk-pkfpaae1.js.map +15 -0
- package/dist/chunk-q4se2rwe.js +181 -0
- package/dist/chunk-q4se2rwe.js.map +14 -0
- package/dist/chunk-v5w3w6bd.js +168 -0
- package/dist/chunk-v5w3w6bd.js.map +11 -0
- package/dist/chunk-ykze151b.js +770 -0
- package/dist/chunk-ykze151b.js.map +16 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +433 -0
- package/dist/cli.js.map +10 -0
- package/dist/encoders/ansi.d.ts +2 -0
- package/dist/encoders/html.d.ts +10 -0
- package/dist/encoders/string.d.ts +2 -0
- package/dist/flow/encode.d.ts +5 -0
- package/dist/flow/index.d.ts +8 -0
- package/dist/flow/index.js +25 -0
- package/dist/flow/index.js.map +9 -0
- package/dist/flow/layout.d.ts +30 -0
- package/dist/flow/parse.d.ts +2 -0
- package/dist/flow/render.d.ts +3 -0
- package/dist/flow/types.d.ts +42 -0
- package/dist/flow/validate.d.ts +3 -0
- package/dist/flow/yaml.d.ts +4 -0
- package/dist/grid.d.ts +14 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +9 -0
- package/dist/miner/history.d.ts +6 -0
- package/dist/miner/index.d.ts +18 -0
- package/dist/miner/index.js +38 -0
- package/dist/miner/index.js.map +9 -0
- package/dist/miner/sessions.d.ts +3 -0
- package/dist/miner/stats.d.ts +2 -0
- package/dist/miner/suggest.d.ts +11 -0
- package/dist/miner/transitions.d.ts +6 -0
- package/dist/miner/types.d.ts +46 -0
- package/dist/miner/workflows.d.ts +11 -0
- package/dist/parse.d.ts +3 -0
- package/dist/render.d.ts +3 -0
- package/dist/types.d.ts +39 -0
- package/package.json +85 -0
- package/skill/SKILL.md +263 -0
- package/skill/evals/evals.json +26 -0
- package/skill/install.sh +38 -0
- package/skill/references/archaeology-guide.md +157 -0
- package/skill/references/skill-template.md +120 -0
- package/src/archaeology/cache.ts +107 -0
- package/src/archaeology/delegate.ts +113 -0
- package/src/archaeology/index.ts +48 -0
- package/src/archaeology/llm.ts +10 -0
- package/src/archaeology/merge.ts +155 -0
- package/src/archaeology/orchestrator.ts +185 -0
- package/src/archaeology/prompts.ts +178 -0
- package/src/archaeology/types.ts +139 -0
- package/src/archaeology/validate.ts +157 -0
- package/src/cli.ts +451 -0
- package/src/encoders/ansi.ts +32 -0
- package/src/encoders/html.ts +78 -0
- package/src/encoders/string.ts +20 -0
- package/src/flow/encode.ts +21 -0
- package/src/flow/index.ts +15 -0
- package/src/flow/layout.ts +150 -0
- package/src/flow/parse.ts +100 -0
- package/src/flow/render.ts +186 -0
- package/src/flow/types.ts +45 -0
- package/src/flow/validate.ts +111 -0
- package/src/flow/yaml.ts +235 -0
- package/src/grid.ts +59 -0
- package/src/index.ts +24 -0
- package/src/miner/history.ts +156 -0
- package/src/miner/index.ts +76 -0
- package/src/miner/sessions.ts +39 -0
- package/src/miner/stats.ts +43 -0
- package/src/miner/suggest.ts +101 -0
- package/src/miner/transitions.ts +62 -0
- package/src/miner/types.ts +45 -0
- package/src/miner/workflows.ts +96 -0
- package/src/parse.ts +321 -0
- package/src/render.ts +182 -0
- package/src/types.ts +62 -0
- package/workflows/docker-deploy.yml +42 -0
- package/workflows/docker-parallel.yml +36 -0
- package/workflows/gh-issue-to-pr.yml +48 -0
- package/workflows/git-pr-flow.yml +36 -0
- package/workflows/kubectl-rollout.yml +37 -0
- package/workflows/npm-publish.yml +42 -0
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createGrid,
|
|
3
|
+
encodeAnsi,
|
|
4
|
+
encodeHtml,
|
|
5
|
+
encodeString,
|
|
6
|
+
writeText
|
|
7
|
+
} from "./chunk-q4se2rwe.js";
|
|
8
|
+
|
|
9
|
+
// src/flow/yaml.ts
|
|
10
|
+
function parseYaml(text) {
|
|
11
|
+
const lines = text.split(`
|
|
12
|
+
`);
|
|
13
|
+
const cleaned = [];
|
|
14
|
+
for (let i = 0;i < lines.length; i++) {
|
|
15
|
+
const raw = lines[i];
|
|
16
|
+
const commentIdx = findCommentStart(raw);
|
|
17
|
+
const withoutComment = commentIdx >= 0 ? raw.slice(0, commentIdx) : raw;
|
|
18
|
+
const trimmed = withoutComment.trimEnd();
|
|
19
|
+
if (!trimmed.trim())
|
|
20
|
+
continue;
|
|
21
|
+
const indent = trimmed.length - trimmed.trimStart().length;
|
|
22
|
+
cleaned.push({ indent, content: trimmed.trim(), lineNum: i + 1 });
|
|
23
|
+
}
|
|
24
|
+
const result = parseBlock(cleaned, 0, 0);
|
|
25
|
+
return result.value;
|
|
26
|
+
}
|
|
27
|
+
function findCommentStart(line) {
|
|
28
|
+
let inString = null;
|
|
29
|
+
for (let i = 0;i < line.length; i++) {
|
|
30
|
+
const ch = line[i];
|
|
31
|
+
if (inString) {
|
|
32
|
+
if (ch === inString && line[i - 1] !== "\\")
|
|
33
|
+
inString = null;
|
|
34
|
+
} else {
|
|
35
|
+
if (ch === '"' || ch === "'")
|
|
36
|
+
inString = ch;
|
|
37
|
+
else if (ch === "#" && (i === 0 || line[i - 1] === " " || line[i - 1] === "\t")) {
|
|
38
|
+
return i;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return -1;
|
|
43
|
+
}
|
|
44
|
+
function parseBlock(lines, startIdx, baseIndent) {
|
|
45
|
+
if (startIdx >= lines.length)
|
|
46
|
+
return { value: null, nextIdx: startIdx };
|
|
47
|
+
const first = lines[startIdx];
|
|
48
|
+
if (first.indent < baseIndent)
|
|
49
|
+
return { value: null, nextIdx: startIdx };
|
|
50
|
+
if (first.content.startsWith("- ") || first.content === "-") {
|
|
51
|
+
return parseList(lines, startIdx, first.indent);
|
|
52
|
+
}
|
|
53
|
+
if (first.content.includes(":")) {
|
|
54
|
+
return parseMap(lines, startIdx, first.indent);
|
|
55
|
+
}
|
|
56
|
+
return { value: parseScalar(first.content), nextIdx: startIdx + 1 };
|
|
57
|
+
}
|
|
58
|
+
function parseList(lines, startIdx, listIndent) {
|
|
59
|
+
const items = [];
|
|
60
|
+
let i = startIdx;
|
|
61
|
+
while (i < lines.length) {
|
|
62
|
+
const line = lines[i];
|
|
63
|
+
if (line.indent < listIndent)
|
|
64
|
+
break;
|
|
65
|
+
if (line.indent > listIndent) {
|
|
66
|
+
i++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!line.content.startsWith("- ") && line.content !== "-")
|
|
70
|
+
break;
|
|
71
|
+
const itemContent = line.content === "-" ? "" : line.content.slice(2);
|
|
72
|
+
if (!itemContent) {
|
|
73
|
+
const childIndent = listIndent + 2;
|
|
74
|
+
const next = parseBlock(lines, i + 1, childIndent);
|
|
75
|
+
items.push(next.value);
|
|
76
|
+
i = next.nextIdx;
|
|
77
|
+
} else if (itemContent.includes(":") && !isFlowScalar(itemContent)) {
|
|
78
|
+
const inlineIndent = listIndent + 2;
|
|
79
|
+
const synthetic = [
|
|
80
|
+
{ indent: inlineIndent, content: itemContent, lineNum: line.lineNum },
|
|
81
|
+
...lines.slice(i + 1)
|
|
82
|
+
];
|
|
83
|
+
const mapResult = parseMap(synthetic, 0, inlineIndent);
|
|
84
|
+
items.push(mapResult.value);
|
|
85
|
+
i = i + 1 + (mapResult.nextIdx - 1);
|
|
86
|
+
} else {
|
|
87
|
+
items.push(parseScalar(itemContent));
|
|
88
|
+
i++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { value: items, nextIdx: i };
|
|
92
|
+
}
|
|
93
|
+
function parseMap(lines, startIdx, mapIndent) {
|
|
94
|
+
const map = {};
|
|
95
|
+
let i = startIdx;
|
|
96
|
+
while (i < lines.length) {
|
|
97
|
+
const line = lines[i];
|
|
98
|
+
if (line.indent < mapIndent)
|
|
99
|
+
break;
|
|
100
|
+
if (line.indent > mapIndent) {
|
|
101
|
+
i++;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const colonIdx = findUnquotedColon(line.content);
|
|
105
|
+
if (colonIdx < 0)
|
|
106
|
+
break;
|
|
107
|
+
const key = line.content.slice(0, colonIdx).trim().replace(/^["']|["']$/g, "");
|
|
108
|
+
const rest = line.content.slice(colonIdx + 1).trim();
|
|
109
|
+
if (!rest) {
|
|
110
|
+
const next = parseBlock(lines, i + 1, mapIndent + 1);
|
|
111
|
+
map[key] = next.value;
|
|
112
|
+
i = next.nextIdx;
|
|
113
|
+
} else if (rest.startsWith("[") && rest.endsWith("]")) {
|
|
114
|
+
map[key] = parseInlineList(rest);
|
|
115
|
+
i++;
|
|
116
|
+
} else if (rest.startsWith("{") && rest.endsWith("}")) {
|
|
117
|
+
map[key] = parseInlineMap(rest);
|
|
118
|
+
i++;
|
|
119
|
+
} else {
|
|
120
|
+
map[key] = parseScalar(rest);
|
|
121
|
+
i++;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { value: map, nextIdx: i };
|
|
125
|
+
}
|
|
126
|
+
function findUnquotedColon(text) {
|
|
127
|
+
let inString = null;
|
|
128
|
+
let bracket = 0;
|
|
129
|
+
for (let i = 0;i < text.length; i++) {
|
|
130
|
+
const ch = text[i];
|
|
131
|
+
if (inString) {
|
|
132
|
+
if (ch === inString && text[i - 1] !== "\\")
|
|
133
|
+
inString = null;
|
|
134
|
+
} else {
|
|
135
|
+
if (ch === '"' || ch === "'")
|
|
136
|
+
inString = ch;
|
|
137
|
+
else if (ch === "[" || ch === "{")
|
|
138
|
+
bracket++;
|
|
139
|
+
else if (ch === "]" || ch === "}")
|
|
140
|
+
bracket--;
|
|
141
|
+
else if (ch === ":" && bracket === 0)
|
|
142
|
+
return i;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return -1;
|
|
146
|
+
}
|
|
147
|
+
function parseScalar(text) {
|
|
148
|
+
const trimmed = text.trim();
|
|
149
|
+
if (trimmed === "" || trimmed === "null" || trimmed === "~")
|
|
150
|
+
return null;
|
|
151
|
+
if (trimmed === "true")
|
|
152
|
+
return true;
|
|
153
|
+
if (trimmed === "false")
|
|
154
|
+
return false;
|
|
155
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
156
|
+
return trimmed.slice(1, -1).replace(/\\"/g, '"').replace(/\\'/g, "'");
|
|
157
|
+
}
|
|
158
|
+
const num = Number(trimmed);
|
|
159
|
+
if (!Number.isNaN(num) && /^-?\d+(\.\d+)?$/.test(trimmed))
|
|
160
|
+
return num;
|
|
161
|
+
return trimmed;
|
|
162
|
+
}
|
|
163
|
+
function isFlowScalar(text) {
|
|
164
|
+
return text.startsWith("[") || text.startsWith("{") || text.startsWith('"') || text.startsWith("'");
|
|
165
|
+
}
|
|
166
|
+
function parseInlineList(text) {
|
|
167
|
+
const inner = text.slice(1, -1).trim();
|
|
168
|
+
if (!inner)
|
|
169
|
+
return [];
|
|
170
|
+
return splitTopLevel(inner, ",").map((item) => parseScalar(item.trim()));
|
|
171
|
+
}
|
|
172
|
+
function parseInlineMap(text) {
|
|
173
|
+
const inner = text.slice(1, -1).trim();
|
|
174
|
+
if (!inner)
|
|
175
|
+
return {};
|
|
176
|
+
const result = {};
|
|
177
|
+
for (const pair of splitTopLevel(inner, ",")) {
|
|
178
|
+
const colonIdx = findUnquotedColon(pair);
|
|
179
|
+
if (colonIdx < 0)
|
|
180
|
+
continue;
|
|
181
|
+
const key = pair.slice(0, colonIdx).trim().replace(/^["']|["']$/g, "");
|
|
182
|
+
const val = pair.slice(colonIdx + 1).trim();
|
|
183
|
+
result[key] = parseScalar(val);
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
function splitTopLevel(text, sep) {
|
|
188
|
+
const parts = [];
|
|
189
|
+
let depth = 0;
|
|
190
|
+
let inString = null;
|
|
191
|
+
let current = "";
|
|
192
|
+
for (let i = 0;i < text.length; i++) {
|
|
193
|
+
const ch = text[i];
|
|
194
|
+
if (inString) {
|
|
195
|
+
if (ch === inString && text[i - 1] !== "\\")
|
|
196
|
+
inString = null;
|
|
197
|
+
current += ch;
|
|
198
|
+
} else {
|
|
199
|
+
if (ch === '"' || ch === "'") {
|
|
200
|
+
inString = ch;
|
|
201
|
+
current += ch;
|
|
202
|
+
} else if (ch === "[" || ch === "{") {
|
|
203
|
+
depth++;
|
|
204
|
+
current += ch;
|
|
205
|
+
} else if (ch === "]" || ch === "}") {
|
|
206
|
+
depth--;
|
|
207
|
+
current += ch;
|
|
208
|
+
} else if (ch === sep && depth === 0) {
|
|
209
|
+
parts.push(current);
|
|
210
|
+
current = "";
|
|
211
|
+
} else {
|
|
212
|
+
current += ch;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (current)
|
|
217
|
+
parts.push(current);
|
|
218
|
+
return parts;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/flow/parse.ts
|
|
222
|
+
function parseWorkflow(yamlText) {
|
|
223
|
+
const raw = parseYaml(yamlText);
|
|
224
|
+
if (!isObject(raw)) {
|
|
225
|
+
throw new Error("Workflow YAML must be a map at the top level");
|
|
226
|
+
}
|
|
227
|
+
const name = asString(raw.name, "name");
|
|
228
|
+
const cli = asString(raw.cli, "cli");
|
|
229
|
+
const description = raw.description ? asString(raw.description, "description") : undefined;
|
|
230
|
+
const version = raw.version ? asString(raw.version, "version") : undefined;
|
|
231
|
+
const nodes = parseNodes(raw.nodes);
|
|
232
|
+
const edges = parseEdges(raw.edges, nodes);
|
|
233
|
+
const workflow = { name, cli, nodes, edges };
|
|
234
|
+
if (description)
|
|
235
|
+
workflow.description = description;
|
|
236
|
+
if (version)
|
|
237
|
+
workflow.version = version;
|
|
238
|
+
return workflow;
|
|
239
|
+
}
|
|
240
|
+
function parseNodes(raw) {
|
|
241
|
+
if (!Array.isArray(raw)) {
|
|
242
|
+
throw new Error("'nodes' must be a list");
|
|
243
|
+
}
|
|
244
|
+
return raw.map((item, idx) => {
|
|
245
|
+
if (!isObject(item)) {
|
|
246
|
+
throw new Error(`nodes[${idx}] must be a map`);
|
|
247
|
+
}
|
|
248
|
+
const id = asString(item.id, `nodes[${idx}].id`);
|
|
249
|
+
const cmdRaw = item.command;
|
|
250
|
+
let command;
|
|
251
|
+
if (typeof cmdRaw === "string") {
|
|
252
|
+
command = cmdRaw.split(/\s+/).filter(Boolean);
|
|
253
|
+
} else if (Array.isArray(cmdRaw)) {
|
|
254
|
+
command = cmdRaw.map((c) => String(c));
|
|
255
|
+
} else {
|
|
256
|
+
throw new Error(`nodes[${idx}].command must be a string or list`);
|
|
257
|
+
}
|
|
258
|
+
const node = { id, command };
|
|
259
|
+
if (typeof item.label === "string")
|
|
260
|
+
node.label = item.label;
|
|
261
|
+
if (typeof item.description === "string")
|
|
262
|
+
node.description = item.description;
|
|
263
|
+
if (typeof item.optional === "boolean")
|
|
264
|
+
node.optional = item.optional;
|
|
265
|
+
return node;
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
function parseEdges(raw, nodes) {
|
|
269
|
+
if (!raw)
|
|
270
|
+
return inferSequentialEdges(nodes);
|
|
271
|
+
if (!Array.isArray(raw)) {
|
|
272
|
+
throw new Error("'edges' must be a list");
|
|
273
|
+
}
|
|
274
|
+
return raw.map((item, idx) => {
|
|
275
|
+
if (typeof item === "string") {
|
|
276
|
+
const parts = item.split("->").map((s) => s.trim());
|
|
277
|
+
if (parts.length !== 2) {
|
|
278
|
+
throw new Error(`edges[${idx}] string must be "from -> to"`);
|
|
279
|
+
}
|
|
280
|
+
return { from: parts[0], to: parts[1] };
|
|
281
|
+
}
|
|
282
|
+
if (!isObject(item)) {
|
|
283
|
+
throw new Error(`edges[${idx}] must be a map or string`);
|
|
284
|
+
}
|
|
285
|
+
const edge = {
|
|
286
|
+
from: asString(item.from, `edges[${idx}].from`),
|
|
287
|
+
to: asString(item.to, `edges[${idx}].to`)
|
|
288
|
+
};
|
|
289
|
+
if (typeof item.condition === "string")
|
|
290
|
+
edge.condition = item.condition;
|
|
291
|
+
if (typeof item.label === "string")
|
|
292
|
+
edge.label = item.label;
|
|
293
|
+
return edge;
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
function inferSequentialEdges(nodes) {
|
|
297
|
+
const edges = [];
|
|
298
|
+
for (let i = 0;i < nodes.length - 1; i++) {
|
|
299
|
+
edges.push({ from: nodes[i].id, to: nodes[i + 1].id });
|
|
300
|
+
}
|
|
301
|
+
return edges;
|
|
302
|
+
}
|
|
303
|
+
function isObject(value) {
|
|
304
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
305
|
+
}
|
|
306
|
+
function asString(value, field) {
|
|
307
|
+
if (typeof value !== "string") {
|
|
308
|
+
throw new Error(`${field} must be a string`);
|
|
309
|
+
}
|
|
310
|
+
return value;
|
|
311
|
+
}
|
|
312
|
+
// src/flow/validate.ts
|
|
313
|
+
function validateWorkflow(workflow, tree) {
|
|
314
|
+
const errors = [];
|
|
315
|
+
const nodeIds = new Set(workflow.nodes.map((n) => n.id));
|
|
316
|
+
if (nodeIds.size !== workflow.nodes.length) {
|
|
317
|
+
errors.push({ severity: "error", message: "Duplicate node IDs found" });
|
|
318
|
+
}
|
|
319
|
+
for (const node of workflow.nodes) {
|
|
320
|
+
if (node.command.length === 0) {
|
|
321
|
+
errors.push({ node: node.id, severity: "error", message: `Node "${node.id}" has empty command` });
|
|
322
|
+
}
|
|
323
|
+
if (node.command[0] !== workflow.cli) {
|
|
324
|
+
errors.push({
|
|
325
|
+
node: node.id,
|
|
326
|
+
severity: "warning",
|
|
327
|
+
message: `Node "${node.id}" command "${node.command[0]}" does not match workflow cli "${workflow.cli}"`
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
for (const edge of workflow.edges) {
|
|
332
|
+
if (!nodeIds.has(edge.from)) {
|
|
333
|
+
errors.push({
|
|
334
|
+
edge: { from: edge.from, to: edge.to },
|
|
335
|
+
severity: "error",
|
|
336
|
+
message: `Edge references unknown node "${edge.from}"`
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
if (!nodeIds.has(edge.to)) {
|
|
340
|
+
errors.push({
|
|
341
|
+
edge: { from: edge.from, to: edge.to },
|
|
342
|
+
severity: "error",
|
|
343
|
+
message: `Edge references unknown node "${edge.to}"`
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (hasCycle(workflow)) {
|
|
348
|
+
errors.push({ severity: "error", message: "Workflow contains a cycle — DAGs only" });
|
|
349
|
+
}
|
|
350
|
+
if (tree) {
|
|
351
|
+
for (const node of workflow.nodes) {
|
|
352
|
+
const subcommand = node.command.slice(1).filter((arg) => !arg.startsWith("-") && !arg.startsWith("$") && /^[a-z]/i.test(arg));
|
|
353
|
+
if (subcommand.length === 0) {
|
|
354
|
+
node.ref = tree;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
const firstMatch = tree.subcommands?.find((s) => s.name === subcommand[0] || s.aliases?.includes(subcommand[0]));
|
|
358
|
+
if (!firstMatch) {
|
|
359
|
+
errors.push({
|
|
360
|
+
node: node.id,
|
|
361
|
+
severity: "warning",
|
|
362
|
+
message: `Command "${node.command.join(" ")}" not found in ${workflow.cli} tree`
|
|
363
|
+
});
|
|
364
|
+
} else {
|
|
365
|
+
node.ref = findInTree(tree, subcommand);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
valid: errors.filter((e) => e.severity === "error").length === 0,
|
|
371
|
+
errors
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function hasCycle(workflow) {
|
|
375
|
+
const adj = new Map;
|
|
376
|
+
for (const node of workflow.nodes)
|
|
377
|
+
adj.set(node.id, []);
|
|
378
|
+
for (const edge of workflow.edges) {
|
|
379
|
+
adj.get(edge.from)?.push(edge.to);
|
|
380
|
+
}
|
|
381
|
+
const WHITE = 0, GRAY = 1, BLACK = 2;
|
|
382
|
+
const color = new Map;
|
|
383
|
+
for (const node of workflow.nodes)
|
|
384
|
+
color.set(node.id, WHITE);
|
|
385
|
+
function visit(u) {
|
|
386
|
+
color.set(u, GRAY);
|
|
387
|
+
for (const v of adj.get(u) ?? []) {
|
|
388
|
+
const c = color.get(v);
|
|
389
|
+
if (c === GRAY)
|
|
390
|
+
return true;
|
|
391
|
+
if (c === WHITE && visit(v))
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
color.set(u, BLACK);
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
for (const node of workflow.nodes) {
|
|
398
|
+
if (color.get(node.id) === WHITE && visit(node.id))
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
function findInTree(tree, path) {
|
|
404
|
+
let current = tree;
|
|
405
|
+
for (const segment of path) {
|
|
406
|
+
const child = current?.subcommands?.find((s) => s.name === segment || s.aliases?.includes(segment));
|
|
407
|
+
if (!child)
|
|
408
|
+
return current;
|
|
409
|
+
current = child;
|
|
410
|
+
}
|
|
411
|
+
return current;
|
|
412
|
+
}
|
|
413
|
+
// src/flow/layout.ts
|
|
414
|
+
function computeLayout(workflow, opts = {}) {
|
|
415
|
+
const boxPaddingX = opts.boxPaddingX ?? 2;
|
|
416
|
+
const colGap = opts.colGap ?? 4;
|
|
417
|
+
const rowGap = opts.rowGap ?? 2;
|
|
418
|
+
const minBoxWidth = opts.minBoxWidth ?? 8;
|
|
419
|
+
const ranks = computeRanks(workflow);
|
|
420
|
+
const byRank = groupByRank(workflow.nodes, ranks);
|
|
421
|
+
const maxRank = Math.max(0, ...Array.from(ranks.values()));
|
|
422
|
+
const allWidths = workflow.nodes.map((n) => boxWidthFor(n, boxPaddingX, minBoxWidth));
|
|
423
|
+
const uniformWidth = Math.max(...allWidths);
|
|
424
|
+
const maxPerRank = Math.max(1, ...Array.from(byRank.values()).map((v) => v.length));
|
|
425
|
+
const totalRowWidth = maxPerRank * uniformWidth + (maxPerRank - 1) * colGap;
|
|
426
|
+
const layoutNodes = [];
|
|
427
|
+
const boxHeight = 3;
|
|
428
|
+
let currentY = 0;
|
|
429
|
+
for (let r = 0;r <= maxRank; r++) {
|
|
430
|
+
const rankNodes = byRank.get(r) ?? [];
|
|
431
|
+
const count = rankNodes.length;
|
|
432
|
+
const rowWidth = count * uniformWidth + (count - 1) * colGap;
|
|
433
|
+
const startX = Math.floor((totalRowWidth - rowWidth) / 2);
|
|
434
|
+
for (let i = 0;i < rankNodes.length; i++) {
|
|
435
|
+
const node = rankNodes[i];
|
|
436
|
+
const x = startX + i * (uniformWidth + colGap);
|
|
437
|
+
layoutNodes.push({
|
|
438
|
+
id: node.id,
|
|
439
|
+
node,
|
|
440
|
+
col: i,
|
|
441
|
+
row: r,
|
|
442
|
+
width: uniformWidth,
|
|
443
|
+
height: boxHeight,
|
|
444
|
+
x,
|
|
445
|
+
y: currentY
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
currentY += boxHeight + rowGap;
|
|
449
|
+
}
|
|
450
|
+
const totalWidth = Math.max(0, ...layoutNodes.map((n) => n.x + n.width));
|
|
451
|
+
const totalHeight = Math.max(0, ...layoutNodes.map((n) => n.y + n.height));
|
|
452
|
+
const edges = workflow.edges.map((e) => ({
|
|
453
|
+
from: e.from,
|
|
454
|
+
to: e.to,
|
|
455
|
+
edge: e
|
|
456
|
+
}));
|
|
457
|
+
return { nodes: layoutNodes, edges, totalWidth, totalHeight };
|
|
458
|
+
}
|
|
459
|
+
function computeRanks(workflow) {
|
|
460
|
+
const ranks = new Map;
|
|
461
|
+
const inEdges = new Map;
|
|
462
|
+
for (const node of workflow.nodes) {
|
|
463
|
+
ranks.set(node.id, 0);
|
|
464
|
+
inEdges.set(node.id, []);
|
|
465
|
+
}
|
|
466
|
+
for (const edge of workflow.edges) {
|
|
467
|
+
inEdges.get(edge.to)?.push(edge.from);
|
|
468
|
+
}
|
|
469
|
+
const visiting = new Set;
|
|
470
|
+
function visit(id) {
|
|
471
|
+
if (visiting.has(id))
|
|
472
|
+
return ranks.get(id) ?? 0;
|
|
473
|
+
visiting.add(id);
|
|
474
|
+
const parents = inEdges.get(id) ?? [];
|
|
475
|
+
if (parents.length === 0) {
|
|
476
|
+
ranks.set(id, 0);
|
|
477
|
+
} else {
|
|
478
|
+
const maxParent = Math.max(...parents.map((p) => visit(p)));
|
|
479
|
+
ranks.set(id, maxParent + 1);
|
|
480
|
+
}
|
|
481
|
+
visiting.delete(id);
|
|
482
|
+
return ranks.get(id);
|
|
483
|
+
}
|
|
484
|
+
for (const node of workflow.nodes) {
|
|
485
|
+
visit(node.id);
|
|
486
|
+
}
|
|
487
|
+
return ranks;
|
|
488
|
+
}
|
|
489
|
+
function groupByRank(nodes, ranks) {
|
|
490
|
+
const byRank = new Map;
|
|
491
|
+
for (const node of nodes) {
|
|
492
|
+
const r = ranks.get(node.id) ?? 0;
|
|
493
|
+
if (!byRank.has(r))
|
|
494
|
+
byRank.set(r, []);
|
|
495
|
+
byRank.get(r).push(node);
|
|
496
|
+
}
|
|
497
|
+
return byRank;
|
|
498
|
+
}
|
|
499
|
+
function boxWidthFor(node, padding, minWidth) {
|
|
500
|
+
const label = node.label ?? node.id;
|
|
501
|
+
const contentWidth = label.length;
|
|
502
|
+
return Math.max(minWidth, contentWidth + padding * 2 + 2);
|
|
503
|
+
}
|
|
504
|
+
function findNode(layout, id) {
|
|
505
|
+
return layout.nodes.find((n) => n.id === id);
|
|
506
|
+
}
|
|
507
|
+
// src/flow/render.ts
|
|
508
|
+
var BOX = {
|
|
509
|
+
topLeft: "┌",
|
|
510
|
+
topRight: "┐",
|
|
511
|
+
botLeft: "└",
|
|
512
|
+
botRight: "┘",
|
|
513
|
+
horiz: "─",
|
|
514
|
+
vert: "│",
|
|
515
|
+
arrowDown: "▼",
|
|
516
|
+
arrowRight: "►",
|
|
517
|
+
junctionDown: "┬",
|
|
518
|
+
junctionUp: "┴",
|
|
519
|
+
junctionLeft: "┤",
|
|
520
|
+
junctionRight: "├",
|
|
521
|
+
cross: "┼"
|
|
522
|
+
};
|
|
523
|
+
var COLOR = {
|
|
524
|
+
boxFrame: "gray",
|
|
525
|
+
label: "cyan",
|
|
526
|
+
optional: "yellow",
|
|
527
|
+
arrow: "gray",
|
|
528
|
+
title: "magenta",
|
|
529
|
+
description: "gray"
|
|
530
|
+
};
|
|
531
|
+
function renderFlow(workflow, opts = {}) {
|
|
532
|
+
const useColor = opts.color ?? true;
|
|
533
|
+
const showDescriptions = opts.showDescriptions ?? true;
|
|
534
|
+
const compact = opts.compact ?? false;
|
|
535
|
+
const layout = computeLayout(workflow);
|
|
536
|
+
const titleLines = buildTitle(workflow, showDescriptions && !compact);
|
|
537
|
+
const titleOffset = titleLines.length;
|
|
538
|
+
const flowHeight = layout.totalHeight;
|
|
539
|
+
const legendLines = compact ? [] : buildLegend(workflow);
|
|
540
|
+
const legendOffset = legendLines.length > 0 ? legendLines.length + 1 : 0;
|
|
541
|
+
const totalHeight = titleOffset + flowHeight + legendOffset + 1;
|
|
542
|
+
const totalWidth = Math.max(layout.totalWidth, maxLineLength(titleLines), maxLineLength(legendLines)) + 2;
|
|
543
|
+
const grid = createGrid(totalWidth);
|
|
544
|
+
for (let i = 0;i < totalHeight; i++)
|
|
545
|
+
grid.addRow();
|
|
546
|
+
let row = 0;
|
|
547
|
+
for (const line of titleLines) {
|
|
548
|
+
writeText(grid, 0, row, line.text, useColor ? line.color : null);
|
|
549
|
+
row++;
|
|
550
|
+
}
|
|
551
|
+
for (const node of layout.nodes) {
|
|
552
|
+
drawBox(grid, node, row + node.y, useColor);
|
|
553
|
+
}
|
|
554
|
+
for (const edge of layout.edges) {
|
|
555
|
+
const from = findNode(layout, edge.from);
|
|
556
|
+
const to = findNode(layout, edge.to);
|
|
557
|
+
if (!from || !to)
|
|
558
|
+
continue;
|
|
559
|
+
drawEdge(grid, from, to, row, useColor);
|
|
560
|
+
}
|
|
561
|
+
if (legendLines.length > 0) {
|
|
562
|
+
let legendRow = row + flowHeight + 1;
|
|
563
|
+
for (const line of legendLines) {
|
|
564
|
+
writeText(grid, 0, legendRow, line.text, useColor ? line.color : null);
|
|
565
|
+
legendRow++;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return grid;
|
|
569
|
+
}
|
|
570
|
+
function buildTitle(workflow, showDescription) {
|
|
571
|
+
const lines = [];
|
|
572
|
+
const title = `${workflow.cli} · ${workflow.name}`;
|
|
573
|
+
lines.push({ text: title, color: COLOR.title });
|
|
574
|
+
if (showDescription && workflow.description) {
|
|
575
|
+
lines.push({ text: workflow.description, color: COLOR.description });
|
|
576
|
+
}
|
|
577
|
+
lines.push({ text: "", color: null });
|
|
578
|
+
return lines;
|
|
579
|
+
}
|
|
580
|
+
function buildLegend(workflow) {
|
|
581
|
+
const hasOptional = workflow.nodes.some((n) => n.optional);
|
|
582
|
+
if (!hasOptional)
|
|
583
|
+
return [];
|
|
584
|
+
return [
|
|
585
|
+
{ text: " ─── required ┈┈┈ optional", color: COLOR.description }
|
|
586
|
+
];
|
|
587
|
+
}
|
|
588
|
+
function maxLineLength(lines) {
|
|
589
|
+
return lines.reduce((max, l) => Math.max(max, l.text.length), 0);
|
|
590
|
+
}
|
|
591
|
+
function drawBox(grid, node, rowOffset, useColor) {
|
|
592
|
+
const y = rowOffset;
|
|
593
|
+
const x = node.x;
|
|
594
|
+
const w = node.width;
|
|
595
|
+
const label = node.node.label ?? node.node.id;
|
|
596
|
+
const isOptional = node.node.optional;
|
|
597
|
+
const labelColor = useColor ? isOptional ? COLOR.optional : COLOR.label : null;
|
|
598
|
+
const frameColor = useColor ? COLOR.boxFrame : null;
|
|
599
|
+
const horizChar = isOptional ? "┈" : BOX.horiz;
|
|
600
|
+
const vertChar = isOptional ? "┊" : BOX.vert;
|
|
601
|
+
const tlChar = isOptional ? "┌" : BOX.topLeft;
|
|
602
|
+
const trChar = isOptional ? "┐" : BOX.topRight;
|
|
603
|
+
const blChar = isOptional ? "└" : BOX.botLeft;
|
|
604
|
+
const brChar = isOptional ? "┘" : BOX.botRight;
|
|
605
|
+
writeText(grid, x, y, tlChar, frameColor);
|
|
606
|
+
for (let i = 1;i < w - 1; i++) {
|
|
607
|
+
writeText(grid, x + i, y, horizChar, frameColor);
|
|
608
|
+
}
|
|
609
|
+
writeText(grid, x + w - 1, y, trChar, frameColor);
|
|
610
|
+
writeText(grid, x, y + 1, vertChar, frameColor);
|
|
611
|
+
const labelStart = x + Math.floor((w - label.length) / 2);
|
|
612
|
+
writeText(grid, labelStart, y + 1, label, labelColor);
|
|
613
|
+
writeText(grid, x + w - 1, y + 1, vertChar, frameColor);
|
|
614
|
+
writeText(grid, x, y + 2, blChar, frameColor);
|
|
615
|
+
for (let i = 1;i < w - 1; i++) {
|
|
616
|
+
writeText(grid, x + i, y + 2, horizChar, frameColor);
|
|
617
|
+
}
|
|
618
|
+
writeText(grid, x + w - 1, y + 2, brChar, frameColor);
|
|
619
|
+
}
|
|
620
|
+
function drawEdge(grid, from, to, rowOffset, useColor) {
|
|
621
|
+
const color = useColor ? COLOR.arrow : null;
|
|
622
|
+
const fromCenterX = from.x + Math.floor(from.width / 2);
|
|
623
|
+
const toCenterX = to.x + Math.floor(to.width / 2);
|
|
624
|
+
const fromBottomY = rowOffset + from.y + from.height - 1;
|
|
625
|
+
const toTopY = rowOffset + to.y;
|
|
626
|
+
if (to.row > from.row) {
|
|
627
|
+
if (fromCenterX === toCenterX) {
|
|
628
|
+
for (let y = fromBottomY + 1;y < toTopY - 1; y++) {
|
|
629
|
+
writeText(grid, fromCenterX, y, BOX.vert, color);
|
|
630
|
+
}
|
|
631
|
+
writeText(grid, fromCenterX, toTopY - 1, BOX.arrowDown, color);
|
|
632
|
+
} else {
|
|
633
|
+
const midY = fromBottomY + 1;
|
|
634
|
+
writeText(grid, fromCenterX, midY, BOX.vert, color);
|
|
635
|
+
const bendY = midY + 1;
|
|
636
|
+
const startX = Math.min(fromCenterX, toCenterX);
|
|
637
|
+
const endX = Math.max(fromCenterX, toCenterX);
|
|
638
|
+
for (let x = startX + 1;x < endX; x++) {
|
|
639
|
+
writeText(grid, x, bendY, BOX.horiz, color);
|
|
640
|
+
}
|
|
641
|
+
if (fromCenterX < toCenterX) {
|
|
642
|
+
writeText(grid, fromCenterX, bendY, "└", color);
|
|
643
|
+
writeText(grid, toCenterX, bendY, "┐", color);
|
|
644
|
+
} else {
|
|
645
|
+
writeText(grid, fromCenterX, bendY, "┘", color);
|
|
646
|
+
writeText(grid, toCenterX, bendY, "┌", color);
|
|
647
|
+
}
|
|
648
|
+
for (let y = bendY + 1;y < toTopY - 1; y++) {
|
|
649
|
+
writeText(grid, toCenterX, y, BOX.vert, color);
|
|
650
|
+
}
|
|
651
|
+
writeText(grid, toCenterX, toTopY - 1, BOX.arrowDown, color);
|
|
652
|
+
}
|
|
653
|
+
} else if (to.row === from.row) {
|
|
654
|
+
const y = rowOffset + from.y + Math.floor(from.height / 2);
|
|
655
|
+
const startX = from.x + from.width;
|
|
656
|
+
const endX = to.x;
|
|
657
|
+
for (let x = startX;x < endX - 1; x++) {
|
|
658
|
+
writeText(grid, x, y, BOX.horiz, color);
|
|
659
|
+
}
|
|
660
|
+
writeText(grid, endX - 1, y, BOX.arrowRight, color);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
// src/flow/encode.ts
|
|
664
|
+
function flowToAnsi(workflow, opts) {
|
|
665
|
+
return encodeAnsi(renderFlow(workflow, opts));
|
|
666
|
+
}
|
|
667
|
+
function flowToString(workflow, opts) {
|
|
668
|
+
return encodeString(renderFlow(workflow, opts));
|
|
669
|
+
}
|
|
670
|
+
function flowToHtml(workflow, opts) {
|
|
671
|
+
return encodeHtml(renderFlow(workflow, opts));
|
|
672
|
+
}
|
|
673
|
+
function printFlow(workflow, opts) {
|
|
674
|
+
console.log(flowToAnsi(workflow, opts));
|
|
675
|
+
}
|
|
676
|
+
export { parseYaml, parseWorkflow, validateWorkflow, computeLayout, renderFlow, flowToAnsi, flowToString, flowToHtml, printFlow };
|
|
677
|
+
|
|
678
|
+
//# debugId=C94663974D261F8E64756E2164756E21
|