opstruth 0.1.1

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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -0
  3. package/bin/opstruth.js +7 -0
  4. package/examples/routes.json +12 -0
  5. package/fixtures/next-app/app/page.tsx +3 -0
  6. package/fixtures/next-app/next.config.ts +5 -0
  7. package/fixtures/next-app/package.json +19 -0
  8. package/fixtures/next-app/tsconfig.json +6 -0
  9. package/fixtures/non-git-folder/README.md +3 -0
  10. package/fixtures/non-git-folder/notes.txt +1 -0
  11. package/fixtures/plain-node-app/package.json +8 -0
  12. package/fixtures/plain-node-app/src/index.js +3 -0
  13. package/fixtures/risky-secret-app/package.json +8 -0
  14. package/fixtures/risky-secret-app/src/config.js +3 -0
  15. package/fixtures/supabase-cloudflare-app/package.json +16 -0
  16. package/fixtures/supabase-cloudflare-app/src/supabaseClient.ts +7 -0
  17. package/fixtures/supabase-cloudflare-app/src/worker.ts +5 -0
  18. package/fixtures/supabase-cloudflare-app/supabase/migrations/001_init.sql +11 -0
  19. package/fixtures/supabase-cloudflare-app/wrangler.toml +6 -0
  20. package/fixtures/vite-react-app/package.json +20 -0
  21. package/fixtures/vite-react-app/src/App.tsx +3 -0
  22. package/fixtures/vite-react-app/tsconfig.json +6 -0
  23. package/fixtures/vite-react-app/vite.config.ts +6 -0
  24. package/package.json +53 -0
  25. package/scripts/demo-fixtures.sh +35 -0
  26. package/scripts/demo-run.sh +32 -0
  27. package/src/cli.js +254 -0
  28. package/src/commands/cloudflare.js +51 -0
  29. package/src/commands/evidence.js +38 -0
  30. package/src/commands/local.js +43 -0
  31. package/src/commands/probes.js +68 -0
  32. package/src/commands/quality.js +66 -0
  33. package/src/commands/repo.js +30 -0
  34. package/src/commands/routes.js +49 -0
  35. package/src/commands/secrets.js +33 -0
  36. package/src/commands/supabase.js +39 -0
  37. package/src/lib/boundary.js +74 -0
  38. package/src/lib/config.js +31 -0
  39. package/src/lib/detect.js +111 -0
  40. package/src/lib/exec.js +28 -0
  41. package/src/lib/fs.js +36 -0
  42. package/src/lib/git.js +27 -0
  43. package/src/lib/http.js +14 -0
  44. package/src/lib/markdown.js +202 -0
  45. package/src/lib/probes.js +489 -0
  46. package/src/lib/redact.js +27 -0
  47. package/src/lib/result.js +63 -0
  48. package/src/lib/scan.js +53 -0
  49. package/src/orchestrator.js +106 -0
