@flowapt/flowiq-cli 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -0
- package/TEAM-GUIDE.md +3 -0
- package/package.json +1 -1
- package/src/commands/team-updates.js +169 -0
- package/src/index.js +34 -0
package/README.md
CHANGED
|
@@ -1131,6 +1131,54 @@ flowiq insights run <organization_id> --from 2026-09-01 --to 2026-09-07 --store-
|
|
|
1131
1131
|
schedule for that period, else standard.
|
|
1132
1132
|
- `enable` / `disable` / `run` are audited (`flowiq audit --endpoint insights`).
|
|
1133
1133
|
|
|
1134
|
+
### Team updates — `flowiq updates status|uncovered|draft|asset|preview|send` (v0.7.1)
|
|
1135
|
+
|
|
1136
|
+
The **"what changed in FlowIQ" email to the Flowapt team** — every super admin
|
|
1137
|
+
gets it. An *issue* is a JSON file composed from `platform_changelog` rows:
|
|
1138
|
+
headline sections (a screenshot straight from the app, what it does, and a
|
|
1139
|
+
**For the team** note: how we use it, what to tell clients), an *Also shipped*
|
|
1140
|
+
list, and a *CLI and MCP* block. The renderer, the send (from
|
|
1141
|
+
flowiq@flowapt.com, reply-to Matt) and the record (`team_updates`, with the exact
|
|
1142
|
+
HTML that went out) live server-side in the `team-update-email` edge function.
|
|
1143
|
+
|
|
1144
|
+
```bash
|
|
1145
|
+
flowiq updates status # uncovered changelog rows · recent sends · recipients
|
|
1146
|
+
flowiq updates uncovered # the rows no issue has emailed yet
|
|
1147
|
+
flowiq updates draft # → ./.flowiq/updates/<date>.json, a default issue to enrich
|
|
1148
|
+
flowiq updates draft --since 2026-09-07T22:00:00+02:00 # compose from a period instead of the last covered point
|
|
1149
|
+
flowiq updates asset ./api-keys-card.png # host a screenshot, prints the https URL for image.url
|
|
1150
|
+
flowiq updates preview .flowiq/updates/2026-09-10.json --open # render to .html exactly as emailed
|
|
1151
|
+
flowiq updates send .flowiq/updates/2026-09-10.json --test # one copy to YOU; nothing marked covered
|
|
1152
|
+
flowiq updates send .flowiq/updates/2026-09-10.json --test --to kiah@flowapt.com
|
|
1153
|
+
flowiq updates send .flowiq/updates/2026-09-10.json --yes # the real send, to every super admin
|
|
1154
|
+
```
|
|
1155
|
+
|
|
1156
|
+
- **Issue shape** (`issue` key in the file): `subject`, `preheader`, `title`,
|
|
1157
|
+
`intro`, `date_label`, `headlines[]`, `also[]`, `tools[]`, `closing`,
|
|
1158
|
+
`changelog_ids[]`. A headline/tool section: `type` (feature | improvement |
|
|
1159
|
+
fix | ui | agent | cli | infra | db | mcp — the chip is the app's own), `major`,
|
|
1160
|
+
`title`, `what[]` (paragraphs), `team[]` (bullets), `image{url,alt,caption}`,
|
|
1161
|
+
`terminal{command,output}`, `link{label,url}`, `org_name`, `version`. An
|
|
1162
|
+
`also[]` item: `type`, `title`, `note`, `link`. Inline markup anywhere:
|
|
1163
|
+
`**bold**`, `` `code` ``, `[label](https://…)`.
|
|
1164
|
+
- **`preview` and `send` lint the file first** (required fields, unknown types,
|
|
1165
|
+
non-https images, em dashes) and stop with line-level help.
|
|
1166
|
+
- **`send` without `--test` needs `--yes`**: it emails the whole team AND moves
|
|
1167
|
+
the covered-up-to mark, so those changelog rows will never be auto-digested.
|
|
1168
|
+
`--test` sends to you (or `--to`) and marks nothing.
|
|
1169
|
+
- **The safety net:** pg_cron `team-update-email-daily` (08:30 SAST) emails a
|
|
1170
|
+
default digest of any changelog row that has gone un-emailed for 20h+, so a
|
|
1171
|
+
ship is never silently missed. The curated `send` is what stops it firing —
|
|
1172
|
+
compose the issue the same day you ship.
|
|
1173
|
+
- **Who may do what:** everyone with a staff key can `status`, `uncovered`,
|
|
1174
|
+
`draft`, `asset`, `preview` and `send --test` (a copy to yourself, nothing
|
|
1175
|
+
marked covered). **The real send is Matt only** — any other key gets
|
|
1176
|
+
`403 Only matt@flowapt.com may post the team update` and the attempt is
|
|
1177
|
+
audited (`ALLOWED_SENDERS` in `api/cli/team-updates.js`).
|
|
1178
|
+
- Every link in the email opens the app's Changelog at that entry
|
|
1179
|
+
(`app.flowiq.live/?changelog=1&entry=<id>`). `send` and `asset` are audited
|
|
1180
|
+
(`flowiq audit --endpoint team-updates`).
|
|
1181
|
+
|
|
1134
1182
|
### WhatsApp templates — `flowiq templates pull|list|show|create|status` (alias `tpl`)
|
|
1135
1183
|
|
|
1136
1184
|
Read an org's live templates straight from Meta (read-only), render any single
|
package/TEAM-GUIDE.md
CHANGED
|
@@ -103,6 +103,9 @@ When you see it, run `npm i -g @flowapt/flowiq-cli` — a stale version also mea
|
|
|
103
103
|
| Agent can't quote ANY price / "I can't pull the live menu" | `flowiq agent config <org_id> --tool product_lookup=true` — name-based fuzzy lookup that works without embeddings or a live OpenAI key (semantic `get_product_info` needs both) |
|
|
104
104
|
| Client wants leads/requests emailed to their team instead of a human escalation | `flowiq agent config <org_id> --tool email_request_tool=true`, then set the recipient list in Agents → Tool Library → Email Request to Team (no CLI path for the addresses yet) |
|
|
105
105
|
| Talk to the live agent safely (no real WhatsApp ever sent) | `flowiq test send <org_id> "hi, do you sell X?"` |
|
|
106
|
+
| **Tell the team what shipped** (the FlowIQ team update email) | `flowiq updates draft` → enrich the JSON (screenshots via `flowiq updates asset`, a *For the team* note per headline) → `flowiq updates preview <file> --open` → `flowiq updates send <file> --test` → `flowiq updates send <file> --yes` |
|
|
107
|
+
| Can I use `flowiq updates`? | Yes: `status`, `uncovered`, `draft`, `preview` and `send --test` (a copy to yourself) work for everyone. Only the real `send --yes` to the whole team is Matt's; anyone else gets a 403 there |
|
|
108
|
+
| Which changelog rows has nobody emailed the team about yet? | `flowiq updates status` / `flowiq updates uncovered` — anything left 20h+ goes out automatically at 08:30 SAST as a plain digest |
|
|
106
109
|
| Set which contact `flowiq test` uses (use a FAKE number!) | `flowiq agent config <org_id> --test-contact-number 27000000001 --test-contact-name "QA Bot"` |
|
|
107
110
|
> **Publishing the CLI (maintainers only):** publish from a clean clone, never
|
|
108
111
|
> from your working tree — `npm publish` packs whatever is on disk. A
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowapt/flowiq-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Command-line tool for FlowIQ staff: round-trip agent prompts, questionnaires, fine-tuning, pin-board tasks, webhooks, templates, agent-updates, chat exports, and live agent testing without ever touching service-role credentials.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// `flowiq updates <verb>` — the "what changed in FlowIQ" email to the Flowapt team.
|
|
2
|
+
// status uncovered changelog rows, last sends, who receives it
|
|
3
|
+
// uncovered list the changelog rows no issue has covered yet
|
|
4
|
+
// draft [--out f] write ./.flowiq/updates/<date>.json — a default issue
|
|
5
|
+
// composed from the uncovered rows, ready to enrich
|
|
6
|
+
// (screenshots, "for the team" notes, a CLI block)
|
|
7
|
+
// asset <file> host an image for the email; prints the public URL
|
|
8
|
+
// preview <file> render the issue to <file>.html (--open shows it)
|
|
9
|
+
// send <file> --test → one copy to you (or --to a@b) without
|
|
10
|
+
// marking anything covered; without --test the issue
|
|
11
|
+
// goes to every super admin and needs --yes
|
|
12
|
+
// The renderer, the send and the record all live server-side
|
|
13
|
+
// (/cli/team-updates → the team-update-email edge fn); every send is audited.
|
|
14
|
+
|
|
15
|
+
import fs from "node:fs/promises";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { spawn } from "node:child_process";
|
|
18
|
+
import { http } from "../http.js";
|
|
19
|
+
|
|
20
|
+
const DIR = path.resolve(process.cwd(), ".flowiq", "updates");
|
|
21
|
+
const TYPES = ["feature", "improvement", "fix", "ui", "agent", "cli", "infra", "db", "mcp"];
|
|
22
|
+
|
|
23
|
+
function sastStamp() {
|
|
24
|
+
return new Date().toLocaleDateString("en-CA", { timeZone: "Africa/Johannesburg" }); // YYYY-MM-DD
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function readIssue(file) {
|
|
28
|
+
const p = path.resolve(process.cwd(), file);
|
|
29
|
+
let raw;
|
|
30
|
+
try { raw = await fs.readFile(p, "utf8"); }
|
|
31
|
+
catch { console.error(`Error: cannot read ${p}`); process.exit(1); }
|
|
32
|
+
let doc;
|
|
33
|
+
try { doc = JSON.parse(raw); }
|
|
34
|
+
catch (e) { console.error(`Error: ${p} is not valid JSON (${e.message})`); process.exit(1); }
|
|
35
|
+
const issue = doc.issue && typeof doc.issue === "object" ? doc.issue : doc;
|
|
36
|
+
const problems = lint(issue);
|
|
37
|
+
if (problems.length) {
|
|
38
|
+
console.error(`Error: ${p} has ${problems.length} problem(s):`);
|
|
39
|
+
for (const m of problems) console.error(` - ${m}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
return { issue, file: p };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The same checks the server applies, so a bad file fails here with line-level help. */
|
|
46
|
+
function lint(issue) {
|
|
47
|
+
const out = [];
|
|
48
|
+
if (!issue.subject) out.push("subject is required");
|
|
49
|
+
if (!issue.title) out.push("title is required");
|
|
50
|
+
const secs = [...(issue.headlines ?? []).map((s, i) => [`headlines[${i}]`, s]), ...(issue.tools ?? []).map((s, i) => [`tools[${i}]`, s])];
|
|
51
|
+
for (const [where, s] of secs) {
|
|
52
|
+
if (!s.title) out.push(`${where}: title is required`);
|
|
53
|
+
if (s.type && !TYPES.includes(s.type)) out.push(`${where}: type '${s.type}' is not one of ${TYPES.join("|")}`);
|
|
54
|
+
if (s.image && !/^https:\/\//.test(s.image.url || "")) out.push(`${where}: image.url must be https (host it with \`flowiq updates asset\`)`);
|
|
55
|
+
if (s.what && !Array.isArray(s.what)) out.push(`${where}: what must be an array of paragraphs`);
|
|
56
|
+
if (s.team && !Array.isArray(s.team)) out.push(`${where}: team must be an array of bullets`);
|
|
57
|
+
}
|
|
58
|
+
(issue.also ?? []).forEach((a, i) => {
|
|
59
|
+
if (!a.title) out.push(`also[${i}]: title is required`);
|
|
60
|
+
if (a.type && !TYPES.includes(a.type)) out.push(`also[${i}]: type '${a.type}' is not valid`);
|
|
61
|
+
});
|
|
62
|
+
const text = JSON.stringify(issue);
|
|
63
|
+
if (/—/.test(text)) out.push("contains an em dash (—) — house style is no em dashes in anything a teammate reads");
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function fmtWhen(iso) {
|
|
68
|
+
return new Date(iso).toLocaleString("en-GB", { timeZone: "Africa/Johannesburg", day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function status(opts) {
|
|
72
|
+
let r;
|
|
73
|
+
try { r = await http.get("team-updates"); }
|
|
74
|
+
catch (e) { console.error(`Status failed: ${e.message}`); process.exit(1); }
|
|
75
|
+
if (opts?.json) { console.log(JSON.stringify(r, null, 2)); return; }
|
|
76
|
+
console.log(`Team updates`);
|
|
77
|
+
console.log(` uncovered: ${r.uncovered} changelog row(s) not yet emailed${r.covered_until ? ` (covered up to ${fmtWhen(r.covered_until)} SAST)` : " (nothing has ever been sent)"}`);
|
|
78
|
+
console.log(` recipients: ${(r.recipients ?? []).map((x) => x.email).join(", ") || "none"}`);
|
|
79
|
+
if (Array.isArray(r.recent) && r.recent.length) {
|
|
80
|
+
console.log(` recent:`);
|
|
81
|
+
for (const s of r.recent) console.log(` ${fmtWhen(s.created_at)} ${s.status.padEnd(6)} ${s.source.padEnd(9)} ${String(s.subject).slice(0, 70)} → ${s.recipients?.length ?? 0}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function uncovered(opts) {
|
|
86
|
+
let r;
|
|
87
|
+
try { r = await http.get("team-updates", { uncovered: 1, limit: opts?.limit ?? 60 }); }
|
|
88
|
+
catch (e) { console.error(`Failed: ${e.message}`); process.exit(1); }
|
|
89
|
+
if (opts?.json) { console.log(JSON.stringify(r, null, 2)); return; }
|
|
90
|
+
if (!r.rows?.length) { console.log("Nothing uncovered — every changelog row has been emailed."); return; }
|
|
91
|
+
console.log(`${r.rows.length} uncovered row(s)${r.since ? ` since ${fmtWhen(r.since)} SAST` : ""}:`);
|
|
92
|
+
for (const row of r.rows) console.log(` ${fmtWhen(row.created_at)} ${row.type.padEnd(11)} ${row.is_major ? "MAJOR " : " "} ${row.title}${row.org_name ? ` [${row.org_name}]` : ""}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function draft(opts) {
|
|
96
|
+
let r;
|
|
97
|
+
try { r = await http.get("team-updates", { draft: 1, limit: opts?.limit ?? 60, since: opts?.since }); }
|
|
98
|
+
catch (e) { console.error(`Draft failed: ${e.message}`); process.exit(1); }
|
|
99
|
+
if (!r.count) { console.log("Nothing to draft — every changelog row has been emailed. Use --since <iso> to re-cover a period."); return; }
|
|
100
|
+
await fs.mkdir(DIR, { recursive: true });
|
|
101
|
+
const out = path.resolve(process.cwd(), opts?.out || path.join(DIR, `${sastStamp()}.json`));
|
|
102
|
+
const doc = {
|
|
103
|
+
_help: [
|
|
104
|
+
"This is a default issue composed from the uncovered changelog rows. Enrich it, then `flowiq updates preview` / `send`.",
|
|
105
|
+
"headlines[]: type, major, title, what[] (paragraphs), team[] (bullets: how WE use it / what to tell clients), image{url,alt,caption} (host with `flowiq updates asset`), terminal{command,output}, link{label,url}.",
|
|
106
|
+
"also[]: short rows (title, note, type). tools[]: CLI / MCP sections, same shape as headlines.",
|
|
107
|
+
"Inline markup in any text: **bold**, `code`, [label](https://…). No emoji, no em dashes.",
|
|
108
|
+
],
|
|
109
|
+
issue: r.issue,
|
|
110
|
+
};
|
|
111
|
+
await fs.writeFile(out, JSON.stringify(doc, null, 2) + "\n");
|
|
112
|
+
console.log(`Drafted ${r.count} row(s) → ${path.relative(process.cwd(), out)}`);
|
|
113
|
+
console.log(` headlines ${r.issue.headlines?.length ?? 0} · also ${r.issue.also?.length ?? 0} · cli/mcp ${r.issue.tools?.length ?? 0}`);
|
|
114
|
+
console.log(` next: enrich it, then \`flowiq updates preview ${path.relative(process.cwd(), out)} --open\``);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function asset(file, opts) {
|
|
118
|
+
const p = path.resolve(process.cwd(), file);
|
|
119
|
+
let buf;
|
|
120
|
+
try { buf = await fs.readFile(p); }
|
|
121
|
+
catch { console.error(`Error: cannot read ${p}`); process.exit(1); }
|
|
122
|
+
const ext = path.extname(p).toLowerCase();
|
|
123
|
+
const type = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".gif": "image/gif" }[ext];
|
|
124
|
+
if (!type) { console.error(`Error: ${ext || "(no extension)"} is not an image type the email can use (png, jpg, webp, gif)`); process.exit(1); }
|
|
125
|
+
let r;
|
|
126
|
+
try { r = await http.post("team-updates", { action: "asset", name: opts?.name || path.basename(p), content_type: type, data_base64: buf.toString("base64") }); }
|
|
127
|
+
catch (e) { console.error(`Upload failed: ${e.message}`); process.exit(1); }
|
|
128
|
+
console.log(r.url);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function preview(file, opts) {
|
|
132
|
+
const { issue, file: p } = await readIssue(file);
|
|
133
|
+
let r;
|
|
134
|
+
try { r = await http.post("team-updates", { action: "preview", issue, to_name: opts?.as || "Matt" }); }
|
|
135
|
+
catch (e) { console.error(`Preview failed: ${e.message}`); process.exit(1); }
|
|
136
|
+
const out = p.replace(/\.json$/i, "") + ".html";
|
|
137
|
+
await fs.writeFile(out, r.html);
|
|
138
|
+
console.log(`Subject: ${r.subject}`);
|
|
139
|
+
console.log(`Rendered → ${path.relative(process.cwd(), out)} (${(r.html.length / 1024).toFixed(1)} KB)`);
|
|
140
|
+
if (opts?.open) spawn(process.platform === "darwin" ? "open" : "xdg-open", [out], { stdio: "ignore", detached: true }).unref();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function send(file, opts) {
|
|
144
|
+
const { issue } = await readIssue(file);
|
|
145
|
+
const to = (opts?.to || "").split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
146
|
+
const test = !!opts?.test;
|
|
147
|
+
if (test && !to.length) {
|
|
148
|
+
let me;
|
|
149
|
+
try { me = await http.get("whoami"); }
|
|
150
|
+
catch (e) { console.error(`whoami failed: ${e.message}`); process.exit(1); }
|
|
151
|
+
const email = me?.user_email || me?.email || me?.userEmail;
|
|
152
|
+
if (!email) { console.error("Could not resolve your email for the test send; pass --to."); process.exit(1); }
|
|
153
|
+
to.push(email);
|
|
154
|
+
}
|
|
155
|
+
if (!test && !opts?.yes) {
|
|
156
|
+
console.error("Refusing: this sends to the whole Flowapt team and marks the changelog rows as covered. Re-run with --yes, or use --test first.");
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
let r;
|
|
160
|
+
try { r = await http.post("team-updates", { action: "send", issue, to: to.length ? to : undefined, test }); }
|
|
161
|
+
catch (e) { console.error(`Send failed: ${e.message}`); process.exit(1); }
|
|
162
|
+
const ok = (r.recipients ?? []).filter((x) => x.status === "sent");
|
|
163
|
+
const bad = (r.recipients ?? []).filter((x) => x.status !== "sent");
|
|
164
|
+
console.log(`${test ? "Test sent" : "Sent"}: "${issue.subject}"`);
|
|
165
|
+
for (const x of ok) console.log(` ✓ ${x.to}`);
|
|
166
|
+
for (const x of bad) console.log(` ✗ ${x.to} ${x.error ?? x.status}`);
|
|
167
|
+
if (!test) console.log(r.covers_until ? ` covered up to ${fmtWhen(r.covers_until)} SAST · record ${r.id}` : ` NOT marked as covered (every send failed)`);
|
|
168
|
+
if (bad.length) process.exitCode = 1;
|
|
169
|
+
}
|
package/src/index.js
CHANGED
|
@@ -21,6 +21,7 @@ import * as templatesCmd from "./commands/templates.js";
|
|
|
21
21
|
import * as orgCmd from "./commands/org.js";
|
|
22
22
|
import * as orgFlagsCmd from "./commands/org-flags.js";
|
|
23
23
|
import * as insightsCmd from "./commands/insights.js";
|
|
24
|
+
import * as teamUpdatesCmd from "./commands/team-updates.js";
|
|
24
25
|
import * as agentConfigCmd from "./commands/agent-config.js";
|
|
25
26
|
import * as agentsCmd from "./commands/agents.js";
|
|
26
27
|
import * as agentUpdatesCmd from "./commands/agent-updates.js";
|
|
@@ -864,6 +865,39 @@ export function run(argv) {
|
|
|
864
865
|
.option("--out <file>", "write the JSON to a file instead of stdout")
|
|
865
866
|
.action((orgId, path, opts) => storeApiCmd.wooGet(orgId, path, opts));
|
|
866
867
|
|
|
868
|
+
// updates — the "what changed in FlowIQ" email to the Flowapt team
|
|
869
|
+
const updates = program.command("updates").description("Team update email: what changed in FlowIQ, sent to every super admin (status / draft / preview / send)");
|
|
870
|
+
updates.command("status")
|
|
871
|
+
.description("uncovered changelog rows, recent sends and who receives the email")
|
|
872
|
+
.option("--json", "machine-readable output")
|
|
873
|
+
.action((opts) => teamUpdatesCmd.status(opts));
|
|
874
|
+
updates.command("uncovered")
|
|
875
|
+
.description("list the changelog rows no team update has covered yet")
|
|
876
|
+
.option("--limit <n>", "rows to list (default 60)")
|
|
877
|
+
.option("--json", "machine-readable output")
|
|
878
|
+
.action((opts) => teamUpdatesCmd.uncovered(opts));
|
|
879
|
+
updates.command("draft")
|
|
880
|
+
.description("write ./.flowiq/updates/<date>.json: a default issue composed from the uncovered rows, ready to enrich")
|
|
881
|
+
.option("--since <iso>", "compose from rows created after this time instead of the last covered point")
|
|
882
|
+
.option("--limit <n>", "rows to include (default 60)")
|
|
883
|
+
.option("--out <file>", "write the issue here instead of ./.flowiq/updates/<date>.json")
|
|
884
|
+
.action((opts) => teamUpdatesCmd.draft(opts));
|
|
885
|
+
updates.command("asset <file>")
|
|
886
|
+
.description("host an image (png/jpg/webp/gif, max 6 MB) for the email and print its public URL")
|
|
887
|
+
.option("--name <name>", "file name to store under (default: the file's own name)")
|
|
888
|
+
.action((file, opts) => teamUpdatesCmd.asset(file, opts));
|
|
889
|
+
updates.command("preview <file>")
|
|
890
|
+
.description("render the issue JSON to <file>.html exactly as it will be emailed")
|
|
891
|
+
.option("--as <first-name>", "greet this name in the preview (default Matt)")
|
|
892
|
+
.option("--open", "open the rendered HTML in the browser")
|
|
893
|
+
.action((file, opts) => teamUpdatesCmd.preview(file, opts));
|
|
894
|
+
updates.command("send <file>")
|
|
895
|
+
.description("email the issue: --test sends one copy to you (or --to) and marks nothing; without --test it goes to every super admin and needs --yes")
|
|
896
|
+
.option("--test", "send a single test copy only (to you unless --to is given); nothing is marked as covered")
|
|
897
|
+
.option("--to <emails>", "comma-separated recipients instead of the team list")
|
|
898
|
+
.option("--yes", "confirm the real send to the whole team")
|
|
899
|
+
.action((file, opts) => teamUpdatesCmd.send(file, opts));
|
|
900
|
+
|
|
867
901
|
// doctor (version + auth + server contract in one read-only command)
|
|
868
902
|
program.command("doctor")
|
|
869
903
|
.description("Check this install: version vs npm, auth, and whether the server still supports it. Exits 1 if something needs fixing")
|