@odavl/guardian 0.1.0-rc1 → 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 +146 -0
- package/README.md +155 -97
- package/bin/guardian.js +1544 -55
- package/config/README.md +59 -0
- package/config/profiles/landing-demo.yaml +16 -0
- package/package.json +26 -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 +587 -12
- package/src/guardian/attempt-registry.js +42 -1
- package/src/guardian/attempt-relevance.js +106 -0
- package/src/guardian/attempt.js +85 -39
- package/src/guardian/attempts-filter.js +63 -0
- package/src/guardian/baseline.js +50 -8
- package/src/guardian/breakage-intelligence.js +1 -0
- package/src/guardian/browser-pool.js +131 -0
- package/src/guardian/browser.js +28 -1
- package/src/guardian/ci-cli.js +121 -0
- package/src/guardian/ci-mode.js +15 -0
- package/src/guardian/ci-output.js +38 -0
- package/src/guardian/cli-summary.js +167 -67
- package/src/guardian/config-loader.js +162 -0
- package/src/guardian/data-guardian-detector.js +189 -0
- package/src/guardian/detection-layers.js +271 -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 +54 -0
- package/src/guardian/flag-validator.js +111 -0
- package/src/guardian/flow-executor.js +309 -44
- 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/language-detection.js +99 -0
- 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 +357 -82
- package/src/guardian/parallel-executor.js +116 -0
- package/src/guardian/pattern-analyzer.js +348 -0
- package/src/guardian/policy.js +80 -3
- package/src/guardian/prerequisite-checker.js +101 -0
- package/src/guardian/preset-loader.js +27 -18
- package/src/guardian/profile-loader.js +96 -0
- package/src/guardian/reality.js +1612 -115
- 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/run-summary.js +20 -0
- package/src/guardian/scan-presets.js +100 -11
- package/src/guardian/selector-fallbacks.js +394 -0
- package/src/guardian/semantic-contact-detection.js +255 -0
- package/src/guardian/semantic-contact-finder.js +201 -0
- package/src/guardian/semantic-targets.js +234 -0
- package/src/guardian/site-introspection.js +257 -0
- package/src/guardian/smoke.js +258 -0
- package/src/guardian/snapshot-schema.js +25 -1
- package/src/guardian/snapshot.js +69 -3
- package/src/guardian/stability-scorer.js +169 -0
- package/src/guardian/success-evaluator.js +214 -0
- package/src/guardian/template-command.js +184 -0
- package/src/guardian/text-formatters.js +426 -0
- package/src/guardian/timeout-profiles.js +57 -0
- package/src/guardian/verdict.js +320 -0
- package/src/guardian/verdicts.js +74 -0
- package/src/guardian/wait-for-outcome.js +120 -0
- package/src/guardian/watch-runner.js +181 -0
- 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
package/bin/guardian.js
CHANGED
|
@@ -1,16 +1,121 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// Windows UTF-8 encoding initialization
|
|
3
|
+
if (process.platform === 'win32') {
|
|
4
|
+
process.stdout.setEncoding('utf-8');
|
|
5
|
+
process.stderr.setEncoding('utf-8');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Minimal DX: handle --version/-v immediately (before any other work)
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
if (args.length === 1 && (args[0] === '--version' || args[0] === '-v')) {
|
|
11
|
+
try {
|
|
12
|
+
const pkg = require('../package.json');
|
|
13
|
+
console.log(pkg.version);
|
|
14
|
+
process.exit(0);
|
|
15
|
+
} catch (e) {
|
|
16
|
+
console.error('Version unavailable');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// GLOBAL HELP (Level 1): provide a real, working guardian --help
|
|
22
|
+
function printGlobalHelp() {
|
|
23
|
+
console.log(`
|
|
24
|
+
ODAVL Guardian — Level 1
|
|
25
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
26
|
+
|
|
27
|
+
Golden Path (Reality Check)
|
|
28
|
+
guardian --url <url>
|
|
29
|
+
guardian reality --url <url>
|
|
30
|
+
|
|
31
|
+
What happens
|
|
32
|
+
- Opens a real browser
|
|
33
|
+
- Runs curated reality attempts
|
|
34
|
+
- Writes reports to ./.odavlguardian/<run>/
|
|
35
|
+
|
|
36
|
+
Reports (Level 1)
|
|
37
|
+
- decision.json (finalVerdict, exitCode, explanation)
|
|
38
|
+
- summary.md (human-readable summary)
|
|
39
|
+
|
|
40
|
+
Canonical Verdicts
|
|
41
|
+
READY | FRICTION | DO_NOT_LAUNCH
|
|
42
|
+
|
|
43
|
+
VS Code (Level 1)
|
|
44
|
+
Command Palette → "Guardian: Run Reality Check"
|
|
45
|
+
- Uses ./.odavlguardian as artifacts directory
|
|
46
|
+
- Shows verdict and opens summary.md
|
|
47
|
+
|
|
48
|
+
Advanced (Level 2+)
|
|
49
|
+
guardian scan <url> One-command full scan (advanced)
|
|
50
|
+
guardian journey-scan <url> Single flow journey (advanced)
|
|
51
|
+
guardian smoke <url> Fast sanity (advanced)
|
|
52
|
+
guardian baseline <save|check> Baselines (advanced)
|
|
53
|
+
guardian list|cleanup Artifacts mgmt (advanced)
|
|
54
|
+
|
|
55
|
+
Tips
|
|
56
|
+
- Pass --artifacts <dir> to override artifacts directory
|
|
57
|
+
- Local config: guardian.config.json (CLI only)
|
|
58
|
+
`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Handle global --help (or no args) before heavy loads
|
|
62
|
+
if (args.length === 0 || (args.length === 1 && (args[0] === '--help' || args[0] === '-h'))) {
|
|
63
|
+
printGlobalHelp();
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// PHASE 6: Early flag validation (before heavy module loads)
|
|
68
|
+
const { validateFlags, reportFlagError } = require('../src/guardian/flag-validator');
|
|
69
|
+
const validation = validateFlags(process.argv);
|
|
70
|
+
if (!validation.valid) {
|
|
71
|
+
reportFlagError(validation);
|
|
72
|
+
process.exit(2);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// PHASE 6: First-run detection (lightweight)
|
|
76
|
+
const { isFirstRun, markAsRun, printWelcome, printFirstRunHint } = require('../src/guardian/first-run');
|
|
77
|
+
|
|
2
78
|
const { runAttemptCLI } = require('../src/guardian/attempt');
|
|
3
79
|
const { runRealityCLI } = require('../src/guardian/reality');
|
|
80
|
+
const { runSmokeCLI } = require('../src/guardian/smoke');
|
|
4
81
|
const { runGuardian } = require('../src/guardian');
|
|
82
|
+
const { runJourneyScanCLI } = require('../src/guardian/journey-scan-cli');
|
|
83
|
+
const { runLiveCLI } = require('../src/guardian/live-cli');
|
|
5
84
|
const { saveBaseline, checkBaseline } = require('../src/guardian/baseline');
|
|
6
85
|
const { getDefaultAttemptIds } = require('../src/guardian/attempt-registry');
|
|
7
86
|
const { initGuardian } = require('../src/guardian/init-command');
|
|
8
87
|
const { printPresets } = require('../src/guardian/preset-loader');
|
|
88
|
+
const { listRuns } = require('../src/guardian/run-list');
|
|
89
|
+
const { cleanup } = require('../src/guardian/run-cleanup');
|
|
90
|
+
const { generateTemplate, listTemplates } = require('../src/guardian/template-command');
|
|
91
|
+
|
|
92
|
+
// Phase 8: Plan enforcement
|
|
93
|
+
const { checkCanScan, performScan, checkFeatureAllowed, getPlanSummary, getUpgradeMessage } = require('../src/plans/plan-manager');
|
|
94
|
+
|
|
95
|
+
// Phase 10: Founder tracking and feedback
|
|
96
|
+
const { registerUser, getFounderMessage, isFoundingUser } = require('../src/founder/founder-tracker');
|
|
97
|
+
const { runFeedbackSession } = require('../src/founder/feedback-system');
|
|
98
|
+
const { recordFirstScan, recordFirstLive, recordFirstUpgrade } = require('../src/founder/usage-signals');
|
|
99
|
+
|
|
100
|
+
// Phase 11: Enterprise features
|
|
101
|
+
const { addSite, removeSite, getSite, getSites, getSitesByProject, listProjects } = require('../src/enterprise/site-manager');
|
|
102
|
+
const { addUser, removeUser, getUsers, getCurrentUser, requirePermission, listRoles } = require('../src/enterprise/rbac');
|
|
103
|
+
const { logAudit, readAuditLogs, getAuditSummary, AUDIT_ACTIONS } = require('../src/enterprise/audit-logger');
|
|
104
|
+
const { exportReportToPDF, listAvailableReports } = require('../src/enterprise/pdf-exporter');
|
|
105
|
+
|
|
106
|
+
// Phase 12.1: Recipes
|
|
107
|
+
const { getAllRecipes, getRecipe, getRecipesByPlatform, addRecipe, removeRecipe, importRecipes, exportRecipes, exportRecipeWithMetadata, importRecipeWithMetadata } = require('../src/recipes/recipe-store');
|
|
108
|
+
const { validateRecipe, formatRecipe } = require('../src/recipes/recipe-engine');
|
|
109
|
+
const { getRegistryEntry, computeRecipeChecksum } = require('../src/recipes/recipe-registry');
|
|
110
|
+
const { resolveScanPreset } = require('../src/guardian/scan-presets');
|
|
9
111
|
|
|
10
112
|
function parseArgs(argv) {
|
|
11
113
|
const args = argv.slice(2);
|
|
12
114
|
const subcommand = args[0];
|
|
13
115
|
|
|
116
|
+
// Note: Early flag validation in main() catches unknown commands
|
|
117
|
+
// so we don't need to re-validate here
|
|
118
|
+
|
|
14
119
|
if (subcommand === 'init') {
|
|
15
120
|
return { subcommand: 'init', config: parseInitArgs(args.slice(1)) };
|
|
16
121
|
}
|
|
@@ -23,6 +128,18 @@ function parseArgs(argv) {
|
|
|
23
128
|
return { subcommand: 'presets', config: {} };
|
|
24
129
|
}
|
|
25
130
|
|
|
131
|
+
if (subcommand === 'template') {
|
|
132
|
+
return { subcommand: 'template', config: parseTemplateArgs(args.slice(1)) };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (subcommand === 'list') {
|
|
136
|
+
return { subcommand: 'list', config: parseListArgs(args.slice(1)) };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (subcommand === 'cleanup') {
|
|
140
|
+
return { subcommand: 'cleanup', config: parseCleanupArgs(args.slice(1)) };
|
|
141
|
+
}
|
|
142
|
+
|
|
26
143
|
if (subcommand === 'attempt') {
|
|
27
144
|
return { subcommand: 'attempt', config: parseAttemptArgs(args.slice(1)) };
|
|
28
145
|
}
|
|
@@ -31,6 +148,14 @@ function parseArgs(argv) {
|
|
|
31
148
|
return { subcommand: 'reality', config: parseRealityArgs(args.slice(1)) };
|
|
32
149
|
}
|
|
33
150
|
|
|
151
|
+
if (subcommand === 'smoke') {
|
|
152
|
+
return { subcommand: 'smoke', config: parseSmokeArgs(args.slice(1)) };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (subcommand === 'check') {
|
|
156
|
+
return { subcommand: 'smoke', config: parseSmokeArgs(args.slice(1)) };
|
|
157
|
+
}
|
|
158
|
+
|
|
34
159
|
if (subcommand === 'baseline') {
|
|
35
160
|
const action = args[1];
|
|
36
161
|
if (action === 'save') {
|
|
@@ -43,13 +168,87 @@ function parseArgs(argv) {
|
|
|
43
168
|
process.exit(0);
|
|
44
169
|
}
|
|
45
170
|
|
|
171
|
+
// MVP: Human Journey Scan
|
|
172
|
+
if (subcommand === 'journey-scan' || subcommand === 'journey') {
|
|
173
|
+
return { subcommand: 'journey-scan', config: parseJourneyScanArgs(args.slice(1)) };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (subcommand === 'live') {
|
|
177
|
+
// Support scheduler subcommands: start|stop|status
|
|
178
|
+
const action = args[1];
|
|
179
|
+
if (action === 'start') {
|
|
180
|
+
return { subcommand: 'live-start', config: parseLiveStartArgs(args.slice(2)) };
|
|
181
|
+
}
|
|
182
|
+
if (action === 'stop') {
|
|
183
|
+
return { subcommand: 'live-stop', config: parseLiveStopArgs(args.slice(2)) };
|
|
184
|
+
}
|
|
185
|
+
if (action === 'status') {
|
|
186
|
+
return { subcommand: 'live-status', config: {} };
|
|
187
|
+
}
|
|
188
|
+
return { subcommand: 'live', config: parseLiveArgs(args.slice(1)) };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (subcommand === 'ci') {
|
|
192
|
+
return { subcommand: 'ci', config: parseJourneyScanArgs(args.slice(1)) };
|
|
193
|
+
}
|
|
194
|
+
|
|
46
195
|
// Phase 6: Productized one-command scan
|
|
47
196
|
if (subcommand === 'scan') {
|
|
48
197
|
return { subcommand: 'scan', config: parseScanArgs(args.slice(1)) };
|
|
49
198
|
}
|
|
50
199
|
|
|
51
|
-
//
|
|
52
|
-
|
|
200
|
+
// Phase 8: Plan management
|
|
201
|
+
if (subcommand === 'plan') {
|
|
202
|
+
return { subcommand: 'plan', config: {} };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (subcommand === 'upgrade') {
|
|
206
|
+
const targetPlan = args[1];
|
|
207
|
+
return { subcommand: 'upgrade', config: { plan: targetPlan } };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Phase 10: Feedback command
|
|
211
|
+
if (subcommand === 'feedback') {
|
|
212
|
+
return { subcommand: 'feedback', config: {} };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Phase 11: Enterprise commands
|
|
216
|
+
if (subcommand === 'sites') {
|
|
217
|
+
return { subcommand: 'sites', config: parseSitesArgs(args.slice(1)) };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (subcommand === 'users') {
|
|
221
|
+
return { subcommand: 'users', config: parseUsersArgs(args.slice(1)) };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (subcommand === 'audit') {
|
|
225
|
+
return { subcommand: 'audit', config: parseAuditArgs(args.slice(1)) };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (subcommand === 'export') {
|
|
229
|
+
return { subcommand: 'export', config: parseExportArgs(args.slice(1)) };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Phase 12.1: Recipes
|
|
233
|
+
if (subcommand === 'recipe') {
|
|
234
|
+
return { subcommand: 'recipe', config: parseRecipeArgs(args.slice(1)) };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// LEVEL 1 GOLDEN PATH: guardian --url routes to Reality (not legacy crawl)
|
|
238
|
+
// If first arg is --url, treat it as guardian reality --url
|
|
239
|
+
if (args.length > 0 && args[0] === '--url') {
|
|
240
|
+
return { subcommand: 'reality', config: parseRealityArgs(args) };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Legacy crawl command (explicit only)
|
|
244
|
+
if (subcommand === 'crawl') {
|
|
245
|
+
return { subcommand: 'crawl', config: parseCrawlArgs(args.slice(1)) };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Unknown command
|
|
249
|
+
console.error(`Unknown command: ${subcommand}`);
|
|
250
|
+
console.error('Run "guardian --help" for Level 1 usage.');
|
|
251
|
+
process.exit(2);
|
|
53
252
|
}
|
|
54
253
|
|
|
55
254
|
function parseCrawlArgs(args) {
|
|
@@ -57,7 +256,7 @@ function parseCrawlArgs(args) {
|
|
|
57
256
|
maxPages: 25,
|
|
58
257
|
maxDepth: 3,
|
|
59
258
|
timeout: 20000,
|
|
60
|
-
artifactsDir: '
|
|
259
|
+
artifactsDir: './.odavlguardian'
|
|
61
260
|
};
|
|
62
261
|
|
|
63
262
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -96,12 +295,162 @@ function parseCrawlArgs(args) {
|
|
|
96
295
|
return config;
|
|
97
296
|
}
|
|
98
297
|
|
|
298
|
+
// Apply preset configuration deterministically across commands
|
|
299
|
+
function applyPresetConfig(config) {
|
|
300
|
+
const presetName = config.preset || 'landing';
|
|
301
|
+
let preset;
|
|
302
|
+
try {
|
|
303
|
+
preset = resolveScanPreset(presetName);
|
|
304
|
+
} catch (err) {
|
|
305
|
+
console.error(`Error: ${err.message}`);
|
|
306
|
+
process.exit(2);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const applied = { ...config };
|
|
310
|
+
applied.preset = preset.id;
|
|
311
|
+
applied.attempts = preset.attempts;
|
|
312
|
+
applied.disabledAttempts = preset.disabledAttempts || [];
|
|
313
|
+
applied.flows = preset.flows;
|
|
314
|
+
applied.policy = applied.policy || preset.policy;
|
|
315
|
+
if (applied.failFast === undefined) {
|
|
316
|
+
applied.failFast = preset.failFast;
|
|
317
|
+
}
|
|
318
|
+
applied.evidencePreset = preset.evidence || {};
|
|
319
|
+
return applied;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// MVP: Journey Scan command parser
|
|
323
|
+
function parseJourneyScanArgs(args) {
|
|
324
|
+
const config = {
|
|
325
|
+
baseUrl: undefined,
|
|
326
|
+
preset: 'saas',
|
|
327
|
+
artifactsDir: './.odavlguardian',
|
|
328
|
+
headless: true,
|
|
329
|
+
timeout: 20000,
|
|
330
|
+
presetProvided: false
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// First arg is URL if it doesn't start with --
|
|
334
|
+
if (args.length > 0 && !args[0].startsWith('--')) {
|
|
335
|
+
config.baseUrl = args[0];
|
|
336
|
+
args = args.slice(1);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
for (let i = 0; i < args.length; i++) {
|
|
340
|
+
const a = args[i];
|
|
341
|
+
if (a === '--url' && args[i + 1]) { config.baseUrl = args[i + 1]; i++; }
|
|
342
|
+
else if (a === '--preset' && args[i + 1]) { config.preset = args[i + 1]; config.presetProvided = true; i++; }
|
|
343
|
+
else if (a === '--out' && args[i + 1]) { config.artifactsDir = args[i + 1]; i++; }
|
|
344
|
+
else if (a === '--artifacts' && args[i + 1]) { config.artifactsDir = args[i + 1]; i++; }
|
|
345
|
+
else if (a === '--timeout' && args[i + 1]) { config.timeout = parseInt(args[i + 1], 10); i++; }
|
|
346
|
+
else if (a === '--headful') { config.headless = false; }
|
|
347
|
+
else if (a === '--help' || a === '-h') { printHelpJourneyScan(); process.exit(0); }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (!config.baseUrl) {
|
|
351
|
+
console.error('Error: <url> is required');
|
|
352
|
+
console.error('Usage: guardian journey-scan <url> [--preset saas|shop|landing] [--out dir]');
|
|
353
|
+
process.exit(2);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return config;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function parseLiveArgs(args) {
|
|
360
|
+
const config = {
|
|
361
|
+
baseUrl: undefined,
|
|
362
|
+
artifactsDir: './.odavlguardian',
|
|
363
|
+
headless: true,
|
|
364
|
+
timeout: 20000,
|
|
365
|
+
intervalMinutes: null,
|
|
366
|
+
preset: 'saas',
|
|
367
|
+
presetProvided: false,
|
|
368
|
+
cooldownMinutes: 60
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
if (args.length > 0 && !args[0].startsWith('--')) {
|
|
372
|
+
config.baseUrl = args[0];
|
|
373
|
+
args = args.slice(1);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
for (let i = 0; i < args.length; i++) {
|
|
377
|
+
const a = args[i];
|
|
378
|
+
if (a === '--url' && args[i + 1]) { config.baseUrl = args[i + 1]; i++; }
|
|
379
|
+
else if (a === '--out' && args[i + 1]) { config.artifactsDir = args[i + 1]; i++; }
|
|
380
|
+
else if (a === '--artifacts' && args[i + 1]) { config.artifactsDir = args[i + 1]; i++; }
|
|
381
|
+
else if (a === '--interval' && args[i + 1]) { config.intervalMinutes = parseFloat(args[i + 1]); i++; }
|
|
382
|
+
else if (a === '--cooldown' && args[i + 1]) { config.cooldownMinutes = parseFloat(args[i + 1]); i++; }
|
|
383
|
+
else if (a === '--timeout' && args[i + 1]) { config.timeout = parseInt(args[i + 1], 10); i++; }
|
|
384
|
+
else if (a === '--preset' && args[i + 1]) { config.preset = args[i + 1]; config.presetProvided = true; i++; }
|
|
385
|
+
else if (a === '--headful') { config.headless = false; }
|
|
386
|
+
else if (a === '--help' || a === '-h') { printHelpLive(); process.exit(0); }
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (!config.baseUrl) {
|
|
390
|
+
console.error('Error: <url> is required');
|
|
391
|
+
console.error('Usage: guardian live <url> [--interval <minutes>] [--out dir]');
|
|
392
|
+
process.exit(2);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return config;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Scheduler start args
|
|
399
|
+
function parseLiveStartArgs(args) {
|
|
400
|
+
const config = {
|
|
401
|
+
baseUrl: undefined,
|
|
402
|
+
preset: 'saas',
|
|
403
|
+
intervalMinutes: undefined,
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
// First arg is URL if present
|
|
407
|
+
if (args.length > 0 && !String(args[0]).startsWith('--')) {
|
|
408
|
+
config.baseUrl = args[0];
|
|
409
|
+
args = args.slice(1);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
for (let i = 0; i < args.length; i++) {
|
|
413
|
+
const a = args[i];
|
|
414
|
+
if (a === '--url' && args[i + 1]) { config.baseUrl = args[i + 1]; i++; }
|
|
415
|
+
else if (a === '--preset' && args[i + 1]) { config.preset = args[i + 1]; i++; }
|
|
416
|
+
else if (a === '--interval' && args[i + 1]) { config.intervalMinutes = parseFloat(args[i + 1]); i++; }
|
|
417
|
+
else if (a === '--help' || a === '-h') {
|
|
418
|
+
console.log('\nUsage: guardian live start --url <url> --interval <minutes> [--preset saas|shop|landing]');
|
|
419
|
+
process.exit(0);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (!config.baseUrl || !config.intervalMinutes || config.intervalMinutes <= 0) {
|
|
424
|
+
console.error('Error: --url and --interval <minutes> are required');
|
|
425
|
+
process.exit(2);
|
|
426
|
+
}
|
|
427
|
+
return config;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Scheduler stop args
|
|
431
|
+
function parseLiveStopArgs(args) {
|
|
432
|
+
const config = { id: undefined };
|
|
433
|
+
if (args.length > 0) {
|
|
434
|
+
config.id = args[0];
|
|
435
|
+
}
|
|
436
|
+
for (let i = 0; i < args.length; i++) {
|
|
437
|
+
const a = args[i];
|
|
438
|
+
if ((a === '--id' || a === '-i') && args[i + 1]) { config.id = args[i + 1]; i++; }
|
|
439
|
+
}
|
|
440
|
+
if (!config.id) {
|
|
441
|
+
console.error('Error: schedule id is required');
|
|
442
|
+
console.error('Usage: guardian live stop <id>');
|
|
443
|
+
process.exit(2);
|
|
444
|
+
}
|
|
445
|
+
return config;
|
|
446
|
+
}
|
|
447
|
+
|
|
99
448
|
// Phase 6: Scan command (one-command value)
|
|
100
449
|
function parseScanArgs(args) {
|
|
101
450
|
const config = {
|
|
102
451
|
// core
|
|
103
452
|
baseUrl: undefined,
|
|
104
|
-
artifactsDir: '
|
|
453
|
+
artifactsDir: './.odavlguardian',
|
|
105
454
|
// enable full pipeline
|
|
106
455
|
enableCrawl: true,
|
|
107
456
|
enableDiscovery: true,
|
|
@@ -117,7 +466,15 @@ function parseScanArgs(args) {
|
|
|
117
466
|
enableTrace: true,
|
|
118
467
|
enableScreenshots: true,
|
|
119
468
|
// preset
|
|
120
|
-
preset: 'landing'
|
|
469
|
+
preset: 'landing',
|
|
470
|
+
watch: false,
|
|
471
|
+
// Phase 7.1: Performance modes
|
|
472
|
+
timeoutProfile: 'default',
|
|
473
|
+
failFast: false,
|
|
474
|
+
fast: false,
|
|
475
|
+
attemptsFilter: null,
|
|
476
|
+
// Phase 7.2: Parallel execution
|
|
477
|
+
parallel: 1
|
|
121
478
|
};
|
|
122
479
|
|
|
123
480
|
// First arg is URL if it doesn't start with --
|
|
@@ -136,43 +493,113 @@ function parseScanArgs(args) {
|
|
|
136
493
|
else if (a === '--headful') { config.headful = true; }
|
|
137
494
|
else if (a === '--no-trace') { config.enableTrace = false; }
|
|
138
495
|
else if (a === '--no-screenshots') { config.enableScreenshots = false; }
|
|
496
|
+
else if (a === '--watch' || a === '-w') { config.watch = true; }
|
|
497
|
+
// Phase 7.1: Performance flags
|
|
498
|
+
else if (a === '--fast') { config.fast = true; config.timeoutProfile = 'fast'; config.enableScreenshots = false; }
|
|
499
|
+
else if (a === '--fail-fast') { config.failFast = true; }
|
|
500
|
+
else if (a === '--timeout-profile' && args[i + 1]) { config.timeoutProfile = args[i + 1]; i++; }
|
|
501
|
+
else if (a === '--attempts' && args[i + 1]) { config.attemptsFilter = args[i + 1]; i++; }
|
|
502
|
+
// Phase 7.2: Parallel execution
|
|
503
|
+
else if (a === '--parallel' && args[i + 1]) { config.parallel = args[i + 1]; i++; }
|
|
139
504
|
else if (a === '--help' || a === '-h') { printHelpScan(); process.exit(0); }
|
|
140
505
|
}
|
|
141
506
|
|
|
142
507
|
if (!config.baseUrl) {
|
|
143
508
|
console.error('Error: <url> is required');
|
|
144
|
-
console.error('Usage: guardian scan <url> [--preset <landing|saas|shop>]');
|
|
509
|
+
console.error('Usage: guardian scan <url> [--preset <landing|landing-demo|saas|shop>]');
|
|
145
510
|
process.exit(2);
|
|
146
511
|
}
|
|
147
512
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
513
|
+
return applyPresetConfig(config);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function parseListArgs(args) {
|
|
517
|
+
const config = {
|
|
518
|
+
artifactsDir: './.odavlguardian',
|
|
519
|
+
filters: {}
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
for (let i = 0; i < args.length; i++) {
|
|
523
|
+
if (args[i] === '--artifacts' && args[i + 1]) {
|
|
524
|
+
config.artifactsDir = args[i + 1];
|
|
525
|
+
i++;
|
|
526
|
+
}
|
|
527
|
+
if (args[i] === '--failed') {
|
|
528
|
+
config.filters.failed = true;
|
|
529
|
+
}
|
|
530
|
+
if (args[i] === '--site' && args[i + 1]) {
|
|
531
|
+
config.filters.site = args[i + 1];
|
|
532
|
+
i++;
|
|
533
|
+
}
|
|
534
|
+
if (args[i] === '--limit' && args[i + 1]) {
|
|
535
|
+
config.filters.limit = parseInt(args[i + 1], 10);
|
|
536
|
+
i++;
|
|
537
|
+
}
|
|
538
|
+
if (args[i] === '--help' || args[i] === '-h') {
|
|
539
|
+
printHelpList();
|
|
540
|
+
process.exit(0);
|
|
157
541
|
}
|
|
158
|
-
} catch (e) {
|
|
159
|
-
// If presets not available, proceed with defaults
|
|
160
542
|
}
|
|
161
543
|
|
|
162
544
|
return config;
|
|
163
545
|
}
|
|
164
546
|
|
|
165
|
-
function
|
|
547
|
+
function parseCleanupArgs(args) {
|
|
166
548
|
const config = {
|
|
167
549
|
artifactsDir: './artifacts',
|
|
550
|
+
olderThan: null,
|
|
551
|
+
keepLatest: null,
|
|
552
|
+
failedOnly: false
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
for (let i = 0; i < args.length; i++) {
|
|
556
|
+
if (args[i] === '--artifacts' && args[i + 1]) {
|
|
557
|
+
config.artifactsDir = args[i + 1];
|
|
558
|
+
i++;
|
|
559
|
+
}
|
|
560
|
+
if (args[i] === '--older-than' && args[i + 1]) {
|
|
561
|
+
config.olderThan = args[i + 1];
|
|
562
|
+
i++;
|
|
563
|
+
}
|
|
564
|
+
if (args[i] === '--keep-latest' && args[i + 1]) {
|
|
565
|
+
config.keepLatest = parseInt(args[i + 1], 10);
|
|
566
|
+
i++;
|
|
567
|
+
}
|
|
568
|
+
if (args[i] === '--failed-only') {
|
|
569
|
+
config.failedOnly = true;
|
|
570
|
+
}
|
|
571
|
+
if (args[i] === '--help' || args[i] === '-h') {
|
|
572
|
+
printHelpCleanup();
|
|
573
|
+
process.exit(0);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return config;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function parseRealityArgs(args) {
|
|
581
|
+
const config = {
|
|
582
|
+
artifactsDir: undefined,
|
|
168
583
|
attempts: getDefaultAttemptIds(),
|
|
584
|
+
disabledAttempts: [],
|
|
169
585
|
headful: false,
|
|
170
586
|
enableTrace: true,
|
|
171
587
|
enableScreenshots: true,
|
|
172
588
|
enableDiscovery: false,
|
|
173
589
|
includeUniversal: false,
|
|
590
|
+
preset: 'landing',
|
|
174
591
|
policy: null,
|
|
175
|
-
webhook: null
|
|
592
|
+
webhook: null,
|
|
593
|
+
watch: false,
|
|
594
|
+
// Phase 7.1: Performance modes
|
|
595
|
+
|
|
596
|
+
timeoutProfile: 'default',
|
|
597
|
+
failFast: false,
|
|
598
|
+
fast: false,
|
|
599
|
+
attemptsFilter: null,
|
|
600
|
+
// Phase 7.2: Parallel execution
|
|
601
|
+
parallel: 1,
|
|
602
|
+
_cliSource: {}
|
|
176
603
|
};
|
|
177
604
|
|
|
178
605
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -181,11 +608,32 @@ function parseRealityArgs(args) {
|
|
|
181
608
|
i++;
|
|
182
609
|
}
|
|
183
610
|
if (args[i] === '--attempts' && args[i + 1]) {
|
|
184
|
-
|
|
611
|
+
// This becomes attemptsFilter for Phase 7.1
|
|
612
|
+
config.attemptsFilter = args[i + 1];
|
|
185
613
|
i++;
|
|
186
614
|
}
|
|
187
615
|
if (args[i] === '--artifacts' && args[i + 1]) {
|
|
188
616
|
config.artifactsDir = args[i + 1];
|
|
617
|
+
config._cliSource.artifactsDir = true;
|
|
618
|
+
i++;
|
|
619
|
+
}
|
|
620
|
+
if (args[i] === '--max-pages' && args[i + 1]) {
|
|
621
|
+
config.maxPages = parseInt(args[i + 1], 10);
|
|
622
|
+
config._cliSource.maxPages = true;
|
|
623
|
+
i++;
|
|
624
|
+
}
|
|
625
|
+
if (args[i] === '--max-depth' && args[i + 1]) {
|
|
626
|
+
config.maxDepth = parseInt(args[i + 1], 10);
|
|
627
|
+
config._cliSource.maxDepth = true;
|
|
628
|
+
i++;
|
|
629
|
+
}
|
|
630
|
+
if (args[i] === '--timeout' && args[i + 1]) {
|
|
631
|
+
config.timeout = parseInt(args[i + 1], 10);
|
|
632
|
+
config._cliSource.timeout = true;
|
|
633
|
+
i++;
|
|
634
|
+
}
|
|
635
|
+
if (args[i] === '--preset' && args[i + 1]) {
|
|
636
|
+
config.preset = args[i + 1];
|
|
189
637
|
i++;
|
|
190
638
|
}
|
|
191
639
|
if (args[i] === '--policy' && args[i + 1]) {
|
|
@@ -205,12 +653,33 @@ function parseRealityArgs(args) {
|
|
|
205
653
|
if (args[i] === '--headful') {
|
|
206
654
|
config.headful = true;
|
|
207
655
|
}
|
|
656
|
+
if (args[i] === '--watch' || args[i] === '-w') {
|
|
657
|
+
config.watch = true;
|
|
658
|
+
}
|
|
208
659
|
if (args[i] === '--no-trace') {
|
|
209
660
|
config.enableTrace = false;
|
|
210
661
|
}
|
|
211
662
|
if (args[i] === '--no-screenshots') {
|
|
212
663
|
config.enableScreenshots = false;
|
|
213
664
|
}
|
|
665
|
+
// Phase 7.1: Performance flags
|
|
666
|
+
if (args[i] === '--fast') {
|
|
667
|
+
config.fast = true;
|
|
668
|
+
config.timeoutProfile = 'fast';
|
|
669
|
+
config.enableScreenshots = false;
|
|
670
|
+
}
|
|
671
|
+
if (args[i] === '--fail-fast') {
|
|
672
|
+
config.failFast = true;
|
|
673
|
+
}
|
|
674
|
+
if (args[i] === '--timeout-profile' && args[i + 1]) {
|
|
675
|
+
config.timeoutProfile = args[i + 1];
|
|
676
|
+
i++;
|
|
677
|
+
}
|
|
678
|
+
// Phase 7.2: Parallel execution
|
|
679
|
+
if (args[i] === '--parallel' && args[i + 1]) {
|
|
680
|
+
config.parallel = args[i + 1];
|
|
681
|
+
i++;
|
|
682
|
+
}
|
|
214
683
|
if (args[i] === '--help' || args[i] === '-h') {
|
|
215
684
|
printHelpReality();
|
|
216
685
|
process.exit(0);
|
|
@@ -223,6 +692,36 @@ function parseRealityArgs(args) {
|
|
|
223
692
|
process.exit(2);
|
|
224
693
|
}
|
|
225
694
|
|
|
695
|
+
return applyPresetConfig(config);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function parseSmokeArgs(args) {
|
|
699
|
+
const config = {
|
|
700
|
+
baseUrl: undefined,
|
|
701
|
+
headful: false,
|
|
702
|
+
timeBudgetMs: null
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
// First arg may be URL
|
|
706
|
+
if (args.length > 0 && !args[0].startsWith('--')) {
|
|
707
|
+
config.baseUrl = args[0];
|
|
708
|
+
args = args.slice(1);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
for (let i = 0; i < args.length; i++) {
|
|
712
|
+
const a = args[i];
|
|
713
|
+
if (a === '--url' && args[i + 1]) { config.baseUrl = args[i + 1]; i++; }
|
|
714
|
+
else if (a === '--headful') { config.headful = true; }
|
|
715
|
+
else if (a === '--budget-ms' && args[i + 1]) { config.timeBudgetMs = parseInt(args[i + 1], 10); i++; }
|
|
716
|
+
else if (a === '--help' || a === '-h') { printHelpSmoke(); process.exit(0); }
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (!config.baseUrl) {
|
|
720
|
+
console.error('Error: <url> is required');
|
|
721
|
+
console.error('Usage: guardian smoke <url>');
|
|
722
|
+
process.exit(2);
|
|
723
|
+
}
|
|
724
|
+
|
|
226
725
|
return config;
|
|
227
726
|
}
|
|
228
727
|
|
|
@@ -245,15 +744,165 @@ function parseInitArgs(args) {
|
|
|
245
744
|
return config;
|
|
246
745
|
}
|
|
247
746
|
|
|
747
|
+
function parseTemplateArgs(args) {
|
|
748
|
+
const config = {
|
|
749
|
+
template: args[0] || null,
|
|
750
|
+
output: null
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
for (let i = 0; i < args.length; i++) {
|
|
754
|
+
if (args[i] === '--output' && args[i + 1]) {
|
|
755
|
+
config.output = args[i + 1];
|
|
756
|
+
i++;
|
|
757
|
+
}
|
|
758
|
+
if (args[i] === '--help' || args[i] === '-h') {
|
|
759
|
+
printHelpTemplate();
|
|
760
|
+
process.exit(0);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return config;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Phase 11: Enterprise command parsers
|
|
768
|
+
function parseSitesArgs(args) {
|
|
769
|
+
const config = {
|
|
770
|
+
action: args[0] || 'list',
|
|
771
|
+
name: null,
|
|
772
|
+
url: null,
|
|
773
|
+
project: 'default'
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
if (config.action === 'add') {
|
|
777
|
+
config.name = args[1];
|
|
778
|
+
config.url = args[2];
|
|
779
|
+
for (let i = 3; i < args.length; i++) {
|
|
780
|
+
if (args[i] === '--project' && args[i + 1]) {
|
|
781
|
+
config.project = args[i + 1];
|
|
782
|
+
i++;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
} else if (config.action === 'remove') {
|
|
786
|
+
config.name = args[1];
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return config;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function parseUsersArgs(args) {
|
|
793
|
+
const config = {
|
|
794
|
+
action: args[0] || 'list',
|
|
795
|
+
username: args[1] || null,
|
|
796
|
+
role: args[2] || 'VIEWER'
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
return config;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function parseAuditArgs(args) {
|
|
803
|
+
const config = {
|
|
804
|
+
action: args[0] || 'list',
|
|
805
|
+
limit: 100,
|
|
806
|
+
actionFilter: null,
|
|
807
|
+
user: null
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
for (let i = 1; i < args.length; i++) {
|
|
811
|
+
if (args[i] === '--limit' && args[i + 1]) {
|
|
812
|
+
config.limit = parseInt(args[i + 1], 10);
|
|
813
|
+
i++;
|
|
814
|
+
}
|
|
815
|
+
if (args[i] === '--action' && args[i + 1]) {
|
|
816
|
+
config.actionFilter = args[i + 1];
|
|
817
|
+
i++;
|
|
818
|
+
}
|
|
819
|
+
if (args[i] === '--user' && args[i + 1]) {
|
|
820
|
+
config.user = args[i + 1];
|
|
821
|
+
i++;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
return config;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function parseExportArgs(args) {
|
|
829
|
+
const config = {
|
|
830
|
+
reportId: args[0] || null,
|
|
831
|
+
format: 'pdf',
|
|
832
|
+
output: null
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
for (let i = 1; i < args.length; i++) {
|
|
836
|
+
if (args[i] === '--format' && args[i + 1]) {
|
|
837
|
+
config.format = args[i + 1];
|
|
838
|
+
i++;
|
|
839
|
+
}
|
|
840
|
+
if (args[i] === '--output' && args[i + 1]) {
|
|
841
|
+
config.output = args[i + 1];
|
|
842
|
+
i++;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return config;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Phase 12.1: Recipe parser
|
|
850
|
+
function parseRecipeArgs(args) {
|
|
851
|
+
const config = {
|
|
852
|
+
action: args[0] || 'list',
|
|
853
|
+
id: args[1] || null,
|
|
854
|
+
url: null,
|
|
855
|
+
file: null,
|
|
856
|
+
out: null,
|
|
857
|
+
force: false,
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
// Positional file support for import
|
|
861
|
+
if (config.action === 'import' && config.id && !config.id.startsWith('--')) {
|
|
862
|
+
config.file = config.id;
|
|
863
|
+
config.id = null;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
for (let i = 2; i < args.length; i++) {
|
|
867
|
+
if (args[i] === '--url' && args[i + 1]) {
|
|
868
|
+
config.url = args[i + 1];
|
|
869
|
+
i++;
|
|
870
|
+
}
|
|
871
|
+
if (args[i] === '--file' && args[i + 1]) {
|
|
872
|
+
config.file = args[i + 1];
|
|
873
|
+
i++;
|
|
874
|
+
}
|
|
875
|
+
if (args[i] === '--out' && args[i + 1]) {
|
|
876
|
+
config.out = args[i + 1];
|
|
877
|
+
i++;
|
|
878
|
+
}
|
|
879
|
+
if (args[i] === '--force') {
|
|
880
|
+
config.force = true;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
return config;
|
|
885
|
+
}
|
|
886
|
+
|
|
248
887
|
function parseProtectArgs(args) {
|
|
249
888
|
const config = {
|
|
250
889
|
artifactsDir: './artifacts',
|
|
251
890
|
attempts: getDefaultAttemptIds(),
|
|
891
|
+
disabledAttempts: [],
|
|
252
892
|
headful: false,
|
|
253
893
|
enableTrace: true,
|
|
254
894
|
enableScreenshots: true,
|
|
255
|
-
policy:
|
|
256
|
-
|
|
895
|
+
policy: null,
|
|
896
|
+
preset: 'startup',
|
|
897
|
+
webhook: null,
|
|
898
|
+
watch: false,
|
|
899
|
+
// Phase 7.1: Performance modes
|
|
900
|
+
timeoutProfile: 'default',
|
|
901
|
+
failFast: false,
|
|
902
|
+
fast: false,
|
|
903
|
+
attemptsFilter: null,
|
|
904
|
+
// Phase 7.2: Parallel execution
|
|
905
|
+
parallel: 1
|
|
257
906
|
};
|
|
258
907
|
|
|
259
908
|
// First arg is URL if it doesn't start with --
|
|
@@ -267,6 +916,10 @@ function parseProtectArgs(args) {
|
|
|
267
916
|
config.baseUrl = args[i + 1];
|
|
268
917
|
i++;
|
|
269
918
|
}
|
|
919
|
+
if (args[i] === '--preset' && args[i + 1]) {
|
|
920
|
+
config.preset = args[i + 1];
|
|
921
|
+
i++;
|
|
922
|
+
}
|
|
270
923
|
if (args[i] === '--policy' && args[i + 1]) {
|
|
271
924
|
config.policy = args[i + 1];
|
|
272
925
|
i++;
|
|
@@ -275,6 +928,31 @@ function parseProtectArgs(args) {
|
|
|
275
928
|
config.webhook = args[i + 1];
|
|
276
929
|
i++;
|
|
277
930
|
}
|
|
931
|
+
if (args[i] === '--watch' || args[i] === '-w') {
|
|
932
|
+
config.watch = true;
|
|
933
|
+
}
|
|
934
|
+
// Phase 7.1: Performance flags
|
|
935
|
+
if (args[i] === '--fast') {
|
|
936
|
+
config.fast = true;
|
|
937
|
+
config.timeoutProfile = 'fast';
|
|
938
|
+
config.enableScreenshots = false;
|
|
939
|
+
}
|
|
940
|
+
if (args[i] === '--fail-fast') {
|
|
941
|
+
config.failFast = true;
|
|
942
|
+
}
|
|
943
|
+
if (args[i] === '--timeout-profile' && args[i + 1]) {
|
|
944
|
+
config.timeoutProfile = args[i + 1];
|
|
945
|
+
i++;
|
|
946
|
+
}
|
|
947
|
+
if (args[i] === '--attempts' && args[i + 1]) {
|
|
948
|
+
config.attemptsFilter = args[i + 1];
|
|
949
|
+
i++;
|
|
950
|
+
}
|
|
951
|
+
// Phase 7.2: Parallel execution
|
|
952
|
+
if (args[i] === '--parallel' && args[i + 1]) {
|
|
953
|
+
config.parallel = args[i + 1];
|
|
954
|
+
i++;
|
|
955
|
+
}
|
|
278
956
|
if (args[i] === '--help' || args[i] === '-h') {
|
|
279
957
|
printHelpProtect();
|
|
280
958
|
process.exit(0);
|
|
@@ -287,7 +965,7 @@ function parseProtectArgs(args) {
|
|
|
287
965
|
process.exit(2);
|
|
288
966
|
}
|
|
289
967
|
|
|
290
|
-
return config;
|
|
968
|
+
return applyPresetConfig(config);
|
|
291
969
|
}
|
|
292
970
|
|
|
293
971
|
function parseAttemptArgs(args) {
|
|
@@ -413,7 +1091,6 @@ WHAT IT DOES:
|
|
|
413
1091
|
|
|
414
1092
|
OPTIONS:
|
|
415
1093
|
--url <url> Target URL (required)
|
|
416
|
-
--attempts <id1,id2> Comma-separated attempt IDs (default: contact_form, language_switch, newsletter_signup)
|
|
417
1094
|
--artifacts <dir> Artifacts directory (default: ./artifacts)
|
|
418
1095
|
--discover Run deterministic CLI discovery and include in snapshot
|
|
419
1096
|
--universal Include Universal Reality Pack attempt
|
|
@@ -422,12 +1099,19 @@ OPTIONS:
|
|
|
422
1099
|
--headful Run headed browser (default: headless)
|
|
423
1100
|
--no-trace Disable trace recording
|
|
424
1101
|
--no-screenshots Disable screenshots
|
|
1102
|
+
|
|
1103
|
+
PERFORMANCE (Phase 7.1):
|
|
1104
|
+
--fast Fast mode (timeout-profile=fast + no screenshots)
|
|
1105
|
+
--fail-fast Stop on FAILURE (not FRICTION)
|
|
1106
|
+
--timeout-profile <name> fast | default | slow
|
|
1107
|
+
--attempts <id1,id2> Comma-separated attempt IDs (default: contact_form, language_switch, newsletter_signup)
|
|
1108
|
+
|
|
425
1109
|
--help Show this help message
|
|
426
1110
|
|
|
427
1111
|
EXIT CODES:
|
|
428
|
-
0
|
|
429
|
-
1
|
|
430
|
-
2
|
|
1112
|
+
0 READY
|
|
1113
|
+
1 FRICTION
|
|
1114
|
+
2 DO_NOT_LAUNCH
|
|
431
1115
|
|
|
432
1116
|
EXAMPLES:
|
|
433
1117
|
First run (baseline auto-created):
|
|
@@ -436,8 +1120,8 @@ EXAMPLES:
|
|
|
436
1120
|
With policy preset:
|
|
437
1121
|
guardian reality --url https://example.com --policy preset:saas
|
|
438
1122
|
|
|
439
|
-
|
|
440
|
-
guardian reality --url https://example.com --
|
|
1123
|
+
Fast mode (performance):
|
|
1124
|
+
guardian reality --url https://example.com --fast --fail-fast
|
|
441
1125
|
`);
|
|
442
1126
|
}
|
|
443
1127
|
|
|
@@ -447,7 +1131,7 @@ Usage: guardian init [options]
|
|
|
447
1131
|
|
|
448
1132
|
WHAT IT DOES:
|
|
449
1133
|
Initialize Guardian in the current directory:
|
|
450
|
-
- Creates guardian.policy.json (default: startup preset)
|
|
1134
|
+
- Creates config/guardian.policy.json (default: startup preset)
|
|
451
1135
|
- Updates .gitignore to exclude Guardian artifacts
|
|
452
1136
|
- Prints next steps
|
|
453
1137
|
|
|
@@ -462,23 +1146,144 @@ EXAMPLE:
|
|
|
462
1146
|
`);
|
|
463
1147
|
}
|
|
464
1148
|
|
|
1149
|
+
function printHelpTemplate() {
|
|
1150
|
+
console.log(`
|
|
1151
|
+
Usage: guardian template [template] [options]
|
|
1152
|
+
|
|
1153
|
+
WHAT IT DOES:
|
|
1154
|
+
Generate a minimal config template for common site types.
|
|
1155
|
+
Templates include sample journeys and policy settings.
|
|
1156
|
+
|
|
1157
|
+
AVAILABLE TEMPLATES:
|
|
1158
|
+
saas SaaS startup flow (signup, login, dashboard)
|
|
1159
|
+
shop E-commerce shop flow (browse, cart, checkout)
|
|
1160
|
+
landing Landing page flow (load, CTA validation)
|
|
1161
|
+
|
|
1162
|
+
OPTIONS:
|
|
1163
|
+
--output <file> Output file name (default: guardian-<template>.json)
|
|
1164
|
+
--help Show this help message
|
|
1165
|
+
|
|
1166
|
+
EXAMPLES:
|
|
1167
|
+
guardian template saas
|
|
1168
|
+
guardian template shop --output my-config.json
|
|
1169
|
+
guardian template landing
|
|
1170
|
+
`);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function printHelpList() {
|
|
1174
|
+
console.log(`
|
|
1175
|
+
Usage: guardian list [options]
|
|
1176
|
+
|
|
1177
|
+
WHAT IT DOES:
|
|
1178
|
+
List all completed Guardian runs with metadata.
|
|
1179
|
+
Scans the artifacts directory for runs with META.json files and displays
|
|
1180
|
+
them in a table sorted by most recent first.
|
|
1181
|
+
|
|
1182
|
+
OPTIONS:
|
|
1183
|
+
--artifacts <dir> Path to artifacts directory
|
|
1184
|
+
Default: ./.odavlguardian
|
|
1185
|
+
--help Show this help message
|
|
1186
|
+
|
|
1187
|
+
OUTPUT COLUMNS:
|
|
1188
|
+
Time Run execution timestamp (YYYY-MM-DD HH:MM:SS)
|
|
1189
|
+
Site Target site slug (extracted from URL)
|
|
1190
|
+
Policy Policy/profile used for the run
|
|
1191
|
+
Result Run result: PASSED, FAILED, or WARN
|
|
1192
|
+
Duration Wall-clock execution time
|
|
1193
|
+
Path Run directory name
|
|
1194
|
+
|
|
1195
|
+
EXAMPLE:
|
|
1196
|
+
guardian list
|
|
1197
|
+
guardian list --artifacts ./.odavlguardian
|
|
1198
|
+
guardian list --failed
|
|
1199
|
+
guardian list --site github-com --limit 5
|
|
1200
|
+
`);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function printHelpCleanup() {
|
|
1204
|
+
console.log(`
|
|
1205
|
+
Usage: guardian cleanup [options]
|
|
1206
|
+
|
|
1207
|
+
WHAT IT DOES:
|
|
1208
|
+
Manage and delete old or failed Guardian runs.
|
|
1209
|
+
Safely removes run directories while optionally preserving recent runs.
|
|
1210
|
+
|
|
1211
|
+
OPTIONS:
|
|
1212
|
+
--artifacts <dir> Path to artifacts directory
|
|
1213
|
+
Default: ./.odavlguardian
|
|
1214
|
+
--older-than <duration> Delete runs older than duration
|
|
1215
|
+
Format: <num>[d|h|m] (e.g., 7d, 24h, 30m)
|
|
1216
|
+
--keep-latest <num> Keep the N most recent runs per site
|
|
1217
|
+
Applied per site, deletes older runs
|
|
1218
|
+
--failed-only Only delete failed runs (result === FAILED)
|
|
1219
|
+
--help Show this help message
|
|
1220
|
+
|
|
1221
|
+
BEHAVIOR:
|
|
1222
|
+
- Filters are applied independently and compose
|
|
1223
|
+
- --failed-only filters to only FAILED status before other filters
|
|
1224
|
+
- --keep-latest keeps N newest per site, regardless of status
|
|
1225
|
+
- Combine flags: guardian cleanup --older-than 7d --failed-only
|
|
1226
|
+
- Deletes using real fs.rmSync() with recursive flag
|
|
1227
|
+
|
|
1228
|
+
EXAMPLE:
|
|
1229
|
+
guardian cleanup --older-than 7d
|
|
1230
|
+
guardian cleanup --keep-latest 3
|
|
1231
|
+
guardian cleanup --older-than 30d --failed-only
|
|
1232
|
+
guardian cleanup --artifacts ./.odavlguardian --keep-latest 5
|
|
1233
|
+
`);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
465
1236
|
function printHelpProtect() {
|
|
466
1237
|
console.log(`
|
|
467
1238
|
Usage: guardian protect <url> [options]
|
|
468
1239
|
|
|
469
1240
|
WHAT IT DOES:
|
|
470
|
-
|
|
471
|
-
|
|
1241
|
+
Full market reality test with startup policy.
|
|
1242
|
+
Deeper than smoke; runs full discovery, attempts, and baseline comparison.
|
|
472
1243
|
|
|
473
1244
|
OPTIONS:
|
|
474
1245
|
<url> Target URL (required)
|
|
475
1246
|
--policy <path|preset> Override policy (default: preset:startup)
|
|
476
1247
|
--webhook <url> Webhook URL for notifications
|
|
1248
|
+
|
|
1249
|
+
PERFORMANCE (Phase 7.1):
|
|
1250
|
+
--fast Fast mode (timeout-profile=fast + no screenshots)
|
|
1251
|
+
--fail-fast Stop on FAILURE (not FRICTION)
|
|
1252
|
+
--timeout-profile <name> fast | default | slow
|
|
1253
|
+
--attempts <id1,id2> Comma-separated attempt IDs (filter)
|
|
1254
|
+
|
|
477
1255
|
--help Show this help message
|
|
478
1256
|
|
|
479
1257
|
EXAMPLES:
|
|
480
1258
|
guardian protect https://example.com
|
|
481
1259
|
guardian protect https://example.com --policy preset:enterprise
|
|
1260
|
+
guardian protect https://example.com --fast --fail-fast
|
|
1261
|
+
`);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function printHelpSmoke() {
|
|
1265
|
+
console.log(`
|
|
1266
|
+
Usage: guardian smoke <url>
|
|
1267
|
+
|
|
1268
|
+
WHAT IT DOES:
|
|
1269
|
+
Fast market sanity check (<30s).
|
|
1270
|
+
Runs only critical paths: homepage reachability, navigation probe,
|
|
1271
|
+
auth (login or signup), and contact/support if present.
|
|
1272
|
+
|
|
1273
|
+
FORCED SETTINGS:
|
|
1274
|
+
timeout-profile=fast, fail-fast=on, parallel=2, browser reuse on,
|
|
1275
|
+
retries=minimal, no baseline compare.
|
|
1276
|
+
|
|
1277
|
+
EXIT CODES:
|
|
1278
|
+
0 Smoke PASS
|
|
1279
|
+
1 Smoke FRICTION
|
|
1280
|
+
2 Smoke FAIL (including time budget exceeded)
|
|
1281
|
+
|
|
1282
|
+
Options:
|
|
1283
|
+
<url> Target URL (required)
|
|
1284
|
+
--headful Run headed browser (default: headless)
|
|
1285
|
+
--budget-ms <n> Override time budget in ms (primarily for CI/tests)
|
|
1286
|
+
--help, -h Show this help message
|
|
482
1287
|
`);
|
|
483
1288
|
}
|
|
484
1289
|
|
|
@@ -503,7 +1308,7 @@ Usage: guardian attempt --url <baseUrl> --attempt <id> [options]
|
|
|
503
1308
|
Options:
|
|
504
1309
|
--url <url> Target URL (required)
|
|
505
1310
|
--attempt <id> Attempt ID (default: contact_form)
|
|
506
|
-
--artifacts <dir> Artifacts directory (default:
|
|
1311
|
+
--artifacts <dir> Artifacts directory (default: ./.odavlguardian)
|
|
507
1312
|
--headful Run with visible browser (default: headless)
|
|
508
1313
|
--no-trace Disable trace recording
|
|
509
1314
|
--no-screenshots Disable screenshot capture
|
|
@@ -516,6 +1321,56 @@ Exit Codes:
|
|
|
516
1321
|
`);
|
|
517
1322
|
}
|
|
518
1323
|
|
|
1324
|
+
function printHelpJourneyScan() {
|
|
1325
|
+
console.log(`
|
|
1326
|
+
Usage: guardian journey-scan <url> [options]
|
|
1327
|
+
|
|
1328
|
+
WHAT IT DOES:
|
|
1329
|
+
Human journey scan — Tests a single critical user flow end-to-end.
|
|
1330
|
+
Opens a real browser, follows a predetermined journey, captures evidence,
|
|
1331
|
+
and outputs a human-readable report with a single decision:
|
|
1332
|
+
|
|
1333
|
+
✅ SAFE (all steps succeeded)
|
|
1334
|
+
⚠️ RISK (partial success with failures)
|
|
1335
|
+
🚫 DO_NOT_LAUNCH (complete failure)
|
|
1336
|
+
|
|
1337
|
+
OPTIONS:
|
|
1338
|
+
<url> Target URL (required)
|
|
1339
|
+
--preset <name> saas | shop | landing (default: saas)
|
|
1340
|
+
--out <dir> Output directory (default: ./.odavlguardian)
|
|
1341
|
+
--timeout <ms> Step timeout in milliseconds (default: 20000)
|
|
1342
|
+
--headful Run headed browser (show UI)
|
|
1343
|
+
--help Show help
|
|
1344
|
+
|
|
1345
|
+
EXAMPLES:
|
|
1346
|
+
guardian journey-scan https://example.com
|
|
1347
|
+
guardian journey-scan https://example.com --preset shop --out ./results
|
|
1348
|
+
guardian journey-scan https://example.com --preset landing --headful
|
|
1349
|
+
|
|
1350
|
+
OUTPUT:
|
|
1351
|
+
SUMMARY.txt Human-readable summary
|
|
1352
|
+
summary.md Markdown summary
|
|
1353
|
+
report.json Full journey results
|
|
1354
|
+
screenshots/ Evidence screenshots per step
|
|
1355
|
+
metadata.json Scan metadata
|
|
1356
|
+
|
|
1357
|
+
EXIT CODES:
|
|
1358
|
+
0 READY (all steps succeeded)
|
|
1359
|
+
1 FRICTION (partial failures)
|
|
1360
|
+
2 DO_NOT_LAUNCH (complete failure)
|
|
1361
|
+
`);
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
function printHelpLive() {
|
|
1365
|
+
console.log('Usage: guardian live <url> [options]');
|
|
1366
|
+
console.log('Options:');
|
|
1367
|
+
console.log(' --interval <minutes> Run periodically; omit to run once');
|
|
1368
|
+
console.log(' --out <dir> Output directory');
|
|
1369
|
+
console.log(' --preset <name> Override journey preset');
|
|
1370
|
+
console.log(' --headful Run with visible browser');
|
|
1371
|
+
console.log(' --timeout <ms> Step timeout');
|
|
1372
|
+
}
|
|
1373
|
+
|
|
519
1374
|
function printHelpScan() {
|
|
520
1375
|
console.log(`
|
|
521
1376
|
Usage: guardian scan <url> [options]
|
|
@@ -530,18 +1385,26 @@ WHAT IT DOES:
|
|
|
530
1385
|
|
|
531
1386
|
OPTIONS:
|
|
532
1387
|
<url> Target URL (required)
|
|
533
|
-
--preset <name> landing | saas | shop (opinionated defaults)
|
|
1388
|
+
--preset <name> landing | landing-demo | saas | shop (opinionated defaults)
|
|
534
1389
|
--policy <path|preset> Override policy file or preset:name
|
|
535
1390
|
--artifacts <dir> Artifacts directory (default: ./artifacts)
|
|
536
1391
|
--headful Run headed browser
|
|
537
1392
|
--no-trace Disable trace
|
|
538
1393
|
--no-screenshots Disable screenshots
|
|
1394
|
+
|
|
1395
|
+
PERFORMANCE (Phase 7.1):
|
|
1396
|
+
--fast Fast mode (timeout-profile=fast + no screenshots)
|
|
1397
|
+
--fail-fast Stop on FAILURE (not FRICTION)
|
|
1398
|
+
--timeout-profile <name> fast | default | slow
|
|
1399
|
+
--attempts <list> Comma-separated attempt IDs (filter)
|
|
1400
|
+
|
|
539
1401
|
--help Show help
|
|
540
1402
|
|
|
541
1403
|
EXAMPLES:
|
|
542
1404
|
guardian scan https://example.com --preset landing
|
|
1405
|
+
guardian scan https://example.com --preset landing-demo
|
|
543
1406
|
guardian scan https://example.com --preset saas
|
|
544
|
-
guardian scan https://example.com --
|
|
1407
|
+
guardian scan https://example.com --fast --fail-fast
|
|
545
1408
|
`);
|
|
546
1409
|
}
|
|
547
1410
|
|
|
@@ -565,7 +1428,7 @@ Options:
|
|
|
565
1428
|
--url <url> Target URL (required)
|
|
566
1429
|
--attempts <id1,id2> Comma-separated attempt IDs (default: curated 3)
|
|
567
1430
|
--name <baselineName> Baseline name (default: baseline)
|
|
568
|
-
--artifacts <dir> Artifacts directory (default:
|
|
1431
|
+
--artifacts <dir> Artifacts directory (default: ./.odavlguardian)
|
|
569
1432
|
--headful Run headed browser (default: headless)
|
|
570
1433
|
--no-trace Disable trace recording
|
|
571
1434
|
--no-screenshots Disable screenshots
|
|
@@ -584,7 +1447,7 @@ Options:
|
|
|
584
1447
|
--url <url> Target URL (required)
|
|
585
1448
|
--name <baselineName> Baseline name to compare against (required)
|
|
586
1449
|
--attempts <id1,id2> Comma-separated attempt IDs (default: curated 3)
|
|
587
|
-
--artifacts <dir> Artifacts directory (default:
|
|
1450
|
+
--artifacts <dir> Artifacts directory (default: ./.odavlguardian)
|
|
588
1451
|
--headful Run headed browser (default: headless)
|
|
589
1452
|
--no-trace Disable trace recording
|
|
590
1453
|
--no-screenshots Disable screenshots
|
|
@@ -602,34 +1465,156 @@ Exit Codes:
|
|
|
602
1465
|
async function main() {
|
|
603
1466
|
const args = process.argv.slice(2);
|
|
604
1467
|
|
|
1468
|
+
// Phase 8: Helper to check plan before scan
|
|
1469
|
+
function checkPlanBeforeScan(config, options = {}) {
|
|
1470
|
+
const { recordUsage = true } = options;
|
|
1471
|
+
try {
|
|
1472
|
+
const url = config.url || config.baseUrl || '';
|
|
1473
|
+
if (!url) return; // No URL to check
|
|
1474
|
+
|
|
1475
|
+
// Phase 10: Register user on first scan
|
|
1476
|
+
registerUser();
|
|
1477
|
+
|
|
1478
|
+
const check = checkCanScan(url);
|
|
1479
|
+
if (!check.allowed) {
|
|
1480
|
+
// LEVEL 1 TRANSPARENT GATING: emit clear message + artifacts
|
|
1481
|
+
const fs = require('fs');
|
|
1482
|
+
const path = require('path');
|
|
1483
|
+
const artifactsDir = config.artifactsDir || './.odavlguardian';
|
|
1484
|
+
try { if (!fs.existsSync(artifactsDir)) fs.mkdirSync(artifactsDir, { recursive: true }); } catch (_) {}
|
|
1485
|
+
|
|
1486
|
+
const now = new Date().toISOString().replace(/[:\-]/g, '').substring(0, 15).replace('T', '-');
|
|
1487
|
+
const runId = `gated-${now}`;
|
|
1488
|
+
const runDir = path.join(artifactsDir, runId);
|
|
1489
|
+
try { fs.mkdirSync(runDir, { recursive: true }); } catch (_) {}
|
|
1490
|
+
|
|
1491
|
+
const upgradeMsg = getUpgradeMessage();
|
|
1492
|
+
|
|
1493
|
+
// Write decision.json with canonical verdict and next steps
|
|
1494
|
+
const decision = {
|
|
1495
|
+
runId,
|
|
1496
|
+
url,
|
|
1497
|
+
timestamp: new Date().toISOString(),
|
|
1498
|
+
preset: config.preset || 'default',
|
|
1499
|
+
policyName: 'Plan Gate',
|
|
1500
|
+
finalVerdict: 'FRICTION',
|
|
1501
|
+
exitCode: 1,
|
|
1502
|
+
reasons: [
|
|
1503
|
+
{ code: 'PLAN_GATE', message: check.message },
|
|
1504
|
+
{ code: 'NEXT_STEPS', message: upgradeMsg }
|
|
1505
|
+
],
|
|
1506
|
+
gating: {
|
|
1507
|
+
reason: check.message,
|
|
1508
|
+
nextSteps: upgradeMsg
|
|
1509
|
+
}
|
|
1510
|
+
};
|
|
1511
|
+
try { fs.writeFileSync(path.join(runDir, 'decision.json'), JSON.stringify(decision, null, 2), 'utf8'); } catch (_) {}
|
|
1512
|
+
|
|
1513
|
+
// Write summary.md with friendly next steps
|
|
1514
|
+
const lines = [];
|
|
1515
|
+
lines.push('# Guardian Reality Summary');
|
|
1516
|
+
lines.push('');
|
|
1517
|
+
lines.push('## Final Verdict');
|
|
1518
|
+
lines.push('- Verdict: FRICTION (exit 1)');
|
|
1519
|
+
lines.push('- Why this verdict: Reality run was gated by plan limits.');
|
|
1520
|
+
lines.push('');
|
|
1521
|
+
lines.push('## What Happened');
|
|
1522
|
+
lines.push(`- ${check.message}`);
|
|
1523
|
+
lines.push('');
|
|
1524
|
+
lines.push('## What To Do Next');
|
|
1525
|
+
lines.push(`- ${upgradeMsg}`);
|
|
1526
|
+
try { fs.writeFileSync(path.join(runDir, 'summary.md'), lines.join('\n'), 'utf8'); } catch (_) {}
|
|
1527
|
+
|
|
1528
|
+
// Crystal-clear CLI message
|
|
1529
|
+
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
1530
|
+
console.log('Guardian Reality was gated by plan limits');
|
|
1531
|
+
console.log('');
|
|
1532
|
+
console.log(`Why: ${check.message}`);
|
|
1533
|
+
console.log(`Next steps: ${upgradeMsg}`);
|
|
1534
|
+
console.log('');
|
|
1535
|
+
console.log(`Artifacts: ${runDir}`);
|
|
1536
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
1537
|
+
process.exit(1);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
// Show usage info
|
|
1541
|
+
if (check.usage && check.usage.scansRemaining !== 'Unlimited') {
|
|
1542
|
+
console.log(`ℹ️ Scans remaining this month: ${check.usage.scansRemaining}\n`);
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
// Record the scan
|
|
1546
|
+
if (recordUsage) {
|
|
1547
|
+
performScan(url);
|
|
1548
|
+
|
|
1549
|
+
// Phase 10: Track first scan signal
|
|
1550
|
+
recordFirstScan();
|
|
1551
|
+
}
|
|
1552
|
+
} catch (error) {
|
|
1553
|
+
console.error(`\n❌ ${error.message}`);
|
|
1554
|
+
console.log(getUpgradeMessage());
|
|
1555
|
+
process.exit(1);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// Minimal release flag: print version and exit
|
|
1560
|
+
// PHASE 6: First-run welcome (only once)
|
|
1561
|
+
if (args.length > 0 && !['--help', '-h', 'init', 'presets', 'template'].includes(args[0])) {
|
|
1562
|
+
if (isFirstRun('.odavl-guardian')) {
|
|
1563
|
+
printWelcome('ODAVL Guardian');
|
|
1564
|
+
printFirstRunHint();
|
|
1565
|
+
markAsRun('.odavl-guardian');
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
|
|
605
1569
|
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
606
1570
|
console.log(`
|
|
607
1571
|
🛡️ ODAVL Guardian — Market Reality Testing Engine
|
|
608
1572
|
|
|
609
1573
|
Usage: guardian <subcommand> [options]
|
|
610
1574
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
1575
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
1576
|
+
LEVEL 1 — GOLDEN PATH (Reality Only)
|
|
1577
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
1578
|
+
|
|
1579
|
+
reality --url <URL> Full Market Reality Check
|
|
1580
|
+
→ Canonical Verdicts: READY / FRICTION / DO_NOT_LAUNCH
|
|
1581
|
+
→ Outputs: summary.md, decision.json, market-report.html
|
|
1582
|
+
→ Artifacts: ./.odavlguardian/
|
|
1583
|
+
|
|
1584
|
+
--url <URL> Alias for: guardian reality --url <URL>
|
|
1585
|
+
|
|
1586
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
1587
|
+
QUICK START
|
|
1588
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
1589
|
+
|
|
1590
|
+
# Run Reality Check (Level 1 Golden Path)
|
|
1591
|
+
guardian reality --url https://example.com
|
|
615
1592
|
|
|
616
|
-
|
|
1593
|
+
# Same, using alias
|
|
1594
|
+
guardian --url https://example.com
|
|
1595
|
+
|
|
1596
|
+
# List completed runs
|
|
1597
|
+
guardian list
|
|
1598
|
+
|
|
1599
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
1600
|
+
ADVANCED COMMANDS (Level 2+)
|
|
1601
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
1602
|
+
|
|
1603
|
+
scan <url> One-command product scan
|
|
1604
|
+
journey-scan <url> Human journey scan
|
|
617
1605
|
attempt Execute a single user attempt
|
|
618
|
-
|
|
619
|
-
baseline check
|
|
1606
|
+
smoke <url> Fast market sanity check (<30s)
|
|
1607
|
+
baseline save/check Baseline management
|
|
1608
|
+
list List completed runs
|
|
1609
|
+
cleanup Manage and delete old runs
|
|
1610
|
+
init Initialize Guardian
|
|
1611
|
+
template <type> Generate config template
|
|
1612
|
+
plan / upgrade Show plan or upgrade
|
|
1613
|
+
sites / users / audit Enterprise management
|
|
1614
|
+
export Export reports (PDF)
|
|
620
1615
|
presets List available policy presets
|
|
621
1616
|
|
|
622
|
-
|
|
623
|
-
# Initialize Guardian
|
|
624
|
-
guardian init
|
|
625
|
-
|
|
626
|
-
# Quick protect (uses startup policy)
|
|
627
|
-
guardian protect https://example.com
|
|
628
|
-
|
|
629
|
-
# Full reality check with policy
|
|
630
|
-
guardian reality --url https://example.com --policy preset:saas
|
|
631
|
-
|
|
632
|
-
Run 'guardian <subcommand> --help' for more information.
|
|
1617
|
+
Run 'guardian <subcommand> --help' for detailed command help.
|
|
633
1618
|
`);
|
|
634
1619
|
process.exit(0);
|
|
635
1620
|
}
|
|
@@ -645,13 +1630,517 @@ Run 'guardian <subcommand> --help' for more information.
|
|
|
645
1630
|
} else if (parsed.subcommand === 'presets') {
|
|
646
1631
|
printPresets();
|
|
647
1632
|
process.exit(0);
|
|
1633
|
+
} else if (parsed.subcommand === 'template') {
|
|
1634
|
+
const template = config.template;
|
|
1635
|
+
if (!template) {
|
|
1636
|
+
console.log('\n📋 Guardian Templates\n');
|
|
1637
|
+
const templates = listTemplates();
|
|
1638
|
+
templates.forEach(t => {
|
|
1639
|
+
console.log(` ${t.name}: ${t.description} (${t.journeys} journeys)`);
|
|
1640
|
+
});
|
|
1641
|
+
console.log('\nUsage: guardian template <saas|shop|landing> [--output file.json]\n');
|
|
1642
|
+
process.exit(0);
|
|
1643
|
+
}
|
|
1644
|
+
try {
|
|
1645
|
+
const result = generateTemplate(template, { output: config.output });
|
|
1646
|
+
console.log(`\n✅ ${result.message}`);
|
|
1647
|
+
console.log(` Generated config ready to use with: guardian reality --config ${result.outputPath}\n`);
|
|
1648
|
+
process.exit(0);
|
|
1649
|
+
} catch (err) {
|
|
1650
|
+
console.error(`\n❌ ${err.message}\n`);
|
|
1651
|
+
process.exit(1);
|
|
1652
|
+
}
|
|
1653
|
+
} else if (parsed.subcommand === 'plan') {
|
|
1654
|
+
// Phase 8: Show current plan and usage
|
|
1655
|
+
const summary = getPlanSummary();
|
|
1656
|
+
console.log(`\n🛡️ ODAVL Guardian Plan\n`);
|
|
1657
|
+
|
|
1658
|
+
// Phase 10: Show founder status
|
|
1659
|
+
const founderMsg = getFounderMessage();
|
|
1660
|
+
if (founderMsg) {
|
|
1661
|
+
console.log(founderMsg);
|
|
1662
|
+
console.log();
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
console.log(`Current Plan: ${summary.plan.name.toUpperCase()}`);
|
|
1666
|
+
if (summary.plan.price > 0) {
|
|
1667
|
+
console.log(`Price: $${summary.plan.price}/month`);
|
|
1668
|
+
}
|
|
1669
|
+
console.log();
|
|
1670
|
+
console.log(`Usage This Month:`);
|
|
1671
|
+
console.log(` Scans: ${summary.limits.scans.used}/${summary.limits.scans.max === -1 ? 'Unlimited' : summary.limits.scans.max} (${summary.limits.scans.remaining} remaining)`);
|
|
1672
|
+
console.log(` Sites: ${summary.limits.sites.used}/${summary.limits.sites.max === -1 ? 'Unlimited' : summary.limits.sites.max}`);
|
|
1673
|
+
console.log();
|
|
1674
|
+
console.log(`Features:`);
|
|
1675
|
+
console.log(` Live Guardian: ${summary.features.liveGuardian ? '✅' : '❌'}`);
|
|
1676
|
+
console.log(` CI/CD Mode: ${summary.features.ciMode ? '✅' : '❌'}`);
|
|
1677
|
+
console.log(` Alerts: ${summary.features.alerts ? '✅' : '❌'}`);
|
|
1678
|
+
console.log();
|
|
1679
|
+
if (summary.plan.id === 'free') {
|
|
1680
|
+
console.log(`Upgrade to Pro for unlimited scans and advanced features.`);
|
|
1681
|
+
console.log(`Run: guardian upgrade pro\n`);
|
|
1682
|
+
}
|
|
1683
|
+
process.exit(0);
|
|
1684
|
+
} else if (parsed.subcommand === 'upgrade') {
|
|
1685
|
+
// Phase 8: Upgrade to a plan
|
|
1686
|
+
const targetPlan = config.plan;
|
|
1687
|
+
if (!targetPlan || !['pro', 'business'].includes(targetPlan.toLowerCase())) {
|
|
1688
|
+
console.error('\n❌ Please specify a valid plan: pro or business');
|
|
1689
|
+
console.log('\nUsage: guardian upgrade <pro|business>\n');
|
|
1690
|
+
process.exit(1);
|
|
1691
|
+
}
|
|
1692
|
+
const { getCheckoutUrl } = require('../src/payments/stripe-checkout');
|
|
1693
|
+
const checkoutUrl = getCheckoutUrl(targetPlan);
|
|
1694
|
+
console.log(`\n🚀 Upgrade to ${targetPlan.toUpperCase()}\n`);
|
|
1695
|
+
console.log(`Open this URL to complete your upgrade:\n`);
|
|
1696
|
+
console.log(` ${checkoutUrl}\n`);
|
|
1697
|
+
console.log(`After payment, your plan will be automatically activated.\n`);
|
|
1698
|
+
|
|
1699
|
+
// Phase 10: Record upgrade signal
|
|
1700
|
+
recordFirstUpgrade(targetPlan.toLowerCase());
|
|
1701
|
+
|
|
1702
|
+
process.exit(0);
|
|
1703
|
+
} else if (parsed.subcommand === 'feedback') {
|
|
1704
|
+
// Phase 10: Feedback session
|
|
1705
|
+
try {
|
|
1706
|
+
await runFeedbackSession();
|
|
1707
|
+
process.exit(0);
|
|
1708
|
+
} catch (err) {
|
|
1709
|
+
console.error(`\n❌ Feedback error: ${err.message}\n`);
|
|
1710
|
+
process.exit(1);
|
|
1711
|
+
}
|
|
1712
|
+
} else if (parsed.subcommand === 'sites') {
|
|
1713
|
+
// Phase 11: Multi-site management
|
|
1714
|
+
try {
|
|
1715
|
+
requirePermission('site:view', 'manage sites');
|
|
1716
|
+
|
|
1717
|
+
if (config.action === 'add') {
|
|
1718
|
+
requirePermission('site:add', 'add sites');
|
|
1719
|
+
if (!config.name || !config.url) {
|
|
1720
|
+
console.error('Usage: guardian sites add <name> <url> [--project <name>]');
|
|
1721
|
+
process.exit(2);
|
|
1722
|
+
}
|
|
1723
|
+
const site = addSite(config.name, config.url, config.project);
|
|
1724
|
+
console.log(`✓ Site added: ${site.name} (${site.project})`);
|
|
1725
|
+
logAudit(AUDIT_ACTIONS.SITE_ADD, { name: site.name, url: site.url, project: site.project });
|
|
1726
|
+
} else if (config.action === 'remove') {
|
|
1727
|
+
requirePermission('site:remove', 'remove sites');
|
|
1728
|
+
if (!config.name) {
|
|
1729
|
+
console.error('Usage: guardian sites remove <name>');
|
|
1730
|
+
process.exit(2);
|
|
1731
|
+
}
|
|
1732
|
+
const site = removeSite(config.name);
|
|
1733
|
+
console.log(`✓ Site removed: ${site.name}`);
|
|
1734
|
+
logAudit(AUDIT_ACTIONS.SITE_REMOVE, { name: site.name });
|
|
1735
|
+
} else {
|
|
1736
|
+
// List sites
|
|
1737
|
+
const data = getSites();
|
|
1738
|
+
if (data.sites.length === 0) {
|
|
1739
|
+
console.log('No sites registered yet.');
|
|
1740
|
+
} else {
|
|
1741
|
+
console.log(`\nTotal sites: ${data.sites.length}\n`);
|
|
1742
|
+
const projects = listProjects();
|
|
1743
|
+
for (const proj of projects) {
|
|
1744
|
+
console.log(`📁 ${proj.name} (${proj.siteCount} site(s))`);
|
|
1745
|
+
const sites = getSitesByProject(proj.name);
|
|
1746
|
+
for (const site of sites) {
|
|
1747
|
+
console.log(` - ${site.name}: ${site.url}`);
|
|
1748
|
+
if (site.lastScannedAt) {
|
|
1749
|
+
console.log(` Last scan: ${site.lastScannedAt} (${site.scanCount} total)`);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
process.exit(0);
|
|
1756
|
+
} catch (err) {
|
|
1757
|
+
console.error(`\n❌ Sites error: ${err.message}\n`);
|
|
1758
|
+
process.exit(1);
|
|
1759
|
+
}
|
|
1760
|
+
} else if (parsed.subcommand === 'users') {
|
|
1761
|
+
// Phase 11: User/role management
|
|
1762
|
+
try {
|
|
1763
|
+
requirePermission('user:view', 'manage users');
|
|
1764
|
+
|
|
1765
|
+
if (config.action === 'add') {
|
|
1766
|
+
requirePermission('user:add', 'add users');
|
|
1767
|
+
if (!config.username) {
|
|
1768
|
+
console.error('Usage: guardian users add <username> [role]');
|
|
1769
|
+
process.exit(2);
|
|
1770
|
+
}
|
|
1771
|
+
const user = addUser(config.username, config.role);
|
|
1772
|
+
console.log(`✓ User added: ${user.username} (${user.role})`);
|
|
1773
|
+
logAudit(AUDIT_ACTIONS.USER_ADD, { username: user.username, role: user.role });
|
|
1774
|
+
} else if (config.action === 'remove') {
|
|
1775
|
+
requirePermission('user:remove', 'remove users');
|
|
1776
|
+
if (!config.username) {
|
|
1777
|
+
console.error('Usage: guardian users remove <username>');
|
|
1778
|
+
process.exit(2);
|
|
1779
|
+
}
|
|
1780
|
+
const user = removeUser(config.username);
|
|
1781
|
+
console.log(`✓ User removed: ${user.username}`);
|
|
1782
|
+
logAudit(AUDIT_ACTIONS.USER_REMOVE, { username: user.username });
|
|
1783
|
+
} else if (config.action === 'roles') {
|
|
1784
|
+
// List roles
|
|
1785
|
+
const roles = listRoles();
|
|
1786
|
+
console.log('\nAvailable roles:\n');
|
|
1787
|
+
for (const role of roles) {
|
|
1788
|
+
console.log(`${role.name}:`);
|
|
1789
|
+
console.log(` Permissions: ${role.permissions.join(', ')}`);
|
|
1790
|
+
}
|
|
1791
|
+
} else {
|
|
1792
|
+
// List users
|
|
1793
|
+
const data = getUsers();
|
|
1794
|
+
console.log(`\nTotal users: ${data.users.length}\n`);
|
|
1795
|
+
for (const user of data.users) {
|
|
1796
|
+
const current = user.username === getCurrentUser().username ? ' (current)' : '';
|
|
1797
|
+
console.log(`- ${user.username}: ${user.role}${current}`);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
process.exit(0);
|
|
1801
|
+
} catch (err) {
|
|
1802
|
+
console.error(`\n❌ Users error: ${err.message}\n`);
|
|
1803
|
+
process.exit(1);
|
|
1804
|
+
}
|
|
1805
|
+
} else if (parsed.subcommand === 'audit') {
|
|
1806
|
+
// Phase 11: Audit log viewing
|
|
1807
|
+
try {
|
|
1808
|
+
requirePermission('audit:view', 'view audit logs');
|
|
1809
|
+
|
|
1810
|
+
if (config.action === 'summary') {
|
|
1811
|
+
const summary = getAuditSummary();
|
|
1812
|
+
console.log('\nAudit Summary:\n');
|
|
1813
|
+
console.log(`Total logs: ${summary.totalLogs}`);
|
|
1814
|
+
console.log(`First log: ${summary.firstLog || 'N/A'}`);
|
|
1815
|
+
console.log(`Last log: ${summary.lastLog || 'N/A'}`);
|
|
1816
|
+
console.log('\nActions:');
|
|
1817
|
+
for (const [action, count] of Object.entries(summary.actionCounts)) {
|
|
1818
|
+
console.log(` ${action}: ${count}`);
|
|
1819
|
+
}
|
|
1820
|
+
console.log('\nUsers:');
|
|
1821
|
+
for (const [user, count] of Object.entries(summary.userCounts)) {
|
|
1822
|
+
console.log(` ${user}: ${count}`);
|
|
1823
|
+
}
|
|
1824
|
+
} else {
|
|
1825
|
+
// List logs
|
|
1826
|
+
const logs = readAuditLogs({
|
|
1827
|
+
limit: config.limit,
|
|
1828
|
+
action: config.actionFilter,
|
|
1829
|
+
user: config.user
|
|
1830
|
+
});
|
|
1831
|
+
|
|
1832
|
+
if (logs.length === 0) {
|
|
1833
|
+
console.log('No audit logs found.');
|
|
1834
|
+
} else {
|
|
1835
|
+
console.log(`\nShowing ${logs.length} log(s):\n`);
|
|
1836
|
+
for (const log of logs) {
|
|
1837
|
+
console.log(`[${log.timestamp}] ${log.user} → ${log.action}`);
|
|
1838
|
+
if (Object.keys(log.details).length > 0) {
|
|
1839
|
+
console.log(` Details: ${JSON.stringify(log.details)}`);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
process.exit(0);
|
|
1845
|
+
} catch (err) {
|
|
1846
|
+
console.error(`\n❌ Audit error: ${err.message}\n`);
|
|
1847
|
+
process.exit(1);
|
|
1848
|
+
}
|
|
1849
|
+
} else if (parsed.subcommand === 'export') {
|
|
1850
|
+
// Phase 11: PDF export
|
|
1851
|
+
try {
|
|
1852
|
+
requirePermission('export:pdf', 'export reports');
|
|
1853
|
+
|
|
1854
|
+
if (!config.reportId) {
|
|
1855
|
+
// List available reports
|
|
1856
|
+
const reports = listAvailableReports();
|
|
1857
|
+
if (reports.length === 0) {
|
|
1858
|
+
console.log('No reports available for export.');
|
|
1859
|
+
} else {
|
|
1860
|
+
console.log(`\nAvailable reports:\n`);
|
|
1861
|
+
for (const report of reports.slice(0, 10)) {
|
|
1862
|
+
console.log(`- ${report.id}`);
|
|
1863
|
+
console.log(` Modified: ${report.modifiedAt}`);
|
|
1864
|
+
}
|
|
1865
|
+
console.log('\nUsage: guardian export <report-id> [--output <path>]');
|
|
1866
|
+
}
|
|
1867
|
+
} else {
|
|
1868
|
+
const result = exportReportToPDF(config.reportId, config.output);
|
|
1869
|
+
console.log(`✓ Report exported to: ${result.outputPath}`);
|
|
1870
|
+
console.log(` Size: ${result.size} bytes`);
|
|
1871
|
+
logAudit(AUDIT_ACTIONS.EXPORT_PDF, { reportId: config.reportId, output: result.outputPath });
|
|
1872
|
+
}
|
|
1873
|
+
process.exit(0);
|
|
1874
|
+
} catch (err) {
|
|
1875
|
+
console.error(`\n❌ Export error: ${err.message}\n`);
|
|
1876
|
+
process.exit(1);
|
|
1877
|
+
}
|
|
1878
|
+
} else if (parsed.subcommand === 'recipe') {
|
|
1879
|
+
// Phase 12.1: Recipes
|
|
1880
|
+
try {
|
|
1881
|
+
const action = config.action;
|
|
1882
|
+
// Ensure registry includes built-ins before any trust checks
|
|
1883
|
+
getAllRecipes();
|
|
1884
|
+
|
|
1885
|
+
if (action === 'list') {
|
|
1886
|
+
const recipes = getAllRecipes();
|
|
1887
|
+
console.log(`\n📚 Available Recipes (${recipes.length} total)\n`);
|
|
1888
|
+
|
|
1889
|
+
// Group by platform
|
|
1890
|
+
const byPlatform = {};
|
|
1891
|
+
for (const recipe of recipes) {
|
|
1892
|
+
if (!byPlatform[recipe.platform]) {
|
|
1893
|
+
byPlatform[recipe.platform] = [];
|
|
1894
|
+
}
|
|
1895
|
+
byPlatform[recipe.platform].push(recipe);
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
for (const platform of Object.keys(byPlatform).sort()) {
|
|
1899
|
+
console.log(`🏪 ${platform.toUpperCase()}`);
|
|
1900
|
+
for (const recipe of byPlatform[platform]) {
|
|
1901
|
+
const reg = getRegistryEntry(recipe.id);
|
|
1902
|
+
const checksum = computeRecipeChecksum(recipe);
|
|
1903
|
+
const mismatch = reg && reg.checksum && reg.checksum !== checksum;
|
|
1904
|
+
const sourceLabel = reg ? reg.source : 'unknown';
|
|
1905
|
+
const trustNote = mismatch ? ' [checksum mismatch]' : '';
|
|
1906
|
+
console.log(` • ${recipe.id} - ${recipe.name} [${sourceLabel}]${trustNote}`);
|
|
1907
|
+
}
|
|
1908
|
+
console.log();
|
|
1909
|
+
}
|
|
1910
|
+
} else if (action === 'show') {
|
|
1911
|
+
if (!config.id) {
|
|
1912
|
+
console.error('Usage: guardian recipe show <id>');
|
|
1913
|
+
process.exit(2);
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
const recipe = getRecipe(config.id);
|
|
1917
|
+
if (!recipe) {
|
|
1918
|
+
console.error(`Recipe not found: ${config.id}`);
|
|
1919
|
+
process.exit(1);
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
console.log();
|
|
1923
|
+
console.log(formatRecipe(recipe));
|
|
1924
|
+
const reg = getRegistryEntry(recipe.id);
|
|
1925
|
+
const checksum = computeRecipeChecksum(recipe);
|
|
1926
|
+
const mismatch = reg && reg.checksum && reg.checksum !== checksum;
|
|
1927
|
+
if (reg) {
|
|
1928
|
+
console.log(`Source: ${reg.source}`);
|
|
1929
|
+
console.log(`Version: ${reg.version}`);
|
|
1930
|
+
console.log(`Checksum: ${reg.checksum}`);
|
|
1931
|
+
if (mismatch) {
|
|
1932
|
+
console.log('⚠️ Warning: checksum mismatch — recipe may have been modified');
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
} else if (action === 'run') {
|
|
1936
|
+
if (!config.id || !config.url) {
|
|
1937
|
+
console.error('Usage: guardian recipe run <id> --url <url>');
|
|
1938
|
+
process.exit(2);
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
const recipe = getRecipe(config.id);
|
|
1942
|
+
if (!recipe) {
|
|
1943
|
+
console.error(`Recipe not found: ${config.id}`);
|
|
1944
|
+
process.exit(1);
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
// Enforce plan limits without counting recipe execution as a scan
|
|
1948
|
+
checkPlanBeforeScan({ url: config.url }, { recordUsage: false });
|
|
1949
|
+
|
|
1950
|
+
// Log recipe execution
|
|
1951
|
+
logAudit('recipe:run', { recipeId: recipe.id, url: config.url });
|
|
1952
|
+
|
|
1953
|
+
// Phase B: Execute recipe as enforced runtime
|
|
1954
|
+
(async () => {
|
|
1955
|
+
const { executeRecipeRuntime } = require('../src/recipes/recipe-runtime');
|
|
1956
|
+
const { recipeFailureToAttempt, assessRecipeImpact } = require('../src/recipes/recipe-failure-analysis');
|
|
1957
|
+
|
|
1958
|
+
console.log(`\n▶️ Executing recipe: ${recipe.name}`);
|
|
1959
|
+
console.log(` URL: ${config.url}`);
|
|
1960
|
+
console.log(` Steps: ${recipe.steps.length}\n`);
|
|
1961
|
+
|
|
1962
|
+
try {
|
|
1963
|
+
const result = await executeRecipeRuntime(config.id, config.url, { timeout: 20000 });
|
|
1964
|
+
|
|
1965
|
+
if (result.success) {
|
|
1966
|
+
console.log(`✅ RECIPE PASSED: ${recipe.name}`);
|
|
1967
|
+
console.log(` Goal reached: ${recipe.expectedGoal}`);
|
|
1968
|
+
console.log(` Duration: ${result.duration}s`);
|
|
1969
|
+
process.exit(0);
|
|
1970
|
+
} else {
|
|
1971
|
+
console.log(`❌ RECIPE FAILED: ${recipe.name}`);
|
|
1972
|
+
console.log(` Reason: ${result.failureReason}`);
|
|
1973
|
+
if (result.failedStep) {
|
|
1974
|
+
const stepNum = parseInt(result.failedStep.split('-').pop(), 10) + 1;
|
|
1975
|
+
console.log(` Failed at step ${stepNum} of ${result.steps.length}`);
|
|
1976
|
+
}
|
|
1977
|
+
console.log(` Duration: ${result.duration}s\n`);
|
|
1978
|
+
|
|
1979
|
+
// Integrate failure into decision engine
|
|
1980
|
+
const attemptForm = recipeFailureToAttempt(result);
|
|
1981
|
+
const impact = assessRecipeImpact(result);
|
|
1982
|
+
|
|
1983
|
+
console.log(` Risk Assessment: ${impact.severity} (score: ${impact.riskScore}/100)`);
|
|
1984
|
+
console.log(` Impact: ${impact.message}\n`);
|
|
1985
|
+
|
|
1986
|
+
process.exit(1);
|
|
1987
|
+
}
|
|
1988
|
+
} catch (err) {
|
|
1989
|
+
console.error(`\n❌ Recipe execution error: ${err.message}\n`);
|
|
1990
|
+
process.exit(2);
|
|
1991
|
+
}
|
|
1992
|
+
})();
|
|
1993
|
+
} else if (action === 'export') {
|
|
1994
|
+
if (!config.id || !config.out) {
|
|
1995
|
+
console.error('Usage: guardian recipe export <id> --out <file>');
|
|
1996
|
+
process.exit(2);
|
|
1997
|
+
}
|
|
1998
|
+
requirePermission('recipe:manage', 'export recipes');
|
|
1999
|
+
const result = exportRecipeWithMetadata(config.id, config.out);
|
|
2000
|
+
logAudit(AUDIT_ACTIONS.RECIPE_EXPORT, { recipeId: config.id, output: config.out, checksum: result.checksum });
|
|
2001
|
+
console.log(`\n✓ Recipe exported`);
|
|
2002
|
+
console.log(` File: ${config.out}`);
|
|
2003
|
+
console.log(` Checksum: ${result.checksum}`);
|
|
2004
|
+
} else if (action === 'import') {
|
|
2005
|
+
if (!config.file) {
|
|
2006
|
+
console.error('Usage: guardian recipe import <file>');
|
|
2007
|
+
process.exit(2);
|
|
2008
|
+
}
|
|
2009
|
+
requirePermission('recipe:manage', 'import recipes');
|
|
2010
|
+
const result = importRecipeWithMetadata(config.file, { force: config.force });
|
|
2011
|
+
logAudit(AUDIT_ACTIONS.RECIPE_IMPORT, { recipeId: result.recipe.id, file: config.file, checksum: result.checksum, force: config.force });
|
|
2012
|
+
console.log(`\n✓ Import complete`);
|
|
2013
|
+
console.log(` Recipe: ${result.recipe.id}`);
|
|
2014
|
+
console.log(` Checksum: ${result.checksum}`);
|
|
2015
|
+
} else {
|
|
2016
|
+
console.error('Unknown recipe action. Use: list, show <id>, run <id> --url <url>, export <id> --out <file>, import <file> [--force]');
|
|
2017
|
+
process.exit(2);
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
process.exit(0);
|
|
2021
|
+
} catch (err) {
|
|
2022
|
+
console.error(`\n❌ Recipe error: ${err.message}\n`);
|
|
2023
|
+
process.exit(1);
|
|
2024
|
+
}
|
|
2025
|
+
} else if (parsed.subcommand === 'list') {
|
|
2026
|
+
const exitCode = listRuns(config.artifactsDir, config.filters);
|
|
2027
|
+
process.exit(exitCode);
|
|
2028
|
+
} else if (parsed.subcommand === 'cleanup') {
|
|
2029
|
+
const result = await cleanup(
|
|
2030
|
+
config.artifactsDir,
|
|
2031
|
+
{
|
|
2032
|
+
olderThan: config.olderThan,
|
|
2033
|
+
keepLatest: config.keepLatest,
|
|
2034
|
+
failedOnly: config.failedOnly
|
|
2035
|
+
}
|
|
2036
|
+
);
|
|
2037
|
+
|
|
2038
|
+
console.log(`\n✓ Cleanup completed`);
|
|
2039
|
+
console.log(` Deleted: ${result.deleted} run(s)`);
|
|
2040
|
+
console.log(` Kept: ${result.kept} run(s)`);
|
|
2041
|
+
|
|
2042
|
+
if (result.errors && result.errors.length > 0) {
|
|
2043
|
+
console.log(` Errors: ${result.errors.length}`);
|
|
2044
|
+
result.errors.forEach(err => {
|
|
2045
|
+
console.log(` - ${err}`);
|
|
2046
|
+
});
|
|
2047
|
+
process.exit(1);
|
|
2048
|
+
}
|
|
2049
|
+
process.exit(0);
|
|
648
2050
|
} else if (parsed.subcommand === 'protect') {
|
|
2051
|
+
// Phase 8: Check plan limits before scan
|
|
2052
|
+
checkPlanBeforeScan(config);
|
|
649
2053
|
await runRealityCLI(config);
|
|
2054
|
+
} else if (parsed.subcommand === 'smoke') {
|
|
2055
|
+
// Phase 8: Check plan limits before scan
|
|
2056
|
+
checkPlanBeforeScan(config);
|
|
2057
|
+
await runSmokeCLI(config);
|
|
650
2058
|
} else if (parsed.subcommand === 'attempt') {
|
|
2059
|
+
// Phase 8: Check plan limits before scan
|
|
2060
|
+
checkPlanBeforeScan(config);
|
|
651
2061
|
await runAttemptCLI(config);
|
|
2062
|
+
} else if (parsed.subcommand === 'journey-scan') {
|
|
2063
|
+
// Phase 8: Check plan limits before scan
|
|
2064
|
+
checkPlanBeforeScan(config);
|
|
2065
|
+
await runJourneyScanCLI(config);
|
|
2066
|
+
} else if (parsed.subcommand === 'live') {
|
|
2067
|
+
// Phase 8: Check feature allowed for live guardian
|
|
2068
|
+
const liveCheck = checkFeatureAllowed('liveGuardian');
|
|
2069
|
+
if (!liveCheck.allowed) {
|
|
2070
|
+
console.error(`\n❌ ${liveCheck.message}`);
|
|
2071
|
+
console.log(getUpgradeMessage());
|
|
2072
|
+
process.exit(1);
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
// Phase 10: Track first live session
|
|
2076
|
+
recordFirstLive();
|
|
2077
|
+
|
|
2078
|
+
await runLiveCLI(config);
|
|
2079
|
+
} else if (parsed.subcommand === 'live-start') {
|
|
2080
|
+
// Feature check
|
|
2081
|
+
const liveCheck = checkFeatureAllowed('liveGuardian');
|
|
2082
|
+
if (!liveCheck.allowed) {
|
|
2083
|
+
console.error(`\n❌ ${liveCheck.message}`);
|
|
2084
|
+
console.log(getUpgradeMessage());
|
|
2085
|
+
process.exit(1);
|
|
2086
|
+
}
|
|
2087
|
+
// RBAC permission
|
|
2088
|
+
try { requirePermission('live:run', 'start live schedule'); } catch (e) { console.error(`\n❌ ${e.message}`); process.exit(1); }
|
|
2089
|
+
const { createSchedule, startBackgroundRunner } = require('../src/guardian/live-scheduler');
|
|
2090
|
+
const entry = createSchedule({ url: config.baseUrl, preset: config.preset, intervalMinutes: config.intervalMinutes });
|
|
2091
|
+
const runner = startBackgroundRunner();
|
|
2092
|
+
console.log('\n🟢 Live schedule started');
|
|
2093
|
+
console.log(` id: ${entry.id}`);
|
|
2094
|
+
console.log(` url: ${entry.url}`);
|
|
2095
|
+
console.log(` preset: ${entry.preset}`);
|
|
2096
|
+
console.log(` every: ${entry.intervalMinutes} min`);
|
|
2097
|
+
console.log(` nextRunAt: ${entry.nextRunAt}`);
|
|
2098
|
+
console.log(` runnerPid: ${runner.pid}`);
|
|
2099
|
+
process.exit(0);
|
|
2100
|
+
} else if (parsed.subcommand === 'live-stop') {
|
|
2101
|
+
// RBAC permission
|
|
2102
|
+
try { requirePermission('live:run', 'stop live schedule'); } catch (e) { console.error(`\n❌ ${e.message}`); process.exit(1); }
|
|
2103
|
+
const { stopSchedule } = require('../src/guardian/live-scheduler');
|
|
2104
|
+
try {
|
|
2105
|
+
const s = stopSchedule(config.id);
|
|
2106
|
+
console.log(`\n🛑 Schedule stopped: ${s.id}`);
|
|
2107
|
+
process.exit(0);
|
|
2108
|
+
} catch (err) {
|
|
2109
|
+
console.error(`\n❌ ${err.message}`);
|
|
2110
|
+
process.exit(1);
|
|
2111
|
+
}
|
|
2112
|
+
} else if (parsed.subcommand === 'live-status') {
|
|
2113
|
+
const { listSchedules, loadState } = require('../src/guardian/live-scheduler');
|
|
2114
|
+
const state = loadState();
|
|
2115
|
+
const schedules = listSchedules();
|
|
2116
|
+
console.log('\n📋 Live schedules:');
|
|
2117
|
+
for (const s of schedules) {
|
|
2118
|
+
console.log(` - ${s.id} | ${s.status} | every ${s.intervalMinutes} min`);
|
|
2119
|
+
console.log(` url: ${s.url} | preset: ${s.preset}`);
|
|
2120
|
+
console.log(` lastRunAt: ${s.lastRunAt || 'n/a'} | nextRunAt: ${s.nextRunAt || 'n/a'}`);
|
|
2121
|
+
}
|
|
2122
|
+
const pid = state.runner?.pid;
|
|
2123
|
+
console.log(`\nRunner: ${pid ? `pid ${pid}` : 'not running'}`);
|
|
2124
|
+
process.exit(0);
|
|
2125
|
+
} else if (parsed.subcommand === 'ci') {
|
|
2126
|
+
// Phase 8: Check feature allowed for CI mode
|
|
2127
|
+
const ciCheck = checkFeatureAllowed('ciMode');
|
|
2128
|
+
if (!ciCheck.allowed) {
|
|
2129
|
+
console.error(`\n❌ ${ciCheck.message}`);
|
|
2130
|
+
console.log(getUpgradeMessage());
|
|
2131
|
+
process.exit(1);
|
|
2132
|
+
}
|
|
2133
|
+
// Phase 4: CI gate mode
|
|
2134
|
+
const { runCIGate } = require('../src/guardian/ci-cli');
|
|
2135
|
+
const exitCode = await runCIGate(config);
|
|
2136
|
+
process.exit(exitCode);
|
|
652
2137
|
} else if (parsed.subcommand === 'reality') {
|
|
2138
|
+
// Phase 8: Check plan limits before scan
|
|
2139
|
+
checkPlanBeforeScan(config);
|
|
653
2140
|
await runRealityCLI(config);
|
|
654
2141
|
} else if (parsed.subcommand === 'scan') {
|
|
2142
|
+
// Phase 8: Check plan limits before scan
|
|
2143
|
+
checkPlanBeforeScan(config);
|
|
655
2144
|
// Phase 6: First-run concise guidance
|
|
656
2145
|
try {
|
|
657
2146
|
const { baselineExists } = require('../src/guardian/baseline-storage');
|