@@ -0,0 +1,106 @@
1
+ import path from 'node:path';
2
+ import { runRepo } from './commands/repo.js';
3
+ import { runSecrets } from './commands/secrets.js';
4
+ import { runQuality } from './commands/quality.js';
5
+ import { runSupabase } from './commands/supabase.js';
6
+ import { runCloudflare } from './commands/cloudflare.js';
7
+ import { runRoutes } from './commands/routes.js';
8
+ import { runLocal } from './commands/local.js';
9
+ import { runEvidence } from './commands/evidence.js';
10
+ import { createResult, finalizeStatus, worstStatus } from './lib/result.js';
11
+ import { detectStack, hasSupabase, hasCloudflare } from './lib/detect.js';
12
+ import { findDefaultRoutesConfig } from './lib/config.js';
13
+ import { pathExists } from './lib/fs.js';
14
+ import { resolveProjectBoundary } from './lib/boundary.js';
15
+ import { selectProbes } from './lib/probes.js';
16
+
17
+ function skippedResult(command, reason, notVerified) {
18
+ return createResult(command, 'skipped', { skipped: [reason], notVerified: [notVerified || command + ' was not verified'] });
19
+ }
20
+
21
+ function nextStepFor(aggregate) {
22
+ if (aggregate.failures.length) return 'Fix the failing check first, then rerun opstruth.';
23
+ if (aggregate.warnings.length) return 'Review warnings, then rerun opstruth or the specific command after addressing them.';
24
+ if (aggregate.skipped.length) return 'Add read-only inputs such as --base-url or --port when route or runtime proof matters.';
25
+ return 'Attach the evidence pack to the change or handoff.';
26
+ }
27
+
28
+ export async function runOrchestrator(options = {}) {
29
+ const startCwd = options.cwd || process.cwd();
30
+ const boundary = await resolveProjectBoundary(startCwd);
31
+ const cwd = boundary.root;
32
+ const stack = await detectStack(cwd);
33
+ const probeSelection = await selectProbes({ root: cwd, stack, boundary, options });
34
+ options = { ...options, cwd };
35
+ const skip = new Set(options.skip || []);
36
+ const childResults = [];
37
+ async function maybe(name, fn) {
38
+ if (skip.has(name)) { childResults.push(skippedResult(name, 'Skipped by user request: --skip ' + name, name + ' was intentionally not checked')); return; }
39
+ childResults.push(await fn());
40
+ }
41
+ await maybe('repo', () => runRepo(options));
42
+ await maybe('secrets', () => runSecrets(options));
43
+ await maybe('quality', () => runQuality(options));
44
+ if (await hasSupabase(cwd)) await maybe('supabase', () => runSupabase(options));
45
+ else childResults.push(skippedResult('supabase', 'Supabase checks skipped because no supabase directory was detected.', 'Supabase database exposure was not checked'));
46
+ if (await hasCloudflare(cwd)) await maybe('cloudflare', () => runCloudflare(options));
47
+ else childResults.push(skippedResult('cloudflare', 'Cloudflare checks skipped because no Wrangler config was detected.', 'Cloudflare deployment configuration was not checked'));
48
+ const routeConfig = await findDefaultRoutesConfig(cwd);
49
+ if (options.baseUrl || options.routesFile || routeConfig) await maybe('routes', () => runRoutes(options));
50
+ else childResults.push(skippedResult('routes', 'Route checks skipped because no base URL or routes config was provided.', 'Production/public route availability was not checked'));
51
+ const hasLocalConfig = await pathExists(path.join(cwd, 'opstruth.local.json'));
52
+ if (options.port?.length || options.healthProvided || options.process || options.service || hasLocalConfig) await maybe('local', () => runLocal(options));
53
+ else childResults.push(skippedResult('local', 'Local runtime checks skipped because no port, health path, process, or service was provided.', 'Local runtime liveness was not checked'));
54
+ const aggregate = createResult('opstruth', worstStatus(childResults.map((item) => item.status)), {
55
+ summary: 'One-command read-only proof run completed.',
56
+ verified: [
57
+ 'Project boundary: ' + cwd,
58
+ 'Probe catalogue entries: ' + probeSelection.catalogueSize,
59
+ 'Automatic safe probes selected: ' + probeSelection.selected.length,
60
+ ...childResults.flatMap((item) => item.verified || []),
61
+ 'Safety boundary observed: no deploys, database mutations, OpenAI calls, publishing, queue triggers, restarts, or kills were run by opstruth'
62
+ ],
63
+ warnings: childResults.flatMap((item) => item.warnings || []),
64
+ failures: childResults.flatMap((item) => item.failures || []),
65
+ findings: childResults.flatMap((item) => item.findings || []),
66
+ skipped: [
67
+ ...(boundary.message ? [boundary.message] : []),
68
+ ...probeSelection.skipped.slice(0, 20).map((probe) => `${probe.id}: ${probe.reason}`),
69
+ ...childResults.flatMap((item) => item.skipped || [])
70
+ ],
71
+ notVerified: ['No production deploy was executed', 'No database mutation was executed', 'No OpenAI usage was monitored', 'No publishing or job side effects were verified'].concat(childResults.flatMap((item) => item.notVerified || [])),
72
+ checks: childResults.flatMap((item) => item.checks || []),
73
+ data: {
74
+ boundary,
75
+ stack,
76
+ probes: {
77
+ catalogueSize: probeSelection.catalogueSize,
78
+ selected: probeSelection.selected.map((probe) => ({
79
+ id: probe.id,
80
+ name: probe.name,
81
+ area: probe.area,
82
+ stack: probe.stack,
83
+ safetyLevel: probe.safetyLevel,
84
+ defaultMode: probe.defaultMode,
85
+ proves: probe.proves,
86
+ doesNotProve: probe.doesNotProve,
87
+ evidenceCollected: probe.evidenceCollected
88
+ })),
89
+ skipped: probeSelection.skipped.map((probe) => ({ id: probe.id, area: probe.area, stack: probe.stack, reason: probe.reason }))
90
+ },
91
+ childResults
92
+ },
93
+ nextSafeStep: 'Review the summary and fill only the proof gaps that matter for this change.'
94
+ });
95
+ finalizeStatus(aggregate, { strict: options.strict });
96
+ if (aggregate.status === 'pass' && (aggregate.skipped.length || aggregate.notVerified.length)) aggregate.status = options.strict ? 'fail' : 'warn';
97
+ aggregate.nextSafeStep = nextStepFor(aggregate);
98
+ if (!skip.has('evidence')) {
99
+ const evidence = await runEvidence({ ...options, out: options.out || 'evidence/opstruth-report.md', aggregate });
100
+ aggregate.data.evidence = evidence.data;
101
+ aggregate.verified.push('Evidence file written: ' + evidence.data.out);
102
+ }
103
+ finalizeStatus(aggregate, { strict: options.strict });
104
+ if (aggregate.status === 'pass' && (aggregate.skipped.length || aggregate.notVerified.length)) aggregate.status = options.strict ? 'fail' : 'warn';
105
+ return aggregate;
106
+ }