@meffecta/agent 1.0.3 → 1.0.8
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 +85 -70
- 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 +4 -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
package/lib/commands.js
CHANGED
|
@@ -1,23 +1,41 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { configPath, decideAnalytics, readConfig, resolvePosthog, writeConfig } from "./analytics.js";
|
|
4
5
|
import { requireDeployment, UserError } from "./config.js";
|
|
6
|
+
import { checkJobs, createJob } from "./create-job.js";
|
|
5
7
|
import { runDoctor } from "./doctor.js";
|
|
6
8
|
import { parseFlags } from "./flags.js";
|
|
7
9
|
import { api, requireCommand, stream } from "./gcloud.js";
|
|
8
10
|
import { connect } from "./integrations.js";
|
|
11
|
+
import { resources } from "./resources.js";
|
|
12
|
+
import { verifyCredentials } from "./verify-credentials.js";
|
|
13
|
+
import { cliVersion, installMethod, invocation, isNewer, latestPublished } from "./version.js";
|
|
9
14
|
|
|
10
15
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
16
|
|
|
17
|
+
// Re-exported so the entrypoint keeps one import site for everything it prints.
|
|
18
|
+
export { cliVersion };
|
|
19
|
+
|
|
12
20
|
/**
|
|
13
21
|
* Where the operator scripts are. In the published package they sit at its root; in a
|
|
14
22
|
* checkout of the engine repo they are one level up, beside cli/. Same files either way —
|
|
15
23
|
* the pack step copies them in, so the CLI is a front on exactly the scripts a deployment
|
|
16
24
|
* would otherwise run by hand.
|
|
25
|
+
*
|
|
26
|
+
* **A checkout wins outright**, even when a vendored copy is present. `prepack` leaves one
|
|
27
|
+
* in cli/scripts, it is gitignored so it never shows up in `git status`, and a linked CLI
|
|
28
|
+
* reading that frozen snapshot is how you edit a script, run the command, and watch it
|
|
29
|
+
* behave exactly as though you had not. Same failure as a stale engine.json pinning a tag
|
|
30
|
+
* that was never published, and it gets the same guard: the engine's source sitting
|
|
31
|
+
* alongside means this is the repo, not an install of it.
|
|
17
32
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
)
|
|
33
|
+
const inCheckout = existsSync(resolve(packageRoot, "..", "src", "index.ts"));
|
|
34
|
+
export const scriptsDir = [
|
|
35
|
+
...(inCheckout ? [resolve(packageRoot, "..", "scripts")] : []),
|
|
36
|
+
resolve(packageRoot, "scripts"),
|
|
37
|
+
resolve(packageRoot, "..", "scripts"),
|
|
38
|
+
].find((candidate) => existsSync(candidate));
|
|
21
39
|
|
|
22
40
|
/**
|
|
23
41
|
* The engine build this CLI was published alongside. CI writes it at pack time, so
|
|
@@ -45,14 +63,6 @@ export function engineTag() {
|
|
|
45
63
|
}
|
|
46
64
|
}
|
|
47
65
|
|
|
48
|
-
export function cliVersion() {
|
|
49
|
-
try {
|
|
50
|
-
return JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf8")).version;
|
|
51
|
-
} catch {
|
|
52
|
-
return "unknown";
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
66
|
/** A command that is one of the shipped scripts, with every argument passed straight on. */
|
|
57
67
|
function script(file, { interpreter } = {}) {
|
|
58
68
|
return async (args) => {
|
|
@@ -64,7 +74,12 @@ function script(file, { interpreter } = {}) {
|
|
|
64
74
|
throw new UserError(`Missing script: ${file}`);
|
|
65
75
|
}
|
|
66
76
|
const runner = interpreter ?? (file.endsWith(".mjs") ? "node" : "bash");
|
|
67
|
-
|
|
77
|
+
// MEFFECTA_CLI is how a script knows it was reached through the CLI, and what to call
|
|
78
|
+
// the next step: `meffecta-agent deploy`, never ./scripts/deploy.sh. A deployment run
|
|
79
|
+
// by someone outside Meffecta cannot clone this repo and has never seen those files.
|
|
80
|
+
const { code, error } = await stream(runner, [path, ...args], {
|
|
81
|
+
env: { ...process.env, MEFFECTA_CLI: invocation() },
|
|
82
|
+
});
|
|
68
83
|
if (error) {
|
|
69
84
|
throw new UserError(`Could not run ${runner}: ${error.message}`);
|
|
70
85
|
}
|
|
@@ -72,6 +87,87 @@ function script(file, { interpreter } = {}) {
|
|
|
72
87
|
};
|
|
73
88
|
}
|
|
74
89
|
|
|
90
|
+
/** The engine tag the deployment is actually running, from the image on its live revision. */
|
|
91
|
+
async function runningEngineTag() {
|
|
92
|
+
try {
|
|
93
|
+
const d = requireDeployment();
|
|
94
|
+
const { capture, gcloudArgs } = await import("./gcloud.js");
|
|
95
|
+
const json = capture(
|
|
96
|
+
"gcloud",
|
|
97
|
+
gcloudArgs(d, [
|
|
98
|
+
"run",
|
|
99
|
+
"services",
|
|
100
|
+
"describe",
|
|
101
|
+
d.SERVICE,
|
|
102
|
+
`--region=${d.REGION}`,
|
|
103
|
+
"--format=value(spec.template.spec.containers[0].image)",
|
|
104
|
+
]),
|
|
105
|
+
);
|
|
106
|
+
return json.trim().split(":").pop() || undefined;
|
|
107
|
+
} catch {
|
|
108
|
+
// No deployment.env, no gcloud, no service yet — all fine, this line is just omitted.
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Am I current? Three numbers, because being "up to date" here has three parts and the
|
|
115
|
+
* dangerous state is between them: new tooling pins a NEW engine tag, but the deployment
|
|
116
|
+
* keeps running the old image until `deploy`. Tooling that provisions a shape the running
|
|
117
|
+
* engine does not implement is exactly what releasing the pair together prevents — and it
|
|
118
|
+
* reopens here, on the operator's machine, at upgrade time.
|
|
119
|
+
*/
|
|
120
|
+
async function upgrade(args) {
|
|
121
|
+
const checkOnly = args.includes("--check");
|
|
122
|
+
const installed = cliVersion();
|
|
123
|
+
const latest = await latestPublished();
|
|
124
|
+
const running = await runningEngineTag();
|
|
125
|
+
const how = installMethod();
|
|
126
|
+
|
|
127
|
+
console.log(`▶ @meffecta/agent`);
|
|
128
|
+
console.log(` installed ${installed}${how.kind === "checkout" ? " (a checkout, not an installed package)" : ""}`);
|
|
129
|
+
console.log(` published ${latest ?? "unknown — could not reach the npm registry"}`);
|
|
130
|
+
if (running) {
|
|
131
|
+
console.log(` engine ${running} (what your deployment is running now)`);
|
|
132
|
+
}
|
|
133
|
+
console.log("");
|
|
134
|
+
|
|
135
|
+
const behind = latest && isNewer(latest, installed);
|
|
136
|
+
if (!latest) {
|
|
137
|
+
console.log("Could not check for a newer version. Being offline is not a problem — this");
|
|
138
|
+
console.log("command only reads the registry; nothing else needs it.");
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
if (!behind) {
|
|
142
|
+
console.log(`Up to date.`);
|
|
143
|
+
} else {
|
|
144
|
+
console.log(`A newer version is out: ${installed} → ${latest}`);
|
|
145
|
+
if (how.note) {
|
|
146
|
+
console.log(` ${how.note}`);
|
|
147
|
+
}
|
|
148
|
+
console.log("");
|
|
149
|
+
console.log(` ${how.upgrade}`);
|
|
150
|
+
if (how.kind !== "checkout") {
|
|
151
|
+
console.log(` meffecta-agent deploy`);
|
|
152
|
+
console.log("");
|
|
153
|
+
console.log("Both lines. The first gets tooling that pins a newer engine; the second is");
|
|
154
|
+
console.log("what actually moves your deployment onto it. Stopping after the first leaves");
|
|
155
|
+
console.log("tooling that provisions a shape the running engine may not implement.");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// A deployment can be behind the CLI without the CLI being behind npm — someone upgraded
|
|
160
|
+
// the tooling and never deployed. That is the half-upgraded state, and it is worth saying
|
|
161
|
+
// out loud rather than leaving to be noticed.
|
|
162
|
+
const { tag, pinned } = engineTag();
|
|
163
|
+
if (pinned && running && running !== tag && !checkOnly) {
|
|
164
|
+
console.log("");
|
|
165
|
+
console.log(`⚠ This CLI deploys engine ${tag}, but the deployment is running ${running}.`);
|
|
166
|
+
console.log(` meffecta-agent deploy (rolls it onto ${tag} and re-syncs the triggers)`);
|
|
167
|
+
}
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
75
171
|
/** What the deployment looks like right now: revision, shape, triggers, pending work. */
|
|
76
172
|
async function status() {
|
|
77
173
|
requireCommand("gcloud", "the CLI reads your deployment through it");
|
|
@@ -116,7 +212,7 @@ async function status() {
|
|
|
116
212
|
const crons = jobs.filter((name) => name.startsWith(`${d.SERVICE}-job-`));
|
|
117
213
|
const sweep = jobs.find((name) => name === `${d.SERVICE}-sweep`);
|
|
118
214
|
console.log(
|
|
119
|
-
` scheduler ${crons.length} cron trigger(s)${sweep ? " + sweep" : " — NO SWEEP (run
|
|
215
|
+
` scheduler ${crons.length} cron trigger(s)${sweep ? " + sweep" : " — NO SWEEP (run sync-triggers)"}`,
|
|
120
216
|
);
|
|
121
217
|
if (crons.length === 0) {
|
|
122
218
|
console.log(" ⚠ scaled to zero with no triggers: this service runs nothing.");
|
|
@@ -223,6 +319,67 @@ async function ask(args) {
|
|
|
223
319
|
return 0;
|
|
224
320
|
}
|
|
225
321
|
|
|
322
|
+
/**
|
|
323
|
+
* `analytics` — the transparency surface, and the switch.
|
|
324
|
+
*
|
|
325
|
+
* Nothing is collected that could identify a deployment, but "trust me" is not a policy.
|
|
326
|
+
* This prints the exhaustive list of what is and is not sent, where the choice is stored,
|
|
327
|
+
* the state and the reason for it, and the one command that turns it off.
|
|
328
|
+
*/
|
|
329
|
+
const ANALYTICS_REASONS = {
|
|
330
|
+
"do-not-track": "the DO_NOT_TRACK environment variable is set",
|
|
331
|
+
"env-disabled": "MEFFECTA_ANALYTICS_DISABLED is set",
|
|
332
|
+
"config-disabled": "you ran `meffecta-agent analytics off`",
|
|
333
|
+
"config-enabled": "you ran `meffecta-agent analytics on`",
|
|
334
|
+
ci: "this looks like CI, where it is off by default",
|
|
335
|
+
default: "the default (on, and one command to turn off)",
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
async function analytics(args) {
|
|
339
|
+
const [action] = args;
|
|
340
|
+
if (action === "on" || action === "enable") {
|
|
341
|
+
writeConfig({ analytics: "enabled" });
|
|
342
|
+
console.log("✔ Anonymous analytics on. It steers what gets fixed and documented next.");
|
|
343
|
+
console.log(` Recorded in ${configPath()}`);
|
|
344
|
+
return 0;
|
|
345
|
+
}
|
|
346
|
+
if (action === "off" || action === "disable") {
|
|
347
|
+
writeConfig({ analytics: "disabled" });
|
|
348
|
+
console.log("✔ Anonymous analytics off. Nothing further is sent from this machine.");
|
|
349
|
+
console.log(` Recorded in ${configPath()}`);
|
|
350
|
+
return 0;
|
|
351
|
+
}
|
|
352
|
+
if (action && action !== "status") {
|
|
353
|
+
throw new UserError(`Unknown: analytics ${action}. Try: analytics status | on | off`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const config = readConfig();
|
|
357
|
+
const decision = decideAnalytics({ config });
|
|
358
|
+
const { host, key } = resolvePosthog();
|
|
359
|
+
console.log(`Analytics: ${decision.enabled ? "ON" : "OFF"} — ${ANALYTICS_REASONS[decision.reason]}.`);
|
|
360
|
+
console.log(` choice stored in ${configPath()}`);
|
|
361
|
+
console.log(` anonymous id ${config.anonymousId ?? "(none yet — assigned on the first run with it on)"}`);
|
|
362
|
+
if (decision.enabled) {
|
|
363
|
+
console.log(` sent to ${key ? host : "nowhere — no key is configured in this build"}`);
|
|
364
|
+
}
|
|
365
|
+
console.log("");
|
|
366
|
+
console.log("Sent, and nothing else:");
|
|
367
|
+
console.log(" • which command you ran, from the fixed list — never a word you typed");
|
|
368
|
+
console.log(" • whether it worked, how long it took, and the class of any error");
|
|
369
|
+
console.log(" • this CLI's version, the engine tag it deploys, Node, OS, architecture");
|
|
370
|
+
console.log(" • how you installed it, and whether a deployment.env was found");
|
|
371
|
+
console.log(" • a random id, made on this machine, so repeat runs count as one install");
|
|
372
|
+
console.log("");
|
|
373
|
+
console.log("Never sent: your GCP project or service name, region, repo URLs, account or");
|
|
374
|
+
console.log("email, file paths, arguments, secret names or values, job names, prompts —");
|
|
375
|
+
console.log("or any error message. Only the error's class, because messages quote paths");
|
|
376
|
+
console.log("and project ids.");
|
|
377
|
+
console.log("");
|
|
378
|
+
console.log("Turn it off: meffecta-agent analytics off (or set DO_NOT_TRACK=1)");
|
|
379
|
+
console.log("Turn it on: meffecta-agent analytics on");
|
|
380
|
+
return 0;
|
|
381
|
+
}
|
|
382
|
+
|
|
226
383
|
async function sweep() {
|
|
227
384
|
requireCommand("gcloud", "the sweep is a Cloud Scheduler job");
|
|
228
385
|
const d = requireDeployment();
|
|
@@ -318,7 +475,7 @@ async function triggers() {
|
|
|
318
475
|
|
|
319
476
|
if (rows.length === 0) {
|
|
320
477
|
console.log("No triggers. If this service is scaled to zero it is running nothing:");
|
|
321
|
-
console.log(" meffecta-agent
|
|
478
|
+
console.log(" meffecta-agent sync-triggers");
|
|
322
479
|
return 0;
|
|
323
480
|
}
|
|
324
481
|
const width = Math.max(...rows.map(([id]) => id.length));
|
|
@@ -341,7 +498,7 @@ async function triggers() {
|
|
|
341
498
|
).split("\t");
|
|
342
499
|
console.log(`\nTask queue ${d.SERVICE}-runs: ${queue[0] ?? "?"}, ${queue[1] || 0} task(s) waiting`);
|
|
343
500
|
} catch {
|
|
344
|
-
console.log(`\nTask queue ${d.SERVICE}-runs: not created — run setup-
|
|
501
|
+
console.log(`\nTask queue ${d.SERVICE}-runs: not created — run setup-infra`);
|
|
345
502
|
}
|
|
346
503
|
return 0;
|
|
347
504
|
}
|
|
@@ -367,22 +524,35 @@ async function logs(args) {
|
|
|
367
524
|
// Scaffolded by `init` into a new content repo. Deliberately short: an empty table with the
|
|
368
525
|
// columns that matter is a thing an operator fills in, where a long example is a thing they
|
|
369
526
|
// delete.
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
527
|
+
/**
|
|
528
|
+
* One example entry, scaffolded so the shape is obvious from a real file rather than from
|
|
529
|
+
* documentation. What the fields MEAN lives in SYSTEM.base.md, because every run needs to
|
|
530
|
+
* know it and no content repo should be carrying its own copy of an engine rule.
|
|
531
|
+
*/
|
|
532
|
+
const REGISTER_EXAMPLE = `---
|
|
533
|
+
system: Example — rename this file after the system, and delete it once you have a real one
|
|
534
|
+
skill: cloudflare
|
|
535
|
+
requires: CLOUDFLARE_API_KEY, CLOUDFLARE_ACCOUNT_ID
|
|
536
|
+
access: read; writes only when a job says so explicitly
|
|
537
|
+
probe: list zones
|
|
538
|
+
---
|
|
375
539
|
|
|
376
|
-
|
|
540
|
+
One file per system, in this directory. The agent reads them **at run time**: its skills
|
|
541
|
+
are shared across every deployment and name no deployment's variables, so a skill says what
|
|
542
|
+
kind of credential it needs and comes here for the name.
|
|
377
543
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
| | | | |
|
|
544
|
+
**A system with no file here is one the agent will report as unavailable.** That is the
|
|
545
|
+
point of the register, and it is why an absence means as much as an entry.
|
|
381
546
|
|
|
382
|
-
|
|
383
|
-
|
|
547
|
+
- \`requires\` is exact variable names, all of which must be set. Use \`none\` for a system
|
|
548
|
+
that authenticates as the runtime service account instead.
|
|
549
|
+
- \`selectors\` is for optional per-project pointers — one per world.
|
|
550
|
+
- \`access\` is what a JOB may do, written honestly: "read-only", "may send mail", "may
|
|
551
|
+
change DNS". It is what a person reads when deciding whether to let a job run unattended.
|
|
552
|
+
- \`probe\` is the cheapest read that proves the credential works, with no side effect —
|
|
553
|
+
list domains rather than send, read quota rather than generate.
|
|
384
554
|
|
|
385
|
-
Set the values
|
|
555
|
+
Set the values with \`meffecta-agent set-secret <NAME>\` (credentials) or
|
|
386
556
|
\`set-env <NAME>=<value>\` (ids, addresses, URLs); \`meffecta-agent connect\` lists the
|
|
387
557
|
systems the engine already has skills for and walks through each.
|
|
388
558
|
`;
|
|
@@ -408,17 +578,19 @@ async function init(args) {
|
|
|
408
578
|
console.log("set of jobs runs as is a fact about the deployment and none of it is secret.\n");
|
|
409
579
|
|
|
410
580
|
// The register is read by the agent itself at run time, not just by people: the skills
|
|
411
|
-
// are shared across deployments and name no deployment's variables, so without
|
|
412
|
-
//
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
581
|
+
// are shared across deployments and name no deployment's variables, so without it a run
|
|
582
|
+
// knows how a system works and not which variable holds its credential.
|
|
583
|
+
const { mkdirSync } = await import("node:fs");
|
|
584
|
+
const registerDir = resolve(process.cwd(), "systems");
|
|
585
|
+
if (exists(registerDir)) {
|
|
586
|
+
console.log("systems/ already exists here. Leaving it alone.\n");
|
|
416
587
|
} else {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
console.log("
|
|
420
|
-
console.log("
|
|
421
|
-
console.log("
|
|
588
|
+
mkdirSync(registerDir, { recursive: true });
|
|
589
|
+
writeFileSync(resolve(registerDir, "example.md"), REGISTER_EXAMPLE);
|
|
590
|
+
console.log("✅ Wrote systems/example.md — the register of what this agent can reach.\n");
|
|
591
|
+
console.log("One file per system, added as you connect each one. The agent reads them at");
|
|
592
|
+
console.log("run time to find out which variable holds which credential, so a system with");
|
|
593
|
+
console.log("no file here is one it will report as unavailable.\n");
|
|
422
594
|
}
|
|
423
595
|
console.log("Next: meffecta-agent steps");
|
|
424
596
|
return 0;
|
|
@@ -444,13 +616,17 @@ export const GROUPS = [
|
|
|
444
616
|
["init", "Start a deployment.env here, from the template", init],
|
|
445
617
|
[
|
|
446
618
|
"setup-infra",
|
|
447
|
-
"Provision GCP: registry, buckets, service account, service shell",
|
|
619
|
+
"Provision GCP: registry, buckets, service account, run queue, service shell",
|
|
448
620
|
script("setup-infrastructure.sh"),
|
|
449
621
|
],
|
|
450
622
|
["set-secret", "AGENT_API_SECRET --random, CLAUDE_CODE_OAUTH_TOKEN, GITHUB_TOKEN", script("set-secret.sh")],
|
|
451
623
|
["set-env", "GIT_REPO_URL — the one setting the service will not boot without", script("set-env.sh")],
|
|
452
624
|
["deploy", "Roll out an engine image and assert the runtime shape", script("deploy.sh")],
|
|
453
|
-
[
|
|
625
|
+
[
|
|
626
|
+
"sync-triggers",
|
|
627
|
+
"Re-sync the Cloud Scheduler triggers without a rollout (deploy does it)",
|
|
628
|
+
script("sync-triggers.sh"),
|
|
629
|
+
],
|
|
454
630
|
],
|
|
455
631
|
},
|
|
456
632
|
{
|
|
@@ -484,15 +660,24 @@ export const GROUPS = [
|
|
|
484
660
|
["secrets", "What is in Secret Manager, and whether the service reads it", secretsList],
|
|
485
661
|
["triggers", "The Cloud Scheduler jobs and task queue that drive it", triggers],
|
|
486
662
|
["logs", "Recent service logs (--limit N)", logs],
|
|
663
|
+
["check-jobs", "Validate the job files here — after you have edited one by hand", checkJobs],
|
|
664
|
+
["analytics", "What anonymous usage data is sent, and how to turn it off", analytics],
|
|
665
|
+
["resources", "Everything the set-up built in Google Cloud, and what each part is for", resources],
|
|
487
666
|
],
|
|
488
667
|
},
|
|
489
668
|
{
|
|
490
669
|
title: "Operate it",
|
|
491
670
|
commands: [
|
|
671
|
+
["create-job", 'Write a new job from a description: create-job "a report every Wednesday 2pm"', createJob],
|
|
492
672
|
["run", "Trigger one job now, or --in <seconds>", runJob],
|
|
493
673
|
["ask", "Ask it something as a one-off run", ask],
|
|
494
674
|
["sweep", "Run the housekeeping sweep now", sweep],
|
|
495
|
-
[
|
|
675
|
+
[
|
|
676
|
+
"verify-credentials",
|
|
677
|
+
"Test every credential the deployment holds, from inside it (--quick skips the slow half)",
|
|
678
|
+
verifyCredentials(ask),
|
|
679
|
+
],
|
|
680
|
+
["upgrade", "Check for a newer CLI, and what your deployment is running", upgrade],
|
|
496
681
|
["update-tooling", "Refresh the shipped scripts from an engine image (docker path)", script("update-tooling.sh")],
|
|
497
682
|
],
|
|
498
683
|
},
|