agent-workflow-kit-cli 1.2.1 → 1.3.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/dist/cli/commands/adr.js +98 -0
- package/dist/cli/commands/export.js +13 -0
- package/dist/cli/commands/profile.js +35 -0
- package/dist/cli/commands/role.js +75 -0
- package/dist/cli/commands/run.js +78 -0
- package/dist/cli/commands/workflow.js +95 -0
- package/dist/cli/index.js +193 -2
- package/dist/core/awos/adr.js +208 -0
- package/dist/core/awos/intelligence.js +235 -0
- package/dist/core/awos/profiles.js +272 -0
- package/dist/core/awos/registry.js +224 -0
- package/dist/core/awos/runtime.js +322 -0
- package/dist/core/awos/types.js +5 -0
- package/dist/core/config.js +27 -0
- package/dist/core/parser.js +143 -0
- package/dist/core/renderer.js +74 -23
- package/package.json +3 -2
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
import { promises as fs } from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { parseImports } from "../parser.js";
|
|
8
|
+
// Built-in Profiles database fallback
|
|
9
|
+
const BUILTIN_PROFILES = {
|
|
10
|
+
"layered": {
|
|
11
|
+
name: "layered",
|
|
12
|
+
version: "1.0.0",
|
|
13
|
+
folderStructure: [
|
|
14
|
+
{
|
|
15
|
+
pathPattern: "**/controller/**",
|
|
16
|
+
allowedImports: ["**/service/**", "**/dto/**", "**/model/**"],
|
|
17
|
+
forbiddenImports: ["**/repository/**", "**/entity/**"],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
pathPattern: "**/service/**",
|
|
21
|
+
allowedImports: ["**/repository/**", "**/dto/**", "**/model/**", "**/entity/**"],
|
|
22
|
+
forbiddenImports: ["**/controller/**"],
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
reviewRules: {
|
|
26
|
+
maxFileLines: 500,
|
|
27
|
+
maxMethodLines: 50,
|
|
28
|
+
requireInterfaceForServices: false,
|
|
29
|
+
},
|
|
30
|
+
testingRequirements: {
|
|
31
|
+
mustHaveTestFile: false,
|
|
32
|
+
namingSuffix: "Test.java",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
"clean-architecture": {
|
|
36
|
+
name: "clean-architecture",
|
|
37
|
+
version: "1.0.0",
|
|
38
|
+
extends: "layered",
|
|
39
|
+
folderStructure: [
|
|
40
|
+
{
|
|
41
|
+
pathPattern: "**/domain/**",
|
|
42
|
+
allowedImports: [],
|
|
43
|
+
forbiddenImports: ["**/infrastructure/**", "**/application/**", "**/presentation/**"],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
pathPattern: "**/application/**",
|
|
47
|
+
allowedImports: ["**/domain/**"],
|
|
48
|
+
forbiddenImports: ["**/infrastructure/**", "**/presentation/**"],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
reviewRules: {
|
|
52
|
+
maxFileLines: 300,
|
|
53
|
+
maxMethodLines: 30,
|
|
54
|
+
requireInterfaceForServices: true,
|
|
55
|
+
},
|
|
56
|
+
testingRequirements: {
|
|
57
|
+
mustHaveTestFile: true,
|
|
58
|
+
namingSuffix: "Test.java",
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
"feature-first": {
|
|
62
|
+
name: "feature-first",
|
|
63
|
+
version: "1.0.0",
|
|
64
|
+
folderStructure: [
|
|
65
|
+
{
|
|
66
|
+
pathPattern: "**/features/*/**",
|
|
67
|
+
mustContainPatterns: ["components", "hooks", "index.ts"],
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
reviewRules: {
|
|
71
|
+
maxFileLines: 400,
|
|
72
|
+
maxMethodLines: 40,
|
|
73
|
+
requireInterfaceForServices: false,
|
|
74
|
+
},
|
|
75
|
+
testingRequirements: {
|
|
76
|
+
mustHaveTestFile: true,
|
|
77
|
+
namingSuffix: ".test.ts",
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Deep merges two architecture profiles for inheritance resolving.
|
|
83
|
+
*/
|
|
84
|
+
export function mergeProfiles(base, extension) {
|
|
85
|
+
return {
|
|
86
|
+
name: extension.name,
|
|
87
|
+
version: extension.version,
|
|
88
|
+
extends: extension.extends,
|
|
89
|
+
folderStructure: [...(base.folderStructure || []), ...(extension.folderStructure || [])],
|
|
90
|
+
reviewRules: {
|
|
91
|
+
...base.reviewRules,
|
|
92
|
+
...extension.reviewRules,
|
|
93
|
+
},
|
|
94
|
+
testingRequirements: {
|
|
95
|
+
...base.testingRequirements,
|
|
96
|
+
...extension.testingRequirements,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Loads a profile by name from disk or built-in registry.
|
|
102
|
+
*/
|
|
103
|
+
export async function loadProfile(profileName, profilesDir) {
|
|
104
|
+
const { loadAWOSPlugins } = await import("./registry.js");
|
|
105
|
+
const registry = await loadAWOSPlugins(process.cwd());
|
|
106
|
+
let profile = registry.profiles.get(profileName) || BUILTIN_PROFILES[profileName];
|
|
107
|
+
if (profilesDir) {
|
|
108
|
+
try {
|
|
109
|
+
const customPath = path.join(profilesDir, `${profileName}.json`);
|
|
110
|
+
const fileContent = await fs.readFile(customPath, "utf8");
|
|
111
|
+
profile = JSON.parse(fileContent);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Fallback to built-in if file read fails
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!profile) {
|
|
118
|
+
throw new Error(`Architecture Profile '${profileName}' not found.`);
|
|
119
|
+
}
|
|
120
|
+
if (profile.extends) {
|
|
121
|
+
const parentProfile = await loadProfile(profile.extends, profilesDir);
|
|
122
|
+
return mergeProfiles(parentProfile, profile);
|
|
123
|
+
}
|
|
124
|
+
return profile;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Matches glob-ish path patterns. Simple helper.
|
|
128
|
+
*/
|
|
129
|
+
function matchPathPattern(filePath, pattern) {
|
|
130
|
+
// Translate simple **/ pattern to regex
|
|
131
|
+
const regexString = pattern
|
|
132
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&") // Escape regex specials
|
|
133
|
+
.replace(/\*\*/g, ".*")
|
|
134
|
+
.replace(/\*/g, "[^/]*");
|
|
135
|
+
const regex = new RegExp(`^${regexString}$|^${regexString}`);
|
|
136
|
+
return regex.test(filePath.replace(/\\/g, "/"));
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Validates a single file content and path against architectural rules.
|
|
140
|
+
*/
|
|
141
|
+
export async function validateFile(filePath, content, profile) {
|
|
142
|
+
const violations = [];
|
|
143
|
+
const relativePath = filePath.replace(/\\/g, "/");
|
|
144
|
+
// 1. Line limits check
|
|
145
|
+
const lines = content.split("\n");
|
|
146
|
+
if (lines.length > profile.reviewRules.maxFileLines) {
|
|
147
|
+
violations.push({
|
|
148
|
+
filePath,
|
|
149
|
+
ruleName: "maxFileLines",
|
|
150
|
+
message: `File has ${lines.length} lines, exceeding the limit of ${profile.reviewRules.maxFileLines}.`,
|
|
151
|
+
severity: "error",
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// 2. Folder structures and imports validation
|
|
155
|
+
for (const rule of profile.folderStructure) {
|
|
156
|
+
if (matchPathPattern(relativePath, rule.pathPattern)) {
|
|
157
|
+
// Check imports
|
|
158
|
+
if (rule.forbiddenImports || rule.allowedImports) {
|
|
159
|
+
const parsedImports = await parseImports(filePath, content);
|
|
160
|
+
for (const importedRef of parsedImports) {
|
|
161
|
+
// Forbidden check
|
|
162
|
+
if (rule.forbiddenImports) {
|
|
163
|
+
for (const forbidden of rule.forbiddenImports) {
|
|
164
|
+
if (matchPathPattern(importedRef, forbidden)) {
|
|
165
|
+
violations.push({
|
|
166
|
+
filePath,
|
|
167
|
+
ruleName: "forbiddenImports",
|
|
168
|
+
message: `Importing '${importedRef}' is forbidden in '${rule.pathPattern}'.`,
|
|
169
|
+
severity: "error",
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Allowed check (if specified, anything not matched is forbidden)
|
|
175
|
+
if (rule.allowedImports && rule.allowedImports.length > 0) {
|
|
176
|
+
let isAllowed = false;
|
|
177
|
+
// Also allow standard library / node modules
|
|
178
|
+
if (!importedRef.startsWith(".") && !importedRef.startsWith("/") && !importedRef.includes("/")) {
|
|
179
|
+
isAllowed = true;
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
for (const allowed of rule.allowedImports) {
|
|
183
|
+
if (matchPathPattern(importedRef, allowed)) {
|
|
184
|
+
isAllowed = true;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (!isAllowed) {
|
|
190
|
+
violations.push({
|
|
191
|
+
filePath,
|
|
192
|
+
ruleName: "allowedImports",
|
|
193
|
+
message: `Importing '${importedRef}' is not explicitly allowed in '${rule.pathPattern}'.`,
|
|
194
|
+
severity: "error",
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Check required patterns
|
|
201
|
+
if (rule.mustContainPatterns) {
|
|
202
|
+
for (const pattern of rule.mustContainPatterns) {
|
|
203
|
+
if (!content.includes(pattern)) {
|
|
204
|
+
violations.push({
|
|
205
|
+
filePath,
|
|
206
|
+
ruleName: "mustContainPatterns",
|
|
207
|
+
message: `File must contain pattern: '${pattern}'.`,
|
|
208
|
+
severity: "warn",
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return violations;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Scans workspace and validates all source files against the ArchitectureProfile.
|
|
219
|
+
*/
|
|
220
|
+
export async function validateArchitecture(workspaceRoot, profile) {
|
|
221
|
+
const violations = [];
|
|
222
|
+
async function traverse(dir) {
|
|
223
|
+
try {
|
|
224
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
225
|
+
for (const entry of entries) {
|
|
226
|
+
const fullPath = path.join(dir, entry.name);
|
|
227
|
+
if (entry.isDirectory()) {
|
|
228
|
+
if (entry.name.startsWith(".") ||
|
|
229
|
+
entry.name === "node_modules" ||
|
|
230
|
+
entry.name === "dist" ||
|
|
231
|
+
entry.name === "build" ||
|
|
232
|
+
entry.name === "target") {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
await traverse(fullPath);
|
|
236
|
+
}
|
|
237
|
+
else if (entry.isFile() && /\.(ts|tsx|java|py)$/.test(entry.name)) {
|
|
238
|
+
// Avoid validating test files for structure limits
|
|
239
|
+
const isTest = entry.name.endsWith(profile.testingRequirements.namingSuffix);
|
|
240
|
+
const content = await fs.readFile(fullPath, "utf8");
|
|
241
|
+
if (!isTest) {
|
|
242
|
+
const fileViolations = await validateFile(fullPath, content, profile);
|
|
243
|
+
violations.push(...fileViolations);
|
|
244
|
+
// Verify test file existence if required
|
|
245
|
+
if (profile.testingRequirements.mustHaveTestFile) {
|
|
246
|
+
const ext = path.extname(entry.name);
|
|
247
|
+
const baseName = path.basename(entry.name, ext);
|
|
248
|
+
const testFileName = baseName + profile.testingRequirements.namingSuffix;
|
|
249
|
+
const testPath = path.join(dir, testFileName);
|
|
250
|
+
try {
|
|
251
|
+
await fs.stat(testPath);
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
violations.push({
|
|
255
|
+
filePath: fullPath,
|
|
256
|
+
ruleName: "mustHaveTestFile",
|
|
257
|
+
message: `Missing matching test file: '${testFileName}' expected in the same directory.`,
|
|
258
|
+
severity: "warn",
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// Ignore read errors
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
await traverse(workspaceRoot);
|
|
271
|
+
return violations;
|
|
272
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
import { promises as fs } from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { validateWorkflowGraph } from "./runtime.js";
|
|
8
|
+
import { loadConfig } from "../config.js";
|
|
9
|
+
/**
|
|
10
|
+
* Registry for discovery, validation, and loading of reusable workflow graph definitions.
|
|
11
|
+
*/
|
|
12
|
+
export class WorkflowRegistry {
|
|
13
|
+
/**
|
|
14
|
+
* Scans a directory recursively for workflow JSON files.
|
|
15
|
+
*/
|
|
16
|
+
static async discover(dir) {
|
|
17
|
+
const workflows = [];
|
|
18
|
+
async function traverse(currentDir) {
|
|
19
|
+
try {
|
|
20
|
+
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
if (entry.name.startsWith(".") ||
|
|
25
|
+
entry.name === "node_modules" ||
|
|
26
|
+
entry.name === "dist" ||
|
|
27
|
+
entry.name === "build") {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
await traverse(fullPath);
|
|
31
|
+
}
|
|
32
|
+
else if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
33
|
+
try {
|
|
34
|
+
const content = await fs.readFile(fullPath, "utf8");
|
|
35
|
+
const parsed = JSON.parse(content);
|
|
36
|
+
if (WorkflowRegistry.isValidSchema(parsed)) {
|
|
37
|
+
// Perform graph cycles check
|
|
38
|
+
validateWorkflowGraph(parsed.graph);
|
|
39
|
+
workflows.push(parsed);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Ignore invalid JSON files or validation failures during traversal
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Ignore read errors
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
await traverse(dir);
|
|
53
|
+
return workflows;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Asserts schema validity for WorkflowDefinition.
|
|
57
|
+
*/
|
|
58
|
+
static isValidSchema(obj) {
|
|
59
|
+
return (obj &&
|
|
60
|
+
typeof obj.id === "string" &&
|
|
61
|
+
typeof obj.name === "string" &&
|
|
62
|
+
typeof obj.description === "string" &&
|
|
63
|
+
typeof obj.version === "string" &&
|
|
64
|
+
Array.isArray(obj.supportedArchitectures) &&
|
|
65
|
+
Array.isArray(obj.requiredRoles) &&
|
|
66
|
+
obj.graph &&
|
|
67
|
+
Array.isArray(obj.graph.nodes) &&
|
|
68
|
+
Array.isArray(obj.graph.edges));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Validates a workflow definition schema and graph.
|
|
72
|
+
*/
|
|
73
|
+
static validate(wf) {
|
|
74
|
+
if (!WorkflowRegistry.isValidSchema(wf)) {
|
|
75
|
+
throw new Error("Workflow schema is invalid. Missing required fields: 'id', 'name', 'version', 'supportedArchitectures', 'requiredRoles', or 'graph'.");
|
|
76
|
+
}
|
|
77
|
+
// Check graph validity
|
|
78
|
+
validateWorkflowGraph(wf.graph);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Loads a specific workflow by ID.
|
|
82
|
+
*/
|
|
83
|
+
static async load(id, dir) {
|
|
84
|
+
const list = await WorkflowRegistry.discover(dir);
|
|
85
|
+
const matched = list.find((w) => w.id === id);
|
|
86
|
+
if (!matched) {
|
|
87
|
+
throw new Error(`Workflow Pack '${id}' not found under directory '${dir}'.`);
|
|
88
|
+
}
|
|
89
|
+
return matched;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Registry for discovery, validation, and loading of agent role profiles.
|
|
94
|
+
*/
|
|
95
|
+
export class RoleRegistry {
|
|
96
|
+
/**
|
|
97
|
+
* Scans a directory recursively for role JSON files.
|
|
98
|
+
*/
|
|
99
|
+
static async discover(dir) {
|
|
100
|
+
const roles = [];
|
|
101
|
+
async function traverse(currentDir) {
|
|
102
|
+
try {
|
|
103
|
+
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
106
|
+
if (entry.isDirectory()) {
|
|
107
|
+
if (entry.name.startsWith(".") ||
|
|
108
|
+
entry.name === "node_modules" ||
|
|
109
|
+
entry.name === "dist" ||
|
|
110
|
+
entry.name === "build") {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
await traverse(fullPath);
|
|
114
|
+
}
|
|
115
|
+
else if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
116
|
+
try {
|
|
117
|
+
const content = await fs.readFile(fullPath, "utf8");
|
|
118
|
+
const parsed = JSON.parse(content);
|
|
119
|
+
if (RoleRegistry.isValidSchema(parsed)) {
|
|
120
|
+
roles.push(parsed);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Ignore invalid files
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Ignore read errors
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
await traverse(dir);
|
|
134
|
+
return roles;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Asserts schema validity for AgentRoleDefinition.
|
|
138
|
+
*/
|
|
139
|
+
static isValidSchema(obj) {
|
|
140
|
+
return (obj &&
|
|
141
|
+
typeof obj.id === "string" &&
|
|
142
|
+
typeof obj.name === "string" &&
|
|
143
|
+
typeof obj.description === "string" &&
|
|
144
|
+
Array.isArray(obj.responsibilities) &&
|
|
145
|
+
Array.isArray(obj.requiredInputs) &&
|
|
146
|
+
Array.isArray(obj.expectedOutputs) &&
|
|
147
|
+
Array.isArray(obj.validationChecklist) &&
|
|
148
|
+
Array.isArray(obj.reviewChecklist));
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Validates a role definition.
|
|
152
|
+
*/
|
|
153
|
+
static validate(role) {
|
|
154
|
+
if (!RoleRegistry.isValidSchema(role)) {
|
|
155
|
+
throw new Error("Agent Role schema is invalid. Missing required fields: 'id', 'name', 'responsibilities', 'requiredInputs', 'expectedOutputs', 'validationChecklist', or 'reviewChecklist'.");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Loads a specific role by ID.
|
|
160
|
+
*/
|
|
161
|
+
static async load(id, dir) {
|
|
162
|
+
const list = await RoleRegistry.discover(dir);
|
|
163
|
+
const matched = list.find((r) => r.id === id);
|
|
164
|
+
if (!matched) {
|
|
165
|
+
throw new Error(`Agent Role '${id}' not found under directory '${dir}'.`);
|
|
166
|
+
}
|
|
167
|
+
return matched;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
export class PluginRegistryImpl {
|
|
171
|
+
analyzers = [];
|
|
172
|
+
profiles = new Map();
|
|
173
|
+
roles = new Map();
|
|
174
|
+
executors = new Map();
|
|
175
|
+
registerAnalyzer(analyzer) {
|
|
176
|
+
this.analyzers.push(analyzer);
|
|
177
|
+
}
|
|
178
|
+
registerArchitectureProfile(profile) {
|
|
179
|
+
this.profiles.set(profile.name, profile);
|
|
180
|
+
}
|
|
181
|
+
registerAgentRole(role) {
|
|
182
|
+
this.roles.set(role.id, role);
|
|
183
|
+
}
|
|
184
|
+
registerExecutor(name, executeFn) {
|
|
185
|
+
this.executors.set(name, executeFn);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
export const pluginRegistry = new PluginRegistryImpl();
|
|
189
|
+
let pluginsLoaded = false;
|
|
190
|
+
export async function loadAWOSPlugins(workspaceRoot) {
|
|
191
|
+
if (pluginsLoaded) {
|
|
192
|
+
return pluginRegistry;
|
|
193
|
+
}
|
|
194
|
+
const config = await loadConfig(workspaceRoot);
|
|
195
|
+
if (config.plugins && config.plugins.length > 0) {
|
|
196
|
+
for (const pluginSpec of config.plugins) {
|
|
197
|
+
try {
|
|
198
|
+
let importPath = pluginSpec;
|
|
199
|
+
if (pluginSpec.startsWith(".") || pluginSpec.startsWith("/") || pluginSpec.includes("\\")) {
|
|
200
|
+
importPath = path.resolve(workspaceRoot, pluginSpec);
|
|
201
|
+
}
|
|
202
|
+
const module = await import(importPath);
|
|
203
|
+
const PluginClass = module.default || module.Plugin || module;
|
|
204
|
+
const pluginInstance = typeof PluginClass === "function" ? new PluginClass() : PluginClass;
|
|
205
|
+
if (pluginInstance && typeof pluginInstance.register === "function") {
|
|
206
|
+
pluginInstance.register(pluginRegistry);
|
|
207
|
+
console.log(`[AWOS Registry] Registered plugin: ${pluginInstance.manifest?.id || pluginSpec}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
console.warn(`[AWOS Registry] Failed to load plugin '${pluginSpec}':`, err);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
pluginsLoaded = true;
|
|
216
|
+
return pluginRegistry;
|
|
217
|
+
}
|
|
218
|
+
export function clearPluginsCache() {
|
|
219
|
+
pluginsLoaded = false;
|
|
220
|
+
pluginRegistry.analyzers = [];
|
|
221
|
+
pluginRegistry.profiles.clear();
|
|
222
|
+
pluginRegistry.roles.clear();
|
|
223
|
+
pluginRegistry.executors.clear();
|
|
224
|
+
}
|