trooth 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +102 -48
  2. package/bin/trooth.mjs +258 -202
  3. package/package.json +18 -20
package/README.md CHANGED
@@ -1,13 +1,16 @@
1
1
  # trooth
2
2
 
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.
3
+ The Trooth Network from your terminal.
4
+
5
+ The Trooth Network gives every company one public, signed record: what it is,
6
+ what it sells, who runs it, where its data lives, how it handles AI, and what a
7
+ buyer needs before signing. DNS tells a machine where a company is. A TLS
8
+ certificate tells it the connection is authentic. Neither says anything about
9
+ the company itself. The Trooth Network is that layer, and this CLI reads it.
7
10
 
8
11
  **`check` reads only public, already-published records. No key, no account.**
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.**
12
+ **`lint` is entirely local: it opens files and opens no sockets.**
13
+ **Trooth publishes facts and counts, never one number that sums a company up.**
11
14
 
12
15
  ## Install / run
13
16
 
@@ -20,38 +23,39 @@ npm i -g trooth
20
23
  trooth check trooth.co
21
24
  ```
22
25
 
23
- Requires Node 18+ (uses built-in `fetch`).
26
+ Requires Node 18+ (uses built-in `fetch`). Zero dependencies.
24
27
 
25
28
  ## Commands
26
29
 
27
30
  | Command | What it does |
28
31
  | --- | --- |
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. |
32
+ | `trooth check <domain>` | Reads a company's 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 id, and the signature and key id you can verify. Prints an honest "not listed yet" when a company has no published record. |
33
+ | `trooth lint [path]` | Reads the infrastructure the given directory **declares** and prints those declarations as facts, plus a canonical SHA-256 digest of them. Entirely local and offline. |
33
34
  | `trooth --help` / `--version` | Help / version. |
34
35
 
35
36
  ## Flags
36
37
 
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
38
+ - `--json`: stdout carries exactly one JSON document and nothing else; every
39
+ diagnostic goes to stderr. On an error the document is
39
40
  `{"ok": false, "error": "...", "exit": N}`.
40
- - `--strict` (`scan` only): exit 1 if any check fails. Default is advisory: exit 0.
41
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.
42
+ Any other flag is a usage error (exit 2) that names the flag and lists the
43
+ known ones.
44
44
 
45
45
  ## Exit codes
46
46
 
47
47
  | Code | Meaning |
48
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 |
49
+ | 0 | ok: company listed; lint read at least one declaration; help / version |
50
+ | 1 | finding: company not listed; lint found nothing to read |
51
+ | 2 | usage error: missing argument, unknown flag or command, unreadable path |
52
+ | 3 | Trooth unreachable, non-2xx response, or malformed response |
53
+
54
+ A company that is not listed and a network that could not be read are different
55
+ answers and they exit differently, so a pipeline never reads a Trooth outage as
56
+ "this vendor is unverified".
53
57
 
54
- ## Examples
58
+ ## `trooth check`
55
59
 
56
60
  ```bash
57
61
  trooth check trooth.co
@@ -65,7 +69,7 @@ Standing: listed and witnessed witnessed 2026-08-30 first published 2026-08-
65
69
  Live probes 64/65 Attestations 27/35
66
70
  Probes are checks Trooth read for itself. Attestations are the company's own declarations.
67
71
  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
72
+ Badge bronze_rw_... Key ed25519-2026-01
69
73
 
70
74
  Recent witness events
71
75
  • 2026-08-30 rewitnessed 64 of 65 live probes re-run
@@ -87,7 +91,6 @@ trooth check trooth.co --json
87
91
  "witnessed_at": "2026-08-30T00:00:00Z",
88
92
  "first_published_at": "2026-08-01T00:00:00Z",
89
93
  "badge_id": "bronze_rw_...",
90
- "scan_id": "rw_...",
91
94
  "probes": { "passed": 64, "total": 65 },
92
95
  "attested": { "passed": 27, "total": 35 },
93
96
  "events": [ { "type": "rewitnessed", "at": "2026-08-30T00:00:00Z", "detail": "64 of 65 live probes re-run" } ],
@@ -98,46 +101,97 @@ trooth check trooth.co --json
98
101
  }
99
102
  ```
100
103
 
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.
104
+ Those are the only fields `check --json` emits. Anything else the feed happens
105
+ to carry is dropped on the way out, so a script written against this shape
106
+ keeps working — and a field that would carry a score, tier, grade, rank or
107
+ rating is dropped no matter what the feed sends.
108
+
109
+ ## `trooth lint`
110
+
111
+ `lint` reads what your infrastructure **declares** and reports it. It does not
112
+ judge it. There is no verdict, no pass mark, no severity and no score, and
113
+ nothing is checked against a named standard or regulation — declaring public
114
+ ingress is not a failing, because a load balancer is supposed to be public.
115
+ What the facts mean is your call.
103
116
 
104
117
  ```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
