trooth 0.2.0 → 0.3.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/README.md +81 -25
- package/bin/trooth.mjs +349 -191
- package/package.json +8 -5
package/README.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# trooth
|
|
2
2
|
|
|
3
|
-
The trust network your terminal can check. Read any company's **witnessed**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
The trust network your terminal can check. Read any company's **witnessed** record on
|
|
4
|
+
the [Trooth Network](https://trooth.co): identity, security, privacy, AI practices and
|
|
5
|
+
more, each witnessed, signed and dated. It also scans your own Terraform plans against
|
|
6
|
+
SOC 2, ISO 27001, GDPR, HIPAA, NIST AI RMF and the EU AI Act.
|
|
7
7
|
|
|
8
8
|
**`check` reads only public, already-published records. No key, no account.**
|
|
9
9
|
**`scan` is advisory and report-only. Trooth never applies changes to your infrastructure.**
|
|
10
|
+
**Trooth publishes facts and counts about a company, never one number that sums it up.**
|
|
10
11
|
|
|
11
12
|
## Install / run
|
|
12
13
|
|
|
@@ -19,16 +20,37 @@ npm i -g trooth
|
|
|
19
20
|
trooth check trooth.co
|
|
20
21
|
```
|
|
21
22
|
|
|
23
|
+
Requires Node 18+ (uses built-in `fetch`).
|
|
24
|
+
|
|
22
25
|
## Commands
|
|
23
26
|
|
|
24
27
|
| Command | What it does |
|
|
25
28
|
| --- | --- |
|
|
26
|
-
| `trooth check <domain>` | Reads a company's witnessed
|
|
27
|
-
| `trooth scan <plan>` | Posts your `terraform show -json` plan to Trooth Pre-Flight (`POST https://api.trooth.co/v1/preflight`) and prints the verdict,
|
|
28
|
-
| `trooth eu ...` | Pushes EU AI Act evidence (datasets
|
|
29
|
-
| `trooth lint [path]` | Local, read-only IaC drift check. Never transmits your code. |
|
|
29
|
+
| `trooth check <domain>` | Reads a company's witnessed record from the live Trooth Network (`GET https://api.trooth.co/directory/api/vendors`) and prints whether it is listed, when it was witnessed and first published, the live-probe and attestation counts, the badge / scan ids, and the signature and key id you can verify. Prints an honest "not listed yet" when a company has no published record. |
|
|
30
|
+
| `trooth scan <plan>` | Posts your `terraform show -json` plan to Trooth Pre-Flight (`POST https://api.trooth.co/v1/preflight`) and prints the verdict, the pass / fail / n-a counts, and the findings. Accepts a JSON file or a binary `.tfplan` (it runs `terraform show -json` for you). |
|
|
31
|
+
| `trooth eu ...` | Pushes EU AI Act evidence (`--datasets`, `--evals`, `--drift`, each a JSON file) into Trooth from CI/CD. Needs `--token` / `TROOTH_TOKEN` or `--company` / `TROOTH_COMPANY`. |
|
|
32
|
+
| `trooth lint [path]` | Local, read-only IaC drift check that runs the `@trooth/os` linter if it is installed. Never transmits your code. `@trooth/os` is not yet on the npm registry, so today this exits 3 with a message saying exactly that. |
|
|
30
33
|
| `trooth --help` / `--version` | Help / version. |
|
|
31
34
|
|
|
35
|
+
## Flags
|
|
36
|
+
|
|
37
|
+
- `--json` (`check`, `scan`, `eu`): stdout carries exactly one JSON document and nothing
|
|
38
|
+
else; every diagnostic goes to stderr. On an error the document is
|
|
39
|
+
`{"ok": false, "error": "...", "exit": N}`.
|
|
40
|
+
- `--strict` (`scan` only): exit 1 if any check fails. Default is advisory: exit 0.
|
|
41
|
+
|
|
42
|
+
Any other flag is a usage error (exit 2) that names the flag and lists the known ones.
|
|
43
|
+
There is no `--fail-on`, no `--all`, no sort or filter switch.
|
|
44
|
+
|
|
45
|
+
## Exit codes
|
|
46
|
+
|
|
47
|
+
| Code | Meaning |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| 0 | ok: company listed; scan ran (no strict failures); help / version |
|
|
50
|
+
| 1 | finding: company not listed; `scan --strict` and at least one check failed |
|
|
51
|
+
| 2 | usage error: missing argument, unknown flag or command, unreadable file |
|
|
52
|
+
| 3 | Trooth unreachable, non-2xx response, malformed response, or `@trooth/os` missing |
|
|
53
|
+
|
|
32
54
|
## Examples
|
|
33
55
|
|
|
34
56
|
```bash
|
|
@@ -38,42 +60,76 @@ trooth check trooth.co
|
|
|
38
60
|
```
|
|
39
61
|
Trooth Network // witnessed · public · read-only //
|
|
40
62
|
Trooth, LLC trooth.co
|
|
41
|
-
Standing:
|
|
42
|
-
|
|
43
|
-
By discipline
|
|
44
|
-
Legal ██████████ 20/20
|
|
45
|
-
Security ████████░░ 16/20
|
|
46
|
-
AI governance ██████████ 19/20
|
|
47
|
-
Business █████████░ 18/20
|
|
48
|
-
IP █████████░ 18/20
|
|
63
|
+
Standing: listed and witnessed witnessed 2026-08-30 first published 2026-08-01
|
|
49
64
|
|
|
50
65
|
Live probes 64/65 Attestations 27/35
|
|
51
|
-
|
|
66
|
+
Probes are checks Trooth read for itself. Attestations are the company's own declarations.
|
|
67
|
+
They are counts, reported apart on purpose. Trooth never adds them up into one number.
|
|
68
|
+
Badge bronze_rw_... Scan rw_... Key ed25519-2026-01
|
|
52
69
|
|
|
53
|
-
|
|
70
|
+
Recent witness events
|
|
71
|
+
• 2026-08-30 rewitnessed 64 of 65 live probes re-run
|
|
72
|
+
|
|
73
|
+
A witnessed, point-in-time reading of public evidence. Not a certification. Not one number.
|
|
74
|
+
Full record: https://trooth.co/network/trooth.co · Signing keys: https://api.trooth.co/public/keys
|
|
54
75
|
```
|
|
55
76
|
|
|
56
77
|
```bash
|
|
57
|
-
# For scripting:
|
|
58
|
-
trooth check
|
|
78
|
+
# For scripting: one JSON document on stdout, exit 1 when a company is not listed.
|
|
79
|
+
trooth check trooth.co --json
|
|
59
80
|
```
|
|
60
81
|
|
|
61
|
-
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"domain": "trooth.co",
|
|
85
|
+
"listed": true,
|
|
86
|
+
"company_name": "Trooth, LLC",
|
|
87
|
+
"witnessed_at": "2026-08-30T00:00:00Z",
|
|
88
|
+
"first_published_at": "2026-08-01T00:00:00Z",
|
|
89
|
+
"badge_id": "bronze_rw_...",
|
|
90
|
+
"scan_id": "rw_...",
|
|
91
|
+
"probes": { "passed": 64, "total": 65 },
|
|
92
|
+
"attested": { "passed": 27, "total": 35 },
|
|
93
|
+
"events": [ { "type": "rewitnessed", "at": "2026-08-30T00:00:00Z", "detail": "64 of 65 live probes re-run" } ],
|
|
94
|
+
"receipt_signature": "...",
|
|
95
|
+
"authority_key_id": "ed25519-2026-01",
|
|
96
|
+
"verify_keys": "https://api.trooth.co/public/keys",
|
|
97
|
+
"record_url": "https://trooth.co/network/trooth.co"
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Those are the only fields `check --json` emits. Anything else the feed happens to carry
|
|
102
|
+
is dropped on the way out, so a script written against this shape keeps working.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
terraform show -json plan.tfplan > plan.json
|
|
106
|
+
trooth scan plan.json # advisory, exit 0
|
|
107
|
+
trooth scan plan.json --strict # exit 1 if any check fails
|
|
108
|
+
```
|
|
62
109
|
|
|
63
|
-
|
|
64
|
-
-
|
|
110
|
+
```
|
|
111
|
+
Trooth Pre-Flight // advisory · report-only //
|
|
112
|
+
Verdict: WARN pass 5 · fail 1 · n/a 2
|
|
113
|
+
|
|
114
|
+
1 finding(s):
|
|
115
|
+
|
|
116
|
+
● MEDIUM S3 bucket allows public read
|
|
117
|
+
↳ SOC2 CC6.1, ISO 27001 A.8.3
|
|
118
|
+
|
|
119
|
+
Advisory. Analyzes declared infrastructure intent, not live production state. Trooth never applies changes.
|
|
120
|
+
```
|
|
65
121
|
|
|
66
122
|
## Notes
|
|
67
123
|
|
|
68
124
|
- `check` reads only the **public** witness directory. There is no key and no account, and
|
|
69
125
|
it never sends anything about you. It is a read.
|
|
70
|
-
- A "not listed" result is not a judgement. It means no witnessed
|
|
126
|
+
- A "not listed" result is not a judgement. It means no witnessed record has been
|
|
71
127
|
published for that domain yet. A company gets witnessed by listing at
|
|
72
128
|
<https://trooth.co/get-started>.
|
|
73
129
|
- `scan` sends only your **declared plan** to the Pre-Flight API; nothing is written back,
|
|
74
130
|
and Trooth never touches live infrastructure.
|
|
75
131
|
- Set `TROOTH_API` to point at a different base URL (defaults to `https://api.trooth.co`).
|
|
76
|
-
-
|
|
132
|
+
- Set `NO_COLOR=1` to disable ANSI colour. Colour is already off when stdout is not a TTY.
|
|
77
133
|
|
|
78
134
|
## Use it from an AI assistant
|
|
79
135
|
|
package/bin/trooth.mjs
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// Copyright (c) 2026 Trooth, LLC. All rights reserved.
|
|
4
4
|
//
|
|
5
5
|
// Commands:
|
|
6
|
-
// trooth check <domain> Read a company's witnessed
|
|
7
|
-
//
|
|
6
|
+
// trooth check <domain> Read a company's witnessed record from the public Trooth
|
|
7
|
+
// Network (read-only, no key, no account).
|
|
8
8
|
// trooth scan <plan> Send a Terraform plan to Trooth Pre-Flight (advisory, report-only).
|
|
9
9
|
// <plan> may be a `terraform show -json` JSON file, OR a binary
|
|
10
10
|
// .tfplan (the CLI will run `terraform show -json` for you).
|
|
@@ -15,48 +15,174 @@
|
|
|
15
15
|
// `check` reads only PUBLIC, already-published witness records. `scan` is ADVISORY and
|
|
16
16
|
// REPORT-ONLY: Trooth never applies changes to your infrastructure, and the scan posts
|
|
17
17
|
// only your declared plan. Trooth automates. Trooth never signs for you.
|
|
18
|
+
//
|
|
19
|
+
// TROOTH DOES NOT SCORE COMPANIES. This binary prints no company score, no tier, no
|
|
20
|
+
// grade, no rank, and no percentage of trust, in any mode. It prints facts: listed or
|
|
21
|
+
// not, witnessed on what date, how many live probes and attestations were read, and
|
|
22
|
+
// the ids and signature you can verify yourself. If an upstream feed still carries a
|
|
23
|
+
// number of that kind, scrub() below drops it before anything reaches stdout.
|
|
24
|
+
//
|
|
25
|
+
// Exit codes (stable, for scripts):
|
|
26
|
+
// 0 ok (listed; scan ran with no strict failure; help/version)
|
|
27
|
+
// 1 finding (domain not listed; `scan --strict` with failing checks)
|
|
28
|
+
// 2 usage error (missing argument, unknown flag or command, unreadable file)
|
|
29
|
+
// 3 network/upstream error (Trooth unreachable, non-2xx, malformed response, missing linter)
|
|
30
|
+
//
|
|
31
|
+
// With --json, stdout carries exactly one JSON document and nothing else. Every
|
|
32
|
+
// diagnostic goes to stderr.
|
|
18
33
|
|
|
19
34
|
import { readFileSync, existsSync } from 'node:fs';
|
|
20
|
-
import { execFileSync } from 'node:child_process';
|
|
35
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
21
36
|
import { createRequire } from 'node:module';
|
|
37
|
+
import { dirname } from 'node:path';
|
|
22
38
|
|
|
23
39
|
const API = process.env.TROOTH_API || 'https://api.trooth.co';
|
|
24
|
-
const J='\x1b[32m', D='\x1b[2m', B='\x1b[1m', R='\x1b[31m', A='\x1b[33m', C='\x1b[36m', X='\x1b[0m';
|
|
25
40
|
const require = createRequire(import.meta.url);
|
|
26
|
-
let VERSION = '0.
|
|
41
|
+
let VERSION = '0.3.0';
|
|
27
42
|
try { VERSION = require('../package.json').version; } catch {}
|
|
28
43
|
|
|
44
|
+
const EXIT = { OK: 0, FINDING: 1, USAGE: 2, UPSTREAM: 3 };
|
|
45
|
+
|
|
46
|
+
// Colour only when stdout is a TTY and NO_COLOR is unset, so piped output is clean.
|
|
47
|
+
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
48
|
+
const c = (code) => (useColor ? code : '');
|
|
49
|
+
const J = c('\x1b[32m'), D = c('\x1b[2m'), B = c('\x1b[1m'), R = c('\x1b[31m'), A = c('\x1b[33m'), C = c('\x1b[36m'), X = c('\x1b[0m');
|
|
50
|
+
|
|
29
51
|
const argv = process.argv.slice(2);
|
|
30
52
|
const cmd = argv[0];
|
|
53
|
+
const asJson = argv.includes('--json');
|
|
54
|
+
|
|
55
|
+
/* ------------------------------------------------------------- output ---- */
|
|
56
|
+
|
|
57
|
+
function out(s) { process.stdout.write(s + '\n'); }
|
|
58
|
+
function diag(s) { process.stderr.write(s + '\n'); }
|
|
59
|
+
function emitJson(obj) { out(JSON.stringify(obj, null, 2)); }
|
|
60
|
+
|
|
61
|
+
/** Print a diagnostic to stderr and, in --json mode, one machine-readable error
|
|
62
|
+
* document to stdout, then exit with the given code. */
|
|
63
|
+
function fail(code, message, extra = {}) {
|
|
64
|
+
diag(`${R}error${X} ${message}`);
|
|
65
|
+
if (asJson) emitJson({ ok: false, error: message, exit: code, ...extra });
|
|
66
|
+
process.exit(code);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Keys that would carry a company score, tier, grade, rank, rating or percentage
|
|
70
|
+
* of trust. Nothing under these names is ever printed, in any mode, no matter
|
|
71
|
+
* what an upstream feed sends. `rate` is matched only as a whole key so that
|
|
72
|
+
* keys like generatedAt survive. */
|
|
73
|
+
const BANNED_KEY = /score|tier|grade|rank|rating|level|percent|^rate$/i;
|
|
74
|
+
|
|
75
|
+
/** Recursively drop banned keys from any JSON-ish value. */
|
|
76
|
+
function scrub(value) {
|
|
77
|
+
if (Array.isArray(value)) return value.map(scrub);
|
|
78
|
+
if (value && typeof value === 'object') {
|
|
79
|
+
const clean = {};
|
|
80
|
+
for (const [k, v] of Object.entries(value)) {
|
|
81
|
+
if (BANNED_KEY.test(k)) continue;
|
|
82
|
+
clean[k] = scrub(v);
|
|
83
|
+
}
|
|
84
|
+
return clean;
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* --------------------------------------------------------------- args ---- */
|
|
90
|
+
|
|
91
|
+
const FLAGS = {
|
|
92
|
+
check: { bool: ['--json'], value: [] },
|
|
93
|
+
scan: { bool: ['--json', '--strict'], value: [] },
|
|
94
|
+
eu: { bool: ['--json'], value: ['--token', '--company', '--datasets', '--evals', '--drift'] },
|
|
95
|
+
lint: { bool: [], value: [] },
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** Parse argv[1..] for one command. Unknown flags are a usage error (exit 2), so a
|
|
99
|
+
* typo or a flag from an old README never silently becomes a filename. */
|
|
100
|
+
function parseArgs(command) {
|
|
101
|
+
const spec = FLAGS[command];
|
|
102
|
+
const flags = {};
|
|
103
|
+
const positional = [];
|
|
104
|
+
for (let i = 1; i < argv.length; i++) {
|
|
105
|
+
const a = argv[i];
|
|
106
|
+
if (a === '--') { positional.push(...argv.slice(i + 1)); break; }
|
|
107
|
+
if (a.startsWith('--')) {
|
|
108
|
+
const [name, inlineVal] = a.split('=', 2);
|
|
109
|
+
if (spec.bool.includes(name)) { flags[name] = true; continue; }
|
|
110
|
+
if (spec.value.includes(name)) {
|
|
111
|
+
const v = inlineVal !== undefined ? inlineVal : argv[++i];
|
|
112
|
+
if (v === undefined || v.startsWith('--')) fail(EXIT.USAGE, `${name} needs a value.`);
|
|
113
|
+
flags[name] = v; continue;
|
|
114
|
+
}
|
|
115
|
+
const known = [...spec.bool, ...spec.value];
|
|
116
|
+
fail(EXIT.USAGE, `unknown flag ${name} for \`trooth ${command}\`. ` +
|
|
117
|
+
(known.length ? `Known flags: ${known.join(', ')}.` : `\`trooth ${command}\` takes no flags.`) +
|
|
118
|
+
` Run \`trooth --help\`.`);
|
|
119
|
+
} else if (a.startsWith('-') && a.length > 1) {
|
|
120
|
+
fail(EXIT.USAGE, `unknown flag ${a} for \`trooth ${command}\`. Run \`trooth --help\`.`);
|
|
121
|
+
} else {
|
|
122
|
+
positional.push(a);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { flags, positional };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* --------------------------------------------------------------- help ---- */
|
|
31
129
|
|
|
32
|
-
function
|
|
33
|
-
|
|
130
|
+
function helpText() {
|
|
131
|
+
return `
|
|
34
132
|
${J}${B}trooth${X} ${D}v${VERSION} · the trust network your terminal can check${X}
|
|
35
133
|
|
|
36
134
|
${B}Usage${X}
|
|
37
|
-
trooth check <domain> Read a company's witnessed
|
|
135
|
+
trooth check <domain> Read a company's witnessed record on the Trooth Network
|
|
38
136
|
trooth scan <plan> Scan a Terraform plan via Trooth Pre-Flight (advisory)
|
|
137
|
+
trooth eu ... Push EU AI Act evidence from CI/CD (needs a token)
|
|
39
138
|
trooth lint [path] Local read-only IaC drift check
|
|
40
139
|
trooth --help | --version
|
|
41
140
|
|
|
42
141
|
${B}Examples${X}
|
|
43
142
|
trooth check stripe.com ${D}# read a company's witnessed record${X}
|
|
44
|
-
trooth check trooth.co --json ${D}#
|
|
143
|
+
trooth check trooth.co --json ${D}# one JSON document on stdout, for scripting${X}
|
|
45
144
|
terraform show -json plan.tfplan > plan.json && trooth scan plan.json
|
|
145
|
+
trooth scan plan.json --strict ${D}# exit 1 if any check fails${X}
|
|
46
146
|
|
|
47
147
|
${B}Flags${X}
|
|
48
|
-
--json
|
|
49
|
-
--strict (scan) exit
|
|
148
|
+
--json (check, scan, eu) machine-readable JSON on stdout; diagnostics on stderr
|
|
149
|
+
--strict (scan) exit 1 if failing checks exist (default: advisory, exit 0)
|
|
150
|
+
|
|
151
|
+
${B}Exit codes${X}
|
|
152
|
+
0 ok 1 finding (not listed, or strict failures) 2 usage error 3 Trooth unreachable
|
|
50
153
|
|
|
51
154
|
${D}check reads only public, already-published witness records. No key, no account.
|
|
155
|
+
Trooth publishes facts and counts about a company, never one number that sums it up.
|
|
52
156
|
scan is advisory and report-only. Trooth automates. Trooth never signs for you.${X}
|
|
53
|
-
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* -------------------------------------------------------------- fetch ---- */
|
|
161
|
+
|
|
162
|
+
/** GET/POST with the CLI user-agent. Any failure to reach Trooth, any non-2xx, and any
|
|
163
|
+
* unparseable body is an upstream error (exit 3), never a finding. */
|
|
164
|
+
async function callTrooth(path, init, what) {
|
|
165
|
+
let res;
|
|
166
|
+
try {
|
|
167
|
+
res = await fetch(`${API}${path}`, {
|
|
168
|
+
...init,
|
|
169
|
+
headers: { accept: 'application/json', 'user-agent': `trooth-cli/${VERSION}`, ...(init && init.headers) },
|
|
170
|
+
});
|
|
171
|
+
} catch (e) {
|
|
172
|
+
fail(EXIT.UPSTREAM, `could not reach ${what} at ${API}: ${e && e.message ? e.message : e}`);
|
|
173
|
+
}
|
|
174
|
+
if (!res.ok) {
|
|
175
|
+
const body = await res.text().catch(() => '');
|
|
176
|
+
fail(EXIT.UPSTREAM, `${what} returned HTTP ${res.status}.${body ? ' ' + body.slice(0, 300).replace(/\s+/g, ' ') : ''}`, { http_status: res.status });
|
|
177
|
+
}
|
|
178
|
+
try { return await res.json(); }
|
|
179
|
+
catch { fail(EXIT.UPSTREAM, `${what} returned a response that is not JSON.`); }
|
|
54
180
|
}
|
|
55
181
|
|
|
56
182
|
/* --------------------------------------------------------------- check ---- */
|
|
57
183
|
|
|
58
184
|
/** Normalize a domain the way the Trooth Network does: strip scheme, path,
|
|
59
|
-
* leading www, case, and trailing
|
|
185
|
+
* leading www, case, and trailing dot. */
|
|
60
186
|
function normalizeDomain(input) {
|
|
61
187
|
if (!input) return '';
|
|
62
188
|
let s = String(input).trim().toLowerCase();
|
|
@@ -67,32 +193,6 @@ function normalizeDomain(input) {
|
|
|
67
193
|
return s;
|
|
68
194
|
}
|
|
69
195
|
|
|
70
|
-
const CATEGORY_LABELS = {
|
|
71
|
-
legal: 'Legal',
|
|
72
|
-
security: 'Security',
|
|
73
|
-
ai: 'AI governance',
|
|
74
|
-
business: 'Business',
|
|
75
|
-
ip: 'IP',
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
/** Pull a 0..100 score out of the feed's score field (object or number). */
|
|
79
|
-
function scorePct(score) {
|
|
80
|
-
if (score == null) return null;
|
|
81
|
-
if (typeof score === 'number') return Math.round(score);
|
|
82
|
-
if (typeof score === 'object') {
|
|
83
|
-
if (typeof score.rate === 'number') return Math.round(score.rate * 100);
|
|
84
|
-
if (typeof score.passed === 'number' && typeof score.total === 'number' && score.total > 0)
|
|
85
|
-
return Math.round((score.passed / score.total) * 100);
|
|
86
|
-
}
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function bar(passed, total) {
|
|
91
|
-
if (!total) return '';
|
|
92
|
-
const filled = Math.round((passed / total) * 10);
|
|
93
|
-
return '█'.repeat(filled) + '░'.repeat(10 - filled);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
196
|
function fmtDate(iso) {
|
|
97
197
|
if (!iso) return '';
|
|
98
198
|
const d = new Date(iso);
|
|
@@ -100,165 +200,177 @@ function fmtDate(iso) {
|
|
|
100
200
|
return d.toISOString().slice(0, 10);
|
|
101
201
|
}
|
|
102
202
|
|
|
203
|
+
function count(obj) {
|
|
204
|
+
if (!obj || typeof obj !== 'object') return null;
|
|
205
|
+
if (typeof obj.passed !== 'number' || typeof obj.total !== 'number') return null;
|
|
206
|
+
return { passed: obj.passed, total: obj.total };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The public record, projected onto the fields this CLI vouches for. Unknown fields
|
|
210
|
+
* from the feed are not carried, so a field added upstream later cannot reintroduce
|
|
211
|
+
* a number Trooth does not publish. Then scrubbed anyway, belt and braces. */
|
|
212
|
+
function projectRecord(v, domain) {
|
|
213
|
+
const events = Array.isArray(v.events)
|
|
214
|
+
? v.events.filter((e) => e && e.type).map((e) => ({ type: String(e.type), at: e.at, detail: e.detail }))
|
|
215
|
+
: [];
|
|
216
|
+
const rec = {
|
|
217
|
+
domain,
|
|
218
|
+
listed: true,
|
|
219
|
+
company_name: v.company_name || domain,
|
|
220
|
+
witnessed_at: v.passed_at || null,
|
|
221
|
+
first_published_at: v.first_published_at || null,
|
|
222
|
+
badge_id: v.badge_id || null,
|
|
223
|
+
scan_id: v.scan_id || null,
|
|
224
|
+
probes: count(v.probes),
|
|
225
|
+
attested: count(v.attested),
|
|
226
|
+
events,
|
|
227
|
+
receipt_signature: v.receipt_signature || null,
|
|
228
|
+
authority_key_id: v.authority_key_id || null,
|
|
229
|
+
verify_keys: `${API}/public/keys`,
|
|
230
|
+
record_url: `https://trooth.co/network/${encodeURIComponent(domain)}`,
|
|
231
|
+
};
|
|
232
|
+
if (v.category) rec.category = String(v.category);
|
|
233
|
+
if (v.description) rec.description = String(v.description);
|
|
234
|
+
return scrub(rec);
|
|
235
|
+
}
|
|
236
|
+
|
|
103
237
|
async function check() {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
const domain = normalizeDomain(
|
|
107
|
-
if (!domain)
|
|
108
|
-
console.error(`${R}error${X} missing <domain>. Try: trooth check stripe.com`);
|
|
109
|
-
process.exit(2);
|
|
110
|
-
}
|
|
238
|
+
const { positional } = parseArgs('check');
|
|
239
|
+
if (positional.length > 1) fail(EXIT.USAGE, `check takes one <domain>, got: ${positional.join(' ')}`);
|
|
240
|
+
const domain = normalizeDomain(positional[0]);
|
|
241
|
+
if (!domain) fail(EXIT.USAGE, 'missing <domain>. Try: trooth check stripe.com');
|
|
111
242
|
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
res = await fetch(`${API}/directory/api/vendors`, {
|
|
115
|
-
headers: { accept: 'application/json', 'user-agent': `trooth-cli/${VERSION}` },
|
|
116
|
-
});
|
|
117
|
-
} catch (e) {
|
|
118
|
-
console.error(`${R}error${X} could not reach the Trooth Network at ${API}: ${e.message}`);
|
|
119
|
-
process.exit(1);
|
|
120
|
-
}
|
|
121
|
-
if (!res.ok) {
|
|
122
|
-
console.error(`${R}error${X} the Trooth Network returned HTTP ${res.status}.`);
|
|
123
|
-
process.exit(1);
|
|
124
|
-
}
|
|
125
|
-
let data;
|
|
126
|
-
try { data = await res.json(); } catch { data = {}; }
|
|
243
|
+
const data = await callTrooth('/directory/api/vendors', { method: 'GET' }, 'the Trooth Network');
|
|
127
244
|
const vendors = Array.isArray(data && data.vendors) ? data.vendors : [];
|
|
128
245
|
const vendor = vendors.find((v) => v && normalizeDomain(v.domain) === domain) || null;
|
|
129
246
|
|
|
130
|
-
if (asJson) {
|
|
131
|
-
console.log(JSON.stringify(vendor || { domain, listed: false }, null, 2));
|
|
132
|
-
process.exit(vendor ? 0 : 1);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
247
|
if (!vendor) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const pct = scorePct(vendor.score);
|
|
145
|
-
const name = vendor.company_name || domain;
|
|
146
|
-
const tier = vendor.tier ? vendor.tier[0].toUpperCase() + vendor.tier.slice(1) : 'Listed';
|
|
147
|
-
const when = fmtDate(vendor.passed_at);
|
|
148
|
-
|
|
149
|
-
console.log(`\n${J}${B}Trooth Network${X} ${D}// witnessed · public · read-only //${X}`);
|
|
150
|
-
console.log(`${B}${name}${X} ${C}${domain}${X}`);
|
|
151
|
-
const scoreStr = pct == null ? `${D}not yet scored${X}` : `${B}${pct}/100${X}`;
|
|
152
|
-
console.log(`Standing: ${scoreStr} ${J}${tier}${X}${when ? ` ${D}witnessed ${when}${X}` : ''}`);
|
|
153
|
-
|
|
154
|
-
// Categories (object keyed by discipline).
|
|
155
|
-
const cats = vendor.categories;
|
|
156
|
-
if (cats && typeof cats === 'object' && !Array.isArray(cats)) {
|
|
157
|
-
console.log(`\n${B}By discipline${X}`);
|
|
158
|
-
for (const key of ['legal', 'security', 'ai', 'business', 'ip']) {
|
|
159
|
-
const c = cats[key];
|
|
160
|
-
if (!c || typeof c.total !== 'number') continue;
|
|
161
|
-
const label = (CATEGORY_LABELS[key] || key).padEnd(14);
|
|
162
|
-
const r = c.total > 0 ? c.passed / c.total : 0;
|
|
163
|
-
const col = r >= 0.9 ? J : r >= 0.5 ? A : R;
|
|
164
|
-
console.log(` ${label} ${col}${bar(c.passed, c.total)}${X} ${String(c.passed).padStart(2)}/${c.total}`);
|
|
248
|
+
if (asJson) {
|
|
249
|
+
emitJson({ domain, listed: false, record_url: `https://trooth.co/network/${encodeURIComponent(domain)}` });
|
|
250
|
+
} else {
|
|
251
|
+
out(`\n${B}${domain}${X} ${D}//${X} ${A}not listed on the Trooth Network yet${X}`);
|
|
252
|
+
out(`\n${D}No witnessed record has been published for this domain. That is not a`);
|
|
253
|
+
out(`judgement. It simply has not been witnessed. A company gets witnessed by`);
|
|
254
|
+
out(`listing at ${X}${C}https://trooth.co/get-started${X}${D}: the scan reads its public surface and`);
|
|
255
|
+
out(`publishes a signed, dated record that anyone can check.${X}\n`);
|
|
165
256
|
}
|
|
257
|
+
process.exit(EXIT.FINDING);
|
|
166
258
|
}
|
|
167
259
|
|
|
260
|
+
const rec = projectRecord(vendor, domain);
|
|
261
|
+
if (asJson) { emitJson(rec); process.exit(EXIT.OK); }
|
|
262
|
+
|
|
263
|
+
const when = fmtDate(rec.witnessed_at);
|
|
264
|
+
const since = fmtDate(rec.first_published_at);
|
|
265
|
+
out(`\n${J}${B}Trooth Network${X} ${D}// witnessed · public · read-only //${X}`);
|
|
266
|
+
out(`${B}${rec.company_name}${X} ${C}${domain}${X}`);
|
|
267
|
+
out(`Standing: ${J}listed and witnessed${X}` +
|
|
268
|
+
(when ? ` ${D}witnessed ${when}${X}` : ` ${D}date not published${X}`) +
|
|
269
|
+
(since ? ` ${D}first published ${since}${X}` : ''));
|
|
270
|
+
|
|
168
271
|
const line = [];
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
|
|
272
|
+
if (rec.probes) line.push(`${B}Live probes${X} ${rec.probes.passed}/${rec.probes.total}`);
|
|
273
|
+
if (rec.attested) line.push(`${B}Attestations${X} ${rec.attested.passed}/${rec.attested.total}`);
|
|
274
|
+
if (line.length) {
|
|
275
|
+
out(`\n${line.join(' ')}`);
|
|
276
|
+
out(`${D}Probes are checks Trooth read for itself. Attestations are the company's own declarations.`);
|
|
277
|
+
out(`They are counts, reported apart on purpose. Trooth never adds them up into one number.${X}`);
|
|
278
|
+
}
|
|
174
279
|
|
|
175
280
|
const ids = [];
|
|
176
|
-
if (
|
|
177
|
-
if (
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (events.length) {
|
|
182
|
-
|
|
183
|
-
for (const e of events.slice(0, 3)) {
|
|
184
|
-
|
|
185
|
-
console.log(` ${J}•${X} ${D}${fmtDate(e.at)}${X} ${e.type.replace(/_/g, ' ')}${e.detail ? `${D} ${e.detail}${X}` : ''}`);
|
|
281
|
+
if (rec.badge_id) ids.push(`${D}Badge ${rec.badge_id}${X}`);
|
|
282
|
+
if (rec.scan_id) ids.push(`${D}Scan ${rec.scan_id}${X}`);
|
|
283
|
+
if (rec.authority_key_id) ids.push(`${D}Key ${rec.authority_key_id}${X}`);
|
|
284
|
+
if (ids.length) out(ids.join(' '));
|
|
285
|
+
|
|
286
|
+
if (rec.events.length) {
|
|
287
|
+
out(`\n${B}Recent witness events${X}`);
|
|
288
|
+
for (const e of rec.events.slice(0, 3)) {
|
|
289
|
+
out(` ${J}•${X} ${D}${fmtDate(e.at)}${X} ${e.type.replace(/_/g, ' ')}${e.detail ? `${D} ${e.detail}${X}` : ''}`);
|
|
186
290
|
}
|
|
187
291
|
}
|
|
188
292
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
process.exit(
|
|
293
|
+
out(`\n${D}A witnessed, point-in-time reading of public evidence. Not a certification. Not one number.`);
|
|
294
|
+
out(`Full record: ${X}${C}${rec.record_url}${X}${D} · Signing keys: ${rec.verify_keys}${X}\n`);
|
|
295
|
+
process.exit(EXIT.OK);
|
|
192
296
|
}
|
|
193
297
|
|
|
194
298
|
/* ---------------------------------------------------------------- scan ---- */
|
|
195
299
|
|
|
196
300
|
function loadPlan(file) {
|
|
197
|
-
if (!file)
|
|
198
|
-
if (!existsSync(file))
|
|
199
|
-
// Try to read as JSON (a `terraform show -json` document).
|
|
301
|
+
if (!file) fail(EXIT.USAGE, 'missing <plan> argument. Try: trooth scan ./plan.json');
|
|
302
|
+
if (!existsSync(file)) fail(EXIT.USAGE, `file not found: ${file}`);
|
|
200
303
|
let raw = '';
|
|
201
|
-
try { raw = readFileSync(file, 'utf8'); } catch (e) {
|
|
304
|
+
try { raw = readFileSync(file, 'utf8'); } catch (e) { fail(EXIT.USAGE, `cannot read ${file}: ${e.message}`); }
|
|
202
305
|
try { return JSON.parse(raw); } catch { /* not JSON, likely a binary .tfplan */ }
|
|
203
306
|
// Fall back: ask Terraform to render the plan as JSON.
|
|
204
307
|
try {
|
|
205
|
-
const
|
|
206
|
-
return JSON.parse(
|
|
207
|
-
} catch
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
process.exit(2);
|
|
308
|
+
const rendered = execFileSync('terraform', ['show', '-json', file], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
309
|
+
return JSON.parse(rendered);
|
|
310
|
+
} catch {
|
|
311
|
+
diag(`${D} Generate JSON first: terraform show -json ${file} > plan.json then trooth scan plan.json${X}`);
|
|
312
|
+
fail(EXIT.USAGE, `${file} is not JSON, and \`terraform show -json\` failed.`);
|
|
211
313
|
}
|
|
212
314
|
}
|
|
213
315
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
process.exit(1);
|
|
235
|
-
}
|
|
236
|
-
const delta = await res.json();
|
|
237
|
-
|
|
238
|
-
if (asJson) { console.log(JSON.stringify(delta, null, 2)); process.exit(strict && (delta.summary?.fail > 0) ? 1 : 0); }
|
|
316
|
+
/** Pre-Flight's response, minus anything that is not a finding, a count or a verdict.
|
|
317
|
+
* The upstream still computes a 0..100 number for the plan; this CLI does not print it. */
|
|
318
|
+
function projectPreflight(delta) {
|
|
319
|
+
const clean = scrub(delta || {});
|
|
320
|
+
const s = clean.summary || {};
|
|
321
|
+
return {
|
|
322
|
+
tool: clean.tool || 'trooth-preflight',
|
|
323
|
+
catalogVersion: clean.catalogVersion,
|
|
324
|
+
format: clean.format,
|
|
325
|
+
generatedAt: clean.generatedAt,
|
|
326
|
+
planResourceCount: clean.planResourceCount,
|
|
327
|
+
verdict: clean.verdict || 'reviewed',
|
|
328
|
+
summary: { pass: s.pass ?? 0, fail: s.fail ?? 0, notApplicable: s.notApplicable ?? 0, bySeverity: s.bySeverity },
|
|
329
|
+
findings: Array.isArray(clean.findings) ? clean.findings : [],
|
|
330
|
+
passedChecks: clean.passedChecks,
|
|
331
|
+
notApplicableChecks: clean.notApplicableChecks,
|
|
332
|
+
frameworkCoverage: clean.frameworkCoverage,
|
|
333
|
+
disclaimer: clean.disclaimer || 'Advisory. Analyzes declared infrastructure intent, not live production state. Trooth never applies changes.',
|
|
334
|
+
};
|
|
335
|
+
}
|
|
239
336
|
|
|
240
|
-
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const
|
|
337
|
+
async function scan() {
|
|
338
|
+
const { flags, positional } = parseArgs('scan');
|
|
339
|
+
if (positional.length > 1) fail(EXIT.USAGE, `scan takes one <plan>, got: ${positional.join(' ')}`);
|
|
340
|
+
const strict = !!flags['--strict'];
|
|
341
|
+
const plan = loadPlan(positional[0]);
|
|
342
|
+
|
|
343
|
+
const delta = await callTrooth('/v1/preflight', {
|
|
344
|
+
method: 'POST',
|
|
345
|
+
headers: { 'content-type': 'application/json' },
|
|
346
|
+
body: JSON.stringify({ plan }),
|
|
347
|
+
}, 'Trooth Pre-Flight');
|
|
348
|
+
|
|
349
|
+
const report = projectPreflight(delta);
|
|
350
|
+
const exitCode = strict && report.summary.fail > 0 ? EXIT.FINDING : EXIT.OK; // advisory by default
|
|
351
|
+
|
|
352
|
+
if (asJson) { emitJson(report); process.exit(exitCode); }
|
|
353
|
+
|
|
354
|
+
const s = report.summary;
|
|
355
|
+
out(`\n${J}${B}Trooth Pre-Flight${X} ${D}// advisory · report-only //${X}`);
|
|
356
|
+
out(`Verdict: ${B}${String(report.verdict).toUpperCase()}${X} ` +
|
|
357
|
+
`${J}pass ${s.pass}${X} · ${A}fail ${s.fail}${X} · ${D}n/a ${s.notApplicable}${X}`);
|
|
358
|
+
const findings = report.findings;
|
|
247
359
|
if (findings.length) {
|
|
248
|
-
|
|
360
|
+
out(`\n${B}${findings.length} finding(s):${X}\n`);
|
|
249
361
|
for (const f of findings.slice(0, 50)) {
|
|
250
|
-
const sev = (f.severity || '').toLowerCase();
|
|
362
|
+
const sev = String(f.severity || '').toLowerCase();
|
|
251
363
|
const tag = sev === 'critical' || sev === 'high' ? `${R}● ${sev.toUpperCase()}${X}`
|
|
252
|
-
: sev === 'medium' ? `${A}● MEDIUM${X}` : `${D}● ${(sev||'LOW').toUpperCase()}${X}`;
|
|
253
|
-
|
|
254
|
-
if (f.control || f.frameworks)
|
|
364
|
+
: sev === 'medium' ? `${A}● MEDIUM${X}` : `${D}● ${(sev || 'LOW').toUpperCase()}${X}`;
|
|
365
|
+
out(` ${tag} ${B}${f.title || f.message || f.check || f.checkId || 'finding'}${X}`);
|
|
366
|
+
if (f.control || f.frameworks) out(` ${J}↳${X} ${f.control || (Array.isArray(f.frameworks) ? f.frameworks.join(', ') : f.frameworks)}`);
|
|
255
367
|
}
|
|
368
|
+
if (findings.length > 50) out(` ${D}… ${findings.length - 50} more. Use --json for the full list.${X}`);
|
|
256
369
|
} else {
|
|
257
|
-
|
|
370
|
+
out(`\n${J}✓ No findings in the declared plan.${X}`);
|
|
258
371
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
process.exit(strict && (s.fail > 0) ? 1 : 0); // advisory by default: exit 0
|
|
372
|
+
out(`\n${D}${report.disclaimer}${X}\n`);
|
|
373
|
+
process.exit(exitCode);
|
|
262
374
|
}
|
|
263
375
|
|
|
264
376
|
/* ------------------------------------------------------------------ eu ---- */
|
|
@@ -266,42 +378,88 @@ async function scan() {
|
|
|
266
378
|
// trooth eu --token <t> --datasets ds.json --evals ev.json --drift dr.json
|
|
267
379
|
// Each file is a JSON array. Auth: TROOTH_TOKEN env or --token (bearer or x-trooth-company).
|
|
268
380
|
async function eu() {
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
381
|
+
const { flags, positional } = parseArgs('eu');
|
|
382
|
+
if (positional.length) fail(EXIT.USAGE, `eu takes no positional arguments, got: ${positional.join(' ')}`);
|
|
383
|
+
const token = flags['--token'] || process.env.TROOTH_TOKEN || '';
|
|
384
|
+
const company = flags['--company'] || process.env.TROOTH_COMPANY || '';
|
|
385
|
+
if (!token && !company) fail(EXIT.USAGE, 'provide --token <session> or --company <id> (or TROOTH_TOKEN / TROOTH_COMPANY env).');
|
|
386
|
+
const load = (f) => {
|
|
387
|
+
if (!f) return undefined;
|
|
388
|
+
try { return JSON.parse(readFileSync(f, 'utf8')); } catch (e) { fail(EXIT.USAGE, `cannot read ${f}: ${e.message}`); }
|
|
389
|
+
};
|
|
390
|
+
const body = { datasets: load(flags['--datasets']), evals: load(flags['--evals']), drift: load(flags['--drift']) };
|
|
391
|
+
if (!body.datasets && !body.evals && !body.drift) fail(EXIT.USAGE, 'provide at least one of --datasets/--evals/--drift <file.json>');
|
|
392
|
+
const headers = { 'content-type': 'application/json' };
|
|
277
393
|
if (token) headers['authorization'] = `Bearer ${token}`;
|
|
278
394
|
if (company) headers['x-trooth-company'] = company;
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
process.exit(
|
|
395
|
+
|
|
396
|
+
const raw = await callTrooth('/v1/eu/ingest', { method: 'POST', headers, body: JSON.stringify(body) }, 'Trooth');
|
|
397
|
+
const result = scrub(raw || {});
|
|
398
|
+
const ingested = result.ingested || {};
|
|
399
|
+
const report = {
|
|
400
|
+
ingested: { datasets: ingested.datasets ?? 0, evals: ingested.evals ?? 0, drift: ingested.drift ?? 0 },
|
|
401
|
+
disclaimer: result.disclaimer || 'Advisory. Trooth automates, computes, and documents; it never signs.',
|
|
402
|
+
};
|
|
403
|
+
if (asJson) { emitJson(report); process.exit(EXIT.OK); }
|
|
404
|
+
out(`\n${J}${B}Trooth EU AI Act${X} ${D}// evidence ingested //${X}`);
|
|
405
|
+
out(`datasets ${B}${report.ingested.datasets}${X} · evals ${B}${report.ingested.evals}${X} · drift ${B}${report.ingested.drift}${X}`);
|
|
406
|
+
out(`\n${D}${report.disclaimer}${X}\n`);
|
|
407
|
+
process.exit(EXIT.OK);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/* ---------------------------------------------------------------- lint ---- */
|
|
411
|
+
|
|
412
|
+
const LINTER_PKG = '@trooth/os';
|
|
413
|
+
|
|
414
|
+
/** Find the linter package if it is installed next to this CLI or in the caller's
|
|
415
|
+
* project. It is NOT fetched from the network: the previous release ran
|
|
416
|
+
* `npx --yes @trooth/os`, and that name is not on the npm registry (404), so every
|
|
417
|
+
* `trooth lint` ended in npx's raw error. */
|
|
418
|
+
function resolveLinter() {
|
|
419
|
+
const roots = [import.meta.url, `file://${process.cwd()}/`];
|
|
420
|
+
for (const root of roots) {
|
|
421
|
+
try {
|
|
422
|
+
const pkgPath = createRequire(root).resolve(`${LINTER_PKG}/package.json`);
|
|
423
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
424
|
+
const bin = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin && Object.values(pkg.bin)[0];
|
|
425
|
+
if (!bin) continue;
|
|
426
|
+
return { dir: dirname(pkgPath), entry: `${dirname(pkgPath)}/${bin}`, version: pkg.version };
|
|
427
|
+
} catch { /* try the next root */ }
|
|
428
|
+
}
|
|
429
|
+
return null;
|
|
288
430
|
}
|
|
289
431
|
|
|
290
432
|
async function lint() {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
433
|
+
const { positional } = parseArgs('lint');
|
|
434
|
+
if (positional.length > 1) fail(EXIT.USAGE, `lint takes one optional [path], got: ${positional.join(' ')}`);
|
|
435
|
+
const target = positional[0] || '.';
|
|
436
|
+
if (!existsSync(target)) fail(EXIT.USAGE, `path not found: ${target}`);
|
|
437
|
+
|
|
438
|
+
const linter = resolveLinter();
|
|
439
|
+
if (!linter) {
|
|
440
|
+
diag(`${R}error${X} \`trooth lint\` needs the ${LINTER_PKG} linter, and it is not available.`);
|
|
441
|
+
diag(`${D} ${LINTER_PKG} is not published on the npm registry and is not installed locally.`);
|
|
442
|
+
diag(` Nothing was linted and nothing left this machine. This CLI does not download it for you.`);
|
|
443
|
+
diag(` When it ships, install it (npm i -D ${LINTER_PKG}) and run \`trooth lint\` again.${X}`);
|
|
444
|
+
process.exit(EXIT.UPSTREAM);
|
|
445
|
+
}
|
|
446
|
+
const r = spawnSync(process.execPath, [linter.entry, target], { stdio: 'inherit' });
|
|
447
|
+
if (r.error) fail(EXIT.UPSTREAM, `could not start ${LINTER_PKG}@${linter.version}: ${r.error.message}`);
|
|
448
|
+
process.exit(r.status ?? EXIT.UPSTREAM);
|
|
297
449
|
}
|
|
298
450
|
|
|
451
|
+
/* ---------------------------------------------------------------- main ---- */
|
|
452
|
+
|
|
299
453
|
(async () => {
|
|
300
|
-
if (cmd === '--version' || cmd === '-v') {
|
|
301
|
-
if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') {
|
|
454
|
+
if (cmd === '--version' || cmd === '-v' || cmd === 'version') { out(VERSION); return; }
|
|
455
|
+
if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') { out(helpText()); return; }
|
|
302
456
|
if (cmd === 'check') return check();
|
|
303
457
|
if (cmd === 'scan') return scan();
|
|
304
458
|
if (cmd === 'eu') return eu();
|
|
305
459
|
if (cmd === 'lint') return lint();
|
|
306
|
-
|
|
307
|
-
|
|
460
|
+
if (cmd.startsWith('-')) fail(EXIT.USAGE, `unknown flag ${cmd}. Run \`trooth --help\`.`);
|
|
461
|
+
diag(helpText());
|
|
462
|
+
fail(EXIT.USAGE, `unknown command: ${cmd}. Commands: check, scan, eu, lint.`);
|
|
463
|
+
})().catch((e) => {
|
|
464
|
+
fail(EXIT.UPSTREAM, `unexpected failure: ${e && e.message ? e.message : e}`);
|
|
465
|
+
});
|
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trooth",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Trooth CLI.
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Trooth CLI. Read any company's witnessed record on the Trooth Network from your terminal (read-only, no key; facts and counts, never a number that sums a company up), and scan a Terraform plan against SOC 2 / ISO 27001 / GDPR / HIPAA / NIST AI RMF / EU AI Act. Advisory and report-only. Trooth never applies changes.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"bin":
|
|
6
|
+
"bin": {
|
|
7
|
+
"trooth": "bin/trooth.mjs"
|
|
8
|
+
},
|
|
7
9
|
"files": [
|
|
8
10
|
"bin/",
|
|
9
11
|
"README.md"
|
|
@@ -13,14 +15,15 @@
|
|
|
13
15
|
},
|
|
14
16
|
"scripts": {
|
|
15
17
|
"check": "node ./bin/trooth.mjs check",
|
|
16
|
-
"scan": "node ./bin/trooth.mjs scan"
|
|
18
|
+
"scan": "node ./bin/trooth.mjs scan",
|
|
19
|
+
"test": "node tests/cli.test.mjs"
|
|
17
20
|
},
|
|
18
21
|
"license": "MIT",
|
|
19
22
|
"author": "Trooth, LLC",
|
|
20
23
|
"homepage": "https://trooth.co",
|
|
21
24
|
"repository": {
|
|
22
25
|
"type": "git",
|
|
23
|
-
"url": "https://github.com/trooth/trooth-cli.git"
|
|
26
|
+
"url": "git+https://github.com/trooth/trooth-cli.git"
|
|
24
27
|
},
|
|
25
28
|
"keywords": [
|
|
26
29
|
"trust",
|