@simplepush/cli 0.2.0 → 0.3.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/README.md +16 -0
- package/dist/main.mjs +340 -28
- package/dist/main.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ sp cancel Withdraw a pending task, subtask, or task group you sent.
|
|
|
22
22
|
sp notify Send a fire-and-forget notification.
|
|
23
23
|
sp collect Collect replies, inputs, or submissions as bounded NDJSON.
|
|
24
24
|
sp download Download one file a collect line referenced, by its ids.
|
|
25
|
+
sp get Read what came back: task listings, one task with its subtasks, a task group, submissions.
|
|
25
26
|
sp events Stream or replay the raw event feed.
|
|
26
27
|
sp auth Log in to an organization (login / logout / status).
|
|
27
28
|
sp org Administer an organization: members, invites, topics, API key, encryption.
|
|
@@ -192,3 +193,18 @@ Bun can compile the CLI into a single-file native binary:
|
|
|
192
193
|
bun run build:binary
|
|
193
194
|
./bin/simplepush --help
|
|
194
195
|
```
|
|
196
|
+
|
|
197
|
+
### Query what came back
|
|
198
|
+
|
|
199
|
+
The read commands mirror the MCP query tools, so an agent can ask questions like "every task that expired unanswered this week, grouped by assignee":
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
sp get tasks --status expired --since 7d --format json # one JSON object per line
|
|
203
|
+
sp get tasks --status pending --member anna --all # follow cursors to the end
|
|
204
|
+
sp get tsk_… # the chain: root + subtasks, decrypted where keys are held
|
|
205
|
+
sp get grptsk_… # per-recipient status of a group send
|
|
206
|
+
sp get submissions --since 24h --member anna
|
|
207
|
+
sp events --since 7d --member anna # events replay, narrowed client-side
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
A page that was cut off ends with `{"type":"more","cursor":"…"}` (`json`) or an info line (`pretty`); pass the cursor back with `--cursor`, or `--all` to page automatically. `--since`/`--until` take ISO-8601 instants or relative windows (`7d`, `12h`). For personal accounts the HTTP query surface (`sp get`) is a subscription feature — `sp events`, `sp collect` and `sp download` stay free.
|
package/dist/main.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
|
9
9
|
import { homedir, tmpdir } from "node:os";
|
|
10
10
|
import { createServer } from "node:http";
|
|
11
11
|
import { spawn } from "node:child_process";
|
|
12
|
-
import { Client, Keyring, OrgClient, TypeFilter, cancelSubtask, cancelTask, cancelTaskGroup, decryptBytes, deriveKey, encrypt, fetchUserInfo, isSubtaskGroupResponse, tryDecryptEventData } from "@simplepush/sdk";
|
|
12
|
+
import { Client, Keyring, OrgClient, TypeFilter, cancelSubtask, cancelTask, cancelTaskGroup, decryptBytes, decryptSubmission, decryptTaskPayload, decryptTaskSummary, deriveKey, encrypt, fetchUserInfo, isSubtaskGroupResponse, tryDecryptEventData } from "@simplepush/sdk";
|
|
13
13
|
import { mkdir, stat, writeFile } from "node:fs/promises";
|
|
14
14
|
import { connect, createConnection } from "node:net";
|
|
15
15
|
import path, { basename, extname, join } from "node:path";
|
|
@@ -546,7 +546,7 @@ var VaultAccess = class extends Effect.Service()("cli/VaultAccess", {
|
|
|
546
546
|
//#region src/global-options.ts
|
|
547
547
|
const DEFAULT_BASE_URL = "https://api.simplepu.sh";
|
|
548
548
|
const toHelp = (e) => HelpDoc.p(e instanceof Error ? e.message : String(e));
|
|
549
|
-
const topicOption = Options.text("topic").pipe(Options.withAlias("t"), Options.withDescription("Topic to send to (`task`) or filter on (`events`, repeatable). Omit on `task` for a self-send to your own devices."), Options.repeated);
|
|
549
|
+
const topicOption$1 = Options.text("topic").pipe(Options.withAlias("t"), Options.withDescription("Topic to send to (`task`) or filter on (`events`, repeatable). Omit on `task` for a self-send to your own devices."), Options.repeated);
|
|
550
550
|
const apiTokenOption = Options.text("api-token").pipe(Options.withDescription("API token. Required for `get`; `collect`, `events`, and `subtask` fall back to the logged-in org session when omitted. Defaults to $SP_API_TOKEN."), Options.withFallbackConfig(Config.string("SP_API_TOKEN")), Options.optional);
|
|
551
551
|
/** Personal-credential commands need the token; fail typed when absent. */
|
|
552
552
|
const requireApiToken = (token) => Option.match(token, {
|
|
@@ -1164,7 +1164,7 @@ function formatSent(groupId, createdAt, members) {
|
|
|
1164
1164
|
type: "sent",
|
|
1165
1165
|
groupId: groupId ?? null,
|
|
1166
1166
|
createdAt: createdAt ?? null,
|
|
1167
|
-
|
|
1167
|
+
instances: members.map((m) => ({
|
|
1168
1168
|
...m.kind === "notification" ? { notificationId: m.id } : { taskId: m.id },
|
|
1169
1169
|
...m.subtaskId !== void 0 ? { subtaskId: m.subtaskId } : {},
|
|
1170
1170
|
recipient: m.recipient
|
|
@@ -1303,14 +1303,14 @@ function fileViewsOf(item) {
|
|
|
1303
1303
|
]).filter(isFileView);
|
|
1304
1304
|
}
|
|
1305
1305
|
/** The terminal line: reason the stream stopped, per-type counts, and (for
|
|
1306
|
-
* group modes) per-
|
|
1306
|
+
* group modes) per-instance status. Always the last line on a clean run. */
|
|
1307
1307
|
function formatEnd(reason, counts, members, errorMsg) {
|
|
1308
1308
|
const obj = {
|
|
1309
1309
|
type: "end",
|
|
1310
1310
|
reason,
|
|
1311
1311
|
counts
|
|
1312
1312
|
};
|
|
1313
|
-
if (members) obj.
|
|
1313
|
+
if (members) obj.instances = members;
|
|
1314
1314
|
if (errorMsg !== void 0) obj.error = errorMsg;
|
|
1315
1315
|
return line(obj);
|
|
1316
1316
|
}
|
|
@@ -1463,7 +1463,7 @@ const SentLine = Schema.Struct({
|
|
|
1463
1463
|
type: Schema.Literal("sent"),
|
|
1464
1464
|
groupId: Schema.optional(Schema.NullOr(Schema.String)),
|
|
1465
1465
|
createdAt: Schema.optional(Schema.NullOr(Schema.String)),
|
|
1466
|
-
|
|
1466
|
+
instances: Schema.optional(Schema.Array(Schema.Struct({
|
|
1467
1467
|
taskId: Schema.optional(Schema.String),
|
|
1468
1468
|
notificationId: Schema.optional(Schema.String),
|
|
1469
1469
|
subtaskId: Schema.optional(Schema.String),
|
|
@@ -1525,9 +1525,9 @@ const loadOrgMasterKeys = Effect.gen(function* () {
|
|
|
1525
1525
|
});
|
|
1526
1526
|
const collectCommand = Command.make("collect", {
|
|
1527
1527
|
group: Options.text("group").pipe(Options.withDescription("Group id (grptsk_…) to collect over. Usually supplied via the piped `sent` line instead."), Options.optional),
|
|
1528
|
-
instance: Options.text("instance").pipe(Options.withDescription("
|
|
1528
|
+
instance: Options.text("instance").pipe(Options.withDescription("Instance id to collect: a task (tsk_…), a notification (ntf_…), or a subtask-scoped pair (tsk_…/sub_…). Repeatable. Augments/overrides the piped `sent` line's instances."), Options.repeated),
|
|
1529
1529
|
replies: Options.boolean("replies").pipe(Options.withDescription("Collect only replies. Default (no mode flag) is the full activity stream: inputs, replies, and completions.")),
|
|
1530
|
-
inputs: Options.boolean("inputs").pipe(Options.withDescription("Collect only input events (waits for every
|
|
1530
|
+
inputs: Options.boolean("inputs").pipe(Options.withDescription("Collect only input events (waits for every instance to complete by default).")),
|
|
1531
1531
|
submissions: Options.boolean("submissions").pipe(Options.withDescription("Collect submissions (your inbox) instead of a group's events.")),
|
|
1532
1532
|
since: mappedText("since", resolveSince).pipe(Options.withDescription("Resume point (`24h`, `7d`, or ISO 8601). Backfills group events or submissions from that point; defaults to the send's createdAt from the piped `sent` line. Implies --direct (the broker can't serve a deep backfill)."), Options.optional),
|
|
1533
1533
|
until: Options.text("until").pipe(Options.withDescription("Stop condition. Repeatable: complete | idle:<dur> | count:<n> | timeout:<dur> | forever (never stop; Ctrl-C to end). Default: complete for group collects; --replies / --submissions watch forever."), Options.repeated),
|
|
@@ -1600,7 +1600,7 @@ const collectCommand = Command.make("collect", {
|
|
|
1600
1600
|
});
|
|
1601
1601
|
}
|
|
1602
1602
|
};
|
|
1603
|
-
for (const m of sent?.
|
|
1603
|
+
for (const m of sent?.instances ?? []) addMember(m.taskId ?? m.notificationId, m.recipient ? {
|
|
1604
1604
|
publicId: m.recipient.publicId,
|
|
1605
1605
|
name: m.recipient.name ?? null
|
|
1606
1606
|
} : null, m.subtaskId);
|
|
@@ -1609,11 +1609,11 @@ const collectCommand = Command.make("collect", {
|
|
|
1609
1609
|
if (id.startsWith("sub_")) return yield* Effect.fail(new UserError({ message: `a subtask cannot be collected by its id alone (events route under the parent task) — pass --instance <parent tsk_…>/${id}` }));
|
|
1610
1610
|
addMember(id, null, subtaskId);
|
|
1611
1611
|
}
|
|
1612
|
-
if (members.length === 0) return yield* Effect.fail(new UserError({ message: "no
|
|
1612
|
+
if (members.length === 0) return yield* Effect.fail(new UserError({ message: "no instances to collect: pipe a send's `sent` line (`sp task --format json | sp collect`) or pass --instance <tsk_…|ntf_…|tsk_…/sub_…> (repeatable)" }));
|
|
1613
1613
|
const notifRoster = members[0].kind === "notification";
|
|
1614
|
-
if (members.some((m) => m.kind === "notification" !== notifRoster)) return yield* Effect.fail(new UserError({ message: "cannot mix task (tsk_…) and notification (ntf_…)
|
|
1614
|
+
if (members.some((m) => m.kind === "notification" !== notifRoster)) return yield* Effect.fail(new UserError({ message: "cannot mix task (tsk_…) and notification (ntf_…) instances in one collect — run one per kind" }));
|
|
1615
1615
|
const subRoster = members[0].subtaskId !== void 0;
|
|
1616
|
-
if (members.some((m) => m.subtaskId !== void 0 !== subRoster)) return yield* Effect.fail(new UserError({ message: "cannot mix subtask-scoped
|
|
1616
|
+
if (members.some((m) => m.subtaskId !== void 0 !== subRoster)) return yield* Effect.fail(new UserError({ message: "cannot mix subtask-scoped instances (a piped `sp subtask --format json` line or --instance tsk_…/sub_…) with plain task/notification instances in one collect" }));
|
|
1617
1617
|
const streamOpts = {
|
|
1618
1618
|
replay: true,
|
|
1619
1619
|
...until.idleMs !== void 0 ? { idleMs: until.idleMs } : {}
|
|
@@ -1675,7 +1675,7 @@ const collectCommand = Command.make("collect", {
|
|
|
1675
1675
|
});
|
|
1676
1676
|
}
|
|
1677
1677
|
if (args.format === "json") yield* out.print(formatSent(groupId, createdAt, members));
|
|
1678
|
-
else yield* out.info(`collecting ${mode} over ${members.length}
|
|
1678
|
+
else yield* out.info(`collecting ${mode} over ${members.length} instance(s)${groupId ? ` of ${groupId}` : ""}`);
|
|
1679
1679
|
yield* collectGroup(watchGroupId, source, members, args.format, until, saveDir);
|
|
1680
1680
|
}));
|
|
1681
1681
|
}));
|
|
@@ -2099,11 +2099,12 @@ function extractRaw(v) {
|
|
|
2099
2099
|
//#endregion
|
|
2100
2100
|
//#region src/commands/events.ts
|
|
2101
2101
|
const eventTypeOption = Options.text("type").pipe(Options.withDescription("Filter by event type. Repeatable."), Options.repeated);
|
|
2102
|
-
const sinceOption = mappedText("since", resolveSince).pipe(Options.withDescription("Replay from this point. Accepts `24h`, `7d`, or an ISO 8601 timestamp."), Options.optional);
|
|
2103
|
-
const untilOption = mappedText("until", resolveUntil).pipe(Options.withDescription("Stop at this timestamp. Forces a finite range, so `--follow` is ignored."), Options.optional);
|
|
2104
|
-
const limitOption = Options.integer("limit").pipe(Options.withDescription("Maximum number of events to print, then exit."), Options.optional);
|
|
2102
|
+
const sinceOption$1 = mappedText("since", resolveSince).pipe(Options.withDescription("Replay from this point. Accepts `24h`, `7d`, or an ISO 8601 timestamp."), Options.optional);
|
|
2103
|
+
const untilOption$1 = mappedText("until", resolveUntil).pipe(Options.withDescription("Stop at this timestamp. Forces a finite range, so `--follow` is ignored."), Options.optional);
|
|
2104
|
+
const limitOption$1 = Options.integer("limit").pipe(Options.withDescription("Maximum number of events to print, then exit."), Options.optional);
|
|
2105
2105
|
const followOption = Options.boolean("follow").pipe(Options.withAlias("f"), Options.withDescription("After history is drained, keep streaming live instead of exiting. Only meaningful with `--since`."));
|
|
2106
|
-
const
|
|
2106
|
+
const memberOption$3 = Options.text("member").pipe(Options.optional, Options.withDescription("Only events by this person: a usr_ id, or the actor's name."));
|
|
2107
|
+
const formatOption$4 = Options.choice("format", [
|
|
2107
2108
|
"json",
|
|
2108
2109
|
"pretty",
|
|
2109
2110
|
"raw"
|
|
@@ -2112,13 +2113,14 @@ const directOption = Options.boolean("direct").pipe(Options.withDescription("Ope
|
|
|
2112
2113
|
const QUIET_EXIT_MS = 2e3;
|
|
2113
2114
|
const eventsCommand = Command.make("events", {
|
|
2114
2115
|
type: eventTypeOption,
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2116
|
+
member: memberOption$3,
|
|
2117
|
+
since: sinceOption$1,
|
|
2118
|
+
until: untilOption$1,
|
|
2119
|
+
limit: limitOption$1,
|
|
2118
2120
|
follow: followOption,
|
|
2119
|
-
format: formatOption$
|
|
2121
|
+
format: formatOption$4,
|
|
2120
2122
|
direct: directOption,
|
|
2121
|
-
topic: topicOption,
|
|
2123
|
+
topic: topicOption$1,
|
|
2122
2124
|
"api-token": apiTokenOption,
|
|
2123
2125
|
password: passwordOption,
|
|
2124
2126
|
"base-url": baseUrlOption,
|
|
@@ -2132,6 +2134,8 @@ const eventsCommand = Command.make("events", {
|
|
|
2132
2134
|
const untilDate = Option.getOrUndefined(args.until);
|
|
2133
2135
|
const limit = Option.getOrUndefined(args.limit);
|
|
2134
2136
|
const filter = new TypeFilter(args.type);
|
|
2137
|
+
const member = Option.getOrUndefined(args.member);
|
|
2138
|
+
const memberMatches = (ev) => member === void 0 || ev.actor?.publicId === member || ev.actor?.name === member;
|
|
2135
2139
|
yield* Effect.forEach(filter.unknown, (u) => out.warn(`ignoring unknown --type \`${u}\``));
|
|
2136
2140
|
const webSocketFactory = args.direct || sinceIso !== void 0 ? Option.none() : yield* sharedWebSocketFactory({
|
|
2137
2141
|
credential: cred.kind === "personal" ? {
|
|
@@ -2194,7 +2198,7 @@ const eventsCommand = Command.make("events", {
|
|
|
2194
2198
|
yield* sdkStream("events stream", (signal) => client.events({
|
|
2195
2199
|
...sinceIso !== void 0 ? { since: sinceIso } : {},
|
|
2196
2200
|
signal: AbortSignal.any([signal, endController.signal])
|
|
2197
|
-
})).pipe(Stream.tap(() => Ref.set(lastActivity, Date.now())), Stream.interruptWhen(Deferred.await(halt)), untilDate !== void 0 ? Stream.takeUntilEffect((ev) => untilReached(ev) ? note("--until reached, exiting").pipe(Effect.zipRight(Deferred.succeed(halt, void 0)), Effect.as(true)) : Effect.succeed(false)) : (s) => s, Stream.filter((ev) => !untilReached(ev) && filter.matches(ev)), Stream.mapEffect((ev) => Effect.gen(function* () {
|
|
2201
|
+
})).pipe(Stream.tap(() => Ref.set(lastActivity, Date.now())), Stream.interruptWhen(Deferred.await(halt)), untilDate !== void 0 ? Stream.takeUntilEffect((ev) => untilReached(ev) ? note("--until reached, exiting").pipe(Effect.zipRight(Deferred.succeed(halt, void 0)), Effect.as(true)) : Effect.succeed(false)) : (s) => s, Stream.filter((ev) => !untilReached(ev) && filter.matches(ev) && memberMatches(ev)), Stream.mapEffect((ev) => Effect.gen(function* () {
|
|
2198
2202
|
const decrypted = keyring ? yield* sdkCall("decrypt event", () => tryDecryptEventData(ev, keyring)) : void 0;
|
|
2199
2203
|
yield* out.print(formatEvent(ev, args.format, decrypted));
|
|
2200
2204
|
const n = yield* Ref.updateAndGet(printed, (x) => x + 1);
|
|
@@ -2212,6 +2216,313 @@ const eventsCommand = Command.make("events", {
|
|
|
2212
2216
|
}));
|
|
2213
2217
|
}));
|
|
2214
2218
|
//#endregion
|
|
2219
|
+
//#region src/commands/query-support.ts
|
|
2220
|
+
/** Options every read command shares: credential, decryption keys, paging, output. */
|
|
2221
|
+
const credentialOptions = {
|
|
2222
|
+
"api-token": apiTokenOption,
|
|
2223
|
+
password: passwordOption,
|
|
2224
|
+
"base-url": baseUrlOption,
|
|
2225
|
+
quiet: quietOption
|
|
2226
|
+
};
|
|
2227
|
+
const formatOption$3 = Options.choice("format", ["json", "pretty"]).pipe(Options.withDefault("json"), Options.withDescription("json = one JSON object per line (agents, jq); pretty = human-readable lines."));
|
|
2228
|
+
const sinceOption = mappedText("since", resolveSince).pipe(Options.optional, Options.withDescription("Only items created at or after this instant: ISO-8601, or a relative window like 7d, 12h, 30m."));
|
|
2229
|
+
const untilOption = mappedText("until", (raw) => resolveUntil(raw).toISOString()).pipe(Options.optional, Options.withDescription("Only items created at or before this instant (same forms as --since)."));
|
|
2230
|
+
const memberOption$2 = Options.text("member").pipe(Options.optional, Options.withDescription("Only items involving this person: a usr_ id, or a member name on an org."));
|
|
2231
|
+
const limitOption = Options.integer("limit").pipe(Options.optional, Options.withDescription("Page size (server default 50). With --all this is the page size, not a cap."));
|
|
2232
|
+
const cursorOption = Options.text("cursor").pipe(Options.optional, Options.withDescription("Continue a previous page: pass the cursor it reported."));
|
|
2233
|
+
const allOption = Options.boolean("all").pipe(Options.withDescription("Follow cursors until the last page instead of printing one page and a cursor."));
|
|
2234
|
+
const STATUSES = [
|
|
2235
|
+
"pending",
|
|
2236
|
+
"completed",
|
|
2237
|
+
"canceled",
|
|
2238
|
+
"declined",
|
|
2239
|
+
"expired"
|
|
2240
|
+
];
|
|
2241
|
+
/** `--status` accepts repeats and comma lists: `--status expired --status declined` or `--status expired,declined`. */
|
|
2242
|
+
const statusOption = Options.text("status").pipe(Options.repeated, Options.mapTryCatch((values) => {
|
|
2243
|
+
const flat = values.flatMap((v) => v.split(",")).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
2244
|
+
const bad = flat.filter((s) => !STATUSES.includes(s));
|
|
2245
|
+
if (bad.length > 0) throw new Error(`unknown status ${bad.join(", ")} — use ${STATUSES.join(", ")}`);
|
|
2246
|
+
return flat;
|
|
2247
|
+
}, (e) => HelpDoc.p(e instanceof Error ? e.message : String(e))), Options.withDescription("Only tasks in these states (repeatable or comma-separated)."));
|
|
2248
|
+
/** Resolves the credential (personal API-Token or the saved org session),
|
|
2249
|
+
* builds the matching client and — when keys are available — a keyring, and
|
|
2250
|
+
* runs `body` inside the client's scope. Mirrors what `sp events` does. */
|
|
2251
|
+
const withQueryClient = (args, body) => Effect.gen(function* () {
|
|
2252
|
+
const out = yield* CliOutput;
|
|
2253
|
+
yield* out.setQuiet(args.quiet);
|
|
2254
|
+
const cred = yield* resolveCredential(args["api-token"], args["base-url"]);
|
|
2255
|
+
const orgKeys = cred.kind === "org" ? yield* loadOrgMasterKeys : void 0;
|
|
2256
|
+
return yield* Effect.scoped(Effect.gen(function* () {
|
|
2257
|
+
const client = cred.kind === "personal" ? yield* acquireClient({
|
|
2258
|
+
baseUrl: cred.baseUrl,
|
|
2259
|
+
apiToken: cred.apiToken,
|
|
2260
|
+
passwords: args.password
|
|
2261
|
+
}) : yield* acquireOrgClient({
|
|
2262
|
+
baseUrl: cred.baseUrl,
|
|
2263
|
+
bearerToken: cred.bearer,
|
|
2264
|
+
...orgKeys !== void 0 ? { orgMasterKeys: orgKeys } : {}
|
|
2265
|
+
});
|
|
2266
|
+
return yield* body({
|
|
2267
|
+
client,
|
|
2268
|
+
keyring: cred.kind === "org" ? orgKeys !== void 0 ? yield* sdkCall("keyring", () => client.keyring()) : void 0 : args.password.length > 0 ? yield* sdkCall("keyring", () => client.keyring({ includePasswordSalt: true })) : void 0,
|
|
2269
|
+
org: cred.kind === "org",
|
|
2270
|
+
out
|
|
2271
|
+
});
|
|
2272
|
+
}));
|
|
2273
|
+
});
|
|
2274
|
+
/** Decrypts what the keyring can and counts what it cannot, so a command can
|
|
2275
|
+
* report "N values left as ciphertext" once instead of per row. Each method
|
|
2276
|
+
* maps one wire shape onto its SDK field-schema decryptor. */
|
|
2277
|
+
var Decryptor = class {
|
|
2278
|
+
undecryptable = 0;
|
|
2279
|
+
constructor(keyring) {
|
|
2280
|
+
this.keyring = keyring;
|
|
2281
|
+
}
|
|
2282
|
+
run(value, go) {
|
|
2283
|
+
if (this.keyring === void 0) return Effect.succeed(value);
|
|
2284
|
+
const keyring = this.keyring;
|
|
2285
|
+
return Effect.promise(() => go(keyring)).pipe(Effect.map((r) => {
|
|
2286
|
+
this.undecryptable += r.undecryptable;
|
|
2287
|
+
return r.value;
|
|
2288
|
+
}));
|
|
2289
|
+
}
|
|
2290
|
+
/** A task or subtask payload (chain reads). */
|
|
2291
|
+
payload(value) {
|
|
2292
|
+
return this.run(value, (kr) => decryptTaskPayload(value, kr));
|
|
2293
|
+
}
|
|
2294
|
+
/** A task index / roster row. */
|
|
2295
|
+
summary(value) {
|
|
2296
|
+
return this.run(value, (kr) => decryptTaskSummary(value, kr));
|
|
2297
|
+
}
|
|
2298
|
+
/** A submission under its feed entry's marker. */
|
|
2299
|
+
submission(value, marker) {
|
|
2300
|
+
return this.run(value, (kr) => decryptSubmission(value, kr, marker));
|
|
2301
|
+
}
|
|
2302
|
+
report(out) {
|
|
2303
|
+
return this.undecryptable > 0 ? out.warn(`${this.undecryptable} value(s) are encrypted under a key this CLI does not hold and were left as ciphertext`) : Effect.void;
|
|
2304
|
+
}
|
|
2305
|
+
};
|
|
2306
|
+
/** Runs `page` for the first cursor, then keeps following cursors when `all`
|
|
2307
|
+
* is set; otherwise reports the cursor of the next page so the caller can
|
|
2308
|
+
* continue by hand. */
|
|
2309
|
+
const forEachPage = (ctx, format, all, first, page, handle) => Effect.gen(function* () {
|
|
2310
|
+
let cursor = first;
|
|
2311
|
+
for (;;) {
|
|
2312
|
+
const p = yield* page(cursor);
|
|
2313
|
+
yield* handle(p);
|
|
2314
|
+
if (p.nextCursor === void 0) return;
|
|
2315
|
+
if (!all) {
|
|
2316
|
+
yield* format === "json" ? ctx.out.print(JSON.stringify({
|
|
2317
|
+
type: "more",
|
|
2318
|
+
cursor: p.nextCursor
|
|
2319
|
+
})) : ctx.out.info(`more available — continue with --cursor ${p.nextCursor} (or pass --all)`);
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
cursor = p.nextCursor;
|
|
2323
|
+
}
|
|
2324
|
+
});
|
|
2325
|
+
function recipientLabels(t) {
|
|
2326
|
+
return t.recipients.map((r) => r.name ?? r.publicId);
|
|
2327
|
+
}
|
|
2328
|
+
/** One line per task: id, state, when, title, who, subtask counts, group. */
|
|
2329
|
+
function formatTaskSummary(t) {
|
|
2330
|
+
const subs = Object.entries(t.subtasks).map(([k, v]) => `${k} ${v}`).join(", ");
|
|
2331
|
+
return [
|
|
2332
|
+
`${t.taskId} [${t.status}] ${t.createdAt}`,
|
|
2333
|
+
t.title !== void 0 ? ` ${t.title}` : void 0,
|
|
2334
|
+
` to: ${recipientLabels(t).join(", ") || "-"}`,
|
|
2335
|
+
t.inputs.length > 0 ? ` inputs: ${t.inputs.join(", ")}` : void 0,
|
|
2336
|
+
t.reply !== void 0 ? ` reply: ${t.reply}` : void 0,
|
|
2337
|
+
subs ? ` subtasks: ${subs}` : void 0,
|
|
2338
|
+
t.expiresAt !== void 0 ? ` expires: ${t.expiresAt}` : void 0,
|
|
2339
|
+
t.groupId !== void 0 ? ` group: ${t.groupId}` : void 0
|
|
2340
|
+
].filter((l) => l !== void 0).join("\n");
|
|
2341
|
+
}
|
|
2342
|
+
//#endregion
|
|
2343
|
+
//#region src/commands/get.ts
|
|
2344
|
+
const topicOption = Options.text("topic").pipe(Options.optional, Options.withDescription("Only tasks sent to this topic: its value (name), or its id."));
|
|
2345
|
+
const groupOption = Options.text("group").pipe(Options.optional, Options.withDescription("Only the instances of this grptsk_ group."));
|
|
2346
|
+
/** Flags that only make sense on the paged listings. Id reads reject them by
|
|
2347
|
+
* name instead of silently ignoring them. */
|
|
2348
|
+
function rejectListFlags(args, target, allowStatus) {
|
|
2349
|
+
const set = [
|
|
2350
|
+
...!allowStatus && args.status.length > 0 ? ["--status"] : [],
|
|
2351
|
+
...Option.isSome(args.since) ? ["--since"] : [],
|
|
2352
|
+
...Option.isSome(args.until) ? ["--until"] : [],
|
|
2353
|
+
...Option.isSome(args.topic) ? ["--topic"] : [],
|
|
2354
|
+
...Option.isSome(args.member) ? ["--member"] : [],
|
|
2355
|
+
...Option.isSome(args.group) ? ["--group"] : [],
|
|
2356
|
+
...Option.isSome(args.limit) ? ["--limit"] : [],
|
|
2357
|
+
...Option.isSome(args.cursor) ? ["--cursor"] : [],
|
|
2358
|
+
...args.all ? ["--all"] : []
|
|
2359
|
+
];
|
|
2360
|
+
return set.length === 0 ? Effect.void : Effect.fail(new UserError({ message: `${set.join(", ")} does not apply when reading ${target}` }));
|
|
2361
|
+
}
|
|
2362
|
+
function getChain(args, ctx) {
|
|
2363
|
+
return Effect.gen(function* () {
|
|
2364
|
+
yield* rejectListFlags(args, "one task", false);
|
|
2365
|
+
const chain = yield* sdkCall("read task chain", () => ctx.client.getTaskChain(args.what));
|
|
2366
|
+
const dec = new Decryptor(ctx.keyring);
|
|
2367
|
+
const task = yield* dec.payload(chain.task);
|
|
2368
|
+
const subtasks = [];
|
|
2369
|
+
for (const s of chain.subtasks) subtasks.push({
|
|
2370
|
+
subtask: yield* dec.payload(s.subtask),
|
|
2371
|
+
createdAt: s.createdAt
|
|
2372
|
+
});
|
|
2373
|
+
const view = {
|
|
2374
|
+
task,
|
|
2375
|
+
createdAt: chain.createdAt,
|
|
2376
|
+
subtasks
|
|
2377
|
+
};
|
|
2378
|
+
if (args.format === "json") yield* ctx.out.print(JSON.stringify(view));
|
|
2379
|
+
else {
|
|
2380
|
+
yield* ctx.out.print(`${task.taskId} [${task.status}] ${chain.createdAt}`);
|
|
2381
|
+
yield* ctx.out.print(JSON.stringify(task, null, 2));
|
|
2382
|
+
for (const s of subtasks) {
|
|
2383
|
+
yield* ctx.out.print(`\n${s.subtask.subtaskId} [${s.subtask.status}] ${s.createdAt}`);
|
|
2384
|
+
yield* ctx.out.print(JSON.stringify(s.subtask, null, 2));
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
yield* dec.report(ctx.out);
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
function getRoster(args, ctx) {
|
|
2391
|
+
return Effect.gen(function* () {
|
|
2392
|
+
yield* rejectListFlags(args, "a task group roster", true);
|
|
2393
|
+
const roster = yield* sdkCall("read task group", () => ctx.client.getTaskGroup(args.what, args.status.length > 0 ? { status: args.status } : {}));
|
|
2394
|
+
const dec = new Decryptor(ctx.keyring);
|
|
2395
|
+
const counts = {};
|
|
2396
|
+
for (const t of roster.tasks) counts[t.status] = (counts[t.status] ?? 0) + 1;
|
|
2397
|
+
if (args.format === "json") {
|
|
2398
|
+
yield* ctx.out.print(JSON.stringify({
|
|
2399
|
+
type: "group",
|
|
2400
|
+
groupId: roster.groupId,
|
|
2401
|
+
counts
|
|
2402
|
+
}));
|
|
2403
|
+
for (const t of roster.tasks) yield* ctx.out.print(JSON.stringify(yield* dec.summary(t)));
|
|
2404
|
+
} else {
|
|
2405
|
+
const summary = Object.entries(counts).map(([k, v]) => `${k} ${v}`).join(", ") || "no instances";
|
|
2406
|
+
yield* ctx.out.print(`${roster.groupId} ${summary}`);
|
|
2407
|
+
for (const t of roster.tasks) yield* ctx.out.print(formatTaskSummary(yield* dec.summary(t)));
|
|
2408
|
+
}
|
|
2409
|
+
yield* dec.report(ctx.out);
|
|
2410
|
+
});
|
|
2411
|
+
}
|
|
2412
|
+
function getTasks(args, ctx) {
|
|
2413
|
+
return Effect.gen(function* () {
|
|
2414
|
+
const dec = new Decryptor(ctx.keyring);
|
|
2415
|
+
const opts = {
|
|
2416
|
+
...args.status.length > 0 ? { status: args.status } : {},
|
|
2417
|
+
...Option.match(args.since, {
|
|
2418
|
+
onNone: () => ({}),
|
|
2419
|
+
onSome: (since) => ({ since })
|
|
2420
|
+
}),
|
|
2421
|
+
...Option.match(args.until, {
|
|
2422
|
+
onNone: () => ({}),
|
|
2423
|
+
onSome: (until) => ({ until })
|
|
2424
|
+
}),
|
|
2425
|
+
...Option.match(args.topic, {
|
|
2426
|
+
onNone: () => ({}),
|
|
2427
|
+
onSome: (topic) => ({ topic })
|
|
2428
|
+
}),
|
|
2429
|
+
...Option.match(args.member, {
|
|
2430
|
+
onNone: () => ({}),
|
|
2431
|
+
onSome: (member) => ({ member })
|
|
2432
|
+
}),
|
|
2433
|
+
...Option.match(args.group, {
|
|
2434
|
+
onNone: () => ({}),
|
|
2435
|
+
onSome: (group) => ({ group })
|
|
2436
|
+
}),
|
|
2437
|
+
...Option.match(args.limit, {
|
|
2438
|
+
onNone: () => ({}),
|
|
2439
|
+
onSome: (limit) => ({ limit })
|
|
2440
|
+
})
|
|
2441
|
+
};
|
|
2442
|
+
yield* forEachPage(ctx, args.format, args.all, Option.getOrUndefined(args.cursor), (cursor) => sdkCall("list tasks", () => ctx.client.listTasks({
|
|
2443
|
+
...opts,
|
|
2444
|
+
...cursor !== void 0 ? { cursor } : {}
|
|
2445
|
+
})), (page) => Effect.forEach(page.tasks, (t) => Effect.gen(function* () {
|
|
2446
|
+
const row = yield* dec.summary(t);
|
|
2447
|
+
yield* ctx.out.print(args.format === "json" ? JSON.stringify(row) : formatTaskSummary(row));
|
|
2448
|
+
}), { discard: true }));
|
|
2449
|
+
yield* dec.report(ctx.out);
|
|
2450
|
+
});
|
|
2451
|
+
}
|
|
2452
|
+
function getSubmissions(args, ctx) {
|
|
2453
|
+
return Effect.gen(function* () {
|
|
2454
|
+
const wrong = [
|
|
2455
|
+
...args.status.length > 0 ? ["--status"] : [],
|
|
2456
|
+
...Option.isSome(args.topic) ? ["--topic"] : [],
|
|
2457
|
+
...Option.isSome(args.group) ? ["--group"] : []
|
|
2458
|
+
];
|
|
2459
|
+
if (wrong.length > 0) return yield* Effect.fail(new UserError({ message: `${wrong.join(", ")} does not apply to submissions` }));
|
|
2460
|
+
const dec = new Decryptor(ctx.keyring);
|
|
2461
|
+
const opts = {
|
|
2462
|
+
...Option.match(args.since, {
|
|
2463
|
+
onNone: () => ({}),
|
|
2464
|
+
onSome: (since) => ({ since })
|
|
2465
|
+
}),
|
|
2466
|
+
...Option.match(args.until, {
|
|
2467
|
+
onNone: () => ({}),
|
|
2468
|
+
onSome: (until) => ({ until })
|
|
2469
|
+
}),
|
|
2470
|
+
...Option.match(args.member, {
|
|
2471
|
+
onNone: () => ({}),
|
|
2472
|
+
onSome: (member) => ({ member })
|
|
2473
|
+
}),
|
|
2474
|
+
...Option.match(args.limit, {
|
|
2475
|
+
onNone: () => ({}),
|
|
2476
|
+
onSome: (limit) => ({ limit })
|
|
2477
|
+
})
|
|
2478
|
+
};
|
|
2479
|
+
yield* forEachPage(ctx, args.format, args.all, Option.getOrUndefined(args.cursor), (cursor) => sdkCall("list submissions", () => ctx.client.listSubmissions({
|
|
2480
|
+
...opts,
|
|
2481
|
+
...cursor !== void 0 ? { cursor } : {}
|
|
2482
|
+
})), (page) => Effect.forEach(page.submissions, (entry) => Effect.gen(function* () {
|
|
2483
|
+
const submission = yield* dec.submission(entry.submission, entry.encryption);
|
|
2484
|
+
const by = entry.actor?.name ?? entry.actor?.publicId;
|
|
2485
|
+
if (args.format === "json") yield* ctx.out.print(JSON.stringify({
|
|
2486
|
+
...submission,
|
|
2487
|
+
...entry.actor !== void 0 ? { actor: entry.actor } : {}
|
|
2488
|
+
}));
|
|
2489
|
+
else {
|
|
2490
|
+
const body = submission.body;
|
|
2491
|
+
const parts = [
|
|
2492
|
+
body?.value !== void 0 ? body.value : void 0,
|
|
2493
|
+
submission.photo !== void 0 ? "[photo]" : void 0,
|
|
2494
|
+
submission.file !== void 0 ? "[file]" : void 0,
|
|
2495
|
+
submission.audio !== void 0 ? "[audio]" : void 0,
|
|
2496
|
+
submission.location !== void 0 ? "[location]" : void 0
|
|
2497
|
+
].filter((p) => p !== void 0);
|
|
2498
|
+
yield* ctx.out.print(`${submission.id} ${submission.createdAt} ${by ?? "-"}: ${parts.join(" ") || "(empty)"}`);
|
|
2499
|
+
}
|
|
2500
|
+
}), { discard: true }));
|
|
2501
|
+
yield* dec.report(ctx.out);
|
|
2502
|
+
});
|
|
2503
|
+
}
|
|
2504
|
+
const getCommand = Command.make("get", {
|
|
2505
|
+
what: Args.text({ name: "what" }).pipe(Args.withDescription("What to read: tasks, submissions, a tsk_ id, or a grptsk_ id.")),
|
|
2506
|
+
status: statusOption,
|
|
2507
|
+
since: sinceOption,
|
|
2508
|
+
until: untilOption,
|
|
2509
|
+
topic: topicOption,
|
|
2510
|
+
member: memberOption$2,
|
|
2511
|
+
group: groupOption,
|
|
2512
|
+
limit: limitOption,
|
|
2513
|
+
cursor: cursorOption,
|
|
2514
|
+
all: allOption,
|
|
2515
|
+
format: formatOption$3,
|
|
2516
|
+
...credentialOptions
|
|
2517
|
+
}, (args) => withQueryClient(args, (ctx) => {
|
|
2518
|
+
if (args.what === "tasks") return getTasks(args, ctx);
|
|
2519
|
+
if (args.what === "submissions") return getSubmissions(args, ctx);
|
|
2520
|
+
if (args.what.startsWith("tsk_")) return getChain(args, ctx);
|
|
2521
|
+
if (args.what.startsWith("grptsk_")) return getRoster(args, ctx);
|
|
2522
|
+
const hint = args.what.startsWith("sub_") ? "a subtask is read through its parent: sp get tsk_…" : args.what.startsWith("sbm_") ? "a single submission has no read endpoint; find it in the listing: sp get submissions --since … --format json" : "expected tasks, submissions, a tsk_ id, or a grptsk_ id";
|
|
2523
|
+
return Effect.fail(new UserError({ message: `cannot read '${args.what}': ${hint}` }));
|
|
2524
|
+
})).pipe(Command.withDescription("Read what came back: 'tasks' or 'submissions' for filtered listings, a tsk_ id for one task with its subtasks, a grptsk_ id for a group's per-recipient roster."));
|
|
2525
|
+
//#endregion
|
|
2215
2526
|
//#region src/input-spec.ts
|
|
2216
2527
|
const SETTING_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
2217
2528
|
function looksLikeSetting(seg) {
|
|
@@ -2621,7 +2932,7 @@ const notifyCommand = Command.make("notify", {
|
|
|
2621
2932
|
shared: sharedOption$1,
|
|
2622
2933
|
format: formatOption$2,
|
|
2623
2934
|
noEncrypt: noEncryptOption$2,
|
|
2624
|
-
topic: topicOption,
|
|
2935
|
+
topic: topicOption$1,
|
|
2625
2936
|
"api-token": apiTokenOption,
|
|
2626
2937
|
password: passwordOption,
|
|
2627
2938
|
"base-url": baseUrlOption,
|
|
@@ -2812,7 +3123,7 @@ const putIntegrationWraps = (id, wraps) => Effect.gen(function* () {
|
|
|
2812
3123
|
yield* (yield* Api).put("integration wrap", `/v1/org/integrations/${encodeURIComponent(id)}/wraps`, { wraps });
|
|
2813
3124
|
});
|
|
2814
3125
|
const nameOption = Options.text("name").pipe(Options.withDescription("Human-readable label, shown in `sp integration list` and nowhere else."));
|
|
2815
|
-
const scopesOption = Options.text("scopes").pipe(Options.withDefault("send,
|
|
3126
|
+
const scopesOption = Options.text("scopes").pipe(Options.withDefault("send,read"), Options.withDescription("Comma-separated scopes (send, read, files:read). org:admin is refused — administration is a human act."));
|
|
2816
3127
|
const createCommand = Command.make("create", {
|
|
2817
3128
|
name: nameOption,
|
|
2818
3129
|
scopes: scopesOption,
|
|
@@ -3535,7 +3846,7 @@ const taskCommand = Command.make("task", {
|
|
|
3535
3846
|
shared: sharedOption,
|
|
3536
3847
|
expires: expiresOption,
|
|
3537
3848
|
format: formatOption$1,
|
|
3538
|
-
topic: topicOption,
|
|
3849
|
+
topic: topicOption$1,
|
|
3539
3850
|
"api-token": apiTokenOption,
|
|
3540
3851
|
password: passwordOption,
|
|
3541
3852
|
"base-url": baseUrlOption,
|
|
@@ -3841,7 +4152,7 @@ const subtaskCommand = Command.make("subtask", {
|
|
|
3841
4152
|
markdown: markdownOption,
|
|
3842
4153
|
"no-encrypt": noEncryptOption,
|
|
3843
4154
|
instance: instanceOption,
|
|
3844
|
-
topic: topicOption,
|
|
4155
|
+
topic: topicOption$1,
|
|
3845
4156
|
"api-token": apiTokenOption,
|
|
3846
4157
|
password: passwordOption,
|
|
3847
4158
|
"base-url": baseUrlOption,
|
|
@@ -4022,7 +4333,8 @@ const root = Command.make("simplepush").pipe(Command.withSubcommands([
|
|
|
4022
4333
|
notifyCommand,
|
|
4023
4334
|
taskCommand,
|
|
4024
4335
|
subtaskCommand,
|
|
4025
|
-
cancelCommand
|
|
4336
|
+
cancelCommand,
|
|
4337
|
+
getCommand
|
|
4026
4338
|
]));
|
|
4027
4339
|
const cli = Command.run(root, {
|
|
4028
4340
|
name: "Simplepush CLI",
|