archbyte 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/README.md +282 -0
- package/bin/archbyte.js +213 -0
- package/dist/agents/core/component-detector.d.ts +2 -0
- package/dist/agents/core/component-detector.js +57 -0
- package/dist/agents/core/connection-mapper.d.ts +2 -0
- package/dist/agents/core/connection-mapper.js +77 -0
- package/dist/agents/core/doc-parser.d.ts +2 -0
- package/dist/agents/core/doc-parser.js +64 -0
- package/dist/agents/core/env-detector.d.ts +2 -0
- package/dist/agents/core/env-detector.js +51 -0
- package/dist/agents/core/event-detector.d.ts +2 -0
- package/dist/agents/core/event-detector.js +59 -0
- package/dist/agents/core/infra-analyzer.d.ts +2 -0
- package/dist/agents/core/infra-analyzer.js +72 -0
- package/dist/agents/core/structure-scanner.d.ts +2 -0
- package/dist/agents/core/structure-scanner.js +55 -0
- package/dist/agents/core/validator.d.ts +2 -0
- package/dist/agents/core/validator.js +74 -0
- package/dist/agents/index.d.ts +24 -0
- package/dist/agents/index.js +73 -0
- package/dist/agents/llm/index.d.ts +8 -0
- package/dist/agents/llm/index.js +185 -0
- package/dist/agents/llm/prompt-builder.d.ts +3 -0
- package/dist/agents/llm/prompt-builder.js +251 -0
- package/dist/agents/llm/response-parser.d.ts +6 -0
- package/dist/agents/llm/response-parser.js +174 -0
- package/dist/agents/llm/types.d.ts +31 -0
- package/dist/agents/llm/types.js +2 -0
- package/dist/agents/pipeline/agents/component-identifier.d.ts +3 -0
- package/dist/agents/pipeline/agents/component-identifier.js +102 -0
- package/dist/agents/pipeline/agents/connection-mapper.d.ts +3 -0
- package/dist/agents/pipeline/agents/connection-mapper.js +126 -0
- package/dist/agents/pipeline/agents/flow-detector.d.ts +3 -0
- package/dist/agents/pipeline/agents/flow-detector.js +101 -0
- package/dist/agents/pipeline/agents/service-describer.d.ts +3 -0
- package/dist/agents/pipeline/agents/service-describer.js +100 -0
- package/dist/agents/pipeline/agents/validator.d.ts +3 -0
- package/dist/agents/pipeline/agents/validator.js +102 -0
- package/dist/agents/pipeline/index.d.ts +13 -0
- package/dist/agents/pipeline/index.js +128 -0
- package/dist/agents/pipeline/merger.d.ts +7 -0
- package/dist/agents/pipeline/merger.js +212 -0
- package/dist/agents/pipeline/response-parser.d.ts +5 -0
- package/dist/agents/pipeline/response-parser.js +43 -0
- package/dist/agents/pipeline/types.d.ts +92 -0
- package/dist/agents/pipeline/types.js +3 -0
- package/dist/agents/prompt-data.d.ts +1 -0
- package/dist/agents/prompt-data.js +15 -0
- package/dist/agents/prompts-encode.d.ts +9 -0
- package/dist/agents/prompts-encode.js +26 -0
- package/dist/agents/prompts.d.ts +12 -0
- package/dist/agents/prompts.js +30 -0
- package/dist/agents/providers/anthropic.d.ts +10 -0
- package/dist/agents/providers/anthropic.js +117 -0
- package/dist/agents/providers/google.d.ts +10 -0
- package/dist/agents/providers/google.js +136 -0
- package/dist/agents/providers/ollama.d.ts +9 -0
- package/dist/agents/providers/ollama.js +162 -0
- package/dist/agents/providers/openai.d.ts +9 -0
- package/dist/agents/providers/openai.js +142 -0
- package/dist/agents/providers/router.d.ts +7 -0
- package/dist/agents/providers/router.js +55 -0
- package/dist/agents/runtime/orchestrator.d.ts +34 -0
- package/dist/agents/runtime/orchestrator.js +193 -0
- package/dist/agents/runtime/registry.d.ts +23 -0
- package/dist/agents/runtime/registry.js +56 -0
- package/dist/agents/runtime/types.d.ts +117 -0
- package/dist/agents/runtime/types.js +29 -0
- package/dist/agents/static/code-sampler.d.ts +3 -0
- package/dist/agents/static/code-sampler.js +153 -0
- package/dist/agents/static/component-detector.d.ts +3 -0
- package/dist/agents/static/component-detector.js +404 -0
- package/dist/agents/static/connection-mapper.d.ts +3 -0
- package/dist/agents/static/connection-mapper.js +280 -0
- package/dist/agents/static/doc-parser.d.ts +3 -0
- package/dist/agents/static/doc-parser.js +358 -0
- package/dist/agents/static/env-detector.d.ts +3 -0
- package/dist/agents/static/env-detector.js +73 -0
- package/dist/agents/static/event-detector.d.ts +3 -0
- package/dist/agents/static/event-detector.js +70 -0
- package/dist/agents/static/file-tree-collector.d.ts +3 -0
- package/dist/agents/static/file-tree-collector.js +51 -0
- package/dist/agents/static/index.d.ts +19 -0
- package/dist/agents/static/index.js +307 -0
- package/dist/agents/static/infra-analyzer.d.ts +3 -0
- package/dist/agents/static/infra-analyzer.js +208 -0
- package/dist/agents/static/structure-scanner.d.ts +3 -0
- package/dist/agents/static/structure-scanner.js +195 -0
- package/dist/agents/static/types.d.ts +165 -0
- package/dist/agents/static/types.js +2 -0
- package/dist/agents/static/utils.d.ts +21 -0
- package/dist/agents/static/utils.js +146 -0
- package/dist/agents/static/validator.d.ts +2 -0
- package/dist/agents/static/validator.js +75 -0
- package/dist/agents/tools/claude-code.d.ts +38 -0
- package/dist/agents/tools/claude-code.js +129 -0
- package/dist/agents/tools/local-fs.d.ts +12 -0
- package/dist/agents/tools/local-fs.js +112 -0
- package/dist/agents/tools/tool-definitions.d.ts +6 -0
- package/dist/agents/tools/tool-definitions.js +66 -0
- package/dist/cli/analyze.d.ts +27 -0
- package/dist/cli/analyze.js +586 -0
- package/dist/cli/auth.d.ts +46 -0
- package/dist/cli/auth.js +397 -0
- package/dist/cli/config.d.ts +11 -0
- package/dist/cli/config.js +177 -0
- package/dist/cli/diff.d.ts +10 -0
- package/dist/cli/diff.js +144 -0
- package/dist/cli/export.d.ts +10 -0
- package/dist/cli/export.js +321 -0
- package/dist/cli/gate.d.ts +13 -0
- package/dist/cli/gate.js +131 -0
- package/dist/cli/generate.d.ts +10 -0
- package/dist/cli/generate.js +213 -0
- package/dist/cli/license-gate.d.ts +27 -0
- package/dist/cli/license-gate.js +121 -0
- package/dist/cli/patrol.d.ts +15 -0
- package/dist/cli/patrol.js +212 -0
- package/dist/cli/run.d.ts +11 -0
- package/dist/cli/run.js +24 -0
- package/dist/cli/serve.d.ts +9 -0
- package/dist/cli/serve.js +65 -0
- package/dist/cli/setup.d.ts +1 -0
- package/dist/cli/setup.js +233 -0
- package/dist/cli/shared.d.ts +68 -0
- package/dist/cli/shared.js +275 -0
- package/dist/cli/stats.d.ts +9 -0
- package/dist/cli/stats.js +158 -0
- package/dist/cli/ui.d.ts +18 -0
- package/dist/cli/ui.js +144 -0
- package/dist/cli/validate.d.ts +54 -0
- package/dist/cli/validate.js +315 -0
- package/dist/cli/workflow.d.ts +10 -0
- package/dist/cli/workflow.js +594 -0
- package/dist/server/src/generator/index.d.ts +123 -0
- package/dist/server/src/generator/index.js +254 -0
- package/dist/server/src/index.d.ts +8 -0
- package/dist/server/src/index.js +1311 -0
- package/package.json +62 -0
- package/ui/dist/assets/index-B66Til39.js +70 -0
- package/ui/dist/assets/index-BE2OWbzu.css +1 -0
- package/ui/dist/index.html +14 -0
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
const WORKFLOWS_DIR = ".archbyte/workflows";
|
|
5
|
+
const STATE_DIR = ".archbyte/workflows/.state";
|
|
6
|
+
// ── Built-in workflows ──
|
|
7
|
+
const BUILTIN_WORKFLOWS = [
|
|
8
|
+
{
|
|
9
|
+
id: "full-analysis",
|
|
10
|
+
name: "Full Analysis Pipeline",
|
|
11
|
+
description: "Complete architecture pipeline: analyze, generate, validate, and report",
|
|
12
|
+
steps: [
|
|
13
|
+
{
|
|
14
|
+
id: "generate",
|
|
15
|
+
name: "Generate Diagram",
|
|
16
|
+
command: "archbyte generate",
|
|
17
|
+
needs: [],
|
|
18
|
+
description: "Generate architecture diagram from analysis JSON",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "validate",
|
|
22
|
+
name: "Validate Architecture",
|
|
23
|
+
command: "archbyte validate",
|
|
24
|
+
needs: ["generate"],
|
|
25
|
+
description: "Run fitness function rules against the architecture",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "stats",
|
|
29
|
+
name: "Architecture Stats",
|
|
30
|
+
command: "archbyte stats",
|
|
31
|
+
needs: ["generate"],
|
|
32
|
+
description: "Display architecture health metrics",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "export",
|
|
36
|
+
name: "Export Mermaid",
|
|
37
|
+
command: "archbyte export --format mermaid",
|
|
38
|
+
needs: ["generate"],
|
|
39
|
+
description: "Export architecture as Mermaid diagram",
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "ci-check",
|
|
45
|
+
name: "CI Architecture Check",
|
|
46
|
+
description: "Lightweight CI pipeline: validate and diff against baseline",
|
|
47
|
+
steps: [
|
|
48
|
+
{
|
|
49
|
+
id: "validate",
|
|
50
|
+
name: "Validate",
|
|
51
|
+
command: "archbyte validate --ci",
|
|
52
|
+
needs: [],
|
|
53
|
+
description: "Run validation in CI mode (JSON output, exit code on failure)",
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "drift-check",
|
|
59
|
+
name: "Architecture Drift Check",
|
|
60
|
+
description: "Check for architecture drift by comparing against a baseline snapshot",
|
|
61
|
+
steps: [
|
|
62
|
+
{
|
|
63
|
+
id: "snapshot",
|
|
64
|
+
name: "Snapshot Baseline",
|
|
65
|
+
command: "cp .archbyte/architecture.json .archbyte/baseline.json",
|
|
66
|
+
needs: [],
|
|
67
|
+
description: "Save current architecture as baseline for drift detection",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "generate",
|
|
71
|
+
name: "Regenerate",
|
|
72
|
+
command: "archbyte generate",
|
|
73
|
+
needs: ["snapshot"],
|
|
74
|
+
description: "Regenerate architecture from latest analysis",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "diff",
|
|
78
|
+
name: "Diff",
|
|
79
|
+
command: "archbyte diff --baseline .archbyte/baseline.json",
|
|
80
|
+
needs: ["generate"],
|
|
81
|
+
description: "Compare regenerated architecture against baseline",
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
// ── YAML parser for workflow files ──
|
|
87
|
+
function parseWorkflowYaml(content, filePath) {
|
|
88
|
+
const lines = content.split("\n");
|
|
89
|
+
const def = { steps: [] };
|
|
90
|
+
let inSteps = false;
|
|
91
|
+
let currentStep = null;
|
|
92
|
+
const flushStep = () => {
|
|
93
|
+
if (currentStep?.id && currentStep.name && currentStep.command) {
|
|
94
|
+
def.steps.push({
|
|
95
|
+
id: currentStep.id,
|
|
96
|
+
name: currentStep.name,
|
|
97
|
+
command: currentStep.command,
|
|
98
|
+
needs: currentStep.needs || [],
|
|
99
|
+
description: currentStep.description,
|
|
100
|
+
retries: currentStep.retries,
|
|
101
|
+
continueOnFail: currentStep.continueOnFail,
|
|
102
|
+
compensation: currentStep.compensation,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
currentStep = null;
|
|
106
|
+
};
|
|
107
|
+
for (const line of lines) {
|
|
108
|
+
const trimmed = line.trimEnd();
|
|
109
|
+
// Top-level keys
|
|
110
|
+
const topMatch = trimmed.match(/^(\w+):\s*(.*)$/);
|
|
111
|
+
if (topMatch && !inSteps) {
|
|
112
|
+
const [, key, val] = topMatch;
|
|
113
|
+
if (key === "id")
|
|
114
|
+
def.id = val.replace(/^["']|["']$/g, "");
|
|
115
|
+
if (key === "name")
|
|
116
|
+
def.name = val.replace(/^["']|["']$/g, "");
|
|
117
|
+
if (key === "description")
|
|
118
|
+
def.description = val.replace(/^["']|["']$/g, "");
|
|
119
|
+
if (key === "steps") {
|
|
120
|
+
inSteps = true;
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!inSteps)
|
|
125
|
+
continue;
|
|
126
|
+
// Exit steps on new top-level key
|
|
127
|
+
if (/^\w/.test(trimmed) && trimmed !== "" && !trimmed.startsWith("#")) {
|
|
128
|
+
flushStep();
|
|
129
|
+
inSteps = false;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (trimmed === "" || trimmed.trim().startsWith("#"))
|
|
133
|
+
continue;
|
|
134
|
+
// New step item: " - id: xxx"
|
|
135
|
+
const itemMatch = trimmed.match(/^ {2}- id:\s*(.+)$/);
|
|
136
|
+
if (itemMatch) {
|
|
137
|
+
flushStep();
|
|
138
|
+
currentStep = { id: itemMatch[1].replace(/^["']|["']$/g, ""), needs: [] };
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (!currentStep)
|
|
142
|
+
continue;
|
|
143
|
+
// Step properties
|
|
144
|
+
const propMatch = trimmed.match(/^ {4}(\w+):\s*(.+)$/);
|
|
145
|
+
if (propMatch) {
|
|
146
|
+
const [, key, val] = propMatch;
|
|
147
|
+
const cleanVal = val.replace(/^["']|["']$/g, "");
|
|
148
|
+
if (key === "name")
|
|
149
|
+
currentStep.name = cleanVal;
|
|
150
|
+
if (key === "command")
|
|
151
|
+
currentStep.command = cleanVal;
|
|
152
|
+
if (key === "description")
|
|
153
|
+
currentStep.description = cleanVal;
|
|
154
|
+
if (key === "needs") {
|
|
155
|
+
// Parse: [step1, step2] or [] or step1
|
|
156
|
+
if (val.trim() === "[]") {
|
|
157
|
+
currentStep.needs = [];
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
const bracketMatch = val.match(/^\[(.+)\]$/);
|
|
161
|
+
if (bracketMatch) {
|
|
162
|
+
currentStep.needs = bracketMatch[1]
|
|
163
|
+
.split(",")
|
|
164
|
+
.map((s) => s.trim().replace(/^["']|["']$/g, ""));
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
currentStep.needs = [cleanVal];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (key === "retries")
|
|
172
|
+
currentStep.retries = parseInt(cleanVal, 10) || 0;
|
|
173
|
+
if (key === "continueOnFail")
|
|
174
|
+
currentStep.continueOnFail = cleanVal === "true";
|
|
175
|
+
if (key === "compensation")
|
|
176
|
+
currentStep.compensation = cleanVal;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
flushStep();
|
|
180
|
+
if (!def.id || !def.name || !def.steps || def.steps.length === 0) {
|
|
181
|
+
console.error(chalk.yellow(` Warning: Invalid workflow file: ${filePath}`));
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
return def;
|
|
185
|
+
}
|
|
186
|
+
// ── Workflow loading ──
|
|
187
|
+
function loadWorkflows() {
|
|
188
|
+
const workflows = [...BUILTIN_WORKFLOWS];
|
|
189
|
+
const dir = path.join(process.cwd(), WORKFLOWS_DIR);
|
|
190
|
+
if (fs.existsSync(dir)) {
|
|
191
|
+
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
|
|
192
|
+
for (const file of files) {
|
|
193
|
+
const content = fs.readFileSync(path.join(dir, file), "utf-8");
|
|
194
|
+
const def = parseWorkflowYaml(content, file);
|
|
195
|
+
if (def) {
|
|
196
|
+
// Custom workflows override built-ins with the same id
|
|
197
|
+
const idx = workflows.findIndex((w) => w.id === def.id);
|
|
198
|
+
if (idx >= 0)
|
|
199
|
+
workflows[idx] = def;
|
|
200
|
+
else
|
|
201
|
+
workflows.push(def);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return workflows;
|
|
206
|
+
}
|
|
207
|
+
function findWorkflow(id) {
|
|
208
|
+
const workflows = loadWorkflows();
|
|
209
|
+
return workflows.find((w) => w.id === id) || null;
|
|
210
|
+
}
|
|
211
|
+
// ── State management ──
|
|
212
|
+
function ensureStateDir() {
|
|
213
|
+
const dir = path.join(process.cwd(), STATE_DIR);
|
|
214
|
+
if (!fs.existsSync(dir)) {
|
|
215
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
216
|
+
}
|
|
217
|
+
return dir;
|
|
218
|
+
}
|
|
219
|
+
function statePath(workflowId) {
|
|
220
|
+
return path.join(ensureStateDir(), `${workflowId}.json`);
|
|
221
|
+
}
|
|
222
|
+
function loadState(workflowId) {
|
|
223
|
+
const p = statePath(workflowId);
|
|
224
|
+
if (!fs.existsSync(p))
|
|
225
|
+
return null;
|
|
226
|
+
try {
|
|
227
|
+
return JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function saveState(state) {
|
|
234
|
+
fs.writeFileSync(statePath(state.workflowId), JSON.stringify(state, null, 2), "utf-8");
|
|
235
|
+
}
|
|
236
|
+
function clearState(workflowId) {
|
|
237
|
+
const p = statePath(workflowId);
|
|
238
|
+
if (fs.existsSync(p))
|
|
239
|
+
fs.unlinkSync(p);
|
|
240
|
+
}
|
|
241
|
+
// ── Topological sort ──
|
|
242
|
+
function topoSort(steps) {
|
|
243
|
+
const stepMap = new Map(steps.map((s) => [s.id, s]));
|
|
244
|
+
const visited = new Set();
|
|
245
|
+
const sorted = [];
|
|
246
|
+
function visit(id) {
|
|
247
|
+
if (visited.has(id))
|
|
248
|
+
return;
|
|
249
|
+
visited.add(id);
|
|
250
|
+
const step = stepMap.get(id);
|
|
251
|
+
if (!step)
|
|
252
|
+
return;
|
|
253
|
+
for (const dep of step.needs) {
|
|
254
|
+
visit(dep);
|
|
255
|
+
}
|
|
256
|
+
sorted.push(step);
|
|
257
|
+
}
|
|
258
|
+
for (const step of steps) {
|
|
259
|
+
visit(step.id);
|
|
260
|
+
}
|
|
261
|
+
return sorted;
|
|
262
|
+
}
|
|
263
|
+
// ── Execution ──
|
|
264
|
+
async function executeStep(step) {
|
|
265
|
+
const { execSync } = await import("child_process");
|
|
266
|
+
const maxRetries = step.retries ?? 0;
|
|
267
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
268
|
+
try {
|
|
269
|
+
execSync(step.command, {
|
|
270
|
+
cwd: process.cwd(),
|
|
271
|
+
stdio: "inherit",
|
|
272
|
+
env: { ...process.env },
|
|
273
|
+
});
|
|
274
|
+
return { success: true };
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
278
|
+
if (attempt < maxRetries) {
|
|
279
|
+
console.log(chalk.yellow(` Retry ${attempt + 1}/${maxRetries}...`));
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
return { success: false, error: msg };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return { success: false, error: "Exhausted retries" };
|
|
286
|
+
}
|
|
287
|
+
async function executeCompensation(step) {
|
|
288
|
+
if (!step.compensation)
|
|
289
|
+
return;
|
|
290
|
+
const { execSync } = await import("child_process");
|
|
291
|
+
console.log(chalk.yellow(` [comp] Running compensation for ${step.name}`));
|
|
292
|
+
console.log(chalk.gray(` $ ${step.compensation}`));
|
|
293
|
+
try {
|
|
294
|
+
execSync(step.compensation, {
|
|
295
|
+
cwd: process.cwd(),
|
|
296
|
+
stdio: "inherit",
|
|
297
|
+
env: { ...process.env },
|
|
298
|
+
});
|
|
299
|
+
console.log(chalk.yellow(` [comp] Compensation completed for ${step.name}`));
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
console.log(chalk.red(` [comp] Compensation failed for ${step.name}`));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async function runWorkflow(workflow) {
|
|
306
|
+
const sorted = topoSort(workflow.steps);
|
|
307
|
+
// Check for existing state (resume support)
|
|
308
|
+
let state = loadState(workflow.id);
|
|
309
|
+
const isResume = state !== null && (state.status === "running" || state.status === "failed");
|
|
310
|
+
if (!state || state.status === "completed") {
|
|
311
|
+
state = {
|
|
312
|
+
workflowId: workflow.id,
|
|
313
|
+
startedAt: new Date().toISOString(),
|
|
314
|
+
status: "running",
|
|
315
|
+
steps: {},
|
|
316
|
+
};
|
|
317
|
+
for (const step of sorted) {
|
|
318
|
+
state.steps[step.id] = { status: "pending" };
|
|
319
|
+
}
|
|
320
|
+
saveState(state);
|
|
321
|
+
}
|
|
322
|
+
else if (state.status === "failed") {
|
|
323
|
+
// Reset failed steps to pending so they can be retried
|
|
324
|
+
state.status = "running";
|
|
325
|
+
for (const [id, stepState] of Object.entries(state.steps)) {
|
|
326
|
+
if (stepState.status === "failed" || stepState.status === "running") {
|
|
327
|
+
state.steps[id] = { status: "pending" };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
saveState(state);
|
|
331
|
+
}
|
|
332
|
+
if (isResume) {
|
|
333
|
+
console.log(chalk.yellow(" Resuming interrupted workflow..."));
|
|
334
|
+
const completed = Object.entries(state.steps)
|
|
335
|
+
.filter(([, s]) => s.status === "completed")
|
|
336
|
+
.map(([id]) => id);
|
|
337
|
+
if (completed.length > 0) {
|
|
338
|
+
console.log(chalk.gray(` Already completed: ${completed.join(", ")}`));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
console.log();
|
|
342
|
+
for (const step of sorted) {
|
|
343
|
+
const stepState = state.steps[step.id];
|
|
344
|
+
// Skip already completed steps (resume)
|
|
345
|
+
if (stepState?.status === "completed") {
|
|
346
|
+
console.log(chalk.green(` [done] ${step.name}`));
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
// Check dependencies
|
|
350
|
+
const unmetDeps = step.needs.filter((dep) => state.steps[dep]?.status !== "completed");
|
|
351
|
+
if (unmetDeps.length > 0) {
|
|
352
|
+
console.log(chalk.yellow(` [skip] ${step.name} — waiting on: ${unmetDeps.join(", ")}`));
|
|
353
|
+
state.steps[step.id] = { status: "pending" };
|
|
354
|
+
saveState(state);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
// Execute
|
|
358
|
+
console.log(chalk.cyan(` [run] ${step.name}`));
|
|
359
|
+
if (step.description) {
|
|
360
|
+
console.log(chalk.gray(` ${step.description}`));
|
|
361
|
+
}
|
|
362
|
+
console.log(chalk.gray(` $ ${step.command}`));
|
|
363
|
+
console.log();
|
|
364
|
+
state.steps[step.id] = {
|
|
365
|
+
status: "running",
|
|
366
|
+
startedAt: new Date().toISOString(),
|
|
367
|
+
};
|
|
368
|
+
saveState(state);
|
|
369
|
+
const result = await executeStep(step);
|
|
370
|
+
if (result.success) {
|
|
371
|
+
state.steps[step.id] = {
|
|
372
|
+
status: "completed",
|
|
373
|
+
startedAt: state.steps[step.id].startedAt,
|
|
374
|
+
completedAt: new Date().toISOString(),
|
|
375
|
+
};
|
|
376
|
+
saveState(state);
|
|
377
|
+
console.log();
|
|
378
|
+
console.log(chalk.green(` [done] ${step.name}`));
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
state.steps[step.id] = {
|
|
382
|
+
status: "failed",
|
|
383
|
+
startedAt: state.steps[step.id].startedAt,
|
|
384
|
+
error: result.error,
|
|
385
|
+
};
|
|
386
|
+
saveState(state);
|
|
387
|
+
console.log();
|
|
388
|
+
console.log(chalk.red(` [fail] ${step.name}`));
|
|
389
|
+
console.log(chalk.red(` ${result.error}`));
|
|
390
|
+
// Run compensation if defined
|
|
391
|
+
await executeCompensation(step);
|
|
392
|
+
// If continueOnFail is set, skip this step and continue the pipeline
|
|
393
|
+
if (step.continueOnFail) {
|
|
394
|
+
console.log(chalk.yellow(` [skip] Continuing despite failure (continueOnFail)`));
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
state.status = "failed";
|
|
398
|
+
saveState(state);
|
|
399
|
+
console.log();
|
|
400
|
+
console.log(chalk.yellow(` Workflow paused. Fix the issue and run again to resume.`));
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
console.log();
|
|
405
|
+
}
|
|
406
|
+
// All done
|
|
407
|
+
state.status = "completed";
|
|
408
|
+
saveState(state);
|
|
409
|
+
console.log(chalk.green.bold(" Workflow completed successfully!"));
|
|
410
|
+
console.log();
|
|
411
|
+
}
|
|
412
|
+
// ── Display ──
|
|
413
|
+
function listWorkflows() {
|
|
414
|
+
const workflows = loadWorkflows();
|
|
415
|
+
const projectName = process.cwd().split("/").pop() || "project";
|
|
416
|
+
console.log();
|
|
417
|
+
console.log(chalk.bold.cyan(` ArchByte Workflows — ${projectName}`));
|
|
418
|
+
console.log();
|
|
419
|
+
for (const w of workflows) {
|
|
420
|
+
const state = loadState(w.id);
|
|
421
|
+
let statusStr = chalk.gray("ready");
|
|
422
|
+
if (state?.status === "running")
|
|
423
|
+
statusStr = chalk.yellow("in progress");
|
|
424
|
+
if (state?.status === "completed")
|
|
425
|
+
statusStr = chalk.green("completed");
|
|
426
|
+
if (state?.status === "failed")
|
|
427
|
+
statusStr = chalk.red("failed");
|
|
428
|
+
const builtinTag = BUILTIN_WORKFLOWS.some((b) => b.id === w.id)
|
|
429
|
+
? chalk.gray(" (built-in)")
|
|
430
|
+
: "";
|
|
431
|
+
console.log(` ${chalk.bold(w.id)}${builtinTag} — ${statusStr}`);
|
|
432
|
+
console.log(chalk.gray(` ${w.description}`));
|
|
433
|
+
console.log(chalk.gray(` Steps: ${w.steps.map((s) => s.id).join(" → ")}`));
|
|
434
|
+
console.log();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function showWorkflow(id) {
|
|
438
|
+
const workflow = findWorkflow(id);
|
|
439
|
+
if (!workflow) {
|
|
440
|
+
console.error(chalk.red(` Workflow not found: ${id}`));
|
|
441
|
+
console.error(chalk.gray(" Run archbyte workflow --list to see available workflows"));
|
|
442
|
+
process.exit(1);
|
|
443
|
+
}
|
|
444
|
+
const state = loadState(id);
|
|
445
|
+
console.log();
|
|
446
|
+
console.log(chalk.bold.cyan(` Workflow: ${workflow.name}`));
|
|
447
|
+
console.log(chalk.gray(` ${workflow.description}`));
|
|
448
|
+
if (state) {
|
|
449
|
+
console.log(chalk.gray(` Started: ${state.startedAt}`));
|
|
450
|
+
console.log(chalk.gray(` Status: ${state.status}`));
|
|
451
|
+
}
|
|
452
|
+
console.log();
|
|
453
|
+
const sorted = topoSort(workflow.steps);
|
|
454
|
+
for (const step of sorted) {
|
|
455
|
+
const stepState = state?.steps[step.id];
|
|
456
|
+
let icon = chalk.gray("*");
|
|
457
|
+
let statusLabel = "";
|
|
458
|
+
if (stepState?.status === "completed") {
|
|
459
|
+
icon = chalk.green("done");
|
|
460
|
+
statusLabel = chalk.gray(` (${stepState.completedAt})`);
|
|
461
|
+
}
|
|
462
|
+
else if (stepState?.status === "running") {
|
|
463
|
+
icon = chalk.yellow(">>>");
|
|
464
|
+
}
|
|
465
|
+
else if (stepState?.status === "failed") {
|
|
466
|
+
icon = chalk.red("FAIL");
|
|
467
|
+
statusLabel = chalk.red(` — ${stepState.error?.slice(0, 60)}`);
|
|
468
|
+
}
|
|
469
|
+
const depsStr = step.needs.length > 0
|
|
470
|
+
? chalk.gray(` [needs: ${step.needs.join(", ")}]`)
|
|
471
|
+
: "";
|
|
472
|
+
console.log(` ${icon} ${chalk.bold(step.name)}${depsStr}${statusLabel}`);
|
|
473
|
+
console.log(chalk.gray(` $ ${step.command}`));
|
|
474
|
+
if (step.description) {
|
|
475
|
+
console.log(chalk.gray(` ${step.description}`));
|
|
476
|
+
}
|
|
477
|
+
console.log();
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function showStatus() {
|
|
481
|
+
const workflows = loadWorkflows();
|
|
482
|
+
const projectName = process.cwd().split("/").pop() || "project";
|
|
483
|
+
console.log();
|
|
484
|
+
console.log(chalk.bold.cyan(` Workflow Status — ${projectName}`));
|
|
485
|
+
console.log();
|
|
486
|
+
let anyActive = false;
|
|
487
|
+
for (const w of workflows) {
|
|
488
|
+
const state = loadState(w.id);
|
|
489
|
+
if (!state)
|
|
490
|
+
continue;
|
|
491
|
+
anyActive = true;
|
|
492
|
+
const completed = Object.values(state.steps).filter((s) => s.status === "completed").length;
|
|
493
|
+
const total = Object.keys(state.steps).length;
|
|
494
|
+
const pct = Math.round((completed / total) * 100);
|
|
495
|
+
const barLen = Math.round((completed / total) * 20);
|
|
496
|
+
const bar = chalk.green("=".repeat(barLen)) + chalk.gray("-".repeat(20 - barLen));
|
|
497
|
+
let statusIcon = chalk.yellow("running");
|
|
498
|
+
if (state.status === "completed")
|
|
499
|
+
statusIcon = chalk.green("done");
|
|
500
|
+
if (state.status === "failed")
|
|
501
|
+
statusIcon = chalk.red("failed");
|
|
502
|
+
console.log(` ${chalk.bold(w.id)} [${bar}] ${pct}% — ${statusIcon}`);
|
|
503
|
+
console.log(chalk.gray(` ${completed}/${total} steps complete`));
|
|
504
|
+
console.log();
|
|
505
|
+
}
|
|
506
|
+
if (!anyActive) {
|
|
507
|
+
console.log(chalk.gray(" No active workflows. Run archbyte workflow --run <id> to start one."));
|
|
508
|
+
console.log();
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function createWorkflowFile(name) {
|
|
512
|
+
const dir = path.join(process.cwd(), WORKFLOWS_DIR);
|
|
513
|
+
if (!fs.existsSync(dir)) {
|
|
514
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
515
|
+
}
|
|
516
|
+
const id = name.replace(/\s+/g, "-").toLowerCase();
|
|
517
|
+
const filePath = path.join(dir, `${id}.yaml`);
|
|
518
|
+
if (fs.existsSync(filePath)) {
|
|
519
|
+
console.error(chalk.red(` Workflow file already exists: ${filePath}`));
|
|
520
|
+
process.exit(1);
|
|
521
|
+
}
|
|
522
|
+
const template = `id: ${id}
|
|
523
|
+
name: "${name}"
|
|
524
|
+
description: "Custom architecture workflow"
|
|
525
|
+
|
|
526
|
+
steps:
|
|
527
|
+
- id: step-1
|
|
528
|
+
name: "First Step"
|
|
529
|
+
command: "echo 'Step 1'"
|
|
530
|
+
needs: []
|
|
531
|
+
description: "Replace with your command"
|
|
532
|
+
|
|
533
|
+
- id: step-2
|
|
534
|
+
name: "Second Step"
|
|
535
|
+
command: "echo 'Step 2'"
|
|
536
|
+
needs: [step-1]
|
|
537
|
+
description: "Runs after step-1 completes"
|
|
538
|
+
`;
|
|
539
|
+
fs.writeFileSync(filePath, template, "utf-8");
|
|
540
|
+
console.log(chalk.green(` Created workflow: ${path.relative(process.cwd(), filePath)}`));
|
|
541
|
+
console.log(chalk.gray(` Edit it to define your pipeline steps, then run:`));
|
|
542
|
+
console.log(chalk.cyan(` archbyte workflow --run ${id}`));
|
|
543
|
+
}
|
|
544
|
+
// ── Main handler ──
|
|
545
|
+
export async function handleWorkflow(options) {
|
|
546
|
+
if (options.list) {
|
|
547
|
+
listWorkflows();
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (options.show) {
|
|
551
|
+
showWorkflow(options.show);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (options.status) {
|
|
555
|
+
showStatus();
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (options.create) {
|
|
559
|
+
createWorkflowFile(options.create);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (options.reset) {
|
|
563
|
+
// Reset all workflow states
|
|
564
|
+
const stateDir = path.join(process.cwd(), STATE_DIR);
|
|
565
|
+
if (fs.existsSync(stateDir)) {
|
|
566
|
+
const files = fs.readdirSync(stateDir).filter((f) => f.endsWith(".json"));
|
|
567
|
+
for (const f of files) {
|
|
568
|
+
fs.unlinkSync(path.join(stateDir, f));
|
|
569
|
+
}
|
|
570
|
+
console.log(chalk.green(` Reset ${files.length} workflow state(s).`));
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
console.log(chalk.gray(" No workflow state to reset."));
|
|
574
|
+
}
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
if (options.run) {
|
|
578
|
+
const workflow = findWorkflow(options.run);
|
|
579
|
+
if (!workflow) {
|
|
580
|
+
console.error(chalk.red(` Workflow not found: ${options.run}`));
|
|
581
|
+
console.error(chalk.gray(" Run archbyte workflow --list to see available workflows"));
|
|
582
|
+
process.exit(1);
|
|
583
|
+
}
|
|
584
|
+
const projectName = process.cwd().split("/").pop() || "project";
|
|
585
|
+
console.log();
|
|
586
|
+
console.log(chalk.bold.cyan(` ArchByte Workflow: ${workflow.name} — ${projectName}`));
|
|
587
|
+
console.log(chalk.gray(` ${workflow.description}`));
|
|
588
|
+
console.log(chalk.gray(` Steps: ${workflow.steps.length}`));
|
|
589
|
+
await runWorkflow(workflow);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
// Default: show list
|
|
593
|
+
listWorkflows();
|
|
594
|
+
}
|