amazingteam 3.0.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/.ai-team/agents/architect.md +144 -0
- package/.ai-team/agents/ci-analyst.md +188 -0
- package/.ai-team/agents/developer.md +176 -0
- package/.ai-team/agents/planner.md +355 -0
- package/.ai-team/agents/qa.md +189 -0
- package/.ai-team/agents/reviewer.md +211 -0
- package/.ai-team/agents/triage.md +146 -0
- package/.ai-team/commands/ci-analyze.md +116 -0
- package/.ai-team/commands/design.md +100 -0
- package/.ai-team/commands/implement.md +108 -0
- package/.ai-team/commands/release-check.md +142 -0
- package/.ai-team/commands/review.md +142 -0
- package/.ai-team/commands/test.md +115 -0
- package/.ai-team/commands/triage.md +138 -0
- package/.ai-team/memory/architect/architecture_notes.md +67 -0
- package/.ai-team/memory/architect/design_rationale.md +113 -0
- package/.ai-team/memory/architect/module_map.md +84 -0
- package/.ai-team/memory/ci-analyst/failure_patterns.md +102 -0
- package/.ai-team/memory/ci-analyst/runbook_references.md +87 -0
- package/.ai-team/memory/developer/bug_investigation.md +102 -0
- package/.ai-team/memory/developer/build_issues.md +115 -0
- package/.ai-team/memory/developer/implementation_notes.md +83 -0
- package/.ai-team/memory/failures/failure_library.md +103 -0
- package/.ai-team/memory/planner/decomposition_notes.md +82 -0
- package/.ai-team/memory/planner/flow_rules.md +86 -0
- package/.ai-team/memory/planner/github_issue_patterns.md +229 -0
- package/.ai-team/memory/qa/regression_cases.md +101 -0
- package/.ai-team/memory/qa/test_strategy.md +138 -0
- package/.ai-team/memory/qa/validation_notes.md +110 -0
- package/.ai-team/memory/reviewer/quality_rules.md +105 -0
- package/.ai-team/memory/reviewer/recurring_risks.md +109 -0
- package/.ai-team/memory/reviewer/review_notes.md +124 -0
- package/.ai-team/memory/triage/classification_heuristics.md +82 -0
- package/.ai-team/memory/triage/debug_notes.md +87 -0
- package/.ai-team/opencode.template.jsonc +216 -0
- package/.ai-team/skills/bugfix-playbook/skill.md +174 -0
- package/.ai-team/skills/ci-failure-analysis/skill.md +176 -0
- package/.ai-team/skills/issue-triage/skill.md +163 -0
- package/.ai-team/skills/regression-checklist/skill.md +176 -0
- package/.ai-team/skills/release-readiness-check/skill.md +216 -0
- package/.ai-team/skills/repo-architecture-reader/skill.md +139 -0
- package/.ai-team/skills/safe-refactor-checklist/skill.md +215 -0
- package/.ai-team/skills/task-breakdown-and-dispatch/skill.md +151 -0
- package/.ai-team/skills/test-first-feature-dev/skill.md +205 -0
- package/.ai-team/workflows/ci.yml +81 -0
- package/.ai-team/workflows/nightly-ai-maintenance.yml +129 -0
- package/.ai-team/workflows/opencode.yml +33 -0
- package/.ai-team/workflows/pr-check.yml +41 -0
- package/.foundation/foundation.lock +38 -0
- package/.foundation/local-overrides.md +97 -0
- package/.foundation/upgrade-history.md +38 -0
- package/.opencode/agents/architect.md +38 -0
- package/.opencode/agents/ci-analyst.md +38 -0
- package/.opencode/agents/developer.md +43 -0
- package/.opencode/agents/planner.md +47 -0
- package/.opencode/agents/qa.md +34 -0
- package/.opencode/agents/reviewer.md +38 -0
- package/.opencode/agents/triage.md +37 -0
- package/.opencode/commands/auto.md +264 -0
- package/.opencode/commands/breakdown-issue.md +94 -0
- package/.opencode/commands/ci-analyze.md +15 -0
- package/.opencode/commands/close-parent-task.md +122 -0
- package/.opencode/commands/design.md +15 -0
- package/.opencode/commands/dispatch-next.md +102 -0
- package/.opencode/commands/implement.md +16 -0
- package/.opencode/commands/release-check.md +16 -0
- package/.opencode/commands/resume.md +88 -0
- package/.opencode/commands/review.md +15 -0
- package/.opencode/commands/show-blockers.md +97 -0
- package/.opencode/commands/summarize-parent.md +121 -0
- package/.opencode/commands/test.md +15 -0
- package/.opencode/commands/triage.md +109 -0
- package/.opencode/skills/bugfix-playbook/SKILL.md +81 -0
- package/.opencode/skills/ci-failure-analysis/SKILL.md +94 -0
- package/.opencode/skills/issue-triage/SKILL.md +80 -0
- package/.opencode/skills/regression-checklist/SKILL.md +81 -0
- package/.opencode/skills/release-readiness-check/SKILL.md +81 -0
- package/.opencode/skills/repo-architecture-reader/SKILL.md +65 -0
- package/.opencode/skills/safe-refactor-checklist/SKILL.md +76 -0
- package/.opencode/skills/task-breakdown-and-dispatch/SKILL.md +255 -0
- package/.opencode/skills/test-first-feature-dev/SKILL.md +78 -0
- package/AGENTS.md +879 -0
- package/CHANGELOG.md +261 -0
- package/LICENSE +21 -0
- package/README.md +1215 -0
- package/VERSION +1 -0
- package/action/__tests__/downloader.test.js +251 -0
- package/action/__tests__/merger.test.js +156 -0
- package/action/__tests__/path-resolver.test.js +199 -0
- package/action/__tests__/validator.test.js +310 -0
- package/action/action.yml +61 -0
- package/action/index.js +223 -0
- package/action/lib/downloader.js +344 -0
- package/action/lib/merger.js +170 -0
- package/action/lib/path-resolver.js +176 -0
- package/action/lib/setup.js +286 -0
- package/action/lib/validator.js +324 -0
- package/cli/__tests__/cli.test.js +270 -0
- package/cli/amazingteam.cjs +225 -0
- package/cli/commands/check-update.cjs +159 -0
- package/cli/commands/init.cjs +412 -0
- package/cli/commands/local.cjs +264 -0
- package/cli/commands/migrate.cjs +316 -0
- package/cli/commands/status.cjs +241 -0
- package/cli/commands/upgrade.cjs +213 -0
- package/cli/commands/validate.cjs +259 -0
- package/cli/commands/version.cjs +59 -0
- package/cli/sync.cjs +237 -0
- package/dist/index.js +35 -0
- package/docs/architecture/overview.md +138 -0
- package/docs/blocker_resolution_design.md +372 -0
- package/docs/bootstrap-model.md +356 -0
- package/docs/config-reference.md +458 -0
- package/docs/how-to-use.md +178 -0
- package/docs/migration-to-v3.md +355 -0
- package/docs/overlay-guide.md +156 -0
- package/docs/patterns/README.md +67 -0
- package/docs/quick-start-v3.md +330 -0
- package/docs/releases/README.md +64 -0
- package/docs/runbooks/ci/README.md +62 -0
- package/docs/runbooks/ci/build-debug.md +120 -0
- package/docs/runbooks/ci/flaky-tests.md +127 -0
- package/docs/runbooks/getting-started.md +81 -0
- package/docs/upgrade-policy.md +188 -0
- package/docs/versioning.md +199 -0
- package/overlays/README.md +30 -0
- package/overlays/ai-agent-product/.ai-team/skills/llm-integration/skill.md +99 -0
- package/overlays/ai-agent-product/docs/ai-agent-architecture.md +68 -0
- package/overlays/ai-agent-product/overlay.yaml +26 -0
- package/overlays/cpp-qt-desktop/.ai-team/skills/qt-signals-slots/skill.md +60 -0
- package/overlays/cpp-qt-desktop/docs/qt-conventions.md +64 -0
- package/overlays/cpp-qt-desktop/overlay.yaml +22 -0
- package/overlays/python-backend/.ai-team/skills/python-testing/skill.md +90 -0
- package/overlays/python-backend/docs/python-style.md +78 -0
- package/overlays/python-backend/overlay.yaml +22 -0
- package/overlays/web-fullstack/.ai-team/skills/frontend-testing/skill.md +70 -0
- package/overlays/web-fullstack/docs/frontend-conventions.md +68 -0
- package/overlays/web-fullstack/overlay.yaml +26 -0
- package/package.json +84 -0
- package/presets/default.yaml +161 -0
- package/presets/go.yaml +43 -0
- package/presets/python.yaml +43 -0
- package/presets/typescript.yaml +40 -0
- package/schemas/config.schema.json +239 -0
- package/scripts/diff_foundation_vs_project.sh +134 -0
- package/scripts/generate_docs.sh +200 -0
- package/scripts/init_project.sh +455 -0
- package/scripts/plan_upgrade.sh +268 -0
- package/scripts/upgrade_foundation.sh +365 -0
- package/scripts/validate-foundation.cjs +278 -0
- package/scripts/validate_foundation.sh +192 -0
- package/scripts/validate_project_setup.sh +171 -0
- package/tasks/README.md +94 -0
- package/tasks/_template/analysis.md +76 -0
- package/tasks/_template/design.md +121 -0
- package/tasks/_template/implementation.md +121 -0
- package/tasks/_template/release.md +119 -0
- package/tasks/_template/review.md +131 -0
- package/tasks/_template/subtasks/task.yaml +24 -0
- package/tasks/_template/task.yaml +75 -0
- package/tasks/_template/validation.md +128 -0
- package/templates/amazingteam.yml +81 -0
- package/templates/gitignore +14 -0
- package/templates/opencode.jsonc +216 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime Setup Module
|
|
3
|
+
* Initializes runtime directories and generates opencode.jsonc
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const PathResolver = require('./path-resolver');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Initialize runtime directories
|
|
12
|
+
* @param {string} projectDir - Project directory
|
|
13
|
+
* @returns {string[]} Created directories
|
|
14
|
+
*/
|
|
15
|
+
function initializeRuntimeDirectories(projectDir) {
|
|
16
|
+
const resolver = new PathResolver(null, projectDir);
|
|
17
|
+
const created = [];
|
|
18
|
+
|
|
19
|
+
const dirs = [
|
|
20
|
+
'.amazingteam',
|
|
21
|
+
'.amazingteam/memory',
|
|
22
|
+
'.amazingteam/memory/planner',
|
|
23
|
+
'.amazingteam/memory/architect',
|
|
24
|
+
'.amazingteam/memory/developer',
|
|
25
|
+
'.amazingteam/memory/qa',
|
|
26
|
+
'.amazingteam/memory/reviewer',
|
|
27
|
+
'.amazingteam/memory/triage',
|
|
28
|
+
'.amazingteam/memory/ci-analyst',
|
|
29
|
+
'.amazingteam/memory/failures',
|
|
30
|
+
'tasks',
|
|
31
|
+
'tasks/_template'
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
for (const dir of dirs) {
|
|
35
|
+
const dirPath = path.join(projectDir, dir);
|
|
36
|
+
if (!fs.existsSync(dirPath)) {
|
|
37
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
38
|
+
created.push(dir);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Create memory placeholder files if they don't exist
|
|
43
|
+
const memoryFiles = {
|
|
44
|
+
'planner': ['decomposition_notes.md', 'flow_rules.md', 'github_issue_patterns.md'],
|
|
45
|
+
'architect': ['architecture_notes.md', 'module_map.md', 'design_rationale.md'],
|
|
46
|
+
'developer': ['implementation_notes.md', 'bug_investigation.md', 'build_issues.md'],
|
|
47
|
+
'qa': ['test_strategy.md', 'regression_cases.md', 'validation_notes.md'],
|
|
48
|
+
'reviewer': ['review_notes.md', 'quality_rules.md', 'recurring_risks.md'],
|
|
49
|
+
'triage': ['classification_heuristics.md', 'debug_notes.md'],
|
|
50
|
+
'ci-analyst': ['failure_patterns.md', 'runbook_references.md']
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
for (const [role, files] of Object.entries(memoryFiles)) {
|
|
54
|
+
for (const file of files) {
|
|
55
|
+
const filePath = path.join(projectDir, '.amazingteam', 'memory', role, file);
|
|
56
|
+
if (!fs.existsSync(filePath)) {
|
|
57
|
+
fs.writeFileSync(filePath, `# ${role} ${file.replace('.md', '').replace(/_/g, ' ')}\n\n`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Create failure library placeholder
|
|
63
|
+
const failuresPath = path.join(projectDir, '.amazingteam', 'memory', 'failures', 'failure_library.md');
|
|
64
|
+
if (!fs.existsSync(failuresPath)) {
|
|
65
|
+
fs.writeFileSync(failuresPath, `# Failure Library\n\nShared failure patterns documented by CI Analyst.\n\n## Categories\n\n### Build Failures\n\n### Test Failures\n\n### Infrastructure Issues\n\n`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return created;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Generate opencode.jsonc from template
|
|
73
|
+
* @param {string} foundationDir - Foundation directory
|
|
74
|
+
* @param {string} projectDir - Project directory
|
|
75
|
+
* @param {Object} config - User configuration
|
|
76
|
+
* @returns {string} Path to generated opencode.jsonc
|
|
77
|
+
*/
|
|
78
|
+
function generateOpenCodeConfig(foundationDir, projectDir, config) {
|
|
79
|
+
const resolver = new PathResolver(foundationDir, projectDir);
|
|
80
|
+
const templatePath = path.join(foundationDir, 'templates', 'opencode.jsonc');
|
|
81
|
+
|
|
82
|
+
let template;
|
|
83
|
+
if (fs.existsSync(templatePath)) {
|
|
84
|
+
template = fs.readFileSync(templatePath, 'utf-8');
|
|
85
|
+
} else {
|
|
86
|
+
// Use default template
|
|
87
|
+
template = getDefaultOpenCodeTemplate();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Replace template variables
|
|
91
|
+
const vars = {
|
|
92
|
+
AI_TEAM_VERSION: config.ai_team?.version || '3.0.0',
|
|
93
|
+
PROJECT_NAME: config.project?.name || 'my-project',
|
|
94
|
+
PROJECT_DESCRIPTION: config.project?.description || '',
|
|
95
|
+
LANGUAGE: config.project?.language || 'typescript',
|
|
96
|
+
FRAMEWORK: config.project?.framework || 'node',
|
|
97
|
+
FOUNDATION_PATH: foundationDir
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
let content = resolver.resolveTemplateVars(template, vars);
|
|
101
|
+
|
|
102
|
+
// Write to project
|
|
103
|
+
const outputPath = resolver.resolveOpenCodeConfigPath();
|
|
104
|
+
fs.writeFileSync(outputPath, content);
|
|
105
|
+
|
|
106
|
+
return outputPath;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get default OpenCode template
|
|
111
|
+
* @returns {string} Default template content
|
|
112
|
+
*/
|
|
113
|
+
function getDefaultOpenCodeTemplate() {
|
|
114
|
+
return `{
|
|
115
|
+
"$schema": "https://opencode.ai/schema.json",
|
|
116
|
+
"version": "{{AI_TEAM_VERSION}}",
|
|
117
|
+
"project": {
|
|
118
|
+
"name": "{{PROJECT_NAME}}",
|
|
119
|
+
"description": "{{PROJECT_DESCRIPTION}}",
|
|
120
|
+
"language": "{{LANGUAGE}}",
|
|
121
|
+
"framework": "{{FRAMEWORK}}"
|
|
122
|
+
},
|
|
123
|
+
"instructions": ["AGENTS.md"],
|
|
124
|
+
"autoupdate": true,
|
|
125
|
+
"permission": {
|
|
126
|
+
"edit": "ask",
|
|
127
|
+
"bash": "ask"
|
|
128
|
+
},
|
|
129
|
+
"tools": {
|
|
130
|
+
"write": true,
|
|
131
|
+
"edit": true,
|
|
132
|
+
"bash": true
|
|
133
|
+
}
|
|
134
|
+
}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Copy task templates from foundation
|
|
139
|
+
* @param {string} foundationDir - Foundation directory
|
|
140
|
+
* @param {string} projectDir - Project directory
|
|
141
|
+
* @returns {string[]} Copied files
|
|
142
|
+
*/
|
|
143
|
+
function copyTaskTemplates(foundationDir, projectDir) {
|
|
144
|
+
const templateDir = path.join(foundationDir, 'tasks', '_template');
|
|
145
|
+
const targetDir = path.join(projectDir, 'tasks', '_template');
|
|
146
|
+
|
|
147
|
+
const copied = [];
|
|
148
|
+
|
|
149
|
+
if (fs.existsSync(templateDir)) {
|
|
150
|
+
const files = fs.readdirSync(templateDir);
|
|
151
|
+
|
|
152
|
+
for (const file of files) {
|
|
153
|
+
const srcPath = path.join(templateDir, file);
|
|
154
|
+
const destPath = path.join(targetDir, file);
|
|
155
|
+
|
|
156
|
+
if (fs.statSync(srcPath).isFile()) {
|
|
157
|
+
fs.copyFileSync(srcPath, destPath);
|
|
158
|
+
copied.push(file);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return copied;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Update .gitignore
|
|
168
|
+
* @param {string} projectDir - Project directory
|
|
169
|
+
* @returns {boolean} Whether .gitignore was updated
|
|
170
|
+
*/
|
|
171
|
+
function updateGitignore(projectDir) {
|
|
172
|
+
const gitignorePath = path.join(projectDir, '.gitignore');
|
|
173
|
+
const additions = [
|
|
174
|
+
'# AmazingTeam local foundation',
|
|
175
|
+
'.amazingteam-local/',
|
|
176
|
+
'',
|
|
177
|
+
'# AmazingTeam cache',
|
|
178
|
+
'.amazingteam-cache/'
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
let content = '';
|
|
182
|
+
if (fs.existsSync(gitignorePath)) {
|
|
183
|
+
content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
184
|
+
|
|
185
|
+
// Check if already has AmazingTeam entries
|
|
186
|
+
if (content.includes('.amazingteam-local/')) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Append additions
|
|
192
|
+
const newContent = content + '\n\n' + additions.join('\n') + '\n';
|
|
193
|
+
fs.writeFileSync(gitignorePath, newContent);
|
|
194
|
+
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Full setup process
|
|
200
|
+
* @param {string} foundationDir - Foundation directory
|
|
201
|
+
* @param {string} projectDir - Project directory
|
|
202
|
+
* @param {Object} config - User configuration
|
|
203
|
+
* @returns {Object} Setup result
|
|
204
|
+
*/
|
|
205
|
+
function setup(foundationDir, projectDir, config) {
|
|
206
|
+
console.log('Setting up AmazingTeam...');
|
|
207
|
+
|
|
208
|
+
// 1. Initialize runtime directories
|
|
209
|
+
console.log('Creating runtime directories...');
|
|
210
|
+
const createdDirs = initializeRuntimeDirectories(projectDir);
|
|
211
|
+
console.log(`Created ${createdDirs.length} directories`);
|
|
212
|
+
|
|
213
|
+
// 2. Copy task templates
|
|
214
|
+
console.log('Copying task templates...');
|
|
215
|
+
const copiedTemplates = copyTaskTemplates(foundationDir, projectDir);
|
|
216
|
+
console.log(`Copied ${copiedTemplates.length} templates`);
|
|
217
|
+
|
|
218
|
+
// 3. Generate opencode.jsonc
|
|
219
|
+
console.log('Generating opencode.jsonc...');
|
|
220
|
+
const opencodePath = generateOpenCodeConfig(foundationDir, projectDir, config);
|
|
221
|
+
console.log(`Generated: ${opencodePath}`);
|
|
222
|
+
|
|
223
|
+
// 4. Update .gitignore
|
|
224
|
+
console.log('Updating .gitignore...');
|
|
225
|
+
const gitignoreUpdated = updateGitignore(projectDir);
|
|
226
|
+
if (gitignoreUpdated) {
|
|
227
|
+
console.log('Updated .gitignore');
|
|
228
|
+
} else {
|
|
229
|
+
console.log('.gitignore already up to date');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
createdDirectories: createdDirs,
|
|
234
|
+
copiedTemplates,
|
|
235
|
+
opencodePath,
|
|
236
|
+
gitignoreUpdated
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Verify setup
|
|
242
|
+
* @param {string} projectDir - Project directory
|
|
243
|
+
* @returns {{ valid: boolean, issues: string[] }}
|
|
244
|
+
*/
|
|
245
|
+
function verifySetup(projectDir) {
|
|
246
|
+
const issues = [];
|
|
247
|
+
|
|
248
|
+
// Check required directories
|
|
249
|
+
const requiredDirs = [
|
|
250
|
+
'.amazingteam',
|
|
251
|
+
'.amazingteam/memory',
|
|
252
|
+
'tasks'
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
for (const dir of requiredDirs) {
|
|
256
|
+
if (!fs.existsSync(path.join(projectDir, dir))) {
|
|
257
|
+
issues.push(`Missing directory: ${dir}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Check required files
|
|
262
|
+
const requiredFiles = [
|
|
263
|
+
'amazingteam.config.yaml',
|
|
264
|
+
'opencode.jsonc'
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
for (const file of requiredFiles) {
|
|
268
|
+
if (!fs.existsSync(path.join(projectDir, file))) {
|
|
269
|
+
issues.push(`Missing file: ${file}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
valid: issues.length === 0,
|
|
275
|
+
issues
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
module.exports = {
|
|
280
|
+
initializeRuntimeDirectories,
|
|
281
|
+
generateOpenCodeConfig,
|
|
282
|
+
copyTaskTemplates,
|
|
283
|
+
updateGitignore,
|
|
284
|
+
setup,
|
|
285
|
+
verifySetup
|
|
286
|
+
};
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Validator Module
|
|
3
|
+
* Validates user configuration against schema
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
// Minimal JSON Schema validator (no external dependencies)
|
|
10
|
+
// For production, consider using ajv
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Validate a value against a schema
|
|
14
|
+
* @param {*} value - Value to validate
|
|
15
|
+
* @param {Object} schema - JSON Schema
|
|
16
|
+
* @param {string} path - Current path (for error messages)
|
|
17
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
18
|
+
*/
|
|
19
|
+
function validateAgainstSchema(value, schema, path = 'root') {
|
|
20
|
+
const errors = [];
|
|
21
|
+
|
|
22
|
+
if (!schema) {
|
|
23
|
+
return { valid: true, errors: [] };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Type validation
|
|
27
|
+
if (schema.type) {
|
|
28
|
+
const actualType = Array.isArray(value) ? 'array' : typeof value;
|
|
29
|
+
if (schema.type === 'integer') {
|
|
30
|
+
if (typeof value !== 'number' || !Number.isInteger(value)) {
|
|
31
|
+
errors.push(`${path}: expected type ${schema.type}, got ${actualType}`);
|
|
32
|
+
}
|
|
33
|
+
} else if (actualType !== schema.type && !(schema.type === 'object' && value === null)) {
|
|
34
|
+
if (schema.nullable !== true || value !== null) {
|
|
35
|
+
errors.push(`${path}: expected type ${schema.type}, got ${actualType}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Enum validation
|
|
41
|
+
if (schema.enum && !schema.enum.includes(value)) {
|
|
42
|
+
errors.push(`${path}: value must be one of [${schema.enum.join(', ')}], got ${value}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// String validations
|
|
46
|
+
if (typeof value === 'string') {
|
|
47
|
+
if (schema.minLength !== undefined && value.length < schema.minLength) {
|
|
48
|
+
errors.push(`${path}: string length ${value.length} is less than minLength ${schema.minLength}`);
|
|
49
|
+
}
|
|
50
|
+
if (schema.maxLength !== undefined && value.length > schema.maxLength) {
|
|
51
|
+
errors.push(`${path}: string length ${value.length} exceeds maxLength ${schema.maxLength}`);
|
|
52
|
+
}
|
|
53
|
+
if (schema.pattern) {
|
|
54
|
+
const regex = new RegExp(schema.pattern);
|
|
55
|
+
if (!regex.test(value)) {
|
|
56
|
+
errors.push(`${path}: string does not match pattern ${schema.pattern}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Number validations
|
|
62
|
+
if (typeof value === 'number') {
|
|
63
|
+
if (schema.minimum !== undefined && value < schema.minimum) {
|
|
64
|
+
errors.push(`${path}: value ${value} is less than minimum ${schema.minimum}`);
|
|
65
|
+
}
|
|
66
|
+
if (schema.maximum !== undefined && value > schema.maximum) {
|
|
67
|
+
errors.push(`${path}: value ${value} exceeds maximum ${schema.maximum}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Array validations
|
|
72
|
+
if (Array.isArray(value) && schema.items) {
|
|
73
|
+
value.forEach((item, index) => {
|
|
74
|
+
const itemResult = validateAgainstSchema(item, schema.items, `${path}[${index}]`);
|
|
75
|
+
errors.push(...itemResult.errors);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (schema.minItems !== undefined && value.length < schema.minItems) {
|
|
79
|
+
errors.push(`${path}: array length ${value.length} is less than minItems ${schema.minItems}`);
|
|
80
|
+
}
|
|
81
|
+
if (schema.maxItems !== undefined && value.length > schema.maxItems) {
|
|
82
|
+
errors.push(`${path}: array length ${value.length} exceeds maxItems ${schema.maxItems}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Object validations
|
|
87
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
88
|
+
// Required properties
|
|
89
|
+
if (schema.required) {
|
|
90
|
+
for (const reqProp of schema.required) {
|
|
91
|
+
if (!(reqProp in value)) {
|
|
92
|
+
errors.push(`${path}: missing required property '${reqProp}'`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Property validations
|
|
98
|
+
if (schema.properties) {
|
|
99
|
+
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
100
|
+
if (propName in value) {
|
|
101
|
+
const propResult = validateAgainstSchema(value[propName], propSchema, `${path}.${propName}`);
|
|
102
|
+
errors.push(...propResult.errors);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Additional properties
|
|
108
|
+
if (schema.additionalProperties === false) {
|
|
109
|
+
const allowedProps = new Set([
|
|
110
|
+
...Object.keys(schema.properties || {}),
|
|
111
|
+
...(schema.required || [])
|
|
112
|
+
]);
|
|
113
|
+
for (const propName of Object.keys(value)) {
|
|
114
|
+
if (!allowedProps.has(propName) && !propName.startsWith('$')) {
|
|
115
|
+
errors.push(`${path}: unknown property '${propName}'`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
valid: errors.length === 0,
|
|
123
|
+
errors
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Load and parse JSON Schema
|
|
129
|
+
* @param {string} schemaPath - Path to schema file
|
|
130
|
+
* @returns {Object} Parsed schema
|
|
131
|
+
*/
|
|
132
|
+
function loadSchema(schemaPath) {
|
|
133
|
+
const schemaContent = fs.readFileSync(schemaPath, 'utf-8');
|
|
134
|
+
return JSON.parse(schemaContent);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Validate user configuration
|
|
139
|
+
* @param {Object} config - User configuration object
|
|
140
|
+
* @param {Object} schema - JSON Schema object
|
|
141
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
142
|
+
*/
|
|
143
|
+
function validateConfig(config, schema) {
|
|
144
|
+
return validateAgainstSchema(config, schema);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Validate required files exist in project
|
|
149
|
+
* @param {string} projectDir - Project directory
|
|
150
|
+
* @param {string[]} requiredFiles - List of required files
|
|
151
|
+
* @returns {{ valid: boolean, missing: string[] }}
|
|
152
|
+
*/
|
|
153
|
+
function validateRequiredFiles(projectDir, requiredFiles) {
|
|
154
|
+
const missing = [];
|
|
155
|
+
|
|
156
|
+
for (const file of requiredFiles) {
|
|
157
|
+
const filePath = path.join(projectDir, file);
|
|
158
|
+
if (!fs.existsSync(filePath)) {
|
|
159
|
+
missing.push(file);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
valid: missing.length === 0,
|
|
165
|
+
missing
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Validate project structure
|
|
171
|
+
* @param {string} projectDir - Project directory
|
|
172
|
+
* @returns {{ valid: boolean, issues: string[] }}
|
|
173
|
+
*/
|
|
174
|
+
function validateProjectStructure(projectDir) {
|
|
175
|
+
const issues = [];
|
|
176
|
+
|
|
177
|
+
// Check required directories
|
|
178
|
+
const requiredDirs = [
|
|
179
|
+
'.amazingteam',
|
|
180
|
+
'.amazingteam/memory',
|
|
181
|
+
'tasks'
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
for (const dir of requiredDirs) {
|
|
185
|
+
const dirPath = path.join(projectDir, dir);
|
|
186
|
+
if (!fs.existsSync(dirPath)) {
|
|
187
|
+
issues.push(`Missing directory: ${dir}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check required files
|
|
192
|
+
const requiredFiles = [
|
|
193
|
+
'amazingteam.config.yaml'
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
const fileResult = validateRequiredFiles(projectDir, requiredFiles);
|
|
197
|
+
issues.push(...fileResult.missing.map(f => `Missing file: ${f}`));
|
|
198
|
+
|
|
199
|
+
// Check opencode.jsonc exists
|
|
200
|
+
const opencodePath = path.join(projectDir, 'opencode.jsonc');
|
|
201
|
+
if (!fs.existsSync(opencodePath)) {
|
|
202
|
+
issues.push('Missing opencode.jsonc (run `amazingteam local` to generate)');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
valid: issues.length === 0,
|
|
207
|
+
issues
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Validate configuration file
|
|
213
|
+
* @param {string} configPath - Path to config file
|
|
214
|
+
* @param {string} schemaPath - Path to schema file
|
|
215
|
+
* @returns {{ valid: boolean, errors: string[], warnings: string[] }}
|
|
216
|
+
*/
|
|
217
|
+
function validateConfigFile(configPath, schemaPath) {
|
|
218
|
+
const errors = [];
|
|
219
|
+
const warnings = [];
|
|
220
|
+
|
|
221
|
+
// Check files exist
|
|
222
|
+
if (!fs.existsSync(configPath)) {
|
|
223
|
+
errors.push(`Config file not found: ${configPath}`);
|
|
224
|
+
return { valid: false, errors, warnings };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!fs.existsSync(schemaPath)) {
|
|
228
|
+
warnings.push(`Schema file not found: ${schemaPath}, skipping schema validation`);
|
|
229
|
+
return { valid: true, errors, warnings };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Load and parse config
|
|
233
|
+
let config;
|
|
234
|
+
try {
|
|
235
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
236
|
+
config = parseYaml(configContent);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
errors.push(`Failed to parse config: ${err.message}`);
|
|
239
|
+
return { valid: false, errors, warnings };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Load schema
|
|
243
|
+
let schema;
|
|
244
|
+
try {
|
|
245
|
+
schema = loadSchema(schemaPath);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
errors.push(`Failed to load schema: ${err.message}`);
|
|
248
|
+
return { valid: false, errors, warnings };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Validate against schema
|
|
252
|
+
const result = validateConfig(config, schema);
|
|
253
|
+
errors.push(...result.errors);
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
valid: errors.length === 0,
|
|
257
|
+
errors,
|
|
258
|
+
warnings
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Simple YAML parser (handles basic YAML)
|
|
264
|
+
* For production, use a proper YAML library
|
|
265
|
+
* @param {string} content - YAML content
|
|
266
|
+
* @returns {Object} Parsed object
|
|
267
|
+
*/
|
|
268
|
+
function parseYaml(content) {
|
|
269
|
+
// This is a minimal YAML parser for simple configs
|
|
270
|
+
// For production, use 'js-yaml' or similar
|
|
271
|
+
const lines = content.split('\n');
|
|
272
|
+
const result = {};
|
|
273
|
+
let currentPath = [];
|
|
274
|
+
let currentObj = result;
|
|
275
|
+
|
|
276
|
+
for (const line of lines) {
|
|
277
|
+
// Skip comments and empty lines
|
|
278
|
+
if (line.trim().startsWith('#') || line.trim() === '') continue;
|
|
279
|
+
|
|
280
|
+
const indent = line.search(/\S/);
|
|
281
|
+
const level = Math.floor(indent / 2);
|
|
282
|
+
const trimmed = line.trim();
|
|
283
|
+
|
|
284
|
+
// Handle key-value pairs
|
|
285
|
+
const colonIndex = trimmed.indexOf(':');
|
|
286
|
+
if (colonIndex > 0) {
|
|
287
|
+
const key = trimmed.substring(0, colonIndex).trim();
|
|
288
|
+
let value = trimmed.substring(colonIndex + 1).trim();
|
|
289
|
+
|
|
290
|
+
// Parse value
|
|
291
|
+
if (value === '') {
|
|
292
|
+
// Nested object
|
|
293
|
+
currentObj[key] = {};
|
|
294
|
+
} else if (value.startsWith('[') && value.endsWith(']')) {
|
|
295
|
+
// Array
|
|
296
|
+
currentObj[key] = value.slice(1, -1).split(',').map(s => s.trim().replace(/"/g, ''));
|
|
297
|
+
} else if (value.startsWith('"') && value.endsWith('"')) {
|
|
298
|
+
// String
|
|
299
|
+
currentObj[key] = value.slice(1, -1);
|
|
300
|
+
} else if (value === 'true' || value === 'false') {
|
|
301
|
+
// Boolean
|
|
302
|
+
currentObj[key] = value === 'true';
|
|
303
|
+
} else if (!isNaN(Number(value))) {
|
|
304
|
+
// Number
|
|
305
|
+
currentObj[key] = Number(value);
|
|
306
|
+
} else if (value === 'null') {
|
|
307
|
+
currentObj[key] = null;
|
|
308
|
+
} else {
|
|
309
|
+
currentObj[key] = value;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return result;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
module.exports = {
|
|
318
|
+
validateAgainstSchema,
|
|
319
|
+
validateConfig,
|
|
320
|
+
validateConfigFile,
|
|
321
|
+
validateRequiredFiles,
|
|
322
|
+
validateProjectStructure,
|
|
323
|
+
loadSchema
|
|
324
|
+
};
|