118
+ trooth lint ./infra
108
119
  ```
109
120
 
110
121
  ```
111
- Trooth Pre-Flight // advisory · report-only //
112
- Verdict: WARN pass 5 · fail 1 · n/a 2
113
-
114
- 1 finding(s):
122
+ trooth lint // local · offline · declarations only //
123
+ ./infra 14 declaration file(s) read
124
+ terraform 11 · kubernetes 3
125
+
126
+ Declared
127
+ Regions and zones eu-west-1, us-east-1
128
+ Storage declarations 9
129
+ of those declaring encryption 9
130
+ Logging declarations 4
131
+ Identity declarations 6
132
+ Open to any address (0.0.0.0/0, ::/0) 1
133
+ Marked public 2
134
+ Inline credential literals 0
135
+
136
+ Digest sha256:a963291330bce2ab…
137
+ ```
115
138
 
116
- ● MEDIUM S3 bucket allows public read
117
- ↳ SOC2 CC6.1, ISO 27001 A.8.3
139
+ It reads `.tf`, `.tf.json`, Kubernetes YAML (anything with `apiVersion` and
140
+ `kind`), `terraform show -json` plan files and Dockerfiles. It is a declaration
141
+ reader, not a full HCL parser, and it says so rather than pretending otherwise.
118
142
 
119
- Advisory. Analyzes declared infrastructure intent, not live production state. Trooth never applies changes.
120
- ```
143
+ **Nothing leaves the machine.** No file name, no line, no code and no value is
144
+ ever printed or transmitted — only counts, resource type names and region
145
+ strings. The credential count is a count: it never shows the literal it found.
121
146
 
122
- ## Notes
147
+ **The digest** is a SHA-256 over the canonical fact document with the timestamp
148
+ excluded, so the same tree always produces the same digest. Record it in CI, or
149
+ on your Trooth record, as evidence that a given state was observed — without
150
+ publishing the tree it came from.
123
151
 
124
- - `check` reads only the **public** witness directory. There is no key and no account, and
125
- it never sends anything about you. It is a read.
126
- - A "not listed" result is not a judgement. It means no witnessed record has been
127
- published for that domain yet. A company gets witnessed by listing at
128
- <https://trooth.co/get-started>.
129
- - `scan` sends only your **declared plan** to the Pre-Flight API; nothing is written back,
130
- and Trooth never touches live infrastructure.
131
- - Set `TROOTH_API` to point at a different base URL (defaults to `https://api.trooth.co`).
132
- - Set `NO_COLOR=1` to disable ANSI colour. Colour is already off when stdout is not a TTY.
152
+ ```bash
153
+ # In CI: keep the fact document as a build artifact.
154
+ trooth lint --json > trooth-attestation.json
155
+ ```
133
156
 
134
157
  ## Use it from an AI assistant
135
158
 
136
- The same public network powers Trooth's read-only MCP server, so ChatGPT, Claude, Cursor,
137
- or any MCP client can ask about a company in plain words:
159
+ The same public network powers Trooth's read-only MCP server, so ChatGPT,
160
+ Claude, Cursor, or any MCP client can ask about a company in plain words:
138
161
 
139
162
  ```
140
163
  https://api.trooth.co/public/mcp
141
164
  ```
142
165
 
166
+ ## Notes
167
+
168
+ - `check` reads only the **public** record. There is no key and no account, and
169
+ it never sends anything about you. It is a read.
170
+ - A "not listed" result is not a judgement. It means no record has been
171
+ published for that domain yet. A company gets one at
172
+ <https://trooth.co/get-started>.
173
+ - Set `TROOTH_API` to point at a different base URL (defaults to
174
+ `https://api.trooth.co`). `lint` ignores it: `lint` makes no requests.
175
+ - Set `NO_COLOR=1` to disable ANSI colour. Colour is already off when stdout is
176
+ not a TTY.
177
+
178
+ ## Changes in 0.4.0
179
+
180
+ Breaking, and deliberately so.
181
+
182
+ - **`scan` and `eu` are removed.** Trooth does not scan infrastructure against a
183
+ standard, does not issue a verdict and does not offer a compliance product.
184
+ Both commands now exit 2 with a sentence saying so, rather than "unknown
185
+ command", so an old CI job or an old README gets an explanation.
186
+ - **`lint` is real.** In 0.3.0 it shelled out to `@trooth/os`, which was never
187
+ published, so the command failed for everyone who ran it. It is now
188
+ implemented in this binary, entirely locally.
189
+ - **`scan_id` is gone from `check --json`.** Every other field is unchanged.
190
+ - **`--strict` is gone**, along with the only command that took it.
191
+ - The package description and keywords no longer name any certification,
192
+ standard or regulation, because Trooth does not offer one.
193
+
194
+ The Trooth Network is Trooth's only product. This CLI, the public API and the
195
+ MCP server are interfaces to it, not separate products.
196
+
143
197
  Trooth automates. Trooth never signs for you.
