@simplepush/cli 0.2.1 → 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 +329 -17
- 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, {
|
|
@@ -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",
|