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.
- package/LICENSE +21 -0
- package/README.md +51 -0
- package/bin/opstruth.js +7 -0
- package/examples/routes.json +12 -0
- package/fixtures/next-app/app/page.tsx +3 -0
- package/fixtures/next-app/next.config.ts +5 -0
- package/fixtures/next-app/package.json +19 -0
- package/fixtures/next-app/tsconfig.json +6 -0
- package/fixtures/non-git-folder/README.md +3 -0
- package/fixtures/non-git-folder/notes.txt +1 -0
- package/fixtures/plain-node-app/package.json +8 -0
- package/fixtures/plain-node-app/src/index.js +3 -0
- package/fixtures/risky-secret-app/package.json +8 -0
- package/fixtures/risky-secret-app/src/config.js +3 -0
- package/fixtures/supabase-cloudflare-app/package.json +16 -0
- package/fixtures/supabase-cloudflare-app/src/supabaseClient.ts +7 -0
- package/fixtures/supabase-cloudflare-app/src/worker.ts +5 -0
- package/fixtures/supabase-cloudflare-app/supabase/migrations/001_init.sql +11 -0
- package/fixtures/supabase-cloudflare-app/wrangler.toml +6 -0
- package/fixtures/vite-react-app/package.json +20 -0
- package/fixtures/vite-react-app/src/App.tsx +3 -0
- package/fixtures/vite-react-app/tsconfig.json +6 -0
- package/fixtures/vite-react-app/vite.config.ts +6 -0
- package/package.json +53 -0
- package/scripts/demo-fixtures.sh +35 -0
- package/scripts/demo-run.sh +32 -0
- package/src/cli.js +254 -0
- package/src/commands/cloudflare.js +51 -0
- package/src/commands/evidence.js +38 -0
- package/src/commands/local.js +43 -0
- package/src/commands/probes.js +68 -0
- package/src/commands/quality.js +66 -0
- package/src/commands/repo.js +30 -0
- package/src/commands/routes.js +49 -0
- package/src/commands/secrets.js +33 -0
- package/src/commands/supabase.js +39 -0
- package/src/lib/boundary.js +74 -0
- package/src/lib/config.js +31 -0
- package/src/lib/detect.js +111 -0
- package/src/lib/exec.js +28 -0
- package/src/lib/fs.js +36 -0
- package/src/lib/git.js +27 -0
- package/src/lib/http.js +14 -0
- package/src/lib/markdown.js +202 -0
- package/src/lib/probes.js +489 -0
- package/src/lib/redact.js +27 -0
- package/src/lib/result.js +63 -0
- package/src/lib/scan.js +53 -0
- 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
|
+
```
|
package/bin/opstruth.js
ADDED
|
@@ -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,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 @@
|
|
|
1
|
+
plain text only
|
|
@@ -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,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
|
+
}
|
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
|
+
}
|