@meffecta/agent 1.0.3 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -1
- package/bin/meffecta-agent.js +65 -13
- package/deployment.env.example +24 -11
- package/engine.json +2 -2
- package/lib/analytics.js +262 -0
- package/lib/commands.js +225 -40
- package/lib/create-job.js +344 -0
- package/lib/doctor.js +20 -7
- package/lib/gcloud.js +25 -0
- package/lib/integrations.js +3 -3
- package/lib/resources.js +300 -0
- package/lib/verify-credentials.js +129 -0
- package/lib/version.js +90 -0
- package/package.json +3 -2
- package/scripts/create-project.sh +3 -1
- package/scripts/deploy.sh +19 -8
- package/scripts/lib/cli-names.sh +55 -0
- package/scripts/lib/deployment.sh +18 -3
- package/scripts/lib/read-json.mjs +1 -1
- package/scripts/lib/tasks-queue.sh +24 -0
- package/scripts/link-billing.sh +1 -1
- package/scripts/mint-gmail-token.mjs +7 -2
- package/scripts/mint-graph-token.mjs +7 -2
- package/scripts/set-secret.sh +1 -1
- package/scripts/setup-infrastructure.sh +187 -100
- package/scripts/{setup-scheduler.sh → sync-triggers.sh} +12 -16
- package/scripts/update-tooling.sh +3 -1
- package/scripts/verify-credentials.mjs +168 -49
|
@@ -32,12 +32,13 @@ set -euo pipefail
|
|
|
32
32
|
# is restarted for them anyway. scripts/deploy.sh does it automatically.
|
|
33
33
|
#
|
|
34
34
|
# Usage:
|
|
35
|
-
# scripts/
|
|
36
|
-
# scripts/
|
|
35
|
+
# scripts/sync-triggers.sh # from the content repo (./deployment.env)
|
|
36
|
+
# scripts/sync-triggers.sh --dry-run # print what it would create, change nothing
|
|
37
37
|
#
|
|
38
38
|
# Which deployment: ./deployment.env, or --config <file>. See deployment.env.example.
|
|
39
39
|
|
|
40
40
|
. "$(dirname "${BASH_SOURCE[0]}")/lib/deployment.sh"
|
|
41
|
+
. "$(dirname "${BASH_SOURCE[0]}")/lib/tasks-queue.sh"
|
|
41
42
|
|
|
42
43
|
DRY_RUN=false
|
|
43
44
|
QUEUE_NAME=""
|
|
@@ -133,7 +134,7 @@ for SECRET_VAR in AGENT_API_SECRET AGENT_WEBHOOK_SECRET; do
|
|
|
133
134
|
done
|
|
134
135
|
if [ -z "${API_SECRET}" ]; then
|
|
135
136
|
echo "Could not resolve AGENT_API_SECRET (or the old AGENT_WEBHOOK_SECRET) from ${SERVICE}." >&2
|
|
136
|
-
echo "
|
|
137
|
+
echo " $(cmd set-secret) AGENT_API_SECRET --random" >&2
|
|
137
138
|
echo "Refusing to create triggers that would 401 on every firing." >&2
|
|
138
139
|
exit 1
|
|
139
140
|
fi
|
|
@@ -148,25 +149,20 @@ if [ "${MIN_SCALE}" != "0" ] || [ "${CPU_THROTTLING}" = "false" ]; then
|
|
|
148
149
|
⚠️ ${SERVICE} still runs always-on (min-instances=${MIN_SCALE}, cpu-throttling=${CPU_THROTTLING}).
|
|
149
150
|
External triggers work either way, but the saving does not arrive until the service
|
|
150
151
|
scales to zero — and until then in-process timers and Cloud Scheduler would BOTH fire,
|
|
151
|
-
so every cron runs twice. The shape lives in deployment.env and
|
|
152
|
+
so every cron runs twice. The shape lives in deployment.env, and is asserted on every
|
|
153
|
+
rollout:
|
|
152
154
|
|
|
153
155
|
SCALING=scale-to-zero # in deployment.env, then
|
|
154
|
-
|
|
156
|
+
$(cmd deploy) # rolls it out (--dry-run to see the shape first)
|
|
155
157
|
|
|
156
158
|
EOF
|
|
157
159
|
fi
|
|
158
160
|
|
|
161
|
+
# Normally already there — setup-infrastructure.sh creates it with the deployment's other
|
|
162
|
+
# static resources. Ensured here too for a deployment provisioned before that was true, and
|
|
163
|
+
# because this script is also run on its own. See lib/tasks-queue.sh.
|
|
159
164
|
echo "📥 Cloud Tasks queue ${QUEUE}..."
|
|
160
|
-
|
|
161
|
-
run ${GC} tasks queues create "${QUEUE}" --location="${REGION}" --quiet
|
|
162
|
-
fi
|
|
163
|
-
# max-concurrent-dispatches leaves room for several held attempts at once; the engine's own
|
|
164
|
-
# queue is serial, so this bounds requests waiting, not jobs running.
|
|
165
|
-
run ${GC} tasks queues update "${QUEUE}" --location="${REGION}" \
|
|
166
|
-
--max-attempts=4 --min-backoff=5s --max-backoff=30s --max-doublings=2 \
|
|
167
|
-
--max-concurrent-dispatches=8 --max-dispatches-per-second=2 --quiet
|
|
168
|
-
run ${GC} tasks queues add-iam-policy-binding "${QUEUE}" --location="${REGION}" \
|
|
169
|
-
--member="serviceAccount:${RUNTIME_SA}" --role="roles/cloudtasks.enqueuer" --quiet
|
|
165
|
+
ensure_tasks_queue "${QUEUE}"
|
|
170
166
|
|
|
171
167
|
CURRENT_QUEUE=$(printf '%s' "${SERVICE_JSON}" | svc_field AGENT_TASKS_QUEUE | cut -f2)
|
|
172
168
|
CURRENT_URL=$(printf '%s' "${SERVICE_JSON}" | svc_field AGENT_PUBLIC_URL | cut -f2)
|
|
@@ -185,7 +181,7 @@ The engine is the only thing that knows which jobs exist — it reads them from
|
|
|
185
181
|
clone of the content repo. So the service has to be deployed and healthy first:
|
|
186
182
|
|
|
187
183
|
curl -fsS ${AGENT_URL}/health
|
|
188
|
-
|
|
184
|
+
$(cmd deploy)
|
|
189
185
|
|
|
190
186
|
A first request after a scale-to-zero idle also has to wait out a cold start, which
|
|
191
187
|
includes cloning the content repo.
|
|
@@ -30,6 +30,8 @@ set -euo pipefail
|
|
|
30
30
|
# Artifact Registry). A deployment using an AR remote repository instead still needs it
|
|
31
31
|
# here — once per engine version, rather than once per deploy.
|
|
32
32
|
|
|
33
|
+
. "$(dirname "${BASH_SOURCE[0]}")/lib/cli-names.sh"
|
|
34
|
+
|
|
33
35
|
GHCR_IMAGE="${GHCR_IMAGE:-ghcr.io/meffecta/agent}"
|
|
34
36
|
TAG="latest"
|
|
35
37
|
IMAGE=""
|
|
@@ -114,5 +116,5 @@ fi
|
|
|
114
116
|
# This script is one of the files being overwritten, and bash reads a script as it runs it.
|
|
115
117
|
# exec hands the process over so nothing further is read from the file underneath us.
|
|
116
118
|
echo "✅ Updated. Review with \`git diff\`, then deploy the matching engine:"
|
|
117
|
-
echo "
|
|
119
|
+
echo " $(cmd deploy)${TAG:+ --tag ${TAG}}"
|
|
118
120
|
exec cp -R "${TMP}/scripts/." "${DEST}/"
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// Never prints a credential value. Exits 1 if anything configured actually fails.
|
|
9
9
|
|
|
10
10
|
import { execFile } from "node:child_process";
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
11
13
|
|
|
12
14
|
const env = process.env;
|
|
13
15
|
const results = [];
|
|
@@ -45,12 +47,21 @@ async function metadataToken(scopes) {
|
|
|
45
47
|
|
|
46
48
|
async function checkGithub() {
|
|
47
49
|
if (!env.GITHUB_TOKEN) {
|
|
48
|
-
|
|
50
|
+
// Not a shrug: the service clones the content repo at boot, so unset is an outage.
|
|
51
|
+
return fail("GITHUB_TOKEN", "unset — the service cannot clone its content repo without it");
|
|
49
52
|
}
|
|
50
|
-
// The content repo (from GIT_REPO_URL, when it
|
|
51
|
-
//
|
|
53
|
+
// The content repo (from GIT_REPO_URL, when it is a GitHub URL), plus any working repo
|
|
54
|
+
// this deployment names in a *_GIT_URL variable — its jobs clone those, so the token has
|
|
55
|
+
// to reach them. Nothing here may name a company: this script ships to every deployment.
|
|
52
56
|
const contentRepo = env.GIT_REPO_URL?.match(/github\.com\/([^/]+\/[^/.]+)/)?.[1];
|
|
53
|
-
const
|
|
57
|
+
const named = Object.entries(env)
|
|
58
|
+
.filter(([k]) => k.endsWith("_GIT_URL"))
|
|
59
|
+
.map(([, v]) => v?.match(/github\.com\/([^/]+\/[^/.]+)/)?.[1])
|
|
60
|
+
.filter(Boolean);
|
|
61
|
+
const repos = [...new Set([...(contentRepo ? [contentRepo] : []), ...named])];
|
|
62
|
+
if (repos.length === 0) {
|
|
63
|
+
return skip("GITHUB_TOKEN", "set, but no GitHub repo to check it against");
|
|
64
|
+
}
|
|
54
65
|
const codes = await Promise.all(
|
|
55
66
|
repos.map(
|
|
56
67
|
async (r) =>
|
|
@@ -87,35 +98,38 @@ async function checkResend() {
|
|
|
87
98
|
: fail("RESEND_API_KEY", `domains → ${status}`);
|
|
88
99
|
}
|
|
89
100
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Every read-only Postgres URL this deployment declares, by the documented convention
|
|
103
|
+
* <WORLD>_POSTGRES_URL_READONLY — the variable names belong to the deployment, so they are
|
|
104
|
+
* discovered rather than listed here.
|
|
105
|
+
*/
|
|
106
|
+
// There is deliberately no check for a deployment's own product API. Probing one means
|
|
107
|
+
// knowing its path and its response shape — /v1/internal/leads was SweepOS's — and this
|
|
108
|
+
// script ships to every deployment. A credential the engine's own skills use is fair game
|
|
109
|
+
// here; a company's API is that company's tooling.
|
|
100
110
|
function checkPostgres() {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
111
|
+
const urls = Object.keys(env).filter((k) => k.endsWith("_POSTGRES_URL_READONLY") && env[k]);
|
|
112
|
+
if (urls.length === 0) {
|
|
113
|
+
skip("POSTGRES (read-only)", "no <WORLD>_POSTGRES_URL_READONLY set");
|
|
114
|
+
return Promise.resolve();
|
|
115
|
+
}
|
|
116
|
+
return Promise.all(
|
|
117
|
+
urls.map(
|
|
118
|
+
(name) =>
|
|
119
|
+
new Promise((resolve) => {
|
|
120
|
+
execFile("psql", [env[name], "-Atc", "SELECT 1"], { timeout: 10_000 }, (err, stdout) => {
|
|
121
|
+
if (err) {
|
|
122
|
+
err.code === "ENOENT"
|
|
123
|
+
? skip(name, "psql not installed here")
|
|
124
|
+
: fail(name, err.message.split("\n")[0].slice(0, 120));
|
|
125
|
+
} else {
|
|
126
|
+
stdout.trim() === "1" ? ok(name) : fail(name, "unexpected result");
|
|
127
|
+
}
|
|
128
|
+
resolve();
|
|
129
|
+
});
|
|
130
|
+
}),
|
|
131
|
+
),
|
|
132
|
+
);
|
|
119
133
|
}
|
|
120
134
|
|
|
121
135
|
async function checkPosthog() {
|
|
@@ -138,11 +152,13 @@ async function checkAhrefs() {
|
|
|
138
152
|
if (!env.AHREFS_API_KEY) {
|
|
139
153
|
return skip("AHREFS_API_KEY", "unset");
|
|
140
154
|
}
|
|
141
|
-
|
|
155
|
+
// example.com rather than a domain belonging to any deployment: this only proves the key
|
|
156
|
+
// is accepted, and the answer for a real domain is a job's business, not a doctor's.
|
|
157
|
+
const { status, json } = await http("https://api.ahrefs.com/v3/public/domain-rating-free?target=example.com", {
|
|
142
158
|
headers: { Authorization: `Bearer ${env.AHREFS_API_KEY}` },
|
|
143
159
|
});
|
|
144
160
|
status === 200
|
|
145
|
-
? ok("AHREFS_API_KEY", `
|
|
161
|
+
? ok("AHREFS_API_KEY", `example.com DR ${json?.domain_rating?.domain_rating ?? "?"}`)
|
|
146
162
|
: fail("AHREFS_API_KEY", `domain-rating-free → ${status}`);
|
|
147
163
|
}
|
|
148
164
|
|
|
@@ -508,8 +524,11 @@ async function checkGa4() {
|
|
|
508
524
|
}
|
|
509
525
|
|
|
510
526
|
async function checkGoogleAds() {
|
|
511
|
-
|
|
512
|
-
|
|
527
|
+
// Any world's customer id proves the developer token and the manager link; which world
|
|
528
|
+
// is this deployment's business, so take the first one it declares.
|
|
529
|
+
const customerId = Object.entries(env).find(([k, v]) => k.endsWith("_GOOGLE_ADS_CUSTOMER_ID") && v)?.[1];
|
|
530
|
+
if (!env.GOOGLE_ADS_DEVELOPER_TOKEN || !customerId) {
|
|
531
|
+
return skip("GOOGLE ADS", "developer token / <WORLD>_GOOGLE_ADS_CUSTOMER_ID not set");
|
|
513
532
|
}
|
|
514
533
|
if (!onCloudRun) {
|
|
515
534
|
return skip("GOOGLE ADS", "needs the metadata server — run on Cloud Run");
|
|
@@ -525,7 +544,7 @@ async function checkGoogleAds() {
|
|
|
525
544
|
headers["login-customer-id"] = env.GOOGLE_ADS_LOGIN_CUSTOMER_ID;
|
|
526
545
|
}
|
|
527
546
|
const { status, json } = await http(
|
|
528
|
-
`https://googleads.googleapis.com/v25/customers/${
|
|
547
|
+
`https://googleads.googleapis.com/v25/customers/${customerId}/googleAds:searchStream`,
|
|
529
548
|
{ method: "POST", headers, body: JSON.stringify({ query: "SELECT customer.id FROM customer LIMIT 1" }) },
|
|
530
549
|
);
|
|
531
550
|
status === 200
|
|
@@ -554,9 +573,46 @@ async function checkDeploymentsContext() {
|
|
|
554
573
|
: fail("Deployments context (run.viewer)", `list → ${status}`);
|
|
555
574
|
}
|
|
556
575
|
|
|
576
|
+
/**
|
|
577
|
+
* The handful of credentials without which there is no agent at all — as opposed to an
|
|
578
|
+
* integration being unavailable, which degrades one job. A deployment can be missing every
|
|
579
|
+
* other credential here and still work; missing one of these and it is simply down.
|
|
580
|
+
*
|
|
581
|
+
* These are also the ones a shipped script CAN test properly, because they are the
|
|
582
|
+
* engine's own and mean the same thing in every deployment.
|
|
583
|
+
*/
|
|
584
|
+
const ESSENTIAL = new Set(["AGENT_API_SECRET", "CLAUDE_CODE_OAUTH_TOKEN", "GITHUB_TOKEN", "GIT_REPO_URL"]);
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Inside a run, three of the four are proven by the fact that this output exists at all —
|
|
588
|
+
* a stronger check than any probe could be. The run is happening, so the Claude credential
|
|
589
|
+
* works; the working directory is the content clone, so GIT_REPO_URL and the token that
|
|
590
|
+
* cloned it work; the request that started it was authenticated, so the API secret works.
|
|
591
|
+
*/
|
|
592
|
+
async function checkEssentials() {
|
|
593
|
+
const insideRun = Boolean(env.AGENT_SPAWN_TOKEN);
|
|
594
|
+
if (!env.GIT_REPO_URL) {
|
|
595
|
+
fail("GIT_REPO_URL", "unset — the service has no content repo to clone, and will not boot");
|
|
596
|
+
} else if (insideRun) {
|
|
597
|
+
ok("GIT_REPO_URL", "this run's own clone came from it");
|
|
598
|
+
} else {
|
|
599
|
+
ok("GIT_REPO_URL", "set (not verifiable from here)");
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
557
603
|
function checkPresence() {
|
|
558
|
-
|
|
559
|
-
|
|
604
|
+
// The token that authorises the whole deployment, and the one credential this check
|
|
605
|
+
// cannot see when it matters most. Run inside a job, `claude` strips its own credential
|
|
606
|
+
// from the environment of every subprocess it spawns — a run cannot read the token that
|
|
607
|
+
// started it, which is exactly right, and means "unset" here would be a lie: the run
|
|
608
|
+
// doing the reporting is itself the proof it is set. AGENT_SPAWN_TOKEN is how we know we
|
|
609
|
+
// are inside a run; the engine sets that one, so `claude` has no reason to strip it.
|
|
610
|
+
if (env.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
611
|
+
ok("CLAUDE_CODE_OAUTH_TOKEN", "present (not remotely testable)");
|
|
612
|
+
} else if (env.AGENT_SPAWN_TOKEN) {
|
|
613
|
+
ok("CLAUDE_CODE_OAUTH_TOKEN", "in use by this very run — a run cannot read it, by design");
|
|
614
|
+
} else {
|
|
615
|
+
skip("CLAUDE_CODE_OAUTH_TOKEN", "unset");
|
|
560
616
|
}
|
|
561
617
|
// Either name satisfies the engine; say which one is actually in play.
|
|
562
618
|
if (env.AGENT_API_SECRET) {
|
|
@@ -585,8 +641,7 @@ const CHECKS = [
|
|
|
585
641
|
["GITHUB_TOKEN", checkGithub],
|
|
586
642
|
["GRAFANA (Loki)", checkGrafana],
|
|
587
643
|
["RESEND_API_KEY", checkResend],
|
|
588
|
-
["
|
|
589
|
-
["SWEEPOS_POSTGRES_URL_READONLY", checkPostgres],
|
|
644
|
+
["POSTGRES (read-only)", checkPostgres],
|
|
590
645
|
["POSTHOG", checkPosthog],
|
|
591
646
|
["AHREFS_API_KEY", checkAhrefs],
|
|
592
647
|
["SERPER_API_KEY", checkSerper],
|
|
@@ -602,6 +657,7 @@ const CHECKS = [
|
|
|
602
657
|
["DWD drive+docs+slides", checkDwdDrive],
|
|
603
658
|
["DWD calendar (write scope)", checkDwdCalendar],
|
|
604
659
|
["SEARCH CONSOLE", checkSearchConsole],
|
|
660
|
+
["Essentials", checkEssentials],
|
|
605
661
|
["GA4", checkGa4],
|
|
606
662
|
["GOOGLE ADS", checkGoogleAds],
|
|
607
663
|
["Deployments context (run.viewer)", checkDeploymentsContext],
|
|
@@ -613,16 +669,79 @@ await Promise.all(
|
|
|
613
669
|
);
|
|
614
670
|
|
|
615
671
|
results.sort((a, b) => a.name.localeCompare(b.name));
|
|
616
|
-
|
|
672
|
+
|
|
673
|
+
// Configured surfaces get a line each. Unconfigured ones get a single line between them,
|
|
674
|
+
// because this script ships to EVERY deployment and knows every integration the engine
|
|
675
|
+
// supports — which is always more than any one deployment uses. Listing them individually
|
|
676
|
+
// asks the operator "why is Kleer in my report?", and the honest answer is that it is not
|
|
677
|
+
// theirs and never was. The deployment's own register of what it can reach is
|
|
678
|
+
// ENVIRONMENT.md in its content repo; the catalogue of what it could add is `connect`.
|
|
679
|
+
// Credentials this script has never heard of.
|
|
680
|
+
//
|
|
681
|
+
// A deployment adds its own systems, and their keys are just secrets bound to the service:
|
|
682
|
+
// ACME_CRM_TOKEN means nothing to an engine that ships to everyone. Testing one needs its
|
|
683
|
+
// endpoint, its auth header and a notion of what a good answer looks like — none of which
|
|
684
|
+
// can be in here. Silence would be the wrong answer though, because "26 working" reads as
|
|
685
|
+
// "everything works" while a set-but-never-exercised key sits beside it.
|
|
686
|
+
//
|
|
687
|
+
// So: name them, say plainly that nothing here can test them, and let the run that called
|
|
688
|
+
// this reconcile them against ENVIRONMENT.md — the deployment's own register does know
|
|
689
|
+
// what they are and which skill reaches them.
|
|
690
|
+
//
|
|
691
|
+
// Known-ness is decided from this file's own source, so the list maintains itself: if the
|
|
692
|
+
// script mentions the variable, or the family it belongs to, it knows about it.
|
|
693
|
+
// Comments stripped first: a variable named in a comment — including the example two
|
|
694
|
+
// paragraphs up — is not a variable this script can test, and counting it as known is how
|
|
695
|
+
// a real unknown gets silently swallowed.
|
|
696
|
+
const selfSource = readFileSync(fileURLToPath(import.meta.url), "utf8")
|
|
697
|
+
.split("\n")
|
|
698
|
+
.filter((line) => !line.trim().startsWith("//"))
|
|
699
|
+
.join("\n");
|
|
700
|
+
const CREDENTIAL_SHAPED = /_(TOKEN|KEY|SECRET|PASSWORD|CREDENTIALS)$/;
|
|
701
|
+
const unknownCredentials = Object.keys(env)
|
|
702
|
+
.filter((name) => CREDENTIAL_SHAPED.test(name))
|
|
703
|
+
.filter((name) => !selfSource.includes(name) && !selfSource.includes(`${name.split("_")[0]}_`))
|
|
704
|
+
.sort();
|
|
705
|
+
|
|
706
|
+
// An essential can never be "not set up here" — that is the whole point of the tier. Do
|
|
707
|
+
// this BEFORE partitioning, or a promoted failure is counted and never printed.
|
|
708
|
+
for (const missing of results.filter((r) => r.status === "skip" && ESSENTIAL.has(r.name))) {
|
|
709
|
+
missing.status = "FAIL";
|
|
710
|
+
missing.detail = `${missing.detail} — the agent cannot run without it`;
|
|
711
|
+
}
|
|
712
|
+
const configured = results.filter((r) => r.status !== "skip");
|
|
713
|
+
const unconfigured = results.filter((r) => r.status === "skip");
|
|
714
|
+
const pad = Math.max(...configured.map((r) => r.name.length), 1) + 2;
|
|
617
715
|
console.log(
|
|
618
|
-
`\nCredential check — ${onCloudRun ? "Cloud Run (all surfaces)" : "local (metadata-server surfaces skipped)"}
|
|
716
|
+
`\nCredential check — ${onCloudRun ? "Cloud Run (all surfaces)" : "local (metadata-server surfaces skipped)"}`,
|
|
619
717
|
);
|
|
620
|
-
|
|
621
|
-
const icon = r.status === "ok" ? "✅" :
|
|
622
|
-
console.log(
|
|
718
|
+
const line = (r) => {
|
|
719
|
+
const icon = r.status === "ok" ? "✅" : "❌";
|
|
720
|
+
console.log(` ${icon} ${r.name.padEnd(pad)} ${r.status === "ok" ? r.detail : r.detail && `— ${r.detail}`}`);
|
|
721
|
+
};
|
|
722
|
+
// Split, because a failure in the first group is an outage and a failure in the second is
|
|
723
|
+
// one job doing less. Reading them in one list makes those look like the same news.
|
|
724
|
+
const essential = configured.filter((r) => ESSENTIAL.has(r.name));
|
|
725
|
+
const integrations = configured.filter((r) => !ESSENTIAL.has(r.name));
|
|
726
|
+
if (essential.length) {
|
|
727
|
+
console.log("\nWithout these there is no agent at all");
|
|
728
|
+
essential.forEach(line);
|
|
729
|
+
}
|
|
730
|
+
if (integrations.length) {
|
|
731
|
+
console.log("\nWhat it can reach");
|
|
732
|
+
integrations.forEach(line);
|
|
623
733
|
}
|
|
624
734
|
const failures = results.filter((r) => r.status === "FAIL");
|
|
625
|
-
console.log(
|
|
626
|
-
|
|
627
|
-
|
|
735
|
+
console.log(`\n${configured.length - failures.length} working, ${failures.length} failing.`);
|
|
736
|
+
if (unknownCredentials.length) {
|
|
737
|
+
// Names only. This script never prints a value, and that holds hardest for the ones it
|
|
738
|
+
// does not understand.
|
|
739
|
+
console.log(`\nSet, but nothing here knows how to test them (${unknownCredentials.length}):`);
|
|
740
|
+
console.log(` ${unknownCredentials.join(", ")}`);
|
|
741
|
+
console.log(" Each should have a row in ENVIRONMENT.md saying which skill reaches it.");
|
|
742
|
+
}
|
|
743
|
+
if (unconfigured.length) {
|
|
744
|
+
console.log(`\nNot set up here (${unconfigured.length}): ${unconfigured.map((r) => r.name).join(", ")}.`);
|
|
745
|
+
console.log("The engine ships more integrations than any deployment uses — `connect` lists them.");
|
|
746
|
+
}
|
|
628
747
|
process.exit(failures.length ? 1 : 0);
|