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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +87 -105
- package/dist/dashboard-files.js +136 -0
- package/dist/http-auth.js +310 -0
- package/dist/http.js +94 -0
- package/dist/index.js +59 -3
- package/dist/reports/api.js +19 -0
- package/dist/reports/dashboards.js +308 -0
- package/dist/server.js +210 -3
- package/dist/widget/dashboard.html +946 -0
- package/dist/widget/report.html +722 -63
- package/dist/widget/view.js +32 -10
- package/package.json +6 -1
- package/skills/report-designer/transport.md +6 -2
- package/skills/report-runner/SKILL.md +20 -1
- package/skills/report-runner/dashboard-loader.js +76 -0
- package/skills/report-runner/reference.md +36 -0
- package/skills/report-runner/transport.md +6 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "salesforce-reports",
|
|
3
|
-
"version": "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
|
-
|
|
3
|
+
[](https://github.com/nelsben/salesforce-reports-mcp/actions/workflows/ci.yml)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**Ask Salesforce a question in plain words. Get the answer, the chart and the caveats.**
|
|
6
6
|
|
|
7
|
-
|
|
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:**
|
|
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
|
-
> *
|
|
13
|
+
> *A recorded Claude Code run through the local MCP server, on a small Developer Edition org ([rehearsal](docs/rehearsals/runner.md)).*
|
|
14
|
+
|
|
15
|
+

|
|
12
16
|
|
|
13
|
-
##
|
|
17
|
+
## The answers are in Salesforce. Most people cannot get them out.
|
|
14
18
|
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
23
|
+
## Three jobs, done by asking
|
|
25
24
|
|
|
26
|
-
**
|
|
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
|
-
|
|
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
|
-

|
|
39
|
+
## Numbers you can repeat in a meeting
|
|
43
40
|
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
+
Salesforce dashboards draw the same way, every component as its own chart.
|
|
85
50
|
|
|
86
|
-
The
|
|
51
|
+

|
|
87
52
|
|
|
88
|
-
|
|
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
|
-
|
|
55
|
+
Claude can find a Salesforce dashboard, read the numbers Salesforce last cached and refresh it when the data is old.
|
|
95
56
|
|
|
96
|
-
##
|
|
57
|
+
## Then keep it as a Claude dashboard
|
|
97
58
|
|
|
98
|
-
|
|
59
|
+
**Ask for a dashboard, and Claude turns the report it just ran into a page you can keep and share.**
|
|
99
60
|
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
+
## What you need, and what you do not
|
|
109
66
|
|
|
110
|
-
|
|
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
|
-
|
|
69
|
+
You need:
|
|
114
70
|
|
|
115
|
-
|
|
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
|
-
|
|
76
|
+
If your company restricts API access or the Salesforce CLI, that is the question to take to your admin.
|
|
118
77
|
|
|
119
|
-
|
|
|
78
|
+
| It does not need | Because |
|
|
120
79
|
| --- | --- |
|
|
121
|
-
|
|
|
122
|
-
|
|
|
123
|
-
|
|
|
124
|
-
|
|
|
125
|
-
|
|
|
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
|
-
##
|
|
86
|
+
## Run it on your own org
|
|
128
87
|
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
+
Then ask:
|
|
145
100
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
140
|
+
## Help
|
|
158
141
|
|
|
159
|
-
|
|
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
|
+
}
|