@simplepush/cli 0.4.0 → 0.6.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/dist/main.mjs CHANGED
@@ -13,6 +13,9 @@ import { Client, DownloadError, OrgClient, TypeFilter, decryptSubmission, decryp
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";
16
+ //#region package.json
17
+ var version = "0.6.0";
18
+ //#endregion
16
19
  //#region src/errors.ts
17
20
  /** No CLI session saved — `sp auth login` hasn't been run (or was logged out). */
18
21
  var NotLoggedIn = class extends Data.TaggedError("NotLoggedIn") {};
@@ -1887,7 +1890,7 @@ const formatOption$4 = Options.choice("format", ["json", "pretty"]).pipe(Options
1887
1890
  const sinceOption$1 = 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."));
1888
1891
  const untilOption$1 = mappedText("until", (raw) => resolveUntil(raw).toISOString()).pipe(Options.optional, Options.withDescription("Only items created at or before this instant (same forms as --since)."));
1889
1892
  const memberOption$3 = Options.text("member").pipe(Options.optional, Options.withDescription("Only items involving this person: a usr_ id, or a member name on an org."));
1890
- const limitOption$1 = Options.integer("limit").pipe(Options.optional, Options.withDescription("Page size (server default 50). With --all this is the page size, not a cap."));
1893
+ const limitOption$2 = Options.integer("limit").pipe(Options.optional, Options.withDescription("Page size (server default 50). With --all this is the page size, not a cap."));
1891
1894
  const cursorOption = Options.text("cursor").pipe(Options.optional, Options.withDescription("Continue a previous page: pass the cursor it reported."));
1892
1895
  const allOption = Options.boolean("all").pipe(Options.withDescription("Follow cursors until the last page instead of printing one page and a cursor."));
1893
1896
  const STATUSES = [
@@ -2119,7 +2122,7 @@ function extractRaw(v) {
2119
2122
  const eventTypeOption = Options.text("type").pipe(Options.withDescription("Filter by event type. Repeatable."), Options.repeated);
2120
2123
  const sinceOption = mappedText("since", resolveSince).pipe(Options.withDescription("Replay from this point. Accepts `24h`, `7d`, or an ISO 8601 timestamp."), Options.optional);
2121
2124
  const untilOption = mappedText("until", resolveUntil).pipe(Options.withDescription("Stop at this timestamp. Forces a finite range, so `--follow` is ignored."), Options.optional);
2122
- const limitOption = Options.integer("limit").pipe(Options.withDescription("Maximum number of events to print, then exit."), Options.optional);
2125
+ const limitOption$1 = Options.integer("limit").pipe(Options.withDescription("Maximum number of events to print, then exit."), Options.optional);
2123
2126
  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`."));
2124
2127
  const memberOption$2 = Options.text("member").pipe(Options.optional, Options.withDescription("Only events by this person: a usr_ id, or the actor's name."));
2125
2128
  const formatOption$3 = Options.choice("format", [
@@ -2134,7 +2137,7 @@ const eventsCommand = Command.make("events", {
2134
2137
  member: memberOption$2,
2135
2138
  since: sinceOption,
2136
2139
  until: untilOption,
2137
- limit: limitOption,
2140
+ limit: limitOption$1,
2138
2141
  follow: followOption,
2139
2142
  format: formatOption$3,
2140
2143
  direct: directOption,
@@ -2417,7 +2420,7 @@ const getCommand = Command.make("get", {
2417
2420
  topic: topicOption,
2418
2421
  member: memberOption$3,
2419
2422
  group: groupOption,
2420
- limit: limitOption$1,
2423
+ limit: limitOption$2,
2421
2424
  cursor: cursorOption,
2422
2425
  all: allOption,
2423
2426
  format: formatOption$4,
@@ -2432,6 +2435,100 @@ const getCommand = Command.make("get", {
2432
2435
  return Effect.fail(new UserError({ message: `cannot read '${args.what}': ${hint}` }));
2433
2436
  })).pipe(Command.withDescription("Read what came back: 'tasks' or 'submissions' for filtered listings, a tsk_ id for one task with its subtasks, a sub_ id for one subtask, a grptsk_ id for a group's per-recipient roster."));
2434
2437
  //#endregion
2438
+ //#region src/commands/search.ts
2439
+ const KINDS = [
2440
+ "task",
2441
+ "subtask",
2442
+ "answer",
2443
+ "reply",
2444
+ "notification",
2445
+ "notification_answer",
2446
+ "submission"
2447
+ ];
2448
+ const kindOption = Options.text("kind").pipe(Options.repeated, Options.mapTryCatch((values) => {
2449
+ const flat = values.flatMap((v) => v.split(",")).map((s) => s.trim()).filter((s) => s.length > 0);
2450
+ const bad = flat.filter((s) => !KINDS.includes(s));
2451
+ if (bad.length > 0) throw new Error(`unknown kind ${bad.join(", ")} — use ${KINDS.join(", ")}`);
2452
+ return flat;
2453
+ }, (e) => HelpDoc.p(e instanceof Error ? e.message : String(e))), Options.withDescription("Only these hit kinds (repeatable or comma-separated)."));
2454
+ function parsePoint(raw) {
2455
+ const parts = raw.split(",");
2456
+ if (parts.length === 2) {
2457
+ const latitude = Number(parts[0].trim());
2458
+ const longitude = Number(parts[1].trim());
2459
+ if (Number.isFinite(latitude) && Number.isFinite(longitude) && Math.abs(latitude) <= 90 && Math.abs(longitude) <= 180) return {
2460
+ latitude,
2461
+ longitude
2462
+ };
2463
+ }
2464
+ throw new Error(`expected \`lat,lng\` with lat in [-90,90] and lng in [-180,180], got '${raw}'`);
2465
+ }
2466
+ const nearOption = Options.text("near").pipe(Options.mapTryCatch(parsePoint, (e) => HelpDoc.p(e instanceof Error ? e.message : String(e))), Options.optional, Options.withDescription("Center of a radius filter as `lat,lng`; requires --radius. Hits come back with their point and its distance, nearest first when no words are given."));
2467
+ const radiusOption = Options.integer("radius").pipe(Options.optional, Options.withDescription("Radius around --near, in meters (1..1000000)."));
2468
+ const withinOption = Options.text("within").pipe(Options.mapTryCatch((raw) => {
2469
+ const points = raw.split(";").map((s) => s.trim()).filter((s) => s.length > 0).map(parsePoint);
2470
+ if (points.length < 3) throw new Error("--within needs at least 3 `lat,lng` points separated by `;`");
2471
+ return points;
2472
+ }, (e) => HelpDoc.p(e instanceof Error ? e.message : String(e))), Options.optional, Options.withDescription("Polygon to search inside: `lat,lng` corners joined with `;` (at least 3). Not combinable with --near/--radius; hits carry no distance and come newest first without words."));
2473
+ const limitOption = Options.integer("limit").pipe(Options.optional, Options.withDescription("Best hits to return; server default 20, at most 100."));
2474
+ /** One block per hit: id, kind, when, then whatever the hit carries. */
2475
+ function formatHit(h) {
2476
+ return [
2477
+ `${h.ref} [${h.kind}] ${h.createdAt}`,
2478
+ h.title !== void 0 ? ` ${h.title}` : void 0,
2479
+ h.actor !== void 0 ? ` by: ${h.actor}` : void 0,
2480
+ h.snippet !== void 0 ? ` ${h.snippet}` : void 0,
2481
+ h.location !== void 0 ? ` at: ${h.location.latitude},${h.location.longitude}${h.location.distanceMeters !== void 0 ? ` (${Math.round(h.location.distanceMeters)}m away)` : ""}` : void 0
2482
+ ].filter((l) => l !== void 0).join("\n");
2483
+ }
2484
+ const searchCommand = Command.make("search", {
2485
+ words: Args.text({ name: "words" }).pipe(Args.withDescription("The words to look for; all must appear. Quote a phrase (nested quotes) for adjacency. Optional when --near/--radius or --within is given."), Args.repeated),
2486
+ kind: kindOption,
2487
+ since: sinceOption$1,
2488
+ until: untilOption$1,
2489
+ member: memberOption$3,
2490
+ near: nearOption,
2491
+ radius: radiusOption,
2492
+ within: withinOption,
2493
+ limit: limitOption,
2494
+ format: formatOption$4,
2495
+ ...credentialOptions
2496
+ }, (args) => withQueryClient(args, (ctx) => Effect.gen(function* () {
2497
+ const query = args.words.join(" ").trim();
2498
+ const near = Option.getOrUndefined(args.near);
2499
+ const radius = Option.getOrUndefined(args.radius);
2500
+ const within = Option.getOrUndefined(args.within);
2501
+ const problem = near !== void 0 && radius === void 0 ? "--near requires --radius (meters)" : radius !== void 0 && near === void 0 ? "--radius requires --near" : within !== void 0 && near !== void 0 ? "--within cannot be combined with --near/--radius" : query === "" && near === void 0 && within === void 0 ? "give words to search for, or an area (--near with --radius, or --within)" : void 0;
2502
+ if (problem !== void 0) return yield* Effect.fail(new UserError({ message: problem }));
2503
+ const opts = {
2504
+ ...args.kind.length > 0 ? { kind: args.kind } : {},
2505
+ ...Option.match(args.since, {
2506
+ onNone: () => ({}),
2507
+ onSome: (since) => ({ since })
2508
+ }),
2509
+ ...Option.match(args.until, {
2510
+ onNone: () => ({}),
2511
+ onSome: (until) => ({ until })
2512
+ }),
2513
+ ...Option.match(args.member, {
2514
+ onNone: () => ({}),
2515
+ onSome: (member) => ({ member })
2516
+ }),
2517
+ ...Option.match(args.limit, {
2518
+ onNone: () => ({}),
2519
+ onSome: (limit) => ({ limit })
2520
+ }),
2521
+ ...near !== void 0 && radius !== void 0 ? {
2522
+ near,
2523
+ radiusMeters: radius
2524
+ } : {},
2525
+ ...within !== void 0 ? { within } : {}
2526
+ };
2527
+ const res = yield* sdkCall("search", () => ctx.client.search(query === "" ? void 0 : query, opts));
2528
+ for (const h of res.hits) yield* ctx.out.print(args.format === "json" ? JSON.stringify(h) : formatHit(h));
2529
+ if (res.hits.length === 0 && args.format === "pretty") yield* ctx.out.info("no hits");
2530
+ }))).pipe(Command.withDescription("Ranked full-text and location search over everything this credential reads, across all time. Words match literally plus stemmed in the org's configured languages; --near/--radius or --within search by place instead of or on top of words."));
2531
+ //#endregion
2435
2532
  //#region src/input-spec.ts
2436
2533
  const SETTING_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
2437
2534
  function looksLikeSetting(seg) {
@@ -2786,6 +2883,7 @@ const broadcastOption$1 = Options.boolean("broadcast").pipe(Options.withAlias("b
2786
2883
  const orgTopicOption$1 = Options.text("org-topic").pipe(Options.withAlias("o"), Options.withDescription("Send to an org topic by value (from `sp org topics list`). Org send; mutually exclusive with --member, --broadcast, and -k/--topic (the personal topic)."), Options.optional);
2787
2884
  const tagOption$1 = Options.text("tag").pipe(Options.withDescription("Optional notification tag — recipients can use it to coalesce / replace prior notifications with the same tag."), Options.optional);
2788
2885
  const imageOption = Options.text("image").pipe(Options.withDescription("Image URL to show in the push (PNG/JPEG/GIF). Renders on iOS + Android. Mutually exclusive with --audio. URLs only — file uploads are SDK-only."), Options.optional);
2886
+ const linkOption$2 = Options.text("link").pipe(Options.withAlias("l"), Options.withDescription("A URL shown as an \"Open link\" button on the push. Any scheme: https opens the browser, an app's deep link (unifi-protect://...) opens that app. Mutually exclusive with the input flags."), Options.optional);
2789
2887
  const audioOption = Options.text("audio").pipe(Options.withDescription("Audio URL to play inline in the push (AIFF/WAV/MP3/M4A; iOS only). Mutually exclusive with --image. URLs only."), Options.optional);
2790
2888
  const noEncryptOption$2 = Options.boolean("no-encrypt").pipe(Options.withDescription("Send the body in plaintext even when an unlocked vault is available."));
2791
2889
  const actionInputOption = Options.text("action-input").pipe(Options.withAlias("a"), Options.withDescription("Add an actions input: tap-buttons the recipient answers with (e.g. Accept/Deny). Format: `[description;]key=Label[:style],...`, actions comma-separated; style is default|destructive. Use `\\,` for a literal comma in a label. A notification carries at most one input."), Options.optional);
@@ -2835,6 +2933,7 @@ const notifyCommand = Command.make("notify", {
2835
2933
  tag: tagOption$1,
2836
2934
  image: imageOption,
2837
2935
  audio: audioOption,
2936
+ link: linkOption$2,
2838
2937
  "text-input": textInputOption,
2839
2938
  "choice-input": choiceInputOption,
2840
2939
  "action-input": actionInputOption,
@@ -2864,6 +2963,7 @@ const notifyCommand = Command.make("notify", {
2864
2963
  const message = args.content;
2865
2964
  const imageUrl = Option.getOrUndefined(args.image);
2866
2965
  const audioUrl = Option.getOrUndefined(args.audio);
2966
+ const linkUrl = Option.getOrUndefined(args.link);
2867
2967
  if (imageUrl !== void 0 && audioUrl !== void 0) return yield* Effect.fail(new UserError({ message: "only one of --image or --audio may be set" }));
2868
2968
  const mediaUrl = imageUrl ?? audioUrl;
2869
2969
  const mediaKind = imageUrl !== void 0 ? "image" : "audio";
@@ -2872,6 +2972,7 @@ const notifyCommand = Command.make("notify", {
2872
2972
  if (notifyMediaContentType(mediaUrl, mediaKind) === null) return yield* Effect.fail(new UserError({ message: `--${mediaKind} URL must point to a supported ${mediaKind} type (by extension); got "${mediaUrl}"` }));
2873
2973
  }
2874
2974
  const input = yield* parseNotificationInput(args["text-input"], Option.getOrUndefined(args["choice-input"]), Option.getOrUndefined(args["action-input"]));
2975
+ if (input !== void 0 && linkUrl !== void 0) return yield* Effect.fail(new UserError({ message: "a notification carries either an input or --link, not both: the input's buttons take the action slots, so the link would never be shown" }));
2875
2976
  if (isOrgTarget) {
2876
2977
  const api = yield* Api;
2877
2978
  const vault = yield* (yield* VaultAccess).forSendOrPlaintext(args.noEncrypt);
@@ -2888,7 +2989,8 @@ const notifyCommand = Command.make("notify", {
2888
2989
  ...tagOpt !== void 0 ? { tag: tagOpt } : {},
2889
2990
  ...input !== void 0 ? { input } : {},
2890
2991
  ...imageUrl !== void 0 ? { image: imageUrl } : {},
2891
- ...audioUrl !== void 0 ? { audio: audioUrl } : {}
2992
+ ...audioUrl !== void 0 ? { audio: audioUrl } : {},
2993
+ ...linkUrl !== void 0 ? { link: linkUrl } : {}
2892
2994
  };
2893
2995
  yield* Effect.scoped(Effect.gen(function* () {
2894
2996
  const client = yield* acquireOrgClient({
@@ -2948,7 +3050,8 @@ const notifyCommand = Command.make("notify", {
2948
3050
  ...tagOpt !== void 0 ? { tag: tagOpt } : {},
2949
3051
  ...input !== void 0 ? { input } : {},
2950
3052
  ...imageUrl !== void 0 ? { image: imageUrl } : {},
2951
- ...audioUrl !== void 0 ? { audio: audioUrl } : {}
3053
+ ...audioUrl !== void 0 ? { audio: audioUrl } : {},
3054
+ ...linkUrl !== void 0 ? { link: linkUrl } : {}
2952
3055
  };
2953
3056
  yield* Effect.scoped(Effect.gen(function* () {
2954
3057
  const client = yield* acquireClient({
@@ -3715,7 +3818,7 @@ const fileInput$1 = repeatedText("file-input", void 0, "Add a file upload input.
3715
3818
  const locationInput$1 = repeatedText("location-input", void 0, "Add a location input (the recipient shares their device GPS position from the app). Format: `description[;key=value...]`. Settings: `required=true|false`. Repeatable.");
3716
3819
  const linkOption$1 = repeatedText("link", "l", "Attach a remote URL (a link attachment). Repeatable. For local files use --file.");
3717
3820
  const fileOption$1 = repeatedText("file", "f", "Attach a local file, uploaded as a file attachment (encrypted under the send's key — topic password or org master key — when the send is encrypted). Repeatable.");
3718
- const submitOption$1 = Options.boolean("submit").pipe(Options.withDescription("Require the recipient to explicitly submit the task. Without this, it auto-completes once the required inputs are filled."));
3821
+ const autoCommitOption$1 = Options.boolean("auto-commit").pipe(Options.withDescription("Complete the task as inputs are filled, without an explicit Submit. Default is form mode: the recipient submits everything at once."));
3719
3822
  const waitOption$1 = Options.boolean("wait").pipe(Options.withDescription("Block until the task is completed; print the result to stdout. Requires exactly one input on the request."));
3720
3823
  const replyOption$1 = Options.choice("reply", [
3721
3824
  "one-shot",
@@ -3744,7 +3847,7 @@ const taskCommand = Command.make("task", {
3744
3847
  "location-input": locationInput$1,
3745
3848
  link: linkOption$1,
3746
3849
  file: fileOption$1,
3747
- submit: submitOption$1,
3850
+ "auto-commit": autoCommitOption$1,
3748
3851
  wait: waitOption$1,
3749
3852
  reply: replyOption$1,
3750
3853
  member: memberOption,
@@ -3795,7 +3898,7 @@ const taskCommand = Command.make("task", {
3795
3898
  inputs,
3796
3899
  links: [...args.link],
3797
3900
  files: [...args.file],
3798
- autoCommit: !args.submit,
3901
+ autoCommit: args["auto-commit"],
3799
3902
  reply: Option.getOrUndefined(args.reply),
3800
3903
  markdown: args.markdown,
3801
3904
  noEncrypt: args["no-encrypt"],
@@ -3822,7 +3925,7 @@ const taskCommand = Command.make("task", {
3822
3925
  inputs,
3823
3926
  links: [...args.link],
3824
3927
  ...files.length > 0 ? { files } : {},
3825
- autoCommit: !args.submit,
3928
+ autoCommit: args["auto-commit"],
3826
3929
  ...Option.isSome(args.reply) ? { reply: args.reply.value } : {},
3827
3930
  ...args.markdown ? { contentFormat: "markdown" } : {},
3828
3931
  ...expiresAt !== void 0 ? { expiresAt } : {}
@@ -4029,7 +4132,7 @@ const photoInput = Options.text("photo-input").pipe(Options.withDescription("Add
4029
4132
  const voiceRecordingInput = Options.text("voice-recording-input").pipe(Options.withDescription("Add a voice recording input. Format: `description[;key=value...]`. Settings: `required=true|false`. Repeatable."), Options.repeated);
4030
4133
  const fileInput = Options.text("file-input").pipe(Options.withDescription("Add a file upload input. Format: `description[;key=value...]`. Settings: `required=true|false`. Repeatable."), Options.repeated);
4031
4134
  const locationInput = Options.text("location-input").pipe(Options.withDescription("Add a location input (the recipient shares their device GPS position from the app). Format: `description[;key=value...]`. Settings: `required=true|false`. Repeatable."), Options.repeated);
4032
- const submitOption = Options.boolean("submit").pipe(Options.withDescription("Require the recipient to explicitly submit the subtask. Without this, it auto-completes once the required inputs are filled."));
4135
+ const autoCommitOption = Options.boolean("auto-commit").pipe(Options.withDescription("Complete the subtask as inputs are filled, without an explicit Submit. Default is form mode: the recipient submits everything at once."));
4033
4136
  const waitOption = Options.boolean("wait").pipe(Options.withDescription("Block until the subtask is completed; print the result to stdout. Requires at least one input on the append. On a group append, the first member's completion wins."));
4034
4137
  const formatOption = Options.choice("format", ["text", "json"]).pipe(Options.withDescription("stdout format for an append: `text` (the bare sub_ id(s), default) or `json` (a `sent` line piped to `sp collect`)."), Options.withDefault("text"));
4035
4138
  const replyOption = Options.choice("reply", [
@@ -4054,7 +4157,7 @@ const subtaskCommand = Command.make("subtask", {
4054
4157
  "location-input": locationInput,
4055
4158
  link: linkOption,
4056
4159
  file: fileOption,
4057
- submit: submitOption,
4160
+ "auto-commit": autoCommitOption,
4058
4161
  wait: waitOption,
4059
4162
  format: formatOption,
4060
4163
  reply: replyOption,
@@ -4084,7 +4187,7 @@ const subtaskCommand = Command.make("subtask", {
4084
4187
  ...content !== void 0 ? { content } : {},
4085
4188
  ...inputs.length > 0 ? { inputs } : {},
4086
4189
  links: [...args.link],
4087
- autoCommit: !args.submit,
4190
+ autoCommit: args["auto-commit"],
4088
4191
  ...Option.isSome(args.reply) ? { reply: args.reply.value } : {},
4089
4192
  ...args.markdown ? { contentFormat: "markdown" } : {}
4090
4193
  };
@@ -4243,11 +4346,12 @@ const root = Command.make("simplepush").pipe(Command.withSubcommands([
4243
4346
  taskCommand,
4244
4347
  subtaskCommand,
4245
4348
  cancelCommand,
4246
- getCommand
4349
+ getCommand,
4350
+ searchCommand
4247
4351
  ]));
4248
4352
  const cli = Command.run(root, {
4249
4353
  name: "Simplepush CLI",
4250
- version: "0.2.0"
4354
+ version
4251
4355
  });
4252
4356
  const MainLive = Layer.mergeAll(CliOutput.Default, Sodium.Default, AuthStore.Default, VaultStore.Default, InviteStore.Default, Api.Default, VaultAccess.Default).pipe(Layer.provideMerge(FetchHttpClient.layer), Layer.provideMerge(NodeContext.layer));
4253
4357
  /** Render any failure through the typed-error table, then re-fail so the