package/bin/trooth.mjs CHANGED
@@ -2,43 +2,42 @@
2
2
  // Trooth CLI. Command name: `trooth`
3
3
  // Copyright (c) 2026 Trooth, LLC. All rights reserved.
4
4
  //
5
- // Commands:
6
- // trooth check <domain> Read a company's witnessed record from the public Trooth
7
- // Network (read-only, no key, no account).
8
- // trooth scan <plan> Send a Terraform plan to Trooth Pre-Flight (advisory, report-only).
9
- // <plan> may be a `terraform show -json` JSON file, OR a binary
10
- // .tfplan (the CLI will run `terraform show -json` for you).
11
- // trooth eu ... Push EU AI Act evidence into Trooth from CI/CD (needs a token).
12
- // trooth lint [path] Local, read-only IaC drift check (never transmits your code).
13
- // trooth --help Show help. trooth --version Show version.
5
+ // The Trooth Network is one public record per company: identity, products and demos,
6
+ // domain and marketing links, people, documents, security and privacy posture,
7
+ // procurement terms, relationships and sub-processors — each one witnessed, signed
8
+ // and dated. This CLI is the terminal interface to that record.
14
9
  //
15
- // `check` reads only PUBLIC, already-published witness records. `scan` is ADVISORY and
16
- // REPORT-ONLY: Trooth never applies changes to your infrastructure, and the scan posts
17
- // only your declared plan. Trooth automates. Trooth never signs for you.
10
+ // Commands:
11
+ // trooth check <domain> Read a company's witnessed record from the public Trooth
12
+ // Network. Read-only. No key, no account, nothing sent about you.
13
+ // trooth lint [path] Read the infrastructure THIS repository declares and print
14
+ // the declared facts plus a canonical digest of them.
15
+ // Fully local. Offline. Your source never leaves the machine.
16
+ // trooth --help Show help. trooth --version Show version.
18
17
  //
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.
18
+ // WHAT THIS TOOL DOES NOT DO, ON PURPOSE:
19
+ // It does not score, rate, rank, grade or tier a company or a repository.
20
+ // It does not check anything against a named certification, standard or regulation.
21
+ // It does not produce a verdict, a pass mark or a percentage.
22
+ // It publishes facts and counts, reported apart, and never adds them into one number.
24
23
  //
25
24
  // 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)
25
+ // 0 ok (listed; lint read at least one declaration; help/version)
26
+ // 1 finding (domain not listed; lint found nothing to read)
27
+ // 2 usage error (missing argument, unknown flag or command, unreadable path)
28
+ // 3 network/upstream error (Trooth unreachable, non-2xx, malformed response)
30
29
  //
31
30
  // With --json, stdout carries exactly one JSON document and nothing else. Every
32
31
  // diagnostic goes to stderr.
33
32
 
34
- import { readFileSync, existsSync } from 'node:fs';
35
- import { execFileSync, spawnSync } from 'node:child_process';
33
+ import { readFileSync, existsSync, statSync, readdirSync } from 'node:fs';
34
+ import { createHash } from 'node:crypto';
36
35
  import { createRequire } from 'node:module';
37
- import { dirname } from 'node:path';
36
+ import { join, relative, extname, basename } from 'node:path';
38
37
 
39
38
  const API = process.env.TROOTH_API || 'https://api.trooth.co';
40
39
  const require = createRequire(import.meta.url);
41
- let VERSION = '0.3.0';
40
+ let VERSION = '0.4.0';
42
41
  try { VERSION = require('../package.json').version; } catch {}
43
42
 
44
43
  const EXIT = { OK: 0, FINDING: 1, USAGE: 2, UPSTREAM: 3 };
@@ -58,21 +57,17 @@ function out(s) { process.stdout.write(s + '\n'); }
58
57
  function diag(s) { process.stderr.write(s + '\n'); }
59
58
  function emitJson(obj) { out(JSON.stringify(obj, null, 2)); }
60
59
 
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
60
  function fail(code, message, extra = {}) {
64
61
  diag(`${R}error${X} ${message}`);
65
62
  if (asJson) emitJson({ ok: false, error: message, exit: code, ...extra });
66
63
  process.exit(code);
67
64
  }
68
65
 
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. */
66
+ /** Keys that would carry a score, tier, grade, rank, rating or percentage. Nothing
67
+ * under these names is ever printed, in any mode, no matter what an upstream feed
68
+ * sends. `rate` is matched only as a whole key so keys like generatedAt survive. */
73
69
  const BANNED_KEY = /score|tier|grade|rank|rating|level|percent|^rate$/i;
74
70
 
