@odavl/guardian 0.2.0 → 1.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/CHANGELOG.md +86 -2
- package/README.md +155 -97
- package/bin/guardian.js +1345 -60
- package/config/README.md +59 -0
- package/config/profiles/landing-demo.yaml +16 -0
- package/package.json +21 -11
- package/policies/landing-demo.json +22 -0
- package/src/enterprise/audit-logger.js +166 -0
- package/src/enterprise/pdf-exporter.js +267 -0
- package/src/enterprise/rbac-gate.js +142 -0
- package/src/enterprise/rbac.js +239 -0
- package/src/enterprise/site-manager.js +180 -0
- package/src/founder/feedback-system.js +156 -0
- package/src/founder/founder-tracker.js +213 -0
- package/src/founder/usage-signals.js +141 -0
- package/src/guardian/alert-ledger.js +121 -0
- package/src/guardian/attempt-engine.js +568 -7
- package/src/guardian/attempt-registry.js +42 -1
- package/src/guardian/attempt-relevance.js +106 -0
- package/src/guardian/attempt.js +24 -0
- package/src/guardian/baseline.js +12 -4
- package/src/guardian/breakage-intelligence.js +1 -0
- package/src/guardian/ci-cli.js +121 -0
- package/src/guardian/ci-output.js +4 -3
- package/src/guardian/cli-summary.js +79 -92
- package/src/guardian/config-loader.js +162 -0
- package/src/guardian/drift-detector.js +100 -0
- package/src/guardian/enhanced-html-reporter.js +221 -4
- package/src/guardian/env-guard.js +127 -0
- package/src/guardian/failure-intelligence.js +173 -0
- package/src/guardian/first-run-profile.js +89 -0
- package/src/guardian/first-run.js +6 -1
- package/src/guardian/flag-validator.js +17 -3
- package/src/guardian/html-reporter.js +2 -0
- package/src/guardian/human-reporter.js +431 -0
- package/src/guardian/index.js +22 -19
- package/src/guardian/init-command.js +9 -5
- package/src/guardian/intent-detector.js +146 -0
- package/src/guardian/journey-definitions.js +132 -0
- package/src/guardian/journey-scan-cli.js +145 -0
- package/src/guardian/journey-scanner.js +583 -0
- package/src/guardian/junit-reporter.js +18 -1
- package/src/guardian/live-cli.js +95 -0
- package/src/guardian/live-scheduler-runner.js +137 -0
- package/src/guardian/live-scheduler.js +146 -0
- package/src/guardian/market-reporter.js +341 -81
- package/src/guardian/pattern-analyzer.js +348 -0
- package/src/guardian/policy.js +80 -3
- package/src/guardian/preset-loader.js +9 -6
- package/src/guardian/reality.js +1278 -117
- package/src/guardian/reporter.js +27 -41
- package/src/guardian/run-artifacts.js +212 -0
- package/src/guardian/run-cleanup.js +207 -0
- package/src/guardian/run-latest.js +90 -0
- package/src/guardian/run-list.js +211 -0
- package/src/guardian/scan-presets.js +100 -11
- package/src/guardian/selector-fallbacks.js +394 -0
- package/src/guardian/semantic-contact-finder.js +2 -1
- package/src/guardian/site-introspection.js +257 -0
- package/src/guardian/smoke.js +2 -2
- package/src/guardian/snapshot-schema.js +25 -1
- package/src/guardian/snapshot.js +46 -2
- package/src/guardian/stability-scorer.js +169 -0
- package/src/guardian/template-command.js +184 -0
- package/src/guardian/text-formatters.js +426 -0
- package/src/guardian/verdict.js +320 -0
- package/src/guardian/verdicts.js +74 -0
- package/src/guardian/watch-runner.js +3 -7
- package/src/payments/stripe-checkout.js +169 -0
- package/src/plans/plan-definitions.js +148 -0
- package/src/plans/plan-manager.js +211 -0
- package/src/plans/usage-tracker.js +210 -0
- package/src/recipes/recipe-engine.js +188 -0
- package/src/recipes/recipe-failure-analysis.js +159 -0
- package/src/recipes/recipe-registry.js +134 -0
- package/src/recipes/recipe-runtime.js +507 -0
- package/src/recipes/recipe-store.js +410 -0
- package/guardian-contract-v1.md +0 -149
- /package/{guardian.config.json → config/guardian.config.json} +0 -0
- /package/{guardian.policy.json → config/guardian.policy.json} +0 -0
- /package/{guardian.profile.docs.yaml → config/profiles/docs.yaml} +0 -0
- /package/{guardian.profile.ecommerce.yaml → config/profiles/ecommerce.yaml} +0 -0
- /package/{guardian.profile.marketing.yaml → config/profiles/marketing.yaml} +0 -0
- /package/{guardian.profile.saas.yaml → config/profiles/saas.yaml} +0 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 12.1: Recipe Engine
|
|
3
|
+
* Core recipe execution and validation system
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validate recipe schema
|
|
11
|
+
*/
|
|
12
|
+
function validateRecipe(recipe) {
|
|
13
|
+
const errors = [];
|
|
14
|
+
|
|
15
|
+
if (!recipe.id || typeof recipe.id !== 'string') {
|
|
16
|
+
errors.push('Recipe must have a string id');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!recipe.name || typeof recipe.name !== 'string') {
|
|
20
|
+
errors.push('Recipe must have a string name');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!recipe.platform || typeof recipe.platform !== 'string') {
|
|
24
|
+
errors.push('Recipe must have a platform (shopify|saas|landing)');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!['shopify', 'saas', 'landing'].includes(recipe.platform)) {
|
|
28
|
+
errors.push(`Invalid platform: ${recipe.platform}. Must be shopify, saas, or landing`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!recipe.intent || typeof recipe.intent !== 'string') {
|
|
32
|
+
errors.push('Recipe must have an intent string');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!Array.isArray(recipe.steps) || recipe.steps.length === 0) {
|
|
36
|
+
errors.push('Recipe must have at least one step');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (recipe.steps && !recipe.steps.every(s => typeof s === 'string')) {
|
|
40
|
+
errors.push('All recipe steps must be strings');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!recipe.expectedGoal || typeof recipe.expectedGoal !== 'string') {
|
|
44
|
+
errors.push('Recipe must have an expectedGoal');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
valid: errors.length === 0,
|
|
49
|
+
errors
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get recipe complexity score
|
|
55
|
+
*/
|
|
56
|
+
function getComplexityScore(recipe) {
|
|
57
|
+
const stepCount = recipe.steps.length;
|
|
58
|
+
|
|
59
|
+
if (stepCount <= 3) return 'simple';
|
|
60
|
+
if (stepCount <= 7) return 'moderate';
|
|
61
|
+
return 'complex';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get estimated execution time (seconds)
|
|
66
|
+
*/
|
|
67
|
+
function getEstimatedTime(recipe) {
|
|
68
|
+
const stepCount = recipe.steps.length;
|
|
69
|
+
const complexity = getComplexityScore(recipe);
|
|
70
|
+
|
|
71
|
+
// Base time per step
|
|
72
|
+
let baseTime = 5;
|
|
73
|
+
|
|
74
|
+
if (complexity === 'complex') {
|
|
75
|
+
baseTime = 8;
|
|
76
|
+
} else if (complexity === 'moderate') {
|
|
77
|
+
baseTime = 6;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return stepCount * baseTime;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Create recipe execution context
|
|
85
|
+
*/
|
|
86
|
+
function createExecutionContext(recipe, baseUrl, options = {}) {
|
|
87
|
+
return {
|
|
88
|
+
recipe,
|
|
89
|
+
baseUrl,
|
|
90
|
+
startedAt: new Date(),
|
|
91
|
+
steps: [],
|
|
92
|
+
state: {
|
|
93
|
+
currentUrl: baseUrl,
|
|
94
|
+
pageTitle: null,
|
|
95
|
+
pageUrl: null,
|
|
96
|
+
lastElement: null,
|
|
97
|
+
data: options.data || {}
|
|
98
|
+
},
|
|
99
|
+
options,
|
|
100
|
+
success: false,
|
|
101
|
+
error: null,
|
|
102
|
+
completedAt: null,
|
|
103
|
+
duration: 0
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Record step execution
|
|
109
|
+
*/
|
|
110
|
+
function recordStep(context, stepIndex, action, result) {
|
|
111
|
+
const step = {
|
|
112
|
+
index: stepIndex,
|
|
113
|
+
action,
|
|
114
|
+
result,
|
|
115
|
+
timestamp: new Date().toISOString(),
|
|
116
|
+
duration: 0
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
context.steps.push(step);
|
|
120
|
+
return step;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Mark execution complete
|
|
125
|
+
*/
|
|
126
|
+
function markComplete(context, success, error = null) {
|
|
127
|
+
context.success = success;
|
|
128
|
+
context.error = error;
|
|
129
|
+
context.completedAt = new Date();
|
|
130
|
+
context.duration = Math.round(
|
|
131
|
+
(context.completedAt - context.startedAt) / 1000
|
|
132
|
+
);
|
|
133
|
+
return context;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Generate human-readable recipe summary
|
|
138
|
+
*/
|
|
139
|
+
function getRecipeSummary(recipe) {
|
|
140
|
+
return {
|
|
141
|
+
id: recipe.id,
|
|
142
|
+
name: recipe.name,
|
|
143
|
+
platform: recipe.platform,
|
|
144
|
+
intent: recipe.intent,
|
|
145
|
+
stepCount: recipe.steps.length,
|
|
146
|
+
complexity: getComplexityScore(recipe),
|
|
147
|
+
estimatedTime: getEstimatedTime(recipe),
|
|
148
|
+
expectedGoal: recipe.expectedGoal
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Format recipe for display
|
|
154
|
+
*/
|
|
155
|
+
function formatRecipe(recipe) {
|
|
156
|
+
const summary = getRecipeSummary(recipe);
|
|
157
|
+
|
|
158
|
+
let output = '';
|
|
159
|
+
output += `📋 ${summary.name}\n`;
|
|
160
|
+
output += ` Platform: ${summary.platform}\n`;
|
|
161
|
+
output += ` Intent: ${summary.intent}\n`;
|
|
162
|
+
output += ` Complexity: ${summary.complexity}\n`;
|
|
163
|
+
output += ` Steps: ${summary.stepCount}\n`;
|
|
164
|
+
output += ` Estimated Time: ${summary.estimatedTime}s\n`;
|
|
165
|
+
output += ` Expected Goal: ${summary.expectedGoal}\n`;
|
|
166
|
+
|
|
167
|
+
if (recipe.notes) {
|
|
168
|
+
output += ` Notes: ${recipe.notes}\n`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
output += `\n Steps:\n`;
|
|
172
|
+
recipe.steps.forEach((step, i) => {
|
|
173
|
+
output += ` ${i + 1}. ${step}\n`;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return output;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = {
|
|
180
|
+
validateRecipe,
|
|
181
|
+
getComplexityScore,
|
|
182
|
+
getEstimatedTime,
|
|
183
|
+
createExecutionContext,
|
|
184
|
+
recordStep,
|
|
185
|
+
markComplete,
|
|
186
|
+
getRecipeSummary,
|
|
187
|
+
formatRecipe
|
|
188
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recipe Failure Analysis & Reporting
|
|
3
|
+
*
|
|
4
|
+
* Integrates recipe failures into the decision engine and breakage intelligence.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Analyze a recipe execution failure
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} recipeResult - Result from executeRecipeRuntime
|
|
11
|
+
* @returns {Object} Intelligence object for reporting
|
|
12
|
+
*/
|
|
13
|
+
function analyzeRecipeFailure(recipeResult) {
|
|
14
|
+
if (recipeResult.success) {
|
|
15
|
+
return null; // No failure to analyze
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { failedStep, failureReason, steps, recipe, recipeName, baseUrl } = recipeResult;
|
|
19
|
+
|
|
20
|
+
// Categorize failure type
|
|
21
|
+
let failureType = 'EXECUTION';
|
|
22
|
+
let severity = 'CRITICAL';
|
|
23
|
+
|
|
24
|
+
if (failureReason.includes('not found')) {
|
|
25
|
+
failureType = 'ELEMENT_NOT_FOUND';
|
|
26
|
+
} else if (failureReason.includes('not visible')) {
|
|
27
|
+
failureType = 'ELEMENT_NOT_VISIBLE';
|
|
28
|
+
} else if (failureReason.includes('Goal not reached')) {
|
|
29
|
+
failureType = 'GOAL_FAILURE';
|
|
30
|
+
} else if (failureReason.includes('Runtime error')) {
|
|
31
|
+
failureType = 'RUNTIME_ERROR';
|
|
32
|
+
severity = 'CRITICAL';
|
|
33
|
+
} else if (failureReason.includes('Recipe not found')) {
|
|
34
|
+
failureType = 'RECIPE_NOT_FOUND';
|
|
35
|
+
severity = 'INFO';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const stepsFailed = steps.filter(s => !s.success).length;
|
|
39
|
+
const stepsTotal = steps.length;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
id: `recipe-${recipe}`,
|
|
43
|
+
name: `Recipe: ${recipeName}`,
|
|
44
|
+
outcome: 'FAILURE',
|
|
45
|
+
failureType,
|
|
46
|
+
severity,
|
|
47
|
+
baseUrl,
|
|
48
|
+
recipeId: recipe,
|
|
49
|
+
recipeName,
|
|
50
|
+
failedStepId: failedStep,
|
|
51
|
+
failedStepNumber: failedStep ? parseInt(failedStep.split('-').pop(), 10) : null,
|
|
52
|
+
failureReason,
|
|
53
|
+
stepProgress: {
|
|
54
|
+
completed: stepsTotal - stepsFailed,
|
|
55
|
+
total: stepsTotal,
|
|
56
|
+
percentage: Math.round(((stepsTotal - stepsFailed) / stepsTotal) * 100)
|
|
57
|
+
},
|
|
58
|
+
humanReadableError: formatRecipeError(recipeName, failedStep, failureReason, stepsTotal),
|
|
59
|
+
source: 'recipe-runtime',
|
|
60
|
+
behavioralSignals: [
|
|
61
|
+
{
|
|
62
|
+
signal: 'RECIPE_EXECUTION_FAILED',
|
|
63
|
+
message: failureReason,
|
|
64
|
+
severity: severity
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Format recipe error for human readability
|
|
72
|
+
*/
|
|
73
|
+
function formatRecipeError(recipeName, failedStep, failureReason, totalSteps) {
|
|
74
|
+
const stepNum = failedStep ? parseInt(failedStep.split('-').pop(), 10) + 1 : 0;
|
|
75
|
+
|
|
76
|
+
return `Recipe '${recipeName}' failed at step ${stepNum} of ${totalSteps}: ${failureReason}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Convert recipe failure to attempt-like structure for reporting compatibility
|
|
81
|
+
*/
|
|
82
|
+
function recipeFailureToAttempt(recipeResult) {
|
|
83
|
+
const intelligence = analyzeRecipeFailure(recipeResult);
|
|
84
|
+
if (!intelligence) return null;
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
attemptId: intelligence.id,
|
|
88
|
+
attemptName: intelligence.name,
|
|
89
|
+
outcome: 'FAILURE',
|
|
90
|
+
error: intelligence.failureReason,
|
|
91
|
+
failureType: intelligence.failureType,
|
|
92
|
+
severity: intelligence.severity,
|
|
93
|
+
baseUrl: intelligence.baseUrl,
|
|
94
|
+
riskCategory: 'TRUST', // Recipes are trust/integrity checks
|
|
95
|
+
source: 'recipe',
|
|
96
|
+
validators: [
|
|
97
|
+
{
|
|
98
|
+
id: 'recipe-step-validation',
|
|
99
|
+
name: 'Recipe Step Execution',
|
|
100
|
+
passed: false,
|
|
101
|
+
evidence: recipeResult.evidence
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
behavioralSignals: intelligence.behavioralSignals
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Integrate recipe failures into market impact analysis
|
|
110
|
+
*/
|
|
111
|
+
function assessRecipeImpact(recipeResult) {
|
|
112
|
+
if (recipeResult.success) {
|
|
113
|
+
return {
|
|
114
|
+
hasRisk: false,
|
|
115
|
+
riskScore: 0,
|
|
116
|
+
severity: 'INFO',
|
|
117
|
+
message: `Recipe '${recipeResult.recipeName}' passed successfully`
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const intelligence = analyzeRecipeFailure(recipeResult);
|
|
122
|
+
|
|
123
|
+
// Recipe failures always elevate risk
|
|
124
|
+
let riskScore = 70; // Base critical risk
|
|
125
|
+
let severity = 'CRITICAL';
|
|
126
|
+
|
|
127
|
+
// Adjust based on step completion
|
|
128
|
+
const progress = intelligence.stepProgress.percentage;
|
|
129
|
+
if (progress === 0) {
|
|
130
|
+
riskScore = 90; // Failed on first step — very bad
|
|
131
|
+
} else if (progress < 50) {
|
|
132
|
+
riskScore = 80; // Failed early
|
|
133
|
+
} else {
|
|
134
|
+
riskScore = 60; // Failed late but still indicates issue
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Type-specific scoring
|
|
138
|
+
if (intelligence.failureType === 'GOAL_FAILURE') {
|
|
139
|
+
riskScore = Math.max(riskScore - 10, 50); // Goal failure less severe than execution error
|
|
140
|
+
severity = 'WARNING';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
hasRisk: true,
|
|
145
|
+
riskScore,
|
|
146
|
+
severity,
|
|
147
|
+
message: intelligence.humanReadableError,
|
|
148
|
+
recipe: intelligence.recipeId,
|
|
149
|
+
failureType: intelligence.failureType,
|
|
150
|
+
stepProgress: intelligence.stepProgress
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = {
|
|
155
|
+
analyzeRecipeFailure,
|
|
156
|
+
formatRecipeError,
|
|
157
|
+
recipeFailureToAttempt,
|
|
158
|
+
assessRecipeImpact
|
|
159
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 12.2: Recipe Registry
|
|
3
|
+
* Local-first registry tracking provenance and integrity of recipes.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
|
|
11
|
+
const RECIPES_DIR = path.join(os.homedir(), '.odavl-guardian', 'recipes');
|
|
12
|
+
const REGISTRY_FILE = path.join(RECIPES_DIR, 'registry.json');
|
|
13
|
+
|
|
14
|
+
function ensureRecipesDir() {
|
|
15
|
+
if (!fs.existsSync(RECIPES_DIR)) {
|
|
16
|
+
fs.mkdirSync(RECIPES_DIR, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function stableStringify(value) {
|
|
21
|
+
if (value === null || typeof value !== 'object') {
|
|
22
|
+
return JSON.stringify(value);
|
|
23
|
+
}
|
|
24
|
+
if (Array.isArray(value)) {
|
|
25
|
+
return '[' + value.map(v => stableStringify(v)).join(',') + ']';
|
|
26
|
+
}
|
|
27
|
+
const keys = Object.keys(value).sort();
|
|
28
|
+
const entries = keys.map(k => `${JSON.stringify(k)}:${stableStringify(value[k])}`);
|
|
29
|
+
return '{' + entries.join(',') + '}';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function computeRecipeChecksum(recipe) {
|
|
33
|
+
const normalized = stableStringify(recipe);
|
|
34
|
+
return crypto.createHash('sha256').update(normalized).digest('hex');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function loadRegistry() {
|
|
38
|
+
ensureRecipesDir();
|
|
39
|
+
if (!fs.existsSync(REGISTRY_FILE)) {
|
|
40
|
+
return { entries: [], updatedAt: null };
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(fs.readFileSync(REGISTRY_FILE, 'utf-8'));
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return { entries: [], updatedAt: null };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function saveRegistry(registry) {
|
|
50
|
+
ensureRecipesDir();
|
|
51
|
+
const data = {
|
|
52
|
+
entries: registry.entries || [],
|
|
53
|
+
updatedAt: new Date().toISOString(),
|
|
54
|
+
};
|
|
55
|
+
fs.writeFileSync(REGISTRY_FILE, JSON.stringify(data, null, 2), 'utf-8');
|
|
56
|
+
return data;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function ensureBuiltInRegistry(builtIns) {
|
|
60
|
+
const registry = loadRegistry();
|
|
61
|
+
let changed = false;
|
|
62
|
+
for (const recipe of builtIns) {
|
|
63
|
+
if (!registry.entries.some(e => e.id === recipe.id)) {
|
|
64
|
+
registry.entries.push({
|
|
65
|
+
id: recipe.id,
|
|
66
|
+
name: recipe.name,
|
|
67
|
+
platform: recipe.platform,
|
|
68
|
+
version: recipe.version || '1.0.0',
|
|
69
|
+
source: 'builtin',
|
|
70
|
+
checksum: computeRecipeChecksum(recipe),
|
|
71
|
+
addedAt: new Date().toISOString(),
|
|
72
|
+
});
|
|
73
|
+
changed = true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (changed) {
|
|
77
|
+
saveRegistry(registry);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function registerRecipe({ id, name, platform, version, source, checksum }) {
|
|
82
|
+
const registry = loadRegistry();
|
|
83
|
+
const existing = registry.entries.find(e => e.id === id);
|
|
84
|
+
if (existing) {
|
|
85
|
+
Object.assign(existing, { name, platform, version, source, checksum });
|
|
86
|
+
} else {
|
|
87
|
+
registry.entries.push({
|
|
88
|
+
id,
|
|
89
|
+
name,
|
|
90
|
+
platform,
|
|
91
|
+
version: version || '1.0.0',
|
|
92
|
+
source: source || 'imported',
|
|
93
|
+
checksum,
|
|
94
|
+
addedAt: new Date().toISOString(),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return saveRegistry(registry);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function removeRegistryEntry(id) {
|
|
101
|
+
const registry = loadRegistry();
|
|
102
|
+
const next = registry.entries.filter(e => e.id !== id);
|
|
103
|
+
registry.entries = next;
|
|
104
|
+
return saveRegistry(registry);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getRegistryEntry(id) {
|
|
108
|
+
const registry = loadRegistry();
|
|
109
|
+
return registry.entries.find(e => e.id === id);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function listRegistryEntries() {
|
|
113
|
+
const registry = loadRegistry();
|
|
114
|
+
return registry.entries;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resetRegistry() {
|
|
118
|
+
ensureRecipesDir();
|
|
119
|
+
if (fs.existsSync(REGISTRY_FILE)) {
|
|
120
|
+
fs.unlinkSync(REGISTRY_FILE);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
computeRecipeChecksum,
|
|
126
|
+
loadRegistry,
|
|
127
|
+
saveRegistry,
|
|
128
|
+
registerRecipe,
|
|
129
|
+
ensureBuiltInRegistry,
|
|
130
|
+
getRegistryEntry,
|
|
131
|
+
listRegistryEntries,
|
|
132
|
+
removeRegistryEntry,
|
|
133
|
+
resetRegistry,
|
|
134
|
+
};
|