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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 opstruth contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # opstruth
2
+
3
+ Read-only operational truth checks for AI-assisted engineering workflows.
4
+
5
+ `opstruth` inspects a local project, detects its stack, runs safe probes, collects evidence, explains proof gaps, and avoids pretending unverified systems are safe.
6
+
7
+ ## Install
8
+
9
+ After npm publication:
10
+
11
+ ```bash
12
+ npm install -g opstruth
13
+ opstruth
14
+ ```
15
+
16
+ One-off usage:
17
+
18
+ ```bash
19
+ npx opstruth
20
+ ```
21
+
22
+ ## Commands
23
+
24
+ ```bash
25
+ opstruth
26
+ opstruth welcome
27
+ opstruth probes
28
+ opstruth secrets
29
+ opstruth routes --base-url https://example.com
30
+ opstruth local --port 3000 --health /health
31
+ ```
32
+
33
+ ## Safety Model
34
+
35
+ opstruth is read-only by default. CLI checks do not deploy, mutate databases, trigger queues or jobs, call OpenAI, restart services, publish content, or print raw secrets.
36
+
37
+ Skipped checks and not-verified areas are reported as proof gaps instead of being treated as safe.
38
+
39
+ ## Repository
40
+
41
+ Source, docs, and release evidence live at:
42
+
43
+ ```text
44
+ https://github.com/AyobamiH/opstruth
45
+ ```
46
+
47
+ The public website is:
48
+
49
+ ```text
50
+ https://opstruth.woeinvests.workers.dev
51
+ ```
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from '../src/cli.js';
3
+
4
+ await runCli(process.argv.slice(2), process.cwd()).catch((error) => {
5
+ console.error(error?.stack || error?.message || String(error));
6
+ process.exitCode = 1;
7
+ });
@@ -0,0 +1,12 @@
1
+ {
2
+ "baseUrl": "https://example.com",
3
+ "routes": [
4
+ { "path": "/", "method": "HEAD", "expectStatus": [200, 301, 302] },
5
+ { "path": "/login", "method": "HEAD", "expectStatus": [200, 301, 302] },
6
+ { "path": "/healthz", "method": "GET", "expectStatus": [200] }
7
+ ],
8
+ "requiredHeaders": [
9
+ "content-security-policy",
10
+ "strict-transport-security"
11
+ ]
12
+ }
@@ -0,0 +1,3 @@
1
+ export default function Page() {
2
+ return <main>opstruth Next.js fixture</main>;
3
+ }
@@ -0,0 +1,5 @@
1
+ const config = {
2
+ reactStrictMode: true
3
+ };
4
+
5
+ export default config;
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "opstruth-fixture-next-app",
3
+ "type": "module",
4
+ "private": true,
5
+ "dependencies": {
6
+ "next": "^15.0.0",
7
+ "react": "^19.0.0",
8
+ "react-dom": "^19.0.0"
9
+ },
10
+ "devDependencies": {
11
+ "typescript": "^5.0.0"
12
+ },
13
+ "scripts": {
14
+ "typecheck": "node --version",
15
+ "lint": "node --version",
16
+ "test": "node --version",
17
+ "build": "node --version"
18
+ }
19
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "compilerOptions": {
3
+ "jsx": "preserve",
4
+ "strict": true
5
+ }
6
+ }
@@ -0,0 +1,3 @@
1
+ # Non-Git Fixture
2
+
3
+ This folder intentionally has no package.json and is copied to a non-git temp directory during fixture runs.
@@ -0,0 +1 @@
1
+ plain text only
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "opstruth-fixture-plain-node-app",
3
+ "type": "module",
4
+ "private": true,
5
+ "scripts": {
6
+ "test": "node --version"
7
+ }
8
+ }
@@ -0,0 +1,3 @@
1
+ export function main() {
2
+ return 'hello from plain node';
3
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "opstruth-fixture-risky-secret-app",
3
+ "type": "module",
4
+ "private": true,
5
+ "scripts": {
6
+ "test": "node --version"
7
+ }
8
+ }
@@ -0,0 +1,3 @@
1
+ export const OPENAI_API_KEY = "fake-openai-key-for-redaction";
2
+ export const SUPABASE_SERVICE_ROLE_KEY = "fake-service-role-for-redaction";
3
+ export const authorization = "Bearer fake-bearer-token-for-redaction";
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "opstruth-fixture-supabase-cloudflare-app",
3
+ "type": "module",
4
+ "private": true,
5
+ "dependencies": {
6
+ "@supabase/supabase-js": "^2.0.0"
7
+ },
8
+ "devDependencies": {
9
+ "typescript": "^5.0.0",
10
+ "wrangler": "^4.0.0"
11
+ },
12
+ "scripts": {
13
+ "typecheck": "node --version",
14
+ "deploy": "wrangler deploy"
15
+ }
16
+ }
@@ -0,0 +1,7 @@
1
+ import { createClient } from '@supabase/supabase-js';
2
+
3
+ const url = 'https://example.supabase.co';
4
+ const anonKey = 'public-anon-placeholder';
5
+
6
+ export const supabase = createClient(url, anonKey);
7
+ export const jobsQuery = supabase.from('agent_jobs').select('*');
@@ -0,0 +1,5 @@
1
+ export default {
2
+ fetch() {
3
+ return new Response('ok');
4
+ }
5
+ };
@@ -0,0 +1,11 @@
1
+ create table public.agent_jobs (
2
+ id uuid primary key,
3
+ status text not null
4
+ );
5
+
6
+ alter table public.agent_jobs enable row level security;
7
+
8
+ create policy "read own jobs"
9
+ on public.agent_jobs
10
+ for select
11
+ using (true);
@@ -0,0 +1,6 @@
1
+ name = "opstruth-fixture-worker"
2
+ main = "src/worker.ts"
3
+ compatibility_date = "2026-01-01"
4
+ routes = [
5
+ { pattern = "fixture.example.com/*", custom_domain = true }
6
+ ]
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "opstruth-fixture-vite-react-app",
3
+ "type": "module",
4
+ "private": true,
5
+ "dependencies": {
6
+ "@vitejs/plugin-react": "^5.0.0",
7
+ "react": "^19.0.0",
8
+ "react-dom": "^19.0.0",
9
+ "vite": "^6.0.0"
10
+ },
11
+ "devDependencies": {
12
+ "typescript": "^5.0.0"
13
+ },
14
+ "scripts": {
15
+ "typecheck": "node --version",
16
+ "lint": "node --version",
17
+ "test": "node --version",
18
+ "build": "node --version"
19
+ }
20
+ }
@@ -0,0 +1,3 @@
1
+ export function App() {
2
+ return <main>opstruth Vite fixture</main>;
3
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "compilerOptions": {
3
+ "jsx": "react-jsx",
4
+ "strict": true
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ export default defineConfig({
5
+ plugins: [react()]
6
+ });
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "opstruth",
3
+ "version": "0.1.1",
4
+ "description": "Read-only operational truth checks for AI-assisted engineering workflows.",
5
+ "type": "module",
6
+ "bin": {
7
+ "opstruth": "./bin/opstruth.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "examples/",
13
+ "fixtures/",
14
+ "scripts/",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "scripts": {
19
+ "typecheck": "node --check bin/opstruth.js",
20
+ "lint": "node --check bin/opstruth.js && find src -name '*.js' -print -exec node --check {} \\;",
21
+ "test": "node --test",
22
+ "build": "node --check bin/opstruth.js",
23
+ "ci": "npm run lint && npm test && npm run build",
24
+ "demo:fixtures": "./scripts/demo-fixtures.sh"
25
+ },
26
+ "keywords": [
27
+ "ai-assisted-development",
28
+ "codex",
29
+ "devtools",
30
+ "cli",
31
+ "operational-truth",
32
+ "cloudflare",
33
+ "supabase",
34
+ "typescript",
35
+ "evidence"
36
+ ],
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/AyobamiH/opstruth.git",
40
+ "directory": "cli"
41
+ },
42
+ "homepage": "https://opstruth.woeinvests.workers.dev",
43
+ "bugs": {
44
+ "url": "https://github.com/AyobamiH/opstruth/issues"
45
+ },
46
+ "license": "MIT",
47
+ "engines": {
48
+ "node": ">=20"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ }
53
+ }
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env sh
2
+ set -eu
3
+
4
+ ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
5
+ REPO_ROOT="$(CDPATH= cd -- "$ROOT/.." && pwd)"
6
+ BIN="$ROOT/bin/opstruth.js"
7
+ OUT_DIR="$REPO_ROOT/evidence/fixture-runs"
8
+ TMP_ROOT="${TMPDIR:-/tmp}/opstruth-fixture-runs-$$"
9
+
10
+ mkdir -p "$OUT_DIR" "$TMP_ROOT"
11
+
12
+ run_fixture() {
13
+ name="$1"
14
+ git_mode="$2"
15
+ src="$ROOT/fixtures/$name"
16
+ work="$TMP_ROOT/$name"
17
+ mkdir -p "$work"
18
+ cp -R "$src/." "$work/"
19
+ if [ "$git_mode" = "git" ]; then
20
+ git init "$work" >/dev/null 2>&1
21
+ fi
22
+ echo "==> $name"
23
+ (cd "$work" && node "$BIN" --out "$OUT_DIR/$name.md") >/dev/null
24
+ echo " wrote $OUT_DIR/$name.md"
25
+ }
26
+
27
+ run_fixture "vite-react-app" "git"
28
+ run_fixture "next-app" "git"
29
+ run_fixture "supabase-cloudflare-app" "git"
30
+ run_fixture "plain-node-app" "git"
31
+ run_fixture "non-git-folder" "nogit"
32
+ run_fixture "risky-secret-app" "git"
33
+
34
+ echo
35
+ echo "Fixture evidence written to $OUT_DIR"
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env sh
2
+ set -eu
3
+
4
+ ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
5
+ REPO_ROOT="$(CDPATH= cd -- "$ROOT/.." && pwd)"
6
+ BIN="$ROOT/bin/opstruth.js"
7
+ OUT_DIR="$REPO_ROOT/evidence/fixture-runs"
8
+ TMP_ROOT="${TMPDIR:-/tmp}/opstruth-demo-run-$$"
9
+
10
+ mkdir -p "$OUT_DIR" "$TMP_ROOT"
11
+
12
+ echo "==> Welcome"
13
+ node "$BIN" welcome
14
+
15
+ echo
16
+ echo "==> Vite React fixture"
17
+ mkdir -p "$TMP_ROOT/vite-react-app"
18
+ cp -R "$ROOT/fixtures/vite-react-app/." "$TMP_ROOT/vite-react-app/"
19
+ git init "$TMP_ROOT/vite-react-app" >/dev/null 2>&1
20
+ (cd "$TMP_ROOT/vite-react-app" && node "$BIN" --out "$OUT_DIR/demo-vite-react-app.md")
21
+
22
+ echo
23
+ echo "==> Risky secret fixture"
24
+ mkdir -p "$TMP_ROOT/risky-secret-app"
25
+ cp -R "$ROOT/fixtures/risky-secret-app/." "$TMP_ROOT/risky-secret-app/"
26
+ git init "$TMP_ROOT/risky-secret-app" >/dev/null 2>&1
27
+ (cd "$TMP_ROOT/risky-secret-app" && node "$BIN" secrets --out "$OUT_DIR/demo-risky-secret-app.md")
28
+
29
+ echo
30
+ echo "Evidence files:"
31
+ echo "- $OUT_DIR/demo-vite-react-app.md"
32
+ echo "- $OUT_DIR/demo-risky-secret-app.md"
package/src/cli.js ADDED
@@ -0,0 +1,254 @@
1
+ import path from 'node:path';
2
+ import { readFile } from 'node:fs/promises';
3
+ import readline from 'node:readline/promises';
4
+ import { runOrchestrator } from './orchestrator.js';
5
+ import { runRepo } from './commands/repo.js';
6
+ import { runQuality } from './commands/quality.js';
7
+ import { runRoutes } from './commands/routes.js';
8
+ import { runSecrets } from './commands/secrets.js';
9
+ import { runSupabase } from './commands/supabase.js';
10
+ import { runCloudflare } from './commands/cloudflare.js';
11
+ import { runLocal } from './commands/local.js';
12
+ import { runEvidence } from './commands/evidence.js';
13
+ import { runProbes } from './commands/probes.js';
14
+ import { resultToMarkdown } from './lib/markdown.js';
15
+ import { writeFileSafe } from './lib/fs.js';
16
+ import { redactObject } from './lib/redact.js';
17
+ import { exitCodeFor } from './lib/result.js';
18
+
19
+ const COMMANDS = new Set(['repo', 'quality', 'routes', 'secrets', 'supabase', 'cloudflare', 'local', 'evidence', 'probes', 'welcome', 'init']);
20
+ function take(args, index) { return args[index + 1]; }
21
+ export function parseArgs(argv) {
22
+ const options = { skip: [], only: [], port: [], protectedTable: [], include: [] };
23
+ let command = null;
24
+ for (let i = 0; i < argv.length; i += 1) {
25
+ const arg = argv[i];
26
+ if (!arg.startsWith('-') && !command && COMMANDS.has(arg)) { command = arg; continue; }
27
+ if (arg === '--help' || arg === '-h') options.help = true;
28
+ if (arg === '--json') options.json = true;
29
+ else if (arg === '--out') options.out = take(argv, i++);
30
+ else if (arg === '--base-url') options.baseUrl = take(argv, i++);
31
+ else if (arg === '--routes') options.routesFile = take(argv, i++);
32
+ else if (arg === '--port') options.port.push(take(argv, i++));
33
+ else if (arg === '--health') { options.health = take(argv, i++); options.healthProvided = true; }
34
+ else if (arg === '--skip') options.skip.push(take(argv, i++));
35
+ else if (arg === '--only') options.only.push(take(argv, i++));
36
+ else if (arg === '--yes' || arg === '-y') options.yes = true;
37
+ else if (arg === '--strict') options.strict = true;
38
+ else if (arg === '--continue') options.continueOnFailure = true;
39
+ else if (arg === '--protected-table') options.protectedTable.push(take(argv, i++));
40
+ else if (arg === '--frontend-dir') options.frontendDir = take(argv, i++);
41
+ else if (arg === '--migrations-dir') options.migrationsDir = take(argv, i++);
42
+ else if (arg === '--url') options.url = take(argv, i++);
43
+ else if (arg === '--title') options.title = take(argv, i++);
44
+ else if (arg === '--phase') options.phase = take(argv, i++);
45
+ else if (arg === '--include') options.include.push(take(argv, i++));
46
+ else if (arg === '--process') options.process = take(argv, i++);
47
+ else if (arg === '--service') options.service = take(argv, i++);
48
+ else if (arg === '--script') { options.scripts ||= []; options.scripts.push(take(argv, i++)); }
49
+ }
50
+ if (!options.protectedTable.length) delete options.protectedTable;
51
+ return { command, options };
52
+ }
53
+ async function dispatch(command, options) {
54
+ if (command === 'welcome') return welcomeText();
55
+ if (command === 'init') return runInit(options);
56
+ if (!command) return runOrchestrator(options);
57
+ if (command === 'repo') return runRepo(options);
58
+ if (command === 'quality') return runQuality(options);
59
+ if (command === 'routes') return runRoutes(options);
60
+ if (command === 'secrets') return runSecrets(options);
61
+ if (command === 'supabase') return runSupabase(options);
62
+ if (command === 'cloudflare') return runCloudflare(options);
63
+ if (command === 'local') return runLocal(options);
64
+ if (command === 'evidence') return runEvidence(options);
65
+ if (command === 'probes') return runProbes(options);
66
+ throw new Error('Unknown command: ' + command);
67
+ }
68
+
69
+ function writeStdout(text) {
70
+ return new Promise((resolve) => {
71
+ process.stdout.write(text, resolve);
72
+ });
73
+ }
74
+
75
+ const ASCII_HEADER = ` ____ _______ __ __
76
+ / __ \\____ / ____(_)___ ____/ /_/ /
77
+ / / / / __ \\/ /_ / / __ \\/ __ / __/
78
+ / /_/ / /_/ / __/ / / / / / /_/ / /_
79
+ \\____/ .___/_/ /_/_/ /_/\\__,_/\\__/
80
+ /_/
81
+
82
+ Operational truth checks for AI-assisted engineering.
83
+ `;
84
+
85
+ function helpText(command) {
86
+ const common = [
87
+ ASCII_HEADER,
88
+ 'Usage:',
89
+ ' opstruth [--strict] [--json] [--out file]',
90
+ ' opstruth <command> [options]',
91
+ '',
92
+ 'Commands:',
93
+ ' welcome Explain what opstruth is and how to use it',
94
+ ' init Create opstruth.config.json after confirmation',
95
+ ' repo Inspect git/repo/stack facts',
96
+ ' quality Run safe package quality scripts that exist',
97
+ ' routes Probe configured HTTP routes with HEAD/GET',
98
+ ' secrets Scan source for risky references with redaction',
99
+ ' supabase Static Supabase safety checks',
100
+ ' cloudflare Static Cloudflare/Wrangler checks',
101
+ ' local Check explicit local ports/processes/services',
102
+ ' evidence Write a markdown evidence pack',
103
+ ' probes Inspect the stack-aware probe catalogue',
104
+ '',
105
+ 'Global options:',
106
+ ' --strict Treat warnings/skips as failing confidence',
107
+ ' --json Print JSON output',
108
+ ' --out <file> Write command output/evidence',
109
+ ' --skip <area|id> Skip a command or probe area',
110
+ ' --only <area|id> Select a probe area or id',
111
+ ' -h, --help Print help and exit 0'
112
+ ];
113
+ const route = [
114
+ ASCII_HEADER,
115
+ 'Usage: opstruth routes --base-url <url> [--routes file] [--strict]',
116
+ '',
117
+ 'Read-only route probes collect URL, method, status, latency, redirects, and security-header evidence.',
118
+ '',
119
+ 'Options:',
120
+ ' --base-url <url> Base URL to probe',
121
+ ' --routes <file> JSON route config',
122
+ ' --json Print JSON output',
123
+ ' -h, --help Print help and exit 0'
124
+ ];
125
+ const repo = [
126
+ ASCII_HEADER,
127
+ 'Usage: opstruth repo [--json] [--strict]',
128
+ '',
129
+ 'Read-only repository inspection reports cwd, git root, branch, latest commit, dirty files, and detected stack.',
130
+ '',
131
+ 'Options:',
132
+ ' --json Print JSON output',
133
+ ' -h, --help Print help and exit 0'
134
+ ];
135
+ if (command === 'routes') return route.join('\n') + '\n';
136
+ if (command === 'repo') return repo.join('\n') + '\n';
137
+ return common.join('\n') + '\n';
138
+ }
139
+
140
+ function welcomeText() {
141
+ return [
142
+ ASCII_HEADER,
143
+ 'Welcome to opstruth.',
144
+ '',
145
+ 'This tool runs read-only operational checks to help you understand:',
146
+ '- what changed',
147
+ '- what is configured',
148
+ '- what looks risky',
149
+ '- what was verified',
150
+ '- what was not verified',
151
+ '',
152
+ 'It will not deploy, mutate databases, trigger jobs, publish content, restart services, call OpenAI, or print raw secrets.',
153
+ '',
154
+ 'Common workflows:',
155
+ '- opstruth',
156
+ '- opstruth --strict',
157
+ '- opstruth routes --base-url https://example.com',
158
+ '- opstruth local --port 3000 --health /health',
159
+ '- opstruth evidence --title "Release proof"',
160
+ '',
161
+ 'Safety philosophy:',
162
+ 'opstruth prefers skipped or not verified over pretending something is safe. Dangerous actions require explicit approval and are not part of the default run.',
163
+ ''
164
+ ].join('\n');
165
+ }
166
+
167
+ const INIT_CONFIG = {
168
+ routes: {
169
+ baseUrl: '',
170
+ paths: ['/', '/login', '/healthz'],
171
+ requiredHeaders: [
172
+ 'content-security-policy',
173
+ 'strict-transport-security',
174
+ 'x-frame-options',
175
+ 'referrer-policy'
176
+ ]
177
+ },
178
+ local: {
179
+ ports: [],
180
+ healthPath: '/health'
181
+ },
182
+ supabase: {
183
+ protectedTables: [
184
+ 'agent_jobs',
185
+ 'platform_credentials',
186
+ 'worker_logs'
187
+ ]
188
+ },
189
+ ignore: [
190
+ '.cache',
191
+ '.agents',
192
+ 'node_modules',
193
+ 'dist',
194
+ 'build'
195
+ ]
196
+ };
197
+
198
+ async function readStdin() {
199
+ if (process.stdin.isTTY) {
200
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
201
+ try {
202
+ return await rl.question('');
203
+ } finally {
204
+ rl.close();
205
+ }
206
+ }
207
+ return new Promise((resolve) => {
208
+ let input = '';
209
+ process.stdin.setEncoding('utf8');
210
+ process.stdin.on('data', (chunk) => { input += chunk; });
211
+ process.stdin.on('end', () => resolve(input));
212
+ });
213
+ }
214
+
215
+ async function runInit(options) {
216
+ const cwd = options.cwd || process.cwd();
217
+ const outPath = path.join(cwd, 'opstruth.config.json');
218
+ try {
219
+ await readFile(outPath, 'utf8');
220
+ return 'opstruth.config.json already exists. No changes made.\n';
221
+ } catch {
222
+ // Continue when the file is absent.
223
+ }
224
+ if (!options.yes) {
225
+ process.stdout.write('Create opstruth.config.json in this directory? Type yes to continue: ');
226
+ const answer = (await readStdin()).trim().toLowerCase();
227
+ if (answer !== 'yes') return 'No changes made.\n';
228
+ }
229
+ await writeFileSafe(outPath, JSON.stringify(INIT_CONFIG, null, 2) + '\n');
230
+ return 'Created opstruth.config.json\n';
231
+ }
232
+
233
+ export async function runCli(argv, cwd = process.cwd()) {
234
+ const { command, options } = parseArgs(argv);
235
+ options.cwd = cwd;
236
+ if (options.help) {
237
+ await writeStdout(helpText(command));
238
+ process.exitCode = 0;
239
+ return;
240
+ }
241
+ const result = await dispatch(command, options);
242
+ if (typeof result === 'string') {
243
+ await writeStdout(result);
244
+ process.exitCode = 0;
245
+ return;
246
+ }
247
+ const output = options.json ? JSON.stringify(redactObject(result), null, 2) + '\n' : resultToMarkdown(result);
248
+ if (options.out && command) {
249
+ const outPath = path.isAbsolute(options.out) ? options.out : path.join(cwd, options.out);
250
+ await writeFileSafe(outPath, output);
251
+ }
252
+ await writeStdout(output);
253
+ process.exitCode = exitCodeFor(result, { strict: options.strict });
254
+ }