salesforce-reports-mcp 0.2.0 → 0.4.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "salesforce-reports",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Explore, design and deploy Salesforce reports with Claude: three skills and an MCP server over the Reports and Dashboards REST API.",
5
5
  "author": { "name": "nelsben" },
6
6
  "homepage": "https://github.com/nelsben/salesforce-reports-mcp",
package/README.md CHANGED
@@ -1,29 +1,30 @@
1
1
  # salesforce-reports-mcp
2
2
 
3
- Claude finds, builds, runs and saves Salesforce reports. You ask a question in plain words and get the answer, with the report behind it.
3
+ [![ci](https://github.com/nelsben/salesforce-reports-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/nelsben/salesforce-reports-mcp/actions/workflows/ci.yml)
4
4
 
5
- This repo is working software, and a demonstration. It takes one useful job, reports, and delivers it to Claude three ways, from a server on your laptop to a feature that lives inside Salesforce. Every design choice is written down with what it costs. Built by [Ben Nelsen](https://github.com/nelsben).
5
+ **Ask Salesforce a question in plain words. Get the answer, the chart and the caveats.**
6
6
 
7
- > **You:** List the report types I can build on, then build me an opportunity report grouped by stage without saving it.
7
+ Claude finds, builds, runs and saves Salesforce reports, and reads your dashboards, from a server on your own machine. Nothing is installed in Salesforce and no Salesforce MCP licence is needed. It works with the Salesforce login, API access and report permissions you already have, through the Salesforce CLI.
8
+
9
+ > **You:** I'm a seller. How is our pipeline looking this fiscal year? Is there anything I should worry about, and can you show it to me by owner too?
8
10
  >
9
- > **Claude:** The org has 156 report types; here they are by category. Opportunities by stage: 8 open deals, $3,091,000. Negotiation/Review holds 3 of them and 39% of the value. Salesforce applied its default filters: close date this fiscal quarter, probability above 0. Nothing was saved. Want it across all close dates, or saved to your Private Reports?
11
+ > **Claude:** $3.09M across 8 open opportunities, here by stage and by owner. Worries: one owner holds 91% of the dollars, one deal alone is 29% of the pipeline, nothing is closed won this fiscal year, and one deal has no amount. Salesforce applied its default probability filter. Nothing was saved; say the word and I will save it as a report.
10
12
  >
11
- > *claude.ai, through the MCP server hosted by Salesforce, signed in as the user ([rehearsal](docs/rehearsals/runner-hosted.md)).*
13
+ > *A recorded Claude Code run through the local MCP server, on a small Developer Edition org ([rehearsal](docs/rehearsals/runner.md)).*
14
+
15
+ ![The report card for an unsaved opportunity report grouped by stage, drawn in light, then dark, then at phone width](docs/assets/report-view.gif)
12
16
 
13
- ## What this repo shows
17
+ ## The answers are in Salesforce. Most people cannot get them out.
14
18
 
15
- | Claim | Where to look | Evidence you can check |
16
- | --- | --- | --- |
17
- | **1. Three ways to connect Salesforce and Claude**, each with its own trade-offs | [Three ways to connect](#three-ways-to-connect) | One tool contract and result shape on every door (12 tools locally, 11 in the org); a [parity check](docs/decisions/0010-verification-by-parity.md) runs the same calls through two doors against a real org and compares them |
18
- | **2. A useful feature added to Salesforce itself**, packaged the way an admin expects | [A feature inside Salesforce](#a-feature-inside-salesforce) | Three package directories; layered Apex with a CI test that fails if the layers leak; 65 Apex tests at 98% coverage |
19
- | **3. Security, authentication and integration** done deliberately, per door | [Security and identity](#security-and-identity) | A different identity per door, a per-user JWT named credential with no stored secret, writes behind a custom permission, and a test that keeps org values out of the repo |
20
- | **4. Product and architecture judgment**, shown in the work | [How it was built](#how-it-was-built) | Personas and [experiences](docs/experiences.md), [6 probes](docs/probes/) before building, [14 decision records](docs/decisions/README.md), [live findings](docs/design.md), [7 rehearsals](docs/rehearsals/README.md) |
19
+ **Reports are how a company answers questions about its customers and its pipeline, and most of the people who need those answers cannot build one.**
21
20
 
22
- ## The product
21
+ A seller wants a number, not Report Builder. An analyst wants to try three versions of a report before saving one. An admin wants a report in another org or in git. Today each of those is a ticket, a wait, or a spreadsheet export. Here each is a sentence typed to Claude.
23
22
 
24
- **The problem.** Reports are how a Salesforce org answers questions, and they are hard to use. A seller wants a number, not Report Builder. An analyst wants to try three versions of a report before saving one. An admin wants a report in another org or in git. As of October 2026, Salesforce's own Claude skills and its standard hosted MCP servers had no report tools, and the closest open-source servers could run or create a report, but none could change, clone or combine one, or run one that was never saved.
23
+ ## Three jobs, done by asking
25
24
 
26
- **Three experiences.** Each is a Claude skill, and Claude picks the right one from what you say.
25
+ **The person with the question gets the answer. No one waits on the one colleague who knows Report Builder.**
26
+
27
+ Each is a Claude skill, and Claude picks the right one from what you say.
27
28
 
28
29
  | Skill | For | What Claude does |
29
30
  | --- | --- | --- |
@@ -35,129 +36,110 @@ This repo is working software, and a demonstration. It takes one useful job, rep
35
36
  >
36
37
  > **Claude:** Preview, with 8 deals under two owners. Saved as a new report in your Private Reports; the original is untouched. Link.
37
38
 
38
- **Answers you can trust.** Every run returns the same compact shape: the filters that were applied, including the ones Salesforce adds on its own, the groups and totals, and notes that say when rows were cut or Salesforce ignored part of the request. The [experiences page](docs/experiences.md) has the personas and the facts that shaped them.
39
-
40
- **A report card in the chat.** In Claude Desktop, a report run on the local server also draws as a card the way Salesforce draws it: the title, the filters, the groups with totals, a link to open it in Salesforce and, for an unsaved report, a button that asks Claude to save it. It is an MCP App that loads nothing from the network. Clients that show only text, such as Claude Code, get the same result as text ([decision 0013](docs/decisions/0013-report-view-as-an-mcp-app.md)).
41
-
42
- ![The report card for an unsaved opportunity report grouped by stage, drawn in light, then dark, then at phone width](docs/assets/report-view.gif)
39
+ ## Numbers you can repeat in a meeting
43
40
 
44
- *Drawn by the [render test](docs/decisions/0014-render-tests-through-a-test-host.md)'s host page from anonymised test data.*
41
+ **Every answer says what was counted, what was filtered and what was left out, so nobody presents a number they cannot explain.**
45
42
 
46
- ## Three ways to connect
47
-
48
- The same tools reach Claude three ways. They differ in where the code runs, who hosts it and how the user signs in.
49
-
50
- ```mermaid
51
- flowchart LR
52
- subgraph Laptop["Your machine"]
53
- CC["Claude Code / Desktop"]
54
- LS["Local MCP server<br/>(Node)"]
55
- CLI["sf CLI"]
56
- end
57
- subgraph Cloud["claude.ai"]
58
- CAI["Claude"]
59
- end
60
- subgraph SF["Salesforce org"]
61
- GW["Hosted MCP<br/>(Salesforce gateway)"]
62
- REST["ReportsApi<br/>(Apex REST)"]
63
- CORE["Reports service<br/>(Apex)"]
64
- API["Reports REST API"]
65
- end
66
- CC -- "1. stdio" --> LS -- "REST" --> API
67
- CC -- "2. skills + CLI" --> CLI -- "Apex REST" --> REST
68
- CAI -- "3. MCP over OAuth" --> GW --> REST
69
- REST --> CORE --> API
70
- ```
43
+ Every run returns the same compact shape: the filters that were applied, including the ones Salesforce adds on its own, the groups and totals, and notes that say when rows were cut or Salesforce ignored part of the request. The [experiences page](docs/experiences.md) has the personas and the facts that shaped them. Nothing is saved or changed unless you ask, and a read-only mode removes every write.
71
44
 
72
- | | 1. Local MCP server | 2. In-org REST door with skills | 3. Hosted MCP door |
73
- | --- | --- | --- | --- |
74
- | **Direction** | Claude reaches into Salesforce | Salesforce offers a feature; Claude calls it | Salesforce serves Claude directly |
75
- | **Runs** | Node on your machine | Apex in the org | Salesforce's MCP gateway, then the same Apex |
76
- | **Installed in the org** | Nothing | `force-app` | `force-app`, `mcp-app`, optionally `callout-app` |
77
- | **Signs in with** | Your `sf` CLI session or a token | Your own Salesforce session | OAuth with PKCE through an External Client App |
78
- | **Clients** | Claude Code, Claude Desktop, any MCP client | Claude Code and any agent with a shell; Flow, Agentforce, any HTTP client | claude.ai and any remote MCP client, with nothing to host |
79
- | **Tools** | All 12, delete only with a flag | 11, no delete | 11, no delete |
80
- | **Choose it when** | One person, no org change allowed | An admin wants the feature in the org, used from scripts and agents | A team wants Claude in the browser, governed by the org |
45
+ ## The answer arrives as a picture
81
46
 
82
- Each door gets the same tool names, the same input and the same result ([decision 0001](docs/decisions/0001-one-contract-three-doors.md)). The skills bind to tool names, not to a door, so one skill works over all three. [docs/architecture.md](docs/architecture.md) has the sequence diagrams.
47
+ **In Claude Desktop the result draws as a card in the chat: the headline, the filters, the groups and a button to save it.**
83
48
 
84
- ## A feature inside Salesforce
49
+ Salesforce dashboards draw the same way, every component as its own chart.
85
50
 
86
- The in-org doors are a Salesforce feature an admin installs, not a script. They ship as three package directories in [salesforce/](salesforce/README.md) ([decision 0009](docs/decisions/0009-two-shipping-units.md)):
51
+ ![The dashboard card for a pipeline dashboard: a metric, a gauge, a donut, bar, line, table, funnel and column charts](docs/assets/dashboard-card.png)
87
52
 
88
- | Package | Holds | Needs |
89
- | --- | --- | --- |
90
- | `force-app` | The Apex service, its tests, two permission sets and a custom permission | Nothing else; complete on its own |
91
- | `mcp-app` | The API Catalog entry, the MCP server definition and the External Client App Claude signs in through. No code. | `force-app` |
92
- | `callout-app` | The per-user named credential that lets the hosted door call the org's own API as the user. No code. | `force-app`; used by the hosted door |
53
+ *Drawn by the [render test](docs/decisions/0014-render-tests-through-a-test-host.md)'s host page from anonymised test data.*
93
54
 
94
- The Apex is layered as ports and adapters ([decision 0003](docs/decisions/0003-one-apex-class-behind-both-in-org-doors.md)). One service does the work; the REST class is only an adapter, and [a test](test/apex-layers.test.ts) fails CI if the core ever names it or sets an HTTP status. Reads use `Reports.ReportManager` inside the org; only what Apex cannot do (list report types, run an unsaved report, save) calls the org's own REST API ([decision 0004](docs/decisions/0004-reportmanager-first-callout-for-the-rest.md)). There is no delete in the org ([decision 0006](docs/decisions/0006-no-delete-in-the-org.md)).
55
+ Claude can find a Salesforce dashboard, read the numbers Salesforce last cached and refresh it when the data is old.
95
56
 
96
- ## Security and identity
57
+ ## Then keep it as a Claude dashboard
97
58
 
98
- Each door signs in differently, and each is least privilege by default.
59
+ **Ask for a dashboard, and Claude turns the report it just ran into a page you can keep and share.**
99
60
 
100
- | Door | Identity | What it can do |
101
- | --- | --- | --- |
102
- | Local | The user's `sf` CLI session, refetched on expiry; never written anywhere | Whatever the user can do. `--read-only` removes every write tool; `delete_report` exists only with `--allow-delete`. |
103
- | REST | The caller's own Salesforce session | Reads with `Salesforce_Reports`. Saves only with `Salesforce_Reports_Write`, checked in the service, not by hiding tools ([decision 0005](docs/decisions/0005-saves-behind-a-custom-permission.md)). |
104
- | Hosted MCP | OAuth with PKCE through the External Client App `SalesforceReportsMcp`, scopes `mcp_api` and `refresh_token` only | The same permission sets. For the calls that need the org's REST API, a per-user JWT named credential mints a token for the signed-in user. |
61
+ In one test on Claude Desktop for Windows in November 2026, a single request ran a pipeline report and built a page with four headline tiles, a chart by stage, the detail table and a note on what was counted, in under three minutes. The page is made with [Claude Data](https://support.claude.com/en/articles/17454700-get-started-with-claude-dashboards), Claude's own dashboard feature, so your Claude plan has to include it.
105
62
 
106
- Sharing, folder access and field-level security apply on every door, because every call runs as the user.
63
+ The page is a copy of the numbers as of the run, not a live view. Check it against the answer in the chat before you share it.
107
64
 
108
- **The hard part, and how it was solved.** Under a hosted MCP sign-in, Apex has no session, and asking for one throws an exception no `catch` block can stop. The first version leaked a raw platform error to claude.ai. The fix came in two steps, each with a decision record:
65
+ ## What you need, and what you do not
109
66
 
110
- 1. The hosted door got its own route, so the code knows which door called before it asks for a session, and answers a clear `CALLOUT_SESSION` instead ([decision 0007](docs/decisions/0007-callout-identity-under-hosted-mcp.md)).
111
- 2. A probe showed that a [per-user JWT named credential](docs/probes/v0.2-phase3c.md) lets the org mint a token for the running user from a certificate it made itself. No secret is stored, no user clicks anything, and the connector's own app keeps its narrow scopes ([decision 0012](docs/decisions/0012-per-user-jwt-named-credential.md)). `npm run callout:renew` renews the certificate.
67
+ **No new licence and no install in Salesforce. A person who can already run reports and use the Salesforce CLI can start asking.**
112
68
 
113
- No token, consumer key, org address or certificate reaches the repo, logs or error messages. The tracked metadata holds placeholders that setup scripts fill at deploy time, and a test enforces it.
69
+ You need:
114
70
 
115
- ## How it was built
71
+ - Claude Code or Claude Desktop. The dashboard step also needs a Claude plan that includes [Claude Data](https://support.claude.com/en/articles/17454700-get-started-with-claude-dashboards).
72
+ - Node.js 20 or later.
73
+ - The [Salesforce CLI](https://developer.salesforce.com/tools/salesforcecli), signed in to your org.
74
+ - A Salesforce user with API access and the Run Reports permission, plus Create and Customize Reports to save a report.
116
75
 
117
- The method was the same for every feature: probe the platform, decide in writing, build, then check against a real org.
76
+ If your company restricts API access or the Salesforce CLI, that is the question to take to your admin.
118
77
 
119
- | Step | What it produced |
78
+ | It does not need | Because |
120
79
  | --- | --- |
121
- | **Probe** before building | [6 probe write-ups](docs/probes/): can a hosted MCP server wrap Apex, can Apex call its own org's API, how big a result fits in the heap, which identity works under a hosted sign-in |
122
- | **Decide** in writing | [14 decision records](docs/decisions/README.md), each with what it costs and what was turned down |
123
- | **Build** to one contract | [design.md](docs/design.md): the input and output shapes, and the live findings where Salesforce did not behave as documented |
124
- | **Verify** against a real org | 161 unit and server tests, 8 render tests that draw the report card in a browser, 65 Apex tests, a live end-to-end check that leaves no report behind, and a parity check across doors |
125
- | **Rehearse** the experience | [7 rehearsals](docs/rehearsals/README.md): real Claude runs of each skill on each door, with what went wrong and what changed |
80
+ | A Salesforce MCP licence | The server talks to the Reports REST API that every org already has |
81
+ | Anything installed in the org | It runs on your machine |
82
+ | A separate integration user | It signs in as you, with the Salesforce CLI session you already have |
83
+ | New access rules | Every call runs as you, so sharing, folder access and field-level security apply as they do in Salesforce |
84
+ | A stored password or key | The session is read from the CLI when needed and never written anywhere |
126
85
 
127
- ## Get started
86
+ ## Run it on your own org
128
87
 
129
- | You want | Start here |
130
- | --- | --- |
131
- | Claude Code or Desktop on your own org, nothing installed in Salesforce | [Local MCP server](docs/install.md#the-local-mcp-server) |
132
- | The feature inside your org, called from the CLI, scripts or agents | [The REST door](docs/install.md#the-rest-door) |
133
- | Claude in the browser, signed in through Salesforce | [The MCP door](docs/install.md#the-mcp-door), then [the callout tools](docs/install.md#the-callout-tools-on-the-mcp-door) |
88
+ **Two commands, and Claude is working on your own data.**
134
89
 
135
- The quickest try is the Claude Code plugin, which bundles the skills and the local server:
90
+ Run them in Claude Code, with Node 20 and the Salesforce CLI signed in:
136
91
 
137
92
  ```
138
93
  claude plugin marketplace add nelsben/salesforce-reports-mcp
139
94
  claude plugin install salesforce-reports@salesforce-reports
140
95
  ```
141
96
 
142
- The tools, the result shape, the safety rails and the limits are in [docs/reference.md](docs/reference.md).
97
+ That installs the three skills and the MCP server. Claude uses your Salesforce CLI default org. To pick another org, start Claude Code with `SF_ORG=<alias> claude`. To remove every tool that can save or change something, add `SF_REPORTS_READ_ONLY=1` the same way.
143
98
 
144
- ## Develop
99
+ Then ask:
145
100
 
146
- ```
147
- npm install
148
- npm run build
149
- npm test
150
- npm run test:render # draw the report card in Chromium and compare screenshots
151
- npm run live -- --org <alias> --allow-delete # end-to-end run against an org you can write to
152
- npm run capture -- --org <alias> # refresh the test fixtures (anonymised)
153
- ```
101
+ - "How is our open pipeline looking by stage, and what should I worry about?"
102
+ - "Start from my pipeline report, group it by owner and save it as a new report."
103
+ - "Build me a dashboard of that to share with my team."
104
+
105
+ For Claude Desktop, [docs/claude-desktop.md](docs/claude-desktop.md) is the full recipe: the config entry with the two paths that trip people up, how to check it works, the safety flags, and what each Claude surface, including mobile, can do with a local server. For claude.ai and your phone, [From claude.ai and your phone](docs/install.md#from-claudeai-and-your-phone) runs the same server over HTTP behind a tunnel. [docs/install.md](docs/install.md#the-local-mcp-server) has every option.
106
+
107
+ ## Where your data goes
108
+
109
+ **Report data goes from your Salesforce org to your machine and into your Claude conversation. The server sends it nowhere else.**
110
+
111
+ - The server runs on your machine and calls only your own Salesforce org, as you. It has no analytics.
112
+ - What a report returns goes into your Claude conversation, under the terms of your Claude plan.
113
+ - Nothing is saved or changed in Salesforce unless you ask.
114
+ - A Claude dashboard is a copy of the numbers in your Claude account, private until you share it. Anyone you share it with sees those numbers, whatever their Salesforce access.
115
+
116
+ [Security and identity](docs/security.md#where-your-data-goes) has the detail.
154
117
 
155
- `npm run live` creates several private reports named `zz-live-check-*`, deletes them, and fails if any remain. Use a sandbox or scratch org.
118
+ ## Limits
119
+
120
+ **Know these before a number goes on a slide.**
121
+
122
+ - A Claude dashboard is a copy as of the run, not a live view.
123
+ - The report card draws amounts with a dollar sign in every org, whatever the org's currency.
124
+ - Salesforce returns at most 2,000 detail rows per run and 100 columns per report. Every answer says when rows were cut.
125
+ - Joined reports are not supported.
126
+ - Salesforce allows 500 report runs per org per hour.
127
+ - Salesforce adds default filters to some report types. They are listed in every answer, so check them against your question.
128
+
129
+ [The reference](docs/reference.md#limits) has the full list.
130
+
131
+ ## Go deeper
132
+
133
+ | Page | What it holds |
134
+ | --- | --- |
135
+ | [Field notes](docs/field-notes.md) | What else was built and tried: the four ways to connect, the feature inside Salesforce, how it was built and checked, and why |
136
+ | [Security and identity](docs/security.md) | Who each call runs as, what it can change, where your data goes, and how secrets are kept out |
137
+ | [Reference](docs/reference.md) | The tools, the result shape, the safety rails and the limits |
138
+ | [Decision records](docs/decisions/README.md) | Every design choice, with what it costs and what was turned down |
156
139
 
157
- ## Roadmap
140
+ ## Help
158
141
 
159
- - Dashboards: list, describe, refresh, and read component data.
160
- - Joined reports, report charts, and subscriptions.
142
+ Questions and problems go to [GitHub Issues](https://github.com/nelsben/salesforce-reports-mcp/issues).
161
143
 
162
144
  ## License
163
145
 
@@ -0,0 +1,136 @@
1
+ /**
2
+ * The dashboard-files command: runs report definitions over the local door, read-only, and writes one ReportResult
3
+ * JSON file per report, ready to attach to a Claude Dashboard. It is the `dashboard-files` subcommand of the server
4
+ * binary: `salesforce-reports-mcp dashboard-files --org <alias> --defs <file.json> --out <dir>`.
5
+ *
6
+ * The defs file is a JSON object mapping a short id to query_report arguments (the definition fields at the top
7
+ * level). The command never uses a default org and never prints a row value, a token or the instance URL. The last
8
+ * line is `dashboard-files: written=<n> failed=<m>`.
9
+ *
10
+ * Importing this module has no side effects. The command runs the reports through a child server: this package's own
11
+ * dist/index.js, read-only, on the local door.
12
+ */
13
+ import { readFileSync } from "node:fs";
14
+ import { mkdir, writeFile } from "node:fs/promises";
15
+ import { join, resolve } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ import { parseArgs } from "node:util";
18
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
19
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
20
+ export const USAGE = "Usage: salesforce-reports-mcp dashboard-files --org <alias> --defs <file.json> --out <dir>";
21
+ const ID_PATTERN = /^[A-Za-z0-9_-]+$/;
22
+ /** Checks the defs object. Returns the entries in file order, or the first problem, naming the id. */
23
+ export function validateDefs(defs) {
24
+ if (typeof defs !== "object" || defs === null || Array.isArray(defs)) {
25
+ return { error: "The defs file must be a JSON object mapping an id to query_report arguments." };
26
+ }
27
+ const entries = [];
28
+ for (const [id, value] of Object.entries(defs)) {
29
+ if (!ID_PATTERN.test(id))
30
+ return { error: `Bad id "${id}": use letters, digits, dash and underscore only.` };
31
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
32
+ return { error: `The value for id "${id}" must be an object of query_report arguments.` };
33
+ }
34
+ entries.push([id, value]);
35
+ }
36
+ return { entries };
37
+ }
38
+ /** The last line of the command. */
39
+ export const summaryLine = (written, failed) => `dashboard-files: written=${written} failed=${failed}`;
40
+ function rowCount(result) {
41
+ const detail = result.detail;
42
+ if (detail && typeof detail.rowCount === "number")
43
+ return detail.rowCount;
44
+ const groups = result.groups;
45
+ return Array.isArray(groups?.rows) ? groups.rows.length : 0;
46
+ }
47
+ /** Runs each definition in order, writes one file per success and always closes the client. */
48
+ export async function runDashboardFiles(options) {
49
+ const { client, defs, outDir, writer, print } = options;
50
+ let written = 0;
51
+ let failed = 0;
52
+ try {
53
+ await writer.mkdir(outDir);
54
+ for (const [id, args] of defs) {
55
+ let answer;
56
+ try {
57
+ answer = (await client.callTool({ name: "query_report", arguments: args }));
58
+ }
59
+ catch {
60
+ answer = undefined;
61
+ }
62
+ const result = answer?.structuredContent;
63
+ if (!answer || answer.isError || typeof result !== "object" || result === null) {
64
+ failed++;
65
+ print(`${id}: failed`);
66
+ continue;
67
+ }
68
+ await writer.writeFile(join(outDir, `${id}.json`), JSON.stringify(result, null, 2) + "\n");
69
+ written++;
70
+ const truncated = result.detail?.truncated === true;
71
+ print(`${id}: rows=${rowCount(result)} truncated=${truncated}`);
72
+ }
73
+ }
74
+ finally {
75
+ await client.close();
76
+ }
77
+ print(summaryLine(written, failed));
78
+ return { written, failed };
79
+ }
80
+ /**
81
+ * Runs the dashboard-files command on its arguments (everything after the subcommand name). Returns the exit code:
82
+ * 0 when every report was written, 1 when any failed or the server could not be reached, 2 on a usage error.
83
+ */
84
+ export async function dashboardFilesCommand(argv) {
85
+ let values;
86
+ try {
87
+ values = parseArgs({
88
+ args: argv,
89
+ options: { org: { type: "string" }, defs: { type: "string" }, out: { type: "string" } },
90
+ strict: true,
91
+ }).values;
92
+ }
93
+ catch {
94
+ console.error(USAGE);
95
+ return 2;
96
+ }
97
+ if (!values.org || !values.defs || !values.out) {
98
+ console.error(USAGE);
99
+ return 2;
100
+ }
101
+ let raw;
102
+ try {
103
+ raw = JSON.parse(readFileSync(values.defs, "utf8"));
104
+ }
105
+ catch {
106
+ console.error("The defs file could not be read as JSON.");
107
+ return 2;
108
+ }
109
+ const checked = validateDefs(raw);
110
+ if ("error" in checked) {
111
+ console.error(checked.error);
112
+ return 2;
113
+ }
114
+ const client = new Client({ name: "dashboard-files", version: "1.0.0" });
115
+ const transport = new StdioClientTransport({
116
+ command: process.execPath,
117
+ args: [fileURLToPath(new URL("./index.js", import.meta.url)), "--org", values.org, "--read-only"],
118
+ stderr: "ignore",
119
+ });
120
+ let failed = 1;
121
+ try {
122
+ await client.connect(transport);
123
+ ({ failed } = await runDashboardFiles({
124
+ client,
125
+ defs: checked.entries,
126
+ outDir: resolve(values.out),
127
+ writer: { mkdir: async (dir) => void (await mkdir(dir, { recursive: true })), writeFile: (path, text) => writeFile(path, text) },
128
+ print: (line) => console.log(line),
129
+ }));
130
+ }
131
+ catch {
132
+ console.error("dashboard-files: the server could not be started or reached.");
133
+ await client.close().catch(() => undefined);
134
+ }
135
+ return failed > 0 ? 1 : 0;
136
+ }