javi-forge 1.5.0 → 1.6.1
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 +191 -3
- package/ci-local/hooks/pre-push +17 -13
- package/dist/commands/analyze.d.ts +1 -1
- package/dist/commands/analyze.js +15 -15
- package/dist/commands/atlassian-mcp.d.ts +42 -0
- package/dist/commands/atlassian-mcp.js +98 -0
- package/dist/commands/ci.d.ts +3 -3
- package/dist/commands/ci.js +185 -147
- package/dist/commands/crash-recovery.d.ts +34 -0
- package/dist/commands/crash-recovery.js +123 -0
- package/dist/commands/doctor.d.ts +2 -2
- package/dist/commands/doctor.js +113 -61
- package/dist/commands/harness-audit.d.ts +35 -0
- package/dist/commands/harness-audit.js +277 -0
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +415 -118
- package/dist/commands/llmstxt.d.ts +1 -1
- package/dist/commands/llmstxt.js +36 -34
- package/dist/commands/parallel-batch.d.ts +42 -0
- package/dist/commands/parallel-batch.js +90 -0
- package/dist/commands/plugin.d.ts +26 -1
- package/dist/commands/plugin.js +138 -24
- package/dist/commands/secret-scanner.d.ts +30 -0
- package/dist/commands/secret-scanner.js +272 -0
- package/dist/commands/security-analysis.d.ts +74 -0
- package/dist/commands/security-analysis.js +487 -0
- package/dist/commands/security.d.ts +31 -0
- package/dist/commands/security.js +445 -0
- package/dist/commands/skill-scanner.d.ts +63 -0
- package/dist/commands/skill-scanner.js +383 -0
- package/dist/commands/skills.d.ts +139 -0
- package/dist/commands/skills.js +895 -0
- package/dist/commands/supply-chain.d.ts +23 -0
- package/dist/commands/supply-chain.js +126 -0
- package/dist/commands/tdd-pipeline.d.ts +17 -0
- package/dist/commands/tdd-pipeline.js +144 -0
- package/dist/commands/tdd.d.ts +21 -0
- package/dist/commands/tdd.js +120 -0
- package/dist/commands/team-presets.d.ts +53 -0
- package/dist/commands/team-presets.js +201 -0
- package/dist/commands/workflow.d.ts +23 -0
- package/dist/commands/workflow.js +114 -0
- package/dist/constants.d.ts +21 -0
- package/dist/constants.js +208 -37
- package/dist/index.js +400 -54
- package/dist/lib/agent-skills.d.ts +73 -0
- package/dist/lib/agent-skills.js +260 -0
- package/dist/lib/auto-skill-install.d.ts +37 -0
- package/dist/lib/auto-skill-install.js +92 -0
- package/dist/lib/auto-wire.d.ts +20 -0
- package/dist/lib/auto-wire.js +240 -0
- package/dist/lib/claudemd.d.ts +20 -0
- package/dist/lib/claudemd.js +222 -0
- package/dist/lib/codex-export.d.ts +16 -0
- package/dist/lib/codex-export.js +109 -0
- package/dist/lib/common.d.ts +1 -1
- package/dist/lib/common.js +52 -44
- package/dist/lib/context.d.ts +27 -0
- package/dist/lib/context.js +204 -0
- package/dist/lib/docker.d.ts +1 -1
- package/dist/lib/docker.js +141 -112
- package/dist/lib/frontmatter.d.ts +1 -1
- package/dist/lib/frontmatter.js +29 -15
- package/dist/lib/plugin.d.ts +19 -1
- package/dist/lib/plugin.js +174 -47
- package/dist/lib/skill-publish.d.ts +40 -0
- package/dist/lib/skill-publish.js +146 -0
- package/dist/lib/stack-detector.d.ts +38 -0
- package/dist/lib/stack-detector.js +207 -0
- package/dist/lib/template.d.ts +16 -1
- package/dist/lib/template.js +46 -17
- package/dist/lib/workflow/discovery.d.ts +19 -0
- package/dist/lib/workflow/discovery.js +68 -0
- package/dist/lib/workflow/index.d.ts +5 -0
- package/dist/lib/workflow/index.js +5 -0
- package/dist/lib/workflow/parser.d.ts +16 -0
- package/dist/lib/workflow/parser.js +198 -0
- package/dist/lib/workflow/renderer.d.ts +9 -0
- package/dist/lib/workflow/renderer.js +152 -0
- package/dist/lib/workflow/validator.d.ts +10 -0
- package/dist/lib/workflow/validator.js +189 -0
- package/dist/tasks/index.d.ts +4 -0
- package/dist/tasks/index.js +4 -0
- package/dist/tasks/scaffold-tasks.d.ts +3 -0
- package/dist/tasks/scaffold-tasks.js +14 -0
- package/dist/tasks/task-id.d.ts +30 -0
- package/dist/tasks/task-id.js +55 -0
- package/dist/tasks/task-tracker.d.ts +15 -0
- package/dist/tasks/task-tracker.js +81 -0
- package/dist/types/index.d.ts +252 -5
- package/dist/types/index.js +11 -1
- package/dist/ui/AnalyzeUI.d.ts +1 -1
- package/dist/ui/AnalyzeUI.js +38 -39
- package/dist/ui/App.d.ts +5 -3
- package/dist/ui/App.js +92 -46
- package/dist/ui/AutoSkills.d.ts +9 -0
- package/dist/ui/AutoSkills.js +124 -0
- package/dist/ui/CI.d.ts +2 -2
- package/dist/ui/CI.js +24 -26
- package/dist/ui/CIContext.d.ts +1 -1
- package/dist/ui/CIContext.js +3 -2
- package/dist/ui/CISelector.d.ts +2 -2
- package/dist/ui/CISelector.js +23 -15
- package/dist/ui/Doctor.d.ts +1 -1
- package/dist/ui/Doctor.js +35 -29
- package/dist/ui/Header.d.ts +1 -1
- package/dist/ui/Header.js +14 -14
- package/dist/ui/HookProfileSelector.d.ts +9 -0
- package/dist/ui/HookProfileSelector.js +54 -0
- package/dist/ui/LlmsTxt.d.ts +1 -1
- package/dist/ui/LlmsTxt.js +31 -22
- package/dist/ui/MemorySelector.d.ts +2 -2
- package/dist/ui/MemorySelector.js +28 -16
- package/dist/ui/NameInput.d.ts +1 -1
- package/dist/ui/NameInput.js +21 -21
- package/dist/ui/OptionSelector.d.ts +8 -2
- package/dist/ui/OptionSelector.js +83 -26
- package/dist/ui/Plugin.d.ts +4 -3
- package/dist/ui/Plugin.js +89 -29
- package/dist/ui/Progress.d.ts +3 -3
- package/dist/ui/Progress.js +23 -22
- package/dist/ui/Skills.d.ts +11 -0
- package/dist/ui/Skills.js +148 -0
- package/dist/ui/StackSelector.d.ts +2 -2
- package/dist/ui/StackSelector.js +26 -16
- package/dist/ui/Summary.d.ts +3 -3
- package/dist/ui/Summary.js +60 -50
- package/dist/ui/Welcome.d.ts +1 -1
- package/dist/ui/Welcome.js +15 -16
- package/dist/ui/theme.d.ts +1 -1
- package/dist/ui/theme.js +6 -6
- package/package.json +9 -6
- package/templates/common/atlassian/mcp-atlassian-snippet.json +16 -0
- package/templates/common/repoforge/mcp-repoforge-snippet.json +11 -0
- package/templates/common/repoforge/repoforge.yaml +34 -0
- package/templates/github/deploy-docker-zero-downtime.yml +140 -0
- package/templates/github/repoforge-graph.yml +45 -0
- package/templates/gitlab/deploy-docker-zero-downtime.yml +57 -0
- package/templates/local-ai/.env.example +17 -0
- package/templates/local-ai/docker-compose.yml +95 -0
- package/templates/security-hooks/claude-settings-security.json +30 -0
- package/templates/security-hooks/commit-msg-signing +29 -0
- package/templates/security-hooks/pre-commit-permissions +74 -0
- package/templates/security-hooks/pre-commit-secrets +74 -0
- package/templates/security-hooks/pre-push-branch-protection +62 -0
- package/templates/security-hooks/pre-push-deps +83 -0
- package/templates/security-hooks/pre-push-signing +67 -0
- package/templates/woodpecker/deploy-docker-zero-downtime.yml +50 -0
- package/templates/workflows/ci-pipeline.dot +15 -0
- package/templates/workflows/feature-flow.dot +21 -0
- package/templates/workflows/release.dot +16 -0
- package/dist/__integration__/helpers.d.ts +0 -20
- package/dist/__integration__/helpers.d.ts.map +0 -1
- package/dist/__integration__/helpers.js +0 -31
- package/dist/__integration__/helpers.js.map +0 -1
- package/dist/commands/analyze.d.ts.map +0 -1
- package/dist/commands/analyze.js.map +0 -1
- package/dist/commands/ci.d.ts.map +0 -1
- package/dist/commands/ci.js.map +0 -1
- package/dist/commands/doctor.d.ts.map +0 -1
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/llmstxt.d.ts.map +0 -1
- package/dist/commands/llmstxt.js.map +0 -1
- package/dist/commands/plugin.d.ts.map +0 -1
- package/dist/commands/plugin.js.map +0 -1
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lib/common.d.ts.map +0 -1
- package/dist/lib/common.js.map +0 -1
- package/dist/lib/docker.d.ts.map +0 -1
- package/dist/lib/docker.js.map +0 -1
- package/dist/lib/frontmatter.d.ts.map +0 -1
- package/dist/lib/frontmatter.js.map +0 -1
- package/dist/lib/plugin.d.ts.map +0 -1
- package/dist/lib/plugin.js.map +0 -1
- package/dist/lib/template.d.ts.map +0 -1
- package/dist/lib/template.js.map +0 -1
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/ui/AnalyzeUI.d.ts.map +0 -1
- package/dist/ui/AnalyzeUI.js.map +0 -1
- package/dist/ui/App.d.ts.map +0 -1
- package/dist/ui/App.js.map +0 -1
- package/dist/ui/CI.d.ts.map +0 -1
- package/dist/ui/CI.js.map +0 -1
- package/dist/ui/CIContext.d.ts.map +0 -1
- package/dist/ui/CIContext.js.map +0 -1
- package/dist/ui/CISelector.d.ts.map +0 -1
- package/dist/ui/CISelector.js.map +0 -1
- package/dist/ui/Doctor.d.ts.map +0 -1
- package/dist/ui/Doctor.js.map +0 -1
- package/dist/ui/Header.d.ts.map +0 -1
- package/dist/ui/Header.js.map +0 -1
- package/dist/ui/LlmsTxt.d.ts.map +0 -1
- package/dist/ui/LlmsTxt.js.map +0 -1
- package/dist/ui/MemorySelector.d.ts.map +0 -1
- package/dist/ui/MemorySelector.js.map +0 -1
- package/dist/ui/NameInput.d.ts.map +0 -1
- package/dist/ui/NameInput.js.map +0 -1
- package/dist/ui/OptionSelector.d.ts.map +0 -1
- package/dist/ui/OptionSelector.js.map +0 -1
- package/dist/ui/Plugin.d.ts.map +0 -1
- package/dist/ui/Plugin.js.map +0 -1
- package/dist/ui/Progress.d.ts.map +0 -1
- package/dist/ui/Progress.js.map +0 -1
- package/dist/ui/StackSelector.d.ts.map +0 -1
- package/dist/ui/StackSelector.js.map +0 -1
- package/dist/ui/Summary.d.ts.map +0 -1
- package/dist/ui/Summary.js.map +0 -1
- package/dist/ui/Welcome.d.ts.map +0 -1
- package/dist/ui/Welcome.js.map +0 -1
- package/dist/ui/theme.d.ts.map +0 -1
- package/dist/ui/theme.js.map +0 -1
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "path";
|
|
3
|
+
// ── Stack-to-Skills Mapping ────────────────────────────────────────────────
|
|
4
|
+
/**
|
|
5
|
+
* Maps detected project signals to recommended AI skills.
|
|
6
|
+
* Each key is a detection signal (file/dependency/pattern),
|
|
7
|
+
* and the value is the list of skills it implies.
|
|
8
|
+
*/
|
|
9
|
+
export const SIGNAL_SKILL_MAP = {
|
|
10
|
+
// Frameworks
|
|
11
|
+
react: ["react-19"],
|
|
12
|
+
next: ["nextjs-15", "react-19"],
|
|
13
|
+
angular: [],
|
|
14
|
+
vue: [],
|
|
15
|
+
django: ["django-drf", "pytest"],
|
|
16
|
+
flask: ["pytest"],
|
|
17
|
+
fastapi: ["pytest"],
|
|
18
|
+
// Languages / runtimes
|
|
19
|
+
typescript: ["typescript"],
|
|
20
|
+
python: ["pytest"],
|
|
21
|
+
go: [],
|
|
22
|
+
rust: [],
|
|
23
|
+
elixir: [],
|
|
24
|
+
// Styling
|
|
25
|
+
tailwindcss: ["tailwind-4"],
|
|
26
|
+
// State management
|
|
27
|
+
zustand: ["zustand-5"],
|
|
28
|
+
// Validation
|
|
29
|
+
zod: ["zod-4"],
|
|
30
|
+
// AI SDKs
|
|
31
|
+
ai: ["ai-sdk-5"],
|
|
32
|
+
"@ai-sdk": ["ai-sdk-5"],
|
|
33
|
+
// Testing
|
|
34
|
+
vitest: [],
|
|
35
|
+
jest: [],
|
|
36
|
+
pytest: ["pytest"],
|
|
37
|
+
playwright: ["playwright"],
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Additional file-based signals that don't come from package.json dependencies.
|
|
41
|
+
*/
|
|
42
|
+
const FILE_SIGNALS = [
|
|
43
|
+
{
|
|
44
|
+
files: ["tailwind.config.js", "tailwind.config.ts", "tailwind.config.mjs"],
|
|
45
|
+
skills: ["tailwind-4"],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
files: ["playwright.config.ts", "playwright.config.js"],
|
|
49
|
+
skills: ["playwright"],
|
|
50
|
+
},
|
|
51
|
+
{ files: ["tsconfig.json"], skills: ["typescript"] },
|
|
52
|
+
{
|
|
53
|
+
files: ["pyproject.toml", "setup.py", "requirements.txt"],
|
|
54
|
+
skills: ["pytest"],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
files: ["next.config.js", "next.config.mjs", "next.config.ts"],
|
|
58
|
+
skills: ["nextjs-15", "react-19"],
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
/**
|
|
62
|
+
* Docker-related files that indicate a containerized project.
|
|
63
|
+
* Used by the init command to suggest Docker zero-downtime deploy scaffolding.
|
|
64
|
+
*/
|
|
65
|
+
export const DOCKER_FILES = [
|
|
66
|
+
"Dockerfile",
|
|
67
|
+
"docker-compose.yml",
|
|
68
|
+
"docker-compose.yaml",
|
|
69
|
+
"compose.yml",
|
|
70
|
+
"compose.yaml",
|
|
71
|
+
];
|
|
72
|
+
/**
|
|
73
|
+
* Detect whether the project uses Docker (Dockerfile or compose file present).
|
|
74
|
+
*/
|
|
75
|
+
export async function detectDockerPresence(projectDir) {
|
|
76
|
+
for (const file of DOCKER_FILES) {
|
|
77
|
+
if (await fs.pathExists(path.join(projectDir, file))) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
// ── Core Detection ─────────────────────────────────────────────────────────
|
|
84
|
+
/**
|
|
85
|
+
* Scan a project directory and detect its tech stack + recommended skills.
|
|
86
|
+
* Reads package.json deps, pyproject.toml, and well-known config files.
|
|
87
|
+
*/
|
|
88
|
+
export async function detectProjectStack(projectDir) {
|
|
89
|
+
const signals = [];
|
|
90
|
+
let stack = null;
|
|
91
|
+
// 1. Check Node.js (package.json)
|
|
92
|
+
const pkgPath = path.join(projectDir, "package.json");
|
|
93
|
+
if (await fs.pathExists(pkgPath)) {
|
|
94
|
+
stack = "node";
|
|
95
|
+
try {
|
|
96
|
+
const pkg = (await fs.readJson(pkgPath));
|
|
97
|
+
const allDeps = {
|
|
98
|
+
...(pkg["dependencies"] ?? {}),
|
|
99
|
+
...(pkg["devDependencies"] ?? {}),
|
|
100
|
+
};
|
|
101
|
+
for (const depName of Object.keys(allDeps)) {
|
|
102
|
+
const normalizedDep = depName.replace(/^@/, "").split("/")[0];
|
|
103
|
+
for (const [signal, skills] of Object.entries(SIGNAL_SKILL_MAP)) {
|
|
104
|
+
if (normalizedDep === signal ||
|
|
105
|
+
depName === signal ||
|
|
106
|
+
depName.startsWith(`@${signal}/`)) {
|
|
107
|
+
if (skills.length > 0) {
|
|
108
|
+
signals.push({ signal: depName, source: "package.json", skills });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
/* corrupt package.json — skip deps detection */
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// 2. Check Python
|
|
119
|
+
const pythonFiles = ["pyproject.toml", "requirements.txt", "setup.py"];
|
|
120
|
+
for (const pyFile of pythonFiles) {
|
|
121
|
+
if (await fs.pathExists(path.join(projectDir, pyFile))) {
|
|
122
|
+
if (!stack)
|
|
123
|
+
stack = "python";
|
|
124
|
+
signals.push({
|
|
125
|
+
signal: pyFile,
|
|
126
|
+
source: "file exists",
|
|
127
|
+
skills: ["pytest"],
|
|
128
|
+
});
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// 3. Check Python deps from pyproject.toml
|
|
133
|
+
const pyprojectPath = path.join(projectDir, "pyproject.toml");
|
|
134
|
+
if (await fs.pathExists(pyprojectPath)) {
|
|
135
|
+
try {
|
|
136
|
+
const content = await fs.readFile(pyprojectPath, "utf-8");
|
|
137
|
+
if (/django/i.test(content)) {
|
|
138
|
+
signals.push({
|
|
139
|
+
signal: "django",
|
|
140
|
+
source: "pyproject.toml",
|
|
141
|
+
skills: ["django-drf", "pytest"],
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
if (/fastapi/i.test(content)) {
|
|
145
|
+
signals.push({
|
|
146
|
+
signal: "fastapi",
|
|
147
|
+
source: "pyproject.toml",
|
|
148
|
+
skills: ["pytest"],
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (/playwright/i.test(content)) {
|
|
152
|
+
signals.push({
|
|
153
|
+
signal: "playwright",
|
|
154
|
+
source: "pyproject.toml",
|
|
155
|
+
skills: ["playwright"],
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
/* skip */
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// 4. Check Go
|
|
164
|
+
if (await fs.pathExists(path.join(projectDir, "go.mod"))) {
|
|
165
|
+
if (!stack)
|
|
166
|
+
stack = "go";
|
|
167
|
+
}
|
|
168
|
+
// 5. Check Rust
|
|
169
|
+
if (await fs.pathExists(path.join(projectDir, "Cargo.toml"))) {
|
|
170
|
+
if (!stack)
|
|
171
|
+
stack = "rust";
|
|
172
|
+
}
|
|
173
|
+
// 6. Check Elixir
|
|
174
|
+
if (await fs.pathExists(path.join(projectDir, "mix.exs"))) {
|
|
175
|
+
if (!stack)
|
|
176
|
+
stack = "elixir";
|
|
177
|
+
}
|
|
178
|
+
// 7. Check Java
|
|
179
|
+
if ((await fs.pathExists(path.join(projectDir, "build.gradle"))) ||
|
|
180
|
+
(await fs.pathExists(path.join(projectDir, "build.gradle.kts")))) {
|
|
181
|
+
if (!stack)
|
|
182
|
+
stack = "java-gradle";
|
|
183
|
+
}
|
|
184
|
+
if (await fs.pathExists(path.join(projectDir, "pom.xml"))) {
|
|
185
|
+
if (!stack)
|
|
186
|
+
stack = "java-maven";
|
|
187
|
+
}
|
|
188
|
+
// 8. File-based signals
|
|
189
|
+
for (const { files, skills } of FILE_SIGNALS) {
|
|
190
|
+
for (const file of files) {
|
|
191
|
+
if (await fs.pathExists(path.join(projectDir, file))) {
|
|
192
|
+
signals.push({ signal: file, source: "file exists", skills });
|
|
193
|
+
break; // Only need one file per signal group
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// De-duplicate recommended skills
|
|
198
|
+
const skillSet = new Set();
|
|
199
|
+
for (const s of signals) {
|
|
200
|
+
for (const skill of s.skills) {
|
|
201
|
+
skillSet.add(skill);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const recommendedSkills = [...skillSet].sort();
|
|
205
|
+
return { stack, signals, recommendedSkills };
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=stack-detector.js.map
|
package/dist/lib/template.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { CIProvider, Stack } from "../types/index.js";
|
|
2
2
|
/**
|
|
3
3
|
* Read a template file and replace __VAR_NAME__ placeholders.
|
|
4
4
|
*/
|
|
@@ -21,4 +21,19 @@ export declare function generateCIWorkflow(stack: Stack, provider: CIProvider):
|
|
|
21
21
|
* Get the destination path for a CI workflow file within a project.
|
|
22
22
|
*/
|
|
23
23
|
export declare function getCIDestination(provider: CIProvider): string;
|
|
24
|
+
/**
|
|
25
|
+
* Get the template path for the zero-downtime Docker deploy workflow.
|
|
26
|
+
* Returns null if no template exists for the given provider.
|
|
27
|
+
*/
|
|
28
|
+
export declare function getDeployTemplatePath(provider: CIProvider): string | null;
|
|
29
|
+
/**
|
|
30
|
+
* Get the destination path for the deploy workflow file within a project.
|
|
31
|
+
* Returns null if no mapping exists for the given provider.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getDeployDestination(provider: CIProvider): string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Generate the zero-downtime Docker deploy workflow content.
|
|
36
|
+
* Replaces __SERVICE_NAME__ with the provided service name.
|
|
37
|
+
*/
|
|
38
|
+
export declare function generateDeployWorkflow(provider: CIProvider, serviceName: string): Promise<string | null>;
|
|
24
39
|
//# sourceMappingURL=template.d.ts.map
|
package/dist/lib/template.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
3
|
-
import {
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { DEPENDABOT_FRAGMENTS_DIR, DEPLOY_DESTINATION_MAP, DEPLOY_TEMPLATE_MAP, STACK_CI_MAP, STACK_DEPENDABOT_MAP, TEMPLATES_DIR, } from "../constants.js";
|
|
4
4
|
/**
|
|
5
5
|
* Read a template file and replace __VAR_NAME__ placeholders.
|
|
6
6
|
*/
|
|
7
7
|
export async function renderTemplate(templatePath, vars) {
|
|
8
|
-
let content = await fs.readFile(templatePath,
|
|
8
|
+
let content = await fs.readFile(templatePath, "utf-8");
|
|
9
9
|
for (const [key, value] of Object.entries(vars)) {
|
|
10
10
|
const placeholder = `__${key}__`;
|
|
11
11
|
content = content.replaceAll(placeholder, value);
|
|
@@ -17,12 +17,12 @@ export async function renderTemplate(templatePath, vars) {
|
|
|
17
17
|
* Always includes github-actions fragment when provider is GitHub.
|
|
18
18
|
*/
|
|
19
19
|
export async function generateDependabotYml(stacks, includeGitHubActions = true) {
|
|
20
|
-
const headerPath = path.join(DEPENDABOT_FRAGMENTS_DIR,
|
|
21
|
-
let content = await fs.readFile(headerPath,
|
|
20
|
+
const headerPath = path.join(DEPENDABOT_FRAGMENTS_DIR, "header.yml");
|
|
21
|
+
let content = await fs.readFile(headerPath, "utf-8");
|
|
22
22
|
// Collect unique fragment names
|
|
23
23
|
const fragments = new Set();
|
|
24
24
|
if (includeGitHubActions)
|
|
25
|
-
fragments.add(
|
|
25
|
+
fragments.add("github-actions");
|
|
26
26
|
for (const stack of stacks) {
|
|
27
27
|
const stackFragments = STACK_DEPENDABOT_MAP[stack] ?? [];
|
|
28
28
|
for (const f of stackFragments)
|
|
@@ -32,8 +32,8 @@ export async function generateDependabotYml(stacks, includeGitHubActions = true)
|
|
|
32
32
|
for (const fragmentName of fragments) {
|
|
33
33
|
const fragmentPath = path.join(DEPENDABOT_FRAGMENTS_DIR, `${fragmentName}.yml`);
|
|
34
34
|
if (await fs.pathExists(fragmentPath)) {
|
|
35
|
-
const fragment = await fs.readFile(fragmentPath,
|
|
36
|
-
content +=
|
|
35
|
+
const fragment = await fs.readFile(fragmentPath, "utf-8");
|
|
36
|
+
content += "\n" + fragment;
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
return content;
|
|
@@ -58,21 +58,50 @@ export async function generateCIWorkflow(stack, provider) {
|
|
|
58
58
|
const templatePath = getCITemplatePath(stack, provider);
|
|
59
59
|
if (!templatePath)
|
|
60
60
|
return null;
|
|
61
|
-
if (!await fs.pathExists(templatePath))
|
|
61
|
+
if (!(await fs.pathExists(templatePath)))
|
|
62
62
|
return null;
|
|
63
|
-
return fs.readFile(templatePath,
|
|
63
|
+
return fs.readFile(templatePath, "utf-8");
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
66
|
* Get the destination path for a CI workflow file within a project.
|
|
67
67
|
*/
|
|
68
68
|
export function getCIDestination(provider) {
|
|
69
69
|
switch (provider) {
|
|
70
|
-
case
|
|
71
|
-
return
|
|
72
|
-
case
|
|
73
|
-
return
|
|
74
|
-
case
|
|
75
|
-
return
|
|
70
|
+
case "github":
|
|
71
|
+
return ".github/workflows/ci.yml";
|
|
72
|
+
case "gitlab":
|
|
73
|
+
return ".gitlab-ci.yml";
|
|
74
|
+
case "woodpecker":
|
|
75
|
+
return ".woodpecker.yml";
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Get the template path for the zero-downtime Docker deploy workflow.
|
|
80
|
+
* Returns null if no template exists for the given provider.
|
|
81
|
+
*/
|
|
82
|
+
export function getDeployTemplatePath(provider) {
|
|
83
|
+
const filename = DEPLOY_TEMPLATE_MAP[provider];
|
|
84
|
+
if (!filename)
|
|
85
|
+
return null;
|
|
86
|
+
return path.join(TEMPLATES_DIR, provider, filename);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get the destination path for the deploy workflow file within a project.
|
|
90
|
+
* Returns null if no mapping exists for the given provider.
|
|
91
|
+
*/
|
|
92
|
+
export function getDeployDestination(provider) {
|
|
93
|
+
return DEPLOY_DESTINATION_MAP[provider] ?? null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Generate the zero-downtime Docker deploy workflow content.
|
|
97
|
+
* Replaces __SERVICE_NAME__ with the provided service name.
|
|
98
|
+
*/
|
|
99
|
+
export async function generateDeployWorkflow(provider, serviceName) {
|
|
100
|
+
const templatePath = getDeployTemplatePath(provider);
|
|
101
|
+
if (!templatePath)
|
|
102
|
+
return null;
|
|
103
|
+
if (!(await fs.pathExists(templatePath)))
|
|
104
|
+
return null;
|
|
105
|
+
return renderTemplate(templatePath, { SERVICE_NAME: serviceName });
|
|
106
|
+
}
|
|
78
107
|
//# sourceMappingURL=template.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { WorkflowDiscoveryEntry, WorkflowFormat } from "../../types/index.js";
|
|
2
|
+
/** Workflow templates directory inside javi-forge package */
|
|
3
|
+
export declare const WORKFLOW_TEMPLATES_DIR: string;
|
|
4
|
+
/**
|
|
5
|
+
* Discover workflow files in a project's `.javi-forge/workflows/` directory.
|
|
6
|
+
*/
|
|
7
|
+
export declare function discoverWorkflows(projectDir: string): Promise<WorkflowDiscoveryEntry[]>;
|
|
8
|
+
/**
|
|
9
|
+
* List available built-in workflow templates.
|
|
10
|
+
*/
|
|
11
|
+
export declare function listBuiltinTemplates(): Promise<WorkflowDiscoveryEntry[]>;
|
|
12
|
+
/**
|
|
13
|
+
* Load a built-in template by name. Returns the file content or null if not found.
|
|
14
|
+
*/
|
|
15
|
+
export declare function loadBuiltinTemplate(name: string): Promise<{
|
|
16
|
+
content: string;
|
|
17
|
+
format: WorkflowFormat;
|
|
18
|
+
} | null>;
|
|
19
|
+
//# sourceMappingURL=discovery.d.ts.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { TEMPLATES_DIR } from "../../constants.js";
|
|
4
|
+
const WORKFLOW_EXTENSIONS = {
|
|
5
|
+
".dot": "dot",
|
|
6
|
+
".mermaid": "mermaid",
|
|
7
|
+
};
|
|
8
|
+
/** Workflow templates directory inside javi-forge package */
|
|
9
|
+
export const WORKFLOW_TEMPLATES_DIR = path.join(TEMPLATES_DIR, "workflows");
|
|
10
|
+
/**
|
|
11
|
+
* Discover workflow files in a project's `.javi-forge/workflows/` directory.
|
|
12
|
+
*/
|
|
13
|
+
export async function discoverWorkflows(projectDir) {
|
|
14
|
+
const workflowDir = path.join(projectDir, ".javi-forge", "workflows");
|
|
15
|
+
if (!(await fs.pathExists(workflowDir))) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
const entries = await fs.readdir(workflowDir);
|
|
19
|
+
const workflows = [];
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
const ext = path.extname(entry);
|
|
22
|
+
const format = WORKFLOW_EXTENSIONS[ext];
|
|
23
|
+
if (format) {
|
|
24
|
+
workflows.push({
|
|
25
|
+
name: path.basename(entry, ext),
|
|
26
|
+
path: path.join(workflowDir, entry),
|
|
27
|
+
format,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return workflows;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* List available built-in workflow templates.
|
|
35
|
+
*/
|
|
36
|
+
export async function listBuiltinTemplates() {
|
|
37
|
+
if (!(await fs.pathExists(WORKFLOW_TEMPLATES_DIR))) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
const entries = await fs.readdir(WORKFLOW_TEMPLATES_DIR);
|
|
41
|
+
const templates = [];
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
const ext = path.extname(entry);
|
|
44
|
+
const format = WORKFLOW_EXTENSIONS[ext];
|
|
45
|
+
if (format) {
|
|
46
|
+
templates.push({
|
|
47
|
+
name: path.basename(entry, ext),
|
|
48
|
+
path: path.join(WORKFLOW_TEMPLATES_DIR, entry),
|
|
49
|
+
format,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return templates;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Load a built-in template by name. Returns the file content or null if not found.
|
|
57
|
+
*/
|
|
58
|
+
export async function loadBuiltinTemplate(name) {
|
|
59
|
+
for (const [ext, format] of Object.entries(WORKFLOW_EXTENSIONS)) {
|
|
60
|
+
const templatePath = path.join(WORKFLOW_TEMPLATES_DIR, `${name}${ext}`);
|
|
61
|
+
if (await fs.pathExists(templatePath)) {
|
|
62
|
+
const content = await fs.readFile(templatePath, "utf-8");
|
|
63
|
+
return { content, format };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=discovery.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { discoverWorkflows, listBuiltinTemplates, loadBuiltinTemplate, WORKFLOW_TEMPLATES_DIR, } from "./discovery.js";
|
|
2
|
+
export { parseDot, parseMermaid, WorkflowParseError } from "./parser.js";
|
|
3
|
+
export { renderAscii } from "./renderer.js";
|
|
4
|
+
export { getAvailableChecks, validateWorkflow } from "./validator.js";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { discoverWorkflows, listBuiltinTemplates, loadBuiltinTemplate, WORKFLOW_TEMPLATES_DIR, } from "./discovery.js";
|
|
2
|
+
export { parseDot, parseMermaid, WorkflowParseError } from "./parser.js";
|
|
3
|
+
export { renderAscii } from "./renderer.js";
|
|
4
|
+
export { getAvailableChecks, validateWorkflow } from "./validator.js";
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { WorkflowGraph } from "../../types/index.js";
|
|
2
|
+
export declare class WorkflowParseError extends Error {
|
|
3
|
+
readonly line?: number | undefined;
|
|
4
|
+
constructor(message: string, line?: number | undefined);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Parse a DOT digraph string into a WorkflowGraph.
|
|
8
|
+
* Supports subset: `digraph [name] { A -> B; A [label="X" check="Y"]; }`
|
|
9
|
+
*/
|
|
10
|
+
export declare function parseDot(content: string, sourceName?: string): WorkflowGraph;
|
|
11
|
+
/**
|
|
12
|
+
* Parse a Mermaid flowchart string into a WorkflowGraph.
|
|
13
|
+
* Supports subset: `flowchart LR/TD`, `A --> B`, `A[Label]`, `A -->|label| B`
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseMermaid(content: string, sourceName?: string): WorkflowGraph;
|
|
16
|
+
//# sourceMappingURL=parser.d.ts.map
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
export class WorkflowParseError extends Error {
|
|
2
|
+
line;
|
|
3
|
+
constructor(message, line) {
|
|
4
|
+
super(line !== undefined ? `Line ${line}: ${message}` : message);
|
|
5
|
+
this.line = line;
|
|
6
|
+
this.name = "WorkflowParseError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
// ── DOT Parser ──────────────────────────────────────────────────────────────
|
|
10
|
+
/**
|
|
11
|
+
* Parse a DOT digraph string into a WorkflowGraph.
|
|
12
|
+
* Supports subset: `digraph [name] { A -> B; A [label="X" check="Y"]; }`
|
|
13
|
+
*/
|
|
14
|
+
export function parseDot(content, sourceName = "untitled") {
|
|
15
|
+
const trimmed = content.trim();
|
|
16
|
+
// Extract digraph header
|
|
17
|
+
const headerMatch = trimmed.match(/^digraph\s+(?:"([^"]+)"|(\w+))?\s*\{/i);
|
|
18
|
+
if (!headerMatch) {
|
|
19
|
+
throw new WorkflowParseError('Expected "digraph [name] {" at start of file');
|
|
20
|
+
}
|
|
21
|
+
const graphName = headerMatch[1] ?? headerMatch[2] ?? sourceName;
|
|
22
|
+
// Extract body between first { and last }
|
|
23
|
+
const openBrace = trimmed.indexOf("{");
|
|
24
|
+
const closeBrace = trimmed.lastIndexOf("}");
|
|
25
|
+
if (openBrace === -1 || closeBrace === -1 || closeBrace <= openBrace) {
|
|
26
|
+
throw new WorkflowParseError("Malformed digraph: missing braces");
|
|
27
|
+
}
|
|
28
|
+
const body = trimmed.slice(openBrace + 1, closeBrace);
|
|
29
|
+
const nodesMap = new Map();
|
|
30
|
+
const edges = [];
|
|
31
|
+
// Split by semicolons or newlines
|
|
32
|
+
const statements = body
|
|
33
|
+
.split(/[;\n]/)
|
|
34
|
+
.map((s) => s.trim())
|
|
35
|
+
.filter((s) => s.length > 0 && !s.startsWith("//") && !s.startsWith("#"));
|
|
36
|
+
for (const stmt of statements) {
|
|
37
|
+
// Skip graph-level attributes like rankdir=LR
|
|
38
|
+
if (/^\w+=/.test(stmt) && !stmt.includes("->"))
|
|
39
|
+
continue;
|
|
40
|
+
// Edge: A -> B [label="..."]
|
|
41
|
+
const edgeMatch = stmt.match(/^"?(\w[\w\s-]*?)"?\s*->\s*"?(\w[\w\s-]*?)"?\s*(?:\[([^\]]*)\])?\s*$/);
|
|
42
|
+
if (edgeMatch) {
|
|
43
|
+
const fromId = edgeMatch[1].trim();
|
|
44
|
+
const toId = edgeMatch[2].trim();
|
|
45
|
+
const attrs = edgeMatch[3] ? parseAttributes(edgeMatch[3]) : {};
|
|
46
|
+
ensureNode(nodesMap, fromId);
|
|
47
|
+
ensureNode(nodesMap, toId);
|
|
48
|
+
edges.push({ from: fromId, to: toId, label: attrs["label"] });
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
// Node: A [label="..." check="..."]
|
|
52
|
+
const nodeMatch = stmt.match(/^"?(\w[\w\s-]*?)"?\s*\[([^\]]+)\]\s*$/);
|
|
53
|
+
if (nodeMatch) {
|
|
54
|
+
const nodeId = nodeMatch[1].trim();
|
|
55
|
+
const attrs = parseAttributes(nodeMatch[2]);
|
|
56
|
+
const node = ensureNode(nodesMap, nodeId);
|
|
57
|
+
if (attrs["label"])
|
|
58
|
+
node.label = attrs["label"];
|
|
59
|
+
if (attrs["check"])
|
|
60
|
+
node.check = attrs["check"];
|
|
61
|
+
// Store remaining attrs as metadata
|
|
62
|
+
const { label: _l, check: _c, ...rest } = attrs;
|
|
63
|
+
if (Object.keys(rest).length > 0)
|
|
64
|
+
node.metadata = rest;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
// Chained edge: A -> B -> C
|
|
68
|
+
const chainMatch = stmt.match(/^[\w\s"-]+(?:\s*->\s*[\w\s"-]+)+$/);
|
|
69
|
+
if (chainMatch) {
|
|
70
|
+
const parts = stmt.split("->").map((p) => p.trim().replace(/^"|"$/g, ""));
|
|
71
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
72
|
+
const fromId = parts[i].trim();
|
|
73
|
+
const toId = parts[i + 1].trim();
|
|
74
|
+
ensureNode(nodesMap, fromId);
|
|
75
|
+
ensureNode(nodesMap, toId);
|
|
76
|
+
edges.push({ from: fromId, to: toId });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (nodesMap.size === 0) {
|
|
81
|
+
throw new WorkflowParseError("No nodes found in digraph");
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
name: graphName,
|
|
85
|
+
nodes: Array.from(nodesMap.values()),
|
|
86
|
+
edges,
|
|
87
|
+
format: "dot",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// ── Mermaid Parser ──────────────────────────────────────────────────────────
|
|
91
|
+
/**
|
|
92
|
+
* Parse a Mermaid flowchart string into a WorkflowGraph.
|
|
93
|
+
* Supports subset: `flowchart LR/TD`, `A --> B`, `A[Label]`, `A -->|label| B`
|
|
94
|
+
*/
|
|
95
|
+
export function parseMermaid(content, sourceName = "untitled") {
|
|
96
|
+
const lines = content
|
|
97
|
+
.split("\n")
|
|
98
|
+
.map((l) => l.trim())
|
|
99
|
+
.filter((l) => l.length > 0);
|
|
100
|
+
if (lines.length === 0) {
|
|
101
|
+
throw new WorkflowParseError("Empty mermaid file");
|
|
102
|
+
}
|
|
103
|
+
// First line must be flowchart directive
|
|
104
|
+
const headerMatch = lines[0].match(/^(?:flowchart|graph)\s+(LR|TD|TB|RL|BT)/i);
|
|
105
|
+
if (!headerMatch) {
|
|
106
|
+
throw new WorkflowParseError('Expected "flowchart LR|TD" or "graph LR|TD" at first line', 1);
|
|
107
|
+
}
|
|
108
|
+
const nodesMap = new Map();
|
|
109
|
+
const edges = [];
|
|
110
|
+
for (let i = 1; i < lines.length; i++) {
|
|
111
|
+
const line = lines[i];
|
|
112
|
+
// Skip comments and style directives
|
|
113
|
+
if (line.startsWith("%%") ||
|
|
114
|
+
line.startsWith("style") ||
|
|
115
|
+
line.startsWith("classDef")) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
// Edge with label: A -->|label| B or A -- label --> B
|
|
119
|
+
const edgeLabelMatch = line.match(/^(\w[\w-]*?)(?:\[([^\]]+)\])?\s*-->?\|([^|]+)\|\s*(\w[\w-]*?)(?:\[([^\]]+)\])?\s*$/);
|
|
120
|
+
if (edgeLabelMatch) {
|
|
121
|
+
const fromId = edgeLabelMatch[1];
|
|
122
|
+
const fromLabel = edgeLabelMatch[2];
|
|
123
|
+
const edgeLabel = edgeLabelMatch[3].trim();
|
|
124
|
+
const toId = edgeLabelMatch[4];
|
|
125
|
+
const toLabel = edgeLabelMatch[5];
|
|
126
|
+
const fromNode = ensureNode(nodesMap, fromId);
|
|
127
|
+
if (fromLabel)
|
|
128
|
+
fromNode.label = fromLabel;
|
|
129
|
+
const toNode = ensureNode(nodesMap, toId);
|
|
130
|
+
if (toLabel)
|
|
131
|
+
toNode.label = toLabel;
|
|
132
|
+
edges.push({ from: fromId, to: toId, label: edgeLabel });
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
// Simple edge: A --> B (possibly chained: A --> B --> C)
|
|
136
|
+
// Also handles node labels: A[Label] --> B[Label]
|
|
137
|
+
const edgeMatch = line.match(/(\w[\w-]*?)(?:\[([^\]]+)\])?\s*-->?\s*/);
|
|
138
|
+
if (edgeMatch && line.includes("-->")) {
|
|
139
|
+
const segments = line.split(/\s*-->\s*/);
|
|
140
|
+
for (let s = 0; s < segments.length; s++) {
|
|
141
|
+
const seg = segments[s].trim();
|
|
142
|
+
const segMatch = seg.match(/^(\w[\w-]*)(?:\[([^\]]+)\])?$/);
|
|
143
|
+
if (segMatch) {
|
|
144
|
+
const nodeId = segMatch[1];
|
|
145
|
+
const nodeLabel = segMatch[2];
|
|
146
|
+
const node = ensureNode(nodesMap, nodeId);
|
|
147
|
+
if (nodeLabel)
|
|
148
|
+
node.label = nodeLabel;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Create edges between consecutive segments
|
|
152
|
+
for (let s = 0; s < segments.length - 1; s++) {
|
|
153
|
+
const fromSeg = segments[s].trim().match(/^(\w[\w-]*)/);
|
|
154
|
+
const toSeg = segments[s + 1].trim().match(/^(\w[\w-]*)/);
|
|
155
|
+
if (fromSeg && toSeg) {
|
|
156
|
+
edges.push({ from: fromSeg[1], to: toSeg[1] });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
// Standalone node with label: A[Label]
|
|
162
|
+
const nodeMatch = line.match(/^(\w[\w-]*)\[([^\]]+)\]\s*$/);
|
|
163
|
+
if (nodeMatch) {
|
|
164
|
+
const node = ensureNode(nodesMap, nodeMatch[1]);
|
|
165
|
+
node.label = nodeMatch[2];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (nodesMap.size === 0) {
|
|
169
|
+
throw new WorkflowParseError("No nodes found in flowchart");
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
name: sourceName,
|
|
173
|
+
nodes: Array.from(nodesMap.values()),
|
|
174
|
+
edges,
|
|
175
|
+
format: "mermaid",
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
179
|
+
function ensureNode(nodesMap, id) {
|
|
180
|
+
let node = nodesMap.get(id);
|
|
181
|
+
if (!node) {
|
|
182
|
+
node = { id, label: id };
|
|
183
|
+
nodesMap.set(id, node);
|
|
184
|
+
}
|
|
185
|
+
return node;
|
|
186
|
+
}
|
|
187
|
+
function parseAttributes(attrStr) {
|
|
188
|
+
const attrs = {};
|
|
189
|
+
// Match key="value" or key=value pairs
|
|
190
|
+
const regex = /(\w+)\s*=\s*(?:"([^"]*)"|(\S+))/g;
|
|
191
|
+
let match;
|
|
192
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: idiomatic regex exec loop
|
|
193
|
+
while ((match = regex.exec(attrStr)) !== null) {
|
|
194
|
+
attrs[match[1]] = match[2] ?? match[3] ?? "";
|
|
195
|
+
}
|
|
196
|
+
return attrs;
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=parser.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { WorkflowGraph, WorkflowValidationResult } from "../../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Render a WorkflowGraph as ASCII art.
|
|
4
|
+
* Linear pipelines render as: [lint] -> [test] -> [build]
|
|
5
|
+
* Branching renders with indentation.
|
|
6
|
+
* Optionally overlays validation results with pass/fail icons.
|
|
7
|
+
*/
|
|
8
|
+
export declare function renderAscii(graph: WorkflowGraph, validationResults?: WorkflowValidationResult[]): string;
|
|
9
|
+
//# sourceMappingURL=renderer.d.ts.map
|