75
- /** Recursively drop banned keys from any JSON-ish value. */
76
71
  function scrub(value) {
77
72
  if (Array.isArray(value)) return value.map(scrub);
78
73
  if (value && typeof value === 'object') {
@@ -90,13 +85,18 @@ function scrub(value) {
90
85
 
91
86
  const FLAGS = {
92
87
  check: { bool: ['--json'], value: [] },
93
- scan: { bool: ['--json', '--strict'], value: [] },
94
- eu: { bool: ['--json'], value: ['--token', '--company', '--datasets', '--evals', '--drift'] },
95
- lint: { bool: [], value: [] },
88
+ lint: { bool: ['--json'], value: [] },
89
+ };
90
+
91
+ /** Commands that existed in an earlier release and are gone. Naming them explicitly
92
+ * means an old README, an old CI job or an old blog post gets a sentence that says
93
+ * what happened, instead of "unknown command". */
94
+ const RETIRED = {
95
+ scan: 'Trooth does not scan infrastructure against a standard and does not issue a verdict. `trooth lint` reads what your infrastructure declares and prints those facts, locally.',
96
+ eu: 'Trooth does not ingest regulation-specific evidence. Publish evidence on your company record at https://trooth.co/dashboard.',
97
+ preflight: 'Retired. `trooth lint` reads declared infrastructure facts locally instead.',
96
98
  };
97
99
 
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
100
  function parseArgs(command) {
101
101
  const spec = FLAGS[command];
102
102
  const flags = {};
@@ -129,38 +129,34 @@ function parseArgs(command) {
129
129
 
130
130
  function helpText() {
131
131
  return `
132
- ${J}${B}trooth${X} ${D}v${VERSION} · the trust network your terminal can check${X}
132
+ ${J}${B}trooth${X} ${D}v${VERSION} · the terminal interface to the Trooth Network${X}
133
133
 
134
134
  ${B}Usage${X}
135
135
  trooth check <domain> Read a company's witnessed record on the Trooth Network
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)
138
- trooth lint [path] Local read-only IaC drift check
136
+ trooth lint [path] Read what your infrastructure declares, locally. Offline.
139
137
  trooth --help | --version
140
138
 
141
139
  ${B}Examples${X}
142
140
  trooth check stripe.com ${D}# read a company's witnessed record${X}
143
141
  trooth check trooth.co --json ${D}# one JSON document on stdout, for scripting${X}
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}
142
+ trooth lint ./infra ${D}# read declared facts + print a canonical digest${X}
143
+ trooth lint --json >> attestation.json ${D}# the same facts, for a CI artifact${X}
146
144
 
147
145
  ${B}Flags${X}
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)
146
+ --json machine-readable JSON on stdout; diagnostics on stderr
150
147
 
151
148
  ${B}Exit codes${X}
152
- 0 ok 1 finding (not listed, or strict failures) 2 usage error 3 Trooth unreachable
149
+ 0 ok 1 finding (not listed / nothing declared) 2 usage error 3 Trooth unreachable
153
150
 
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.
156
- scan is advisory and report-only. Trooth automates. Trooth never signs for you.${X}
151
+ ${D}check reads only public, already-published records. No key, no account.
152
+ lint is entirely local: it opens files, and opens no sockets. Your source never leaves.
153
+ Trooth publishes facts and counts, never one number that sums a company up.
154
+ Trooth automates. Trooth never signs for you.${X}
157
155
  `;
158
156
  }
159
157
 
160
158
  /* -------------------------------------------------------------- fetch ---- */
161
159
 
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
160
  async function callTrooth(path, init, what) {
165
161
  let res;
166
162
  try {
@@ -181,15 +177,13 @@ async function callTrooth(path, init, what) {
181
177
 
182
178
  /* --------------------------------------------------------------- check ---- */
183
179
 
184
- /** Normalize a domain the way the Trooth Network does: strip scheme, path,
185
- * leading www, case, and trailing dot. */
186
180
  function normalizeDomain(input) {
187
181
  if (!input) return '';
188
182
  let s = String(input).trim().toLowerCase();
189
- s = s.replace(/^[a-z]+:\/\//, ''); // scheme
190
- s = s.replace(/\/.*$/, ''); // path/query/hash
191
- s = s.replace(/^www\./, ''); // leading www
192
- s = s.replace(/\.$/, ''); // trailing dot
183
+ s = s.replace(/^[a-z]+:\/\//, '');
184
+ s = s.replace(/\/.*$/, '');
185
+ s = s.replace(/^www\./, '');
186
+ s = s.replace(/\.$/, '');
193
187
  return s;
194
188
  }
195
189
 
@@ -206,9 +200,6 @@ function count(obj) {
206
200
  return { passed: obj.passed, total: obj.total };
207
201
  }
208
202
 
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
203
  function projectRecord(v, domain) {
213
204
  const events = Array.isArray(v.events)
214
205
  ? v.events.filter((e) => e && e.type).map((e) => ({ type: String(e.type), at: e.at, detail: e.detail }))
@@ -220,7 +211,6 @@ function projectRecord(v, domain) {
220
211
  witnessed_at: v.passed_at || null,
221
212
  first_published_at: v.first_published_at || null,
222
213
  badge_id: v.badge_id || null,
223
- scan_id: v.scan_id || null,
224
214
  probes: count(v.probes),
225
215
  attested: count(v.attested),
226
216
  events,
@@ -250,9 +240,9 @@ async function check() {
250
240
  } else {
251
241
  out(`\n${B}${domain}${X} ${D}//${X} ${A}not listed on the Trooth Network yet${X}`);
252
242
  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`);
243
+ out(`judgement. It simply has not been witnessed. A company gets a record by`);
244
+ out(`listing at ${X}${C}https://trooth.co/get-started${X}${D}: Trooth reads its public surface and`);
245
+ out(`publishes a signed, dated record that anyone — or any agent — can check.${X}\n`);
256
246
  }
257
247
  process.exit(EXIT.FINDING);
258
248
  }
@@ -279,7 +269,6 @@ async function check() {
279
269
 
280
270
  const ids = [];
281
271
  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
272
  if (rec.authority_key_id) ids.push(`${D}Key ${rec.authority_key_id}${X}`);
284
273
  if (ids.length) out(ids.join(' '));
285
274
 
@@ -295,157 +284,223 @@ async function check() {
295
284
  process.exit(EXIT.OK);
296
285
  }
297
286
 
298
- /* ---------------------------------------------------------------- scan ---- */
287
+ /* ---------------------------------------------------------------- lint ---- */
288
+ /* WHAT lint IS, AND WHAT IT IS CAREFULLY NOT.
289
+ *
290
+ * It reads the infrastructure a repository DECLARES and reports those
291
+ * declarations as facts: how many storage resources declare encryption, which
292
+ * regions appear, how many rules declare exposure to the whole internet. It
293
+ * does not judge them. There is no verdict, no pass mark, no severity, no
294
+ * score, and nothing is checked against a named standard or regulation.
295
+ * Declaring public ingress is not a failing; a load balancer is supposed to be
296
+ * public. What the facts mean is the reader's call.
297
+ *
298
+ * It opens files and opens no sockets. Nothing about the repository leaves the
299
+ * machine. The digest at the end is a SHA-256 over the canonical fact document
300
+ * with the timestamp excluded, so the same tree always produces the same digest
301
+ * and you can record it as evidence that a given state was observed, without
302
+ * publishing the tree it came from.
303
+ *
304
+ * It is a declaration reader, not an HCL parser: .tf.json, Kubernetes YAML and
305
+ * terraform plan JSON are parsed properly, and .tf files are read at the
306
+ * attribute level. That limit is stated in the output rather than hidden. */
307
+
308
+ const SKIP_DIRS = new Set([
309
+ 'node_modules', '.git', '.terraform', '.next', 'dist', 'build', 'vendor',
310
+ '.venv', 'venv', '__pycache__', '.cache', 'coverage', '.turbo',
311
+ ]);
312
+ const MAX_FILES = 5000;
313
+ const MAX_BYTES = 4 * 1024 * 1024;
314
+
315
+ function walk(root) {
316
+ const found = [];
317
+ const stack = [root];
318
+ let st;
319
+ try { st = statSync(root); } catch { return found; }
320
+ if (st.isFile()) return [root];
321
+ while (stack.length && found.length < MAX_FILES) {
322
+ const dir = stack.pop();
323
+ let entries;
324
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { continue; }
325
+ for (const e of entries) {
326
+ if (e.isDirectory()) { if (!SKIP_DIRS.has(e.name)) stack.push(join(dir, e.name)); continue; }
327
+ if (!e.isFile()) continue;
328
+ const n = e.name.toLowerCase();
329
+ const ext = extname(n);
330
+ const keep =
331
+ ext === '.tf' || n.endsWith('.tf.json') ||
332
+ ext === '.yaml' || ext === '.yml' || ext === '.json' ||
333
+ n === 'dockerfile' || n.startsWith('dockerfile.');
334
+ if (keep) found.push(join(dir, e.name));
335
+ if (found.length >= MAX_FILES) break;
336
+ }
337
+ }
338
+ return found;
339
+ }
299
340
 
300
- function loadPlan(file) {
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}`);
303
- let raw = '';
304
- try { raw = readFileSync(file, 'utf8'); } catch (e) { fail(EXIT.USAGE, `cannot read ${file}: ${e.message}`); }
305
- try { return JSON.parse(raw); } catch { /* not JSON, likely a binary .tfplan */ }
306
- // Fall back: ask Terraform to render the plan as JSON.
307
- try {
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.`);
341
+ const REGION_RE = /\b(?:region|location|availability_zone|aws_region)\s*[:=]\s*["']([A-Za-z0-9][A-Za-z0-9._-]{2,40})["']/g;
342
+ const RESOURCE_RE = /\bresource\s+"([a-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
343
+ const ENCRYPT_RE = /(?:encrypted|encryption|kms_key|sse_algorithm|server_side_encryption|encrypt_at_rest)/i;
344
+ // Classification runs on the RESOURCE TYPE TOKEN (aws_db_instance, not the body
345
+ // text), matched as a substring. An earlier version wrapped these in \b, which
346
+ // never fires inside a snake_case identifier: \bdynamodb\b cannot match
347
+ // aws_dynamodb_table because the underscore either side is a word character, so
348
+ // every storage resource whose name was not the bare word "bucket" went
349
+ // uncounted. Substring matching on the type is both simpler and correct.
350
+ const STORAGE_TYPE = /(bucket|storage|blob|disk|volume|filestore|_db|database|rds|dynamodb|_sql|redis|memcache|elasticache|efs|fileshare|cosmos|bigtable|spanner)/i;
351
+ const LOGGING_TYPE = /(log|trail|audit|monitor|insight|diagnostic)/i;
352
+ const IDENTITY_TYPE = /(iam|role|policy|service_account|serviceaccount|rbac|identity|access_key|keyring|secret)/i;
353
+ const OPEN_CIDR_RE = /(?:"0\.0\.0\.0\/0"|'0\.0\.0\.0\/0'|"::\/0"|'::\/0')/;
354
+ const PUBLIC_RE = /\b(?:publicly_accessible\s*[:=]\s*true|acl\s*[:=]\s*["']public-read|public_network_access_enabled\s*[:=]\s*true|type\s*:\s*LoadBalancer|type\s*:\s*NodePort)\b/;
355
+ // A literal that looks like a credential sitting in the file. Reported as a
356
+ // count only: no file name, no line, and never the value itself.
357
+ const SECRET_RE = /\b(?:password|secret|api[_-]?key|access[_-]?key|token|private[_-]?key)\s*[:=]\s*["'][^"'${}\n]{8,}["']/i;
358
+
359
+ function classify(file, text) {
360
+ const n = basename(file).toLowerCase();
361
+ if (n.endsWith('.tf')) return 'terraform';
362
+ if (n.endsWith('.tf.json')) return 'terraform';
363
+ if (n === 'dockerfile' || n.startsWith('dockerfile.')) return 'container';
364
+ if (n.endsWith('.yaml') || n.endsWith('.yml')) {
365
+ return /^\s*apiVersion\s*:/m.test(text) && /^\s*kind\s*:/m.test(text) ? 'kubernetes' : null;
313
366
  }
367
+ if (n.endsWith('.json')) {
368
+ return /"terraform_version"\s*:/.test(text) &&
369
+ (/"planned_values"\s*:/.test(text) || /"resource_changes"\s*:/.test(text)) ? 'terraform-plan' : null;
370
+ }
371
+ return null;
314
372
  }
315
373
 
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
- };
374
+ /** Canonical JSON: object keys sorted at every depth, so the digest depends on
375
+ * the facts and not on the order a walk happened to produce them. */
376
+ function canonical(v) {
377
+ if (Array.isArray(v)) return '[' + v.map(canonical).join(',') + ']';
378
+ if (v && typeof v === 'object') {
379
+ return '{' + Object.keys(v).sort().map((k) => JSON.stringify(k) + ':' + canonical(v[k])).join(',') + '}';
380
+ }
381
+ return JSON.stringify(v === undefined ? null : v);
335
382
  }
336
383
 
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;
359
- if (findings.length) {
360
- out(`\n${B}${findings.length} finding(s):${X}\n`);
361
- for (const f of findings.slice(0, 50)) {
362
- const sev = String(f.severity || '').toLowerCase();
363
- const tag = sev === 'critical' || sev === 'high' ? `${R}● ${sev.toUpperCase()}${X}`
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)}`);
384
+ function lint() {
385
+ const { positional } = parseArgs('lint');
386
+ if (positional.length > 1) fail(EXIT.USAGE, `lint takes one optional [path], got: ${positional.join(' ')}`);
387
+ const target = positional[0] || '.';
388
+ if (!existsSync(target)) fail(EXIT.USAGE, `path not found: ${target}`);
389
+
390
+ const files = walk(target);
391
+ const byKind = { terraform: 0, 'terraform-plan': 0, kubernetes: 0, container: 0 };
392
+ const regions = new Set();
393
+ const resourceTypes = new Map();
394
+ let read = 0, storage = 0, storageEncrypted = 0, logging = 0, identity = 0;
395
+ let openIngress = 0, publicAccess = 0, inlineCredentials = 0;
396
+
397
+ for (const f of files) {
398
+ let text;
399
+ try {
400
+ if (statSync(f).size > MAX_BYTES) continue;
401
+ text = readFileSync(f, 'utf8');
402
+ } catch { continue; }
403
+ const kind = classify(f, text);
404
+ if (!kind) continue;
405
+ byKind[kind]++;
406
+ read++;
407
+
408
+ for (const m of text.matchAll(REGION_RE)) regions.add(m[1]);
409
+
410
+ if (kind === 'terraform') {
411
+ for (const m of text.matchAll(RESOURCE_RE)) {
412
+ resourceTypes.set(m[1], (resourceTypes.get(m[1]) || 0) + 1);
413
+ }
367
414
  }
368
- if (findings.length > 50) out(` ${D}… ${findings.length - 50} more. Use --json for the full list.${X}`);
369
- } else {
370
- out(`\n${J}✓ No findings in the declared plan.${X}`);
415
+
416
+ // Terraform is split on top-level resource blocks so an attribute in one
417
+ // resource is never credited to another; every other kind is read whole.
418
+ const blocks = kind === 'terraform' ? text.split(/\n(?=resource\s+")/) : [text];
419
+ for (const b of blocks) {
420
+ const header = b.match(/^resource\s+"([a-z0-9_]+)"/);
421
+ const subject = header ? header[1] : b;
422
+ if (STORAGE_TYPE.test(subject)) { storage++; if (ENCRYPT_RE.test(b)) storageEncrypted++; }
423
+ if (LOGGING_TYPE.test(subject)) logging++;
424
+ if (IDENTITY_TYPE.test(subject)) identity++;
425
+ if (OPEN_CIDR_RE.test(b)) openIngress++;
426
+ if (PUBLIC_RE.test(b)) publicAccess++;
427
+ }
428
+ for (const line of text.split('\n')) if (SECRET_RE.test(line)) inlineCredentials++;
429
+ }
430
+
431
+ if (read === 0) {
432
+ const msg = `no infrastructure declarations found under ${target}.`;
433
+ diag(`${A}nothing to read${X} ${msg}`);
434
+ diag(`${D} lint reads .tf, .tf.json, Kubernetes YAML (apiVersion + kind), terraform plan`);
435
+ diag(` JSON and Dockerfiles. ${files.length} file(s) were opened and none matched.${X}`);
436
+ if (asJson) emitJson({ ok: false, error: msg, exit: EXIT.FINDING, files_opened: files.length });
437
+ process.exit(EXIT.FINDING);
371
438
  }
372
- out(`\n${D}${report.disclaimer}${X}\n`);
373
- process.exit(exitCode);
374
- }
375
439
 
376
- /* ------------------------------------------------------------------ eu ---- */
377
- // `trooth eu` pushes EU AI Act evidence into Trooth from CI/CD (automated observability).
378
- // trooth eu --token <t> --datasets ds.json --evals ev.json --drift dr.json
379
- // Each file is a JSON array. Auth: TROOTH_TOKEN env or --token (bearer or x-trooth-company).
380
- async function eu() {
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}`); }
440
+ const topTypes = [...resourceTypes.entries()]
441
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
442
+ .slice(0, 12)
443
+ .map(([type, n]) => ({ type, count: n }));
444
+
445
+ const facts = {
446
+ declarations_read: read,
447
+ sources: Object.fromEntries(Object.entries(byKind).filter(([, n]) => n > 0)),
448
+ regions_and_zones_declared: [...regions].sort(),
449
+ resource_types: topTypes,
450
+ storage_declarations: storage,
451
+ storage_declaring_encryption: storageEncrypted,
452
+ logging_declarations: logging,
453
+ identity_declarations: identity,
454
+ declarations_open_to_any_address: openIngress,
455
+ declarations_marked_public: publicAccess,
456
+ inline_credential_literals: inlineCredentials,
389
457
  };
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' };
393
- if (token) headers['authorization'] = `Bearer ${token}`;
394
- if (company) headers['x-trooth-company'] = company;
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.',
458
+
459
+ const digest = createHash('sha256').update(canonical(facts)).digest('hex');
460
+ const doc = {
461
+ tool: 'trooth-lint',
462
+ cli_version: VERSION,
463
+ observed_at: new Date().toISOString(),
464
+ root: (() => { const r = relative(process.cwd(), target); return !r ? '.' : r.startsWith('..') ? target : r; })(),
465
+ facts,
466
+ digest: `sha256:${digest}`,
467
+ note: 'Declared facts only. Read locally; nothing was transmitted. No score, no verdict, no assessment against any standard.',
402
468
  };
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
469
 
410
- /* ---------------------------------------------------------------- lint ---- */
470
+ if (asJson) { emitJson(doc); process.exit(EXIT.OK); }
411
471
 
412
- const LINTER_PKG = '@trooth/os';
472
+ out(`\n${J}${B}trooth lint${X} ${D}// local · offline · declarations only //${X}`);
473
+ out(`${D}${doc.root} ${read} declaration file(s) read${X}`);
413
474
 
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;
430
- }
475
+ const src = Object.entries(facts.sources).map(([k, n]) => `${k} ${n}`).join(' · ');
476
+ if (src) out(`${D}${src}${X}`);
431
477
 
432
- async function lint() {
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}`);
478
+ out(`\n${B}Declared${X}`);
479
+ const row = (label, value) => out(` ${label.padEnd(40)} ${value}`);
480
+ row('Regions and zones', facts.regions_and_zones_declared.length ? facts.regions_and_zones_declared.join(', ') : `${D}none declared${X}`);
481
+ row('Storage declarations', `${facts.storage_declarations}`);
482
+ row(' of those declaring encryption', `${facts.storage_declaring_encryption}`);
483
+ row('Logging declarations', `${facts.logging_declarations}`);
484
+ row('Identity declarations', `${facts.identity_declarations}`);
485
+ row('Open to any address (0.0.0.0/0, ::/0)', `${facts.declarations_open_to_any_address}`);
486
+ row('Marked public', `${facts.declarations_marked_public}`);
487
+ row('Inline credential literals', `${facts.inline_credential_literals}`);
437
488
 
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);
489
+ if (topTypes.length) {
490
+ out(`\n${B}Most declared resource types${X}`);
491
+ for (const t of topTypes.slice(0, 6)) out(` ${String(t.count).padStart(4)} ${D}${t.type}${X}`);
445
492
  }
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);
493
+
494
+ out(`\n${B}Digest${X} ${C}${doc.digest}${X}`);
495
+ out(`${D}A SHA-256 over the facts above, in canonical form, with the timestamp excluded.`);
496
+ out(`The same tree always produces the same digest, so you can record it as evidence`);
497
+ out(`that a state was observed without publishing the tree it came from.${X}`);
498
+
499
+ out(`\n${D}Counts of what the files declare. Not a judgement: a public load balancer is`);
500
+ out(`supposed to be public. Trooth issues no verdict here and checks nothing against`);
501
+ out(`any standard. Nothing left this machine: lint opens files and opens no sockets.`);
502
+ out(`Publish what you choose on your record at ${X}${C}https://trooth.co/dashboard${X}${D}.${X}\n`);
503
+ process.exit(EXIT.OK);
449
504
  }
450
505
 
451
506
  /* ---------------------------------------------------------------- main ---- */
@@ -454,12 +509,13 @@ async function lint() {
454
509
  if (cmd === '--version' || cmd === '-v' || cmd === 'version') { out(VERSION); return; }
455
510
  if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') { out(helpText()); return; }
456
511
  if (cmd === 'check') return check();
457
- if (cmd === 'scan') return scan();
458
- if (cmd === 'eu') return eu();
459
512
  if (cmd === 'lint') return lint();
513
+ if (Object.prototype.hasOwnProperty.call(RETIRED, cmd)) {
514
+ fail(EXIT.USAGE, `\`trooth ${cmd}\` is retired. ${RETIRED[cmd]} Run \`trooth --help\`.`);
515
+ }
460
516
  if (cmd.startsWith('-')) fail(EXIT.USAGE, `unknown flag ${cmd}. Run \`trooth --help\`.`);
461
517
  diag(helpText());
462
- fail(EXIT.USAGE, `unknown command: ${cmd}. Commands: check, scan, eu, lint.`);
518
+ fail(EXIT.USAGE, `unknown command: ${cmd}. Commands: check, lint.`);
463
519
  })().catch((e) => {
464
520
  fail(EXIT.UPSTREAM, `unexpected failure: ${e && e.message ? e.message : e}`);
465
521
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "trooth",
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.",
3
+ "version": "0.4.1",
4
+ "description": "The Trooth Network from your terminal. Read any company's public, signed record with no key and no account: identity, products, terms, people, documents, security and AI posture. Also reads what your own repo declares, offline. No score, no verdict.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "trooth": "bin/trooth.mjs"
@@ -15,33 +15,31 @@
15
15
  },
16
16
  "scripts": {
17
17
  "check": "node ./bin/trooth.mjs check",
18
- "scan": "node ./bin/trooth.mjs scan",
19
- "test": "node tests/cli.test.mjs"
18
+ "lint": "node ./bin/trooth.mjs lint"
20
19
  },
21
20
  "license": "MIT",
22
21
  "author": "Trooth, LLC",
23
22
  "homepage": "https://trooth.co",
24
- "repository": {
25
- "type": "git",
26
- "url": "git+https://github.com/trooth/trooth-cli.git"
23
+ "bugs": {
24
+ "url": "https://trooth.co/contact"
27
25
  },
28
26
  "keywords": [
29
- "trust",
30
- "vendor-trust",
31
- "trust-network",
32
- "company-check",
27
+ "trooth",
28
+ "trooth-network",
29
+ "company-lookup",
30
+ "vendor-lookup",
33
31
  "due-diligence",
32
+ "procurement",
33
+ "trust-network",
34
+ "company-directory",
34
35
  "mcp",
35
36
  "ai-agents",
36
- "compliance",
37
- "soc2",
38
- "iso27001",
39
- "gdpr",
40
- "hipaa",
41
- "nist-ai-rmf",
42
- "eu-ai-act",
37
+ "agent-tools",
38
+ "signed-records",
39
+ "provenance",
40
+ "infrastructure-as-code",
43
41
  "terraform",
44
- "iac",
45
- "trooth"
42
+ "kubernetes",
43
+ "cli"
46
44
  ]
47
45
  }