owflow 0.1.10 → 0.2.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 +20 -0
- package/dist/configuration/agents-config.js +66 -0
- package/dist/configuration/commands-config.js +21 -0
- package/dist/configuration/skills-config.js +10 -0
- package/dist/constants.js +4 -0
- package/dist/hooks/before-tool.js +19 -0
- package/dist/hooks/session-compaction.js +19 -0
- package/dist/index.js +11 -142
- package/dist/utils/file-util.js +19 -0
- package/package.json +6 -4
- package/dist/__tests__/verify_template.test.js +0 -102
package/README.md
CHANGED
|
@@ -18,6 +18,26 @@ Started as a simple OpenCode fork of [Maister Claude Code plugin](https://github
|
|
|
18
18
|
|
|
19
19
|
## Getting Started
|
|
20
20
|
|
|
21
|
+
### Directory Structure
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
owflow/
|
|
25
|
+
├── docs/ # Plugin documentation (commands, workflows)
|
|
26
|
+
├── scripts/ # Build and utility scripts
|
|
27
|
+
└── src/ # Source code
|
|
28
|
+
├── __tests__/ # Test suites
|
|
29
|
+
├── agents/ # Agent definitions and prompts (.md)
|
|
30
|
+
├── commands/ # OpenCode slash commands (.md)
|
|
31
|
+
├── configuration/ # Handlers for agent, command, and skill configurations
|
|
32
|
+
├── hooks/ # OpenCode lifecycle hooks (tool execution, compaction)
|
|
33
|
+
├── skills/ # Workflows, orchestrators, and reusable skills
|
|
34
|
+
├── templates/ # Orchestrator state and task YAML templates
|
|
35
|
+
├── tools/ # Custom OpenCode tool implementations
|
|
36
|
+
├── types/ # TypeScript type definitions
|
|
37
|
+
├── utils/ # Internal utility functions
|
|
38
|
+
└── index.ts # Main plugin entry point
|
|
39
|
+
```
|
|
40
|
+
|
|
21
41
|
### Prerequisites
|
|
22
42
|
|
|
23
43
|
- [Opencode](https://opencode.ai/) CLI installed and configured
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { loadMarkdownDir } from "../utils/file-util.js";
|
|
2
|
+
import matter from "gray-matter";
|
|
3
|
+
/**
|
|
4
|
+
* Resolves model aliases to the configured small_model value.
|
|
5
|
+
* Maps common small/fast model aliases to OpenCode's small_model config.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} modelValue - The model value from agent frontmatter
|
|
8
|
+
* @param {string} smallModel - The small_model from OpenCode config
|
|
9
|
+
* @returns {string|undefined} - Resolved model value or undefined
|
|
10
|
+
*/
|
|
11
|
+
const resolveModelAlias = (modelValue, smallModel) => {
|
|
12
|
+
// Aliases that should map to small_model
|
|
13
|
+
const SMALL_MODEL_ALIASES = [
|
|
14
|
+
"haiku", // Claude Code alias
|
|
15
|
+
"gpt-4o-mini", // OpenAI small model
|
|
16
|
+
"small", // Generic alias
|
|
17
|
+
"fast", // Generic alias
|
|
18
|
+
];
|
|
19
|
+
// If no model specified or it's 'inherit', return undefined (let OpenCode decide)
|
|
20
|
+
if (!modelValue || modelValue === "inherit") {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
// If it's a small model alias and we have a configured small_model, use it
|
|
24
|
+
if (SMALL_MODEL_ALIASES.includes(modelValue.toLowerCase())) {
|
|
25
|
+
if (smallModel) {
|
|
26
|
+
return smallModel;
|
|
27
|
+
}
|
|
28
|
+
// If no small_model configured, return undefined (inherit)
|
|
29
|
+
console.warn(`[owflow] Agent uses '${modelValue}' but no small_model configured in opencode.json - falling back to inherit`);
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
// Otherwise, pass through the original value
|
|
33
|
+
return modelValue;
|
|
34
|
+
};
|
|
35
|
+
const prepareAgent = ({ data, content }, smallModel) => {
|
|
36
|
+
return {
|
|
37
|
+
prompt: content,
|
|
38
|
+
...(data.description && { description: data.description }),
|
|
39
|
+
// Resolve model aliases (haiku, gpt-4o-mini, etc.) to small_model
|
|
40
|
+
...(() => {
|
|
41
|
+
const resolvedModel = resolveModelAlias(data.model, smallModel);
|
|
42
|
+
return resolvedModel ? { model: resolvedModel } : {};
|
|
43
|
+
})(),
|
|
44
|
+
...(data.mode && { mode: data.mode }),
|
|
45
|
+
// Add skills array if present
|
|
46
|
+
...(data.skills && Array.isArray(data.skills) && { skills: data.skills }),
|
|
47
|
+
...(data.hidden === "true" && { hidden: true }),
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
export const configureAgents = (config, smallModel) => {
|
|
51
|
+
config.agent = config.agent || {};
|
|
52
|
+
for (const markdown of loadMarkdownDir("agents")) {
|
|
53
|
+
const name = markdown.data.name;
|
|
54
|
+
if (!name)
|
|
55
|
+
continue;
|
|
56
|
+
// Don't overwrite agents the user has explicitly configured
|
|
57
|
+
if (config.agent[name])
|
|
58
|
+
continue;
|
|
59
|
+
try {
|
|
60
|
+
config.agent[name] = prepareAgent(markdown, smallModel);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
console.warn(`[owflow] Failed to register agent '${name}': ${error.message}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { loadMarkdownDir } from "../utils/file-util.js";
|
|
2
|
+
export const configureCommands = (config) => {
|
|
3
|
+
config.command = config.command || {};
|
|
4
|
+
for (const { data, content } of loadMarkdownDir("commands")) {
|
|
5
|
+
const name = data.name;
|
|
6
|
+
if (!name)
|
|
7
|
+
continue;
|
|
8
|
+
// Don't overwrite commands the user has explicitly configured
|
|
9
|
+
if (config.command[name])
|
|
10
|
+
continue;
|
|
11
|
+
config.command[name] = {
|
|
12
|
+
template: content,
|
|
13
|
+
...(data.description && { description: data.description }),
|
|
14
|
+
...(data.agent && { agent: data.agent }),
|
|
15
|
+
...(data.model && { model: data.model }),
|
|
16
|
+
...(data.subtask !== undefined && {
|
|
17
|
+
subtask: data.subtask === "true",
|
|
18
|
+
}),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { PLUGIN_ROOT } from "../constants.js";
|
|
3
|
+
export const configureSkills = (config) => {
|
|
4
|
+
config.skills = config.skills || {};
|
|
5
|
+
config.skills.paths = config.skills.paths || [];
|
|
6
|
+
const skillsDir = path.join(PLUGIN_ROOT, "skills");
|
|
7
|
+
if (!config.skills.paths.includes(skillsDir)) {
|
|
8
|
+
config.skills.paths.push(skillsDir);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const guardAgainstDestructiveActions = async (input, output, agentBySessionMap) => {
|
|
2
|
+
if (input.tool !== "bash")
|
|
3
|
+
return;
|
|
4
|
+
const agentName = agentBySessionMap.get(input.sessionID) ?? "main";
|
|
5
|
+
const WHITELIST = [
|
|
6
|
+
"task-group-implementer",
|
|
7
|
+
"test-suite-runner",
|
|
8
|
+
"e2e-test-verifier",
|
|
9
|
+
"user-docs-generator",
|
|
10
|
+
"docs-operator",
|
|
11
|
+
];
|
|
12
|
+
if (WHITELIST.includes(agentName))
|
|
13
|
+
return;
|
|
14
|
+
const cmd = output?.args?.command ?? "";
|
|
15
|
+
const DESTRUCTIVE = /git\s+stash|git\s+reset\s+--hard|git\s+checkout\s+--\s+\.|git\s+checkout\s+\.\s*(?:$|\s)|git\s+clean|git\s+push\s+(?:--force|-f)|rm\s+-[rf]{2}/i;
|
|
16
|
+
if (DESTRUCTIVE.test(cmd)) {
|
|
17
|
+
throw new Error(`Blocked: destructive command not permitted for agent "${agentName}"`);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { $ } from "bun";
|
|
2
|
+
/**
|
|
3
|
+
* Post-compaction reminder.
|
|
4
|
+
* Injects orchestrator state reminder into the compaction context so the
|
|
5
|
+
* AI knows to re-read orchestrator-state.yml after the context is rebuilt.
|
|
6
|
+
*/
|
|
7
|
+
export const rereadOrchestratorState = async (directory, output) => {
|
|
8
|
+
const tasksDir = `${directory}/.owflow/tasks`;
|
|
9
|
+
try {
|
|
10
|
+
await $ `test -d ${tasksDir}`;
|
|
11
|
+
output.context.push(`## owflow Workflow State
|
|
12
|
+
If an orchestrator workflow was active before compaction, you MUST re-read
|
|
13
|
+
orchestrator-state.yml in that task's directory to verify completed_phases
|
|
14
|
+
and determine the next phase. Use the question tool at Phase Gates.`);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// No .owflow/tasks directory — not a owflow project, skip injection
|
|
18
|
+
}
|
|
19
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -4,72 +4,13 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import fs from "node:fs";
|
|
7
|
-
import { fileURLToPath } from "url";
|
|
8
|
-
import matter from "gray-matter";
|
|
9
7
|
import { verify_template } from "./tools/verify_template.js";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* @param {string} modelValue - The model value from agent frontmatter
|
|
17
|
-
* @param {string} smallModel - The small_model from OpenCode config
|
|
18
|
-
* @returns {string|undefined} - Resolved model value or undefined
|
|
19
|
-
*/
|
|
20
|
-
const resolveModelAlias = (modelValue, smallModel) => {
|
|
21
|
-
// Aliases that should map to small_model
|
|
22
|
-
const SMALL_MODEL_ALIASES = [
|
|
23
|
-
"haiku", // Claude Code alias
|
|
24
|
-
"gpt-4o-mini", // OpenAI small model
|
|
25
|
-
"small", // Generic alias
|
|
26
|
-
"fast", // Generic alias
|
|
27
|
-
];
|
|
28
|
-
// If no model specified or it's 'inherit', return undefined (let OpenCode decide)
|
|
29
|
-
if (!modelValue || modelValue === "inherit") {
|
|
30
|
-
return undefined;
|
|
31
|
-
}
|
|
32
|
-
// If it's a small model alias and we have a configured small_model, use it
|
|
33
|
-
if (SMALL_MODEL_ALIASES.includes(modelValue.toLowerCase())) {
|
|
34
|
-
if (smallModel) {
|
|
35
|
-
return smallModel;
|
|
36
|
-
}
|
|
37
|
-
// If no small_model configured, return undefined (inherit)
|
|
38
|
-
console.warn(`[owflow] Agent uses '${modelValue}' but no small_model configured in opencode.json - falling back to inherit`);
|
|
39
|
-
return undefined;
|
|
40
|
-
}
|
|
41
|
-
// Otherwise, pass through the original value
|
|
42
|
-
return modelValue;
|
|
43
|
-
};
|
|
44
|
-
const loadMarkdownDir = (dirPath) => {
|
|
45
|
-
try {
|
|
46
|
-
return fs
|
|
47
|
-
.readdirSync(dirPath)
|
|
48
|
-
.filter((f) => f.endsWith(".md"))
|
|
49
|
-
.map((f) => {
|
|
50
|
-
const raw = fs.readFileSync(path.join(dirPath, f), "utf8");
|
|
51
|
-
return matter(raw);
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
return [];
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
const prepareAgent = ({ data, content }, smallModel) => {
|
|
59
|
-
return {
|
|
60
|
-
prompt: content,
|
|
61
|
-
...(data.description && { description: data.description }),
|
|
62
|
-
// Resolve model aliases (haiku, gpt-4o-mini, etc.) to small_model
|
|
63
|
-
...(() => {
|
|
64
|
-
const resolvedModel = resolveModelAlias(data.model, smallModel);
|
|
65
|
-
return resolvedModel ? { model: resolvedModel } : {};
|
|
66
|
-
})(),
|
|
67
|
-
...(data.mode && { mode: data.mode }),
|
|
68
|
-
// Add skills array if present
|
|
69
|
-
...(data.skills && Array.isArray(data.skills) && { skills: data.skills }),
|
|
70
|
-
...(data.hidden === "true" && { hidden: true }),
|
|
71
|
-
};
|
|
72
|
-
};
|
|
8
|
+
import { rereadOrchestratorState } from "./hooks/session-compaction.js";
|
|
9
|
+
import { guardAgainstDestructiveActions } from "./hooks/before-tool.js";
|
|
10
|
+
import { configureSkills } from "./configuration/skills-config.js";
|
|
11
|
+
import { configureCommands } from "./configuration/commands-config.js";
|
|
12
|
+
import { configureAgents } from "./configuration/agents-config.js";
|
|
13
|
+
// Only the plugin factory method should be exported from this file due to OpenCode plugin handling
|
|
73
14
|
const OwflowPlugin = async ({ $, directory }) => {
|
|
74
15
|
const agentBySession = new Map();
|
|
75
16
|
return {
|
|
@@ -100,70 +41,14 @@ const OwflowPlugin = async ({ $, directory }) => {
|
|
|
100
41
|
console.warn(`Couldn't read or parse opencode.json config - small_model remains undefined`);
|
|
101
42
|
}
|
|
102
43
|
}
|
|
103
|
-
|
|
104
|
-
typedConfig
|
|
105
|
-
typedConfig
|
|
106
|
-
const skillsDir = path.join(PLUGIN_ROOT, "skills");
|
|
107
|
-
if (!typedConfig.skills.paths.includes(skillsDir)) {
|
|
108
|
-
typedConfig.skills.paths.push(skillsDir);
|
|
109
|
-
}
|
|
110
|
-
// --- Commands ---
|
|
111
|
-
typedConfig.command = typedConfig.command || {};
|
|
112
|
-
const commandsDir = path.join(PLUGIN_ROOT, "commands");
|
|
113
|
-
for (const { data, content } of loadMarkdownDir(commandsDir)) {
|
|
114
|
-
const name = data.name;
|
|
115
|
-
if (!name)
|
|
116
|
-
continue;
|
|
117
|
-
// Don't overwrite commands the user has explicitly configured
|
|
118
|
-
if (typedConfig.command[name])
|
|
119
|
-
continue;
|
|
120
|
-
typedConfig.command[name] = {
|
|
121
|
-
template: content,
|
|
122
|
-
...(data.description && { description: data.description }),
|
|
123
|
-
...(data.agent && { agent: data.agent }),
|
|
124
|
-
...(data.model && { model: data.model }),
|
|
125
|
-
...(data.subtask !== undefined && {
|
|
126
|
-
subtask: data.subtask === "true",
|
|
127
|
-
}),
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
// --- Agents ---
|
|
131
|
-
typedConfig.agent = typedConfig.agent || {};
|
|
132
|
-
const agentsDir = path.join(PLUGIN_ROOT, "agents");
|
|
133
|
-
for (const markdown of loadMarkdownDir(agentsDir)) {
|
|
134
|
-
const name = markdown.data.name;
|
|
135
|
-
if (!name)
|
|
136
|
-
continue;
|
|
137
|
-
// Don't overwrite agents the user has explicitly configured
|
|
138
|
-
if (typedConfig.agent[name])
|
|
139
|
-
continue;
|
|
140
|
-
try {
|
|
141
|
-
typedConfig.agent[name] = prepareAgent(markdown, smallModel);
|
|
142
|
-
}
|
|
143
|
-
catch (error) {
|
|
144
|
-
console.warn(`[owflow] Failed to register agent '${name}': ${error.message}`);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
44
|
+
configureSkills(typedConfig);
|
|
45
|
+
configureCommands(typedConfig);
|
|
46
|
+
configureAgents(typedConfig, smallModel);
|
|
147
47
|
config = typedConfig;
|
|
148
48
|
console.log("[owflow] Plugin installation completed successfully.");
|
|
149
49
|
},
|
|
150
|
-
/**
|
|
151
|
-
* Post-compaction reminder.
|
|
152
|
-
* Injects orchestrator state reminder into the compaction context so the
|
|
153
|
-
* AI knows to re-read orchestrator-state.yml after the context is rebuilt.
|
|
154
|
-
*/
|
|
155
50
|
"experimental.session.compacting": async (input, output) => {
|
|
156
|
-
|
|
157
|
-
try {
|
|
158
|
-
await $ `test -d ${tasksDir}`;
|
|
159
|
-
output.context.push(`## owflow Workflow State
|
|
160
|
-
If an orchestrator workflow was active before compaction, you MUST re-read
|
|
161
|
-
orchestrator-state.yml in that task's directory to verify completed_phases
|
|
162
|
-
and determine the next phase. Use the question tool at Phase Gates.`);
|
|
163
|
-
}
|
|
164
|
-
catch {
|
|
165
|
-
// No .owflow/tasks directory — not a owflow project, skip injection
|
|
166
|
-
}
|
|
51
|
+
await rereadOrchestratorState(directory, output);
|
|
167
52
|
},
|
|
168
53
|
"chat.message": async (input, _output) => {
|
|
169
54
|
try {
|
|
@@ -175,23 +60,7 @@ and determine the next phase. Use the question tool at Phase Gates.`);
|
|
|
175
60
|
},
|
|
176
61
|
"tool.execute.before": async (input, output) => {
|
|
177
62
|
try {
|
|
178
|
-
|
|
179
|
-
return;
|
|
180
|
-
const agentName = agentBySession.get(input.sessionID) ?? "main";
|
|
181
|
-
const WHITELIST = [
|
|
182
|
-
"task-group-implementer",
|
|
183
|
-
"test-suite-runner",
|
|
184
|
-
"e2e-test-verifier",
|
|
185
|
-
"user-docs-generator",
|
|
186
|
-
"docs-operator",
|
|
187
|
-
];
|
|
188
|
-
if (WHITELIST.includes(agentName))
|
|
189
|
-
return;
|
|
190
|
-
const cmd = output?.args?.command ?? "";
|
|
191
|
-
const DESTRUCTIVE = /git\s+stash|git\s+reset\s+--hard|git\s+checkout\s+--\s+\.|git\s+checkout\s+\.\s*(?:$|\s)|git\s+clean|git\s+push\s+(?:--force|-f)|rm\s+-[rf]{2}/i;
|
|
192
|
-
if (DESTRUCTIVE.test(cmd)) {
|
|
193
|
-
throw new Error(`Blocked: destructive command not permitted for agent "${agentName}"`);
|
|
194
|
-
}
|
|
63
|
+
await guardAgainstDestructiveActions(input, output, agentBySession);
|
|
195
64
|
}
|
|
196
65
|
catch (e) {
|
|
197
66
|
// Re-throw only intentional blocks; swallow unexpected errors
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import matter from "gray-matter";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { PLUGIN_ROOT } from "../constants.js";
|
|
5
|
+
export const loadMarkdownDir = (dirPath) => {
|
|
6
|
+
const fullPath = path.join(PLUGIN_ROOT, dirPath);
|
|
7
|
+
try {
|
|
8
|
+
return fs
|
|
9
|
+
.readdirSync(fullPath)
|
|
10
|
+
.filter((f) => f.endsWith(".md"))
|
|
11
|
+
.map((f) => {
|
|
12
|
+
const raw = fs.readFileSync(path.join(fullPath, f), "utf8");
|
|
13
|
+
return matter(raw);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "owflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Spec-driven development AI workflow for OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
"LICENSE"
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
|
-
"build": "rm -rf dist &&
|
|
16
|
-
"copy-markdowns": "
|
|
15
|
+
"build": "rm -rf dist && bun run type-check && bun run test && bun run copy-markdowns",
|
|
16
|
+
"copy-markdowns": "bun run ./scripts/copy-markdowns.js",
|
|
17
|
+
"local-install": "bun run build && opencode plugin \"$(pwd)\" --global --force",
|
|
17
18
|
"prepack": "bun run build",
|
|
18
|
-
"test": "bun test"
|
|
19
|
+
"test": "bun test src",
|
|
20
|
+
"type-check": "bun tsc"
|
|
19
21
|
},
|
|
20
22
|
"keywords": [
|
|
21
23
|
"opencode",
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { expect, test, describe, beforeAll, afterAll } from "bun:test";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { verify_template } from "../tools/verify_template.js";
|
|
6
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
describe("verify_template tool", () => {
|
|
8
|
-
const templatesDir = path.join(__dirname, "../templates");
|
|
9
|
-
const testDir = path.join(__dirname, "test_tmp_dir");
|
|
10
|
-
const testTemplateName = "test-template.yml";
|
|
11
|
-
const testTemplatePath = path.join(templatesDir, testTemplateName);
|
|
12
|
-
beforeAll(() => {
|
|
13
|
-
// Ensure templates dir exists
|
|
14
|
-
if (!fs.existsSync(templatesDir)) {
|
|
15
|
-
fs.mkdirSync(templatesDir, { recursive: true });
|
|
16
|
-
}
|
|
17
|
-
// Create a dummy template
|
|
18
|
-
fs.writeFileSync(testTemplatePath, `
|
|
19
|
-
key1: value1
|
|
20
|
-
key2:
|
|
21
|
-
subKey1: subValue1
|
|
22
|
-
`);
|
|
23
|
-
// Ensure test dir exists
|
|
24
|
-
if (!fs.existsSync(testDir)) {
|
|
25
|
-
fs.mkdirSync(testDir, { recursive: true });
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
afterAll(() => {
|
|
29
|
-
// Cleanup
|
|
30
|
-
if (fs.existsSync(testTemplatePath)) {
|
|
31
|
-
fs.rmSync(testTemplatePath);
|
|
32
|
-
}
|
|
33
|
-
if (fs.existsSync(testDir)) {
|
|
34
|
-
fs.rmSync(testDir, { recursive: true, force: true });
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
test("should return error if file not found", async () => {
|
|
38
|
-
const result = await verify_template.execute({ filePath: "nonexistent.yml", templateName: testTemplateName }, { directory: testDir });
|
|
39
|
-
expect(result.output).toContain("File not found at nonexistent.yml");
|
|
40
|
-
});
|
|
41
|
-
test("should return error if template not found", async () => {
|
|
42
|
-
const filePath = "target.yml";
|
|
43
|
-
fs.writeFileSync(path.join(testDir, filePath), "key1: value");
|
|
44
|
-
const result = await verify_template.execute({ filePath, templateName: "nonexistent-template.yml" }, { directory: testDir });
|
|
45
|
-
expect(result.output).toContain("Template 'nonexistent-template.yml' not found");
|
|
46
|
-
});
|
|
47
|
-
test("should return error if target file has invalid YAML syntax", async () => {
|
|
48
|
-
const filePath = "invalid.yml";
|
|
49
|
-
fs.writeFileSync(path.join(testDir, filePath), "key1: : value\n invalid");
|
|
50
|
-
const result = await verify_template.execute({ filePath, templateName: testTemplateName }, { directory: testDir });
|
|
51
|
-
expect(result.output).toContain("YAML Syntax Error in invalid.yml");
|
|
52
|
-
});
|
|
53
|
-
test("should return error if target file is missing keys from template", async () => {
|
|
54
|
-
const filePath = "missing-keys.yml";
|
|
55
|
-
// Missing key2
|
|
56
|
-
fs.writeFileSync(path.join(testDir, filePath), "key1: some-value");
|
|
57
|
-
const result = await verify_template.execute({ filePath, templateName: testTemplateName }, { directory: testDir });
|
|
58
|
-
expect(result.output).toContain("YAML Structure Validation Failed");
|
|
59
|
-
expect(result.output).toContain("- Missing key: key2");
|
|
60
|
-
});
|
|
61
|
-
test("should pass if target file matches template structure", async () => {
|
|
62
|
-
const filePath = "valid.yml";
|
|
63
|
-
fs.writeFileSync(path.join(testDir, filePath), `
|
|
64
|
-
key1: different_value
|
|
65
|
-
key2:
|
|
66
|
-
subKey1: different_sub_value
|
|
67
|
-
extraKey: this is fine
|
|
68
|
-
`);
|
|
69
|
-
const result = await verify_template.execute({ filePath, templateName: testTemplateName }, { directory: testDir });
|
|
70
|
-
expect(result.output).toBe("File exists and follows the correct YAML structure.");
|
|
71
|
-
});
|
|
72
|
-
test("should pass if template has null or non-object values and target has them too", async () => {
|
|
73
|
-
const nullTemplateName = "null-template.yml";
|
|
74
|
-
const nullTemplatePath = path.join(templatesDir, nullTemplateName);
|
|
75
|
-
fs.writeFileSync(nullTemplatePath, "key: null\nother: 123");
|
|
76
|
-
const filePath = "valid-null.yml";
|
|
77
|
-
fs.writeFileSync(path.join(testDir, filePath), "key: something\nother: 456");
|
|
78
|
-
const result = await verify_template.execute({ filePath, templateName: nullTemplateName }, { directory: testDir });
|
|
79
|
-
expect(result.output).toBe("File exists and follows the correct YAML structure.");
|
|
80
|
-
fs.rmSync(nullTemplatePath);
|
|
81
|
-
});
|
|
82
|
-
describe("real templates validation", () => {
|
|
83
|
-
let templateFiles = [];
|
|
84
|
-
try {
|
|
85
|
-
const files = fs.readdirSync(templatesDir);
|
|
86
|
-
templateFiles = files.filter(f => f.endsWith('.yml') || f.endsWith('.yaml') || f.endsWith('.md'));
|
|
87
|
-
// Filter out our dummy test template
|
|
88
|
-
templateFiles = templateFiles.filter(f => f !== "test-template.yml" && f !== "null-template.yml");
|
|
89
|
-
}
|
|
90
|
-
catch (e) {
|
|
91
|
-
// templates dir might not exist or be readable in some contexts, though beforeAll ensures it exists
|
|
92
|
-
}
|
|
93
|
-
for (const templateName of templateFiles) {
|
|
94
|
-
test(`should successfully validate a valid instance of ${templateName}`, async () => {
|
|
95
|
-
// We use the templates directory as the working directory
|
|
96
|
-
// and the template name as the filePath, effectively comparing the template to itself to ensure it's structurally valid.
|
|
97
|
-
const result = await verify_template.execute({ filePath: templateName, templateName }, { directory: templatesDir });
|
|
98
|
-
expect(result.output).toBe("File exists and follows the correct YAML structure.");
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
});
|