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.
Files changed (3) hide show
  1. package/README.md +81 -25
  2. package/bin/trooth.mjs +349 -191
  3. 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** trust
4
- standing from the [Trooth Network](https://trooth.co). Identity, security, privacy,
5
- AI practices, and more, each witnessed, signed, and dated. It also scans your own Terraform
6
- plans against SOC 2, ISO 27001, GDPR, HIPAA, NIST AI RMF, and the EU AI Act.
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 standing from the live Trooth Network (`GET https://api.trooth.co/directory/api/vendors`) and prints the score, the per-discipline breakdown, live probes, attestations, and the badge / scan IDs you can verify. Returns an honest "not listed yet" when a company has no published standing. |
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, score, and findings. Accepts a JSON file or a binary `.tfplan` (it runs `terraform show -json` for you). |
28
- | `trooth eu ...` | Pushes EU AI Act evidence (datasets, evals, drift) into Trooth from CI/CD. Needs a session token or company id. |
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: 91/100 Listed witnessed 2026-08-30
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
- Badge bronze_rw_... Scan rw_...
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
- A witnessed, point-in-time reading of public evidence. Not a certification.
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: raw JSON, exit code 1 when a company is not listed:
58
- trooth check acme.com --json
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
- ## Flags
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
- - `--json`: print the raw JSON record instead of the formatted view.
64
- - `--strict`: scan only, exit non-zero if there are findings (default: exit 0, advisory).
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 standing has been
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
- - Requires Node 18+ (uses built-in `fetch`).
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 trust standing from the
7
- // public Trooth Network (read-only, no key, no account).
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.2.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 help() {
33
- console.log(`
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 standing on the Trooth Network
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}# raw JSON, for scripting${X}
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 Print raw JSON instead of the formatted view
49
- --strict (scan) exit non-zero if findings exist (default: advisory, exit 0)
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 slash. */
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 domainArg = argv.find((a, i) => i > 0 && !a.startsWith('--'));
105
- const asJson = argv.includes('--json');
106
- const domain = normalizeDomain(domainArg);
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
- let res;
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
- console.log(`\n${B}${domain}${X} ${D}//${X} ${A}not listed on the Trooth Network yet${X}`);
137
- console.log(`\n${D}No witnessed standing has been published for this domain. That is not a`);
138
- console.log(`judgement. It simply has not been witnessed. A company gets witnessed by`);
139
- console.log(`listing at ${X}${C}https://trooth.co/get-started${X}${D}: the scan reads its public surface and`);
140
- console.log(`publishes a signed, dated standing that anyone can check.${X}\n`);
141
- process.exit(1);
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 (vendor.probes && typeof vendor.probes.total === 'number')
170
- line.push(`${B}Live probes${X} ${vendor.probes.passed}/${vendor.probes.total}`);
171
- if (vendor.attested && typeof vendor.attested.total === 'number')
172
- line.push(`${B}Attestations${X} ${vendor.attested.passed}/${vendor.attested.total}`);
173
- if (line.length) console.log(`\n${line.join(' ')}`);
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 (vendor.badge_id) ids.push(`${D}Badge ${vendor.badge_id}${X}`);
177
- if (vendor.scan_id) ids.push(`${D}Scan ${vendor.scan_id}${X}`);
178
- if (ids.length) console.log(ids.join(' '));
179
-
180
- const events = Array.isArray(vendor.events) ? vendor.events : [];
181
- if (events.length) {
182
- console.log(`\n${B}Recent witness events${X}`);
183
- for (const e of events.slice(0, 3)) {
184
- if (!e || !e.type) continue;
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
- console.log(`\n${D}A witnessed, point-in-time reading of public evidence. Not a certification.`);
190
- console.log(`Full record and directory: ${X}${C}https://trooth.co/network${X}${D} · Signing keys: ${API}/public/keys${X}\n`);
191
- process.exit(0);
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) { console.error(`${R}error${X} missing <plan> argument. Try: trooth scan ./plan.json`); process.exit(2); }
198
- if (!existsSync(file)) { console.error(`${R}error${X} file not found: ${file}`); process.exit(2); }
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) { console.error(`${R}error${X} cannot read ${file}: ${e.message}`); process.exit(2); }
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 out = execFileSync('terraform', ['show', '-json', file], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
206
- return JSON.parse(out);
207
- } catch (e) {
208
- console.error(`${R}error${X} ${file} is not JSON, and \`terraform show -json\` failed.`);
209
- console.error(`${D} Generate JSON first: terraform show -json ${file} > plan.json then trooth scan plan.json${X}`);
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
- async function scan() {
215
- const file = argv.find((a, i) => i > 0 && !a.startsWith('--'));
216
- const strict = argv.includes('--strict');
217
- const asJson = argv.includes('--json');
218
- const plan = loadPlan(file);
219
-
220
- let res;
221
- try {
222
- res = await fetch(`${API}/v1/preflight`, {
223
- method: 'POST',
224
- headers: { 'content-type': 'application/json', 'user-agent': `trooth-cli/${VERSION}` },
225
- body: JSON.stringify({ plan }),
226
- });
227
- } catch (e) {
228
- console.error(`${R}error${X} could not reach Trooth Pre-Flight at ${API}: ${e.message}`);
229
- process.exit(1);
230
- }
231
- if (!res.ok) {
232
- const body = await res.text().catch(() => '');
233
- console.error(`${R}error${X} Pre-Flight returned HTTP ${res.status}. ${body.slice(0, 300)}`);
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
- const score = delta.score ?? '-';
241
- const verdict = (delta.verdict || 'reviewed').toUpperCase();
242
- const s = delta.summary || {};
243
- console.log(`\n${J}${B}Trooth Pre-Flight${X} ${D}// advisory · report-only //${X}`);
244
- console.log(`Verdict: ${B}${verdict}${X} Score: ${B}${score}${typeof score === 'number' ? '/100' : ''}${X} ` +
245
- `${J}pass ${s.pass ?? 0}${X} · ${A}fail ${s.fail ?? 0}${X} · ${D}n/a ${s.notApplicable ?? 0}${X}`);
246
- const findings = delta.findings || [];
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
- console.log(`\n${B}${findings.length} finding(s):${X}\n`);
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
- console.log(` ${tag} ${B}${f.title || f.message || f.check || 'finding'}${X}`);
254
- if (f.control || f.frameworks) console.log(` ${J}↳${X} ${f.control || (Array.isArray(f.frameworks) ? f.frameworks.join(', ') : 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
- console.log(`\n${J}✓ No findings in the declared plan.${X}`);
370
+ out(`\n${J}✓ No findings in the declared plan.${X}`);
258
371
  }
259
- console.log(`\n${D}${delta.disclaimer || 'Advisory. Analyzes declared infrastructure intent, not live production state. Trooth never applies changes.'}${X}\n`);
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 flag = (name) => { const i = argv.indexOf(name); return i >= 0 ? argv[i + 1] : null; };
270
- const token = flag('--token') || process.env.TROOTH_TOKEN || '';
271
- const company = flag('--company') || process.env.TROOTH_COMPANY || '';
272
- if (!token && !company) { console.error(`${R}error${X} provide --token <session> or --company <id> (or TROOTH_TOKEN / TROOTH_COMPANY env).`); process.exit(2); }
273
- const load = (f) => { if (!f) return undefined; try { return JSON.parse(readFileSync(f, 'utf8')); } catch (e) { console.error(`${R}error${X} cannot read ${f}: ${e.message}`); process.exit(2); } };
274
- const body = { datasets: load(flag('--datasets')), evals: load(flag('--evals')), drift: load(flag('--drift')) };
275
- if (!body.datasets && !body.evals && !body.drift) { console.error(`${R}error${X} provide at least one of --datasets/--evals/--drift <file.json>`); process.exit(2); }
276
- const headers = { 'content-type': 'application/json', 'user-agent': `trooth-cli/${VERSION}` };
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
- let res;
280
- try { res = await fetch(`${API}/v1/eu/ingest`, { method: 'POST', headers, body: JSON.stringify(body) }); }
281
- catch (e) { console.error(`${R}error${X} could not reach Trooth at ${API}: ${e.message}`); process.exit(1); }
282
- if (!res.ok) { console.error(`${R}error${X} HTTP ${res.status}. ${(await res.text().catch(()=> '')).slice(0,300)}`); process.exit(1); }
283
- const out = await res.json();
284
- console.log(`\n${J}${B}Trooth EU AI Act${X} ${D}// evidence ingested //${X}`);
285
- console.log(`datasets ${B}${out.ingested?.datasets ?? 0}${X} · evals ${B}${out.ingested?.evals ?? 0}${X} · drift ${B}${out.ingested?.drift ?? 0}${X}`);
286
- console.log(`\n${D}${out.disclaimer || 'Advisory. Trooth automates, computes, and documents; it never signs.'}${X}\n`);
287
- process.exit(0);
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
- // Local, read-only .tf drift check. Reuses the published @trooth/os linter if present,
292
- // otherwise runs a built-in minimal ruleset. Never transmits your code.
293
- const path = argv[1] && !argv[1].startsWith('--') ? argv[1] : '.';
294
- const { spawnSync } = await import('node:child_process');
295
- const r = spawnSync('npx', ['--yes', '@trooth/os', path], { stdio: 'inherit' });
296
- process.exit(r.status ?? 0);
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') { console.log(VERSION); return; }
301
- if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') { help(); return; }
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
- console.error(`${R}error${X} unknown command: ${cmd}\n`); help(); process.exit(2);
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.2.0",
4
- "description": "Trooth CLI. Check any company's witnessed trust standing from your terminal (read-only, no key), 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.",
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": "bin/trooth.mjs",
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",