@simplepush/cli 0.2.1 → 0.4.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 +19 -2
- package/dist/main.mjs +404 -183
- package/dist/main.mjs.map +1 -1
- package/package.json +2 -2
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,
|
|
12
|
+
import { Client, DownloadError, OrgClient, TypeFilter, decryptSubmission, decryptTaskPayload, decryptTaskSummary, 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, {
|
|
@@ -1525,7 +1525,7 @@ 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("Instance id to collect: a task (tsk_…), a notification (ntf_…), or a subtask
|
|
1528
|
+
instance: Options.text("instance").pipe(Options.withDescription("Instance id to collect: a task (tsk_…), a notification (ntf_…), or a subtask (sub_…, or the tsk_…/sub_… pair). 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
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.")),
|
|
@@ -1606,14 +1606,16 @@ const collectCommand = Command.make("collect", {
|
|
|
1606
1606
|
} : null, m.subtaskId);
|
|
1607
1607
|
for (const spec of args.instance) {
|
|
1608
1608
|
const [id, subtaskId] = spec.split("/", 2);
|
|
1609
|
-
if (id.startsWith("sub_"))
|
|
1610
|
-
|
|
1609
|
+
if (id.startsWith("sub_")) {
|
|
1610
|
+
const parent = yield* sdkCall("read subtask", () => client.getSubtask(id));
|
|
1611
|
+
addMember(String(parent.parentTaskId), null, id);
|
|
1612
|
+
} else addMember(id, null, subtaskId);
|
|
1611
1613
|
}
|
|
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_…|
|
|
1614
|
+
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_…|sub_…> (repeatable)" }));
|
|
1613
1615
|
const notifRoster = members[0].kind === "notification";
|
|
1614
1616
|
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
1617
|
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 instances (a piped `sp subtask --format json` line or --instance
|
|
1618
|
+
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 sub_…) with plain task/notification instances in one collect" }));
|
|
1617
1619
|
const streamOpts = {
|
|
1618
1620
|
replay: true,
|
|
1619
1621
|
...until.idleMs !== void 0 ? { idleMs: until.idleMs } : {}
|
|
@@ -1782,7 +1784,7 @@ const reasonOption = Options.choice("reason", [
|
|
|
1782
1784
|
"answered",
|
|
1783
1785
|
"superseded"
|
|
1784
1786
|
]).pipe(Options.withDescription("Why: plain withdrawal (default), another recipient's answer made the rest moot (`answered`), or a replacement exists (`superseded`)."), Options.withDefault("canceled"));
|
|
1785
|
-
const noteOption = Options.text("note").pipe(Options.withDescription("Free-text explanation shown on the recipients' canceled card.
|
|
1787
|
+
const noteOption = Options.text("note").pipe(Options.withDescription("Free-text explanation shown on the recipients' canceled card. Sealed under the target's key when this CLI holds it: the org vault on an org session, or the matching -p/--password (`pw@topic`, or a bare `pw` for a self-send)."), Options.optional);
|
|
1786
1788
|
const supersededByOption = Options.text("superseded-by").pipe(Options.withDescription("The replacement's id — a task id, a subtask id of the same chain, or (for a group cancel) the replacement group id. Requires --reason superseded."), Options.optional);
|
|
1787
1789
|
const noEncryptOption$3 = Options.boolean("no-encrypt").pipe(Options.withDescription("For org cancels: send the note in plaintext even when the org vault is unlocked."));
|
|
1788
1790
|
const cancelCommand = Command.make("cancel", {
|
|
@@ -1801,89 +1803,46 @@ const cancelCommand = Command.make("cancel", {
|
|
|
1801
1803
|
const note = Option.getOrUndefined(args.note);
|
|
1802
1804
|
const supersededBy = Option.getOrUndefined(args["superseded-by"]);
|
|
1803
1805
|
if (supersededBy !== void 0 && args.reason !== "superseded") return yield* Effect.fail(new UserError({ message: "--superseded-by requires --reason superseded" }));
|
|
1804
|
-
const
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1806
|
+
const kind = args.id.startsWith("grptsk_") ? "group" : args.id.startsWith("tsk_") ? "task" : args.id.startsWith("sub_") ? "subtask" : void 0;
|
|
1807
|
+
if (kind === void 0) return yield* Effect.fail(new UserError({ message: `cannot cancel \`${args.id}\`: expected a tsk_…, sub_…, or grptsk_… id` }));
|
|
1808
|
+
const cred = yield* resolveCredential(args["api-token"], args["base-url"]);
|
|
1809
|
+
const orgKeys = cred.kind === "org" && !args["no-encrypt"] ? yield* loadOrgMasterKeys : void 0;
|
|
1810
|
+
return yield* Effect.scoped(Effect.gen(function* () {
|
|
1811
|
+
const client = cred.kind === "personal" ? yield* acquireClient({
|
|
1812
|
+
baseUrl: cred.baseUrl,
|
|
1813
|
+
apiToken: cred.apiToken,
|
|
1814
|
+
passwords: args.password
|
|
1815
|
+
}) : yield* acquireOrgClient({
|
|
1816
|
+
baseUrl: cred.baseUrl,
|
|
1817
|
+
bearerToken: cred.bearer,
|
|
1818
|
+
...orgKeys !== void 0 ? { orgMasterKeys: orgKeys } : {}
|
|
1819
|
+
});
|
|
1820
|
+
const opts = {
|
|
1821
|
+
reason: args.reason,
|
|
1822
|
+
...note !== void 0 ? { note } : {},
|
|
1823
|
+
...supersededBy !== void 0 ? { supersededBy } : {}
|
|
1814
1824
|
};
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1825
|
+
if (note !== void 0) {
|
|
1826
|
+
const marker = yield* sdkCall("read cancel target", async () => kind === "task" ? (await client.getTask(args.id)).encryption : kind === "subtask" ? (await client.getSubtask(args.id)).encryption : (await client.getTaskGroup(args.id)).tasks[0]?.encryption);
|
|
1827
|
+
if (marker !== void 0) {
|
|
1828
|
+
if ((yield* sdkCall("keyring", async () => (await client.keyring({ includePasswordSalt: marker.type === "personal" })).keyForMarker(marker))) === void 0) yield* out.warn(marker.type === "org" ? "note sent unencrypted (org vault locked, or --no-encrypt)" : "note sent unencrypted — seal it with --password <pw>@<topic> (topic send) or a bare --password (self-send)");
|
|
1829
|
+
}
|
|
1818
1830
|
}
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
yield* out.warn("note sent unencrypted — seal it with --password <pw>@<topic> (topic send) or a bare --password (self-send)");
|
|
1826
|
-
} else if (Array.isArray(flag)) {
|
|
1827
|
-
const derived = yield* sdkCall("derive note key", () => deriveKey(flag[0], flag[1]));
|
|
1828
|
-
noteFields = {
|
|
1829
|
-
note: yield* sdkCall("encrypt note", () => encrypt(derived.symmetricKey, note)),
|
|
1830
|
-
encryption: {
|
|
1831
|
-
type: "personal",
|
|
1832
|
-
keyFingerprint: derived.fingerprint
|
|
1833
|
-
}
|
|
1834
|
-
};
|
|
1835
|
-
} else {
|
|
1836
|
-
const info = yield* sdkCall("fetch password salt", () => fetchUserInfo({
|
|
1837
|
-
baseUrl: new URL(credential.baseUrl),
|
|
1838
|
-
apiToken: credential.apiToken,
|
|
1839
|
-
fetch
|
|
1831
|
+
if (kind === "group") {
|
|
1832
|
+
const result = yield* sdkCall("cancel task group", () => client.cancelTaskGroup(args.id, opts));
|
|
1833
|
+
yield* out.print(JSON.stringify({
|
|
1834
|
+
type: "canceled",
|
|
1835
|
+
groupId: args.id,
|
|
1836
|
+
...result
|
|
1840
1837
|
}));
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
}
|
|
1848
|
-
};
|
|
1838
|
+
} else if (kind === "task") {
|
|
1839
|
+
yield* sdkCall("cancel task", () => client.cancelTask(args.id, opts));
|
|
1840
|
+
yield* out.info(`canceled ${args.id}`);
|
|
1841
|
+
} else {
|
|
1842
|
+
yield* sdkCall("cancel subtask", () => client.cancelSubtask(args.id, opts));
|
|
1843
|
+
yield* out.info(`canceled ${args.id}`);
|
|
1849
1844
|
}
|
|
1850
|
-
}
|
|
1851
|
-
const body = {
|
|
1852
|
-
reason: args.reason,
|
|
1853
|
-
...noteFields,
|
|
1854
|
-
...supersededBy !== void 0 ? { supersededBy } : {}
|
|
1855
|
-
};
|
|
1856
|
-
const baseUrl = new URL(credential.baseUrl);
|
|
1857
|
-
const authHeaders = credential.kind === "personal" ? { "API-Token": credential.apiToken } : { Authorization: `Bearer ${credential.bearer}` };
|
|
1858
|
-
if (args.id.startsWith("grptsk_")) {
|
|
1859
|
-
const result = yield* sdkCall("cancel task group", () => cancelTaskGroup({
|
|
1860
|
-
baseUrl,
|
|
1861
|
-
groupId: args.id,
|
|
1862
|
-
body,
|
|
1863
|
-
authHeaders
|
|
1864
|
-
}));
|
|
1865
|
-
yield* out.print(JSON.stringify({
|
|
1866
|
-
type: "canceled",
|
|
1867
|
-
groupId: args.id,
|
|
1868
|
-
...result
|
|
1869
|
-
}));
|
|
1870
|
-
} else if (args.id.startsWith("tsk_")) {
|
|
1871
|
-
yield* sdkCall("cancel task", () => cancelTask({
|
|
1872
|
-
baseUrl,
|
|
1873
|
-
taskId: args.id,
|
|
1874
|
-
body,
|
|
1875
|
-
authHeaders
|
|
1876
|
-
}));
|
|
1877
|
-
yield* out.info(`canceled ${args.id}`);
|
|
1878
|
-
} else if (args.id.startsWith("sub_")) {
|
|
1879
|
-
yield* sdkCall("cancel subtask", () => cancelSubtask({
|
|
1880
|
-
baseUrl,
|
|
1881
|
-
subtaskId: args.id,
|
|
1882
|
-
body,
|
|
1883
|
-
authHeaders
|
|
1884
|
-
}));
|
|
1885
|
-
yield* out.info(`canceled ${args.id}`);
|
|
1886
|
-
} else return yield* Effect.fail(new UserError({ message: `cannot cancel \`${args.id}\`: expected a tsk_…, sub_…, or grptsk_… id` }));
|
|
1845
|
+
}));
|
|
1887
1846
|
})).pipe(Command.withDescription("Cancel a pending task, subtask, or task group you sent (sender-side withdrawal)."));
|
|
1888
1847
|
//#endregion
|
|
1889
1848
|
//#region src/commands/daemon.ts
|
|
@@ -1916,6 +1875,133 @@ const daemonCommand = Command.make("daemon", {
|
|
|
1916
1875
|
});
|
|
1917
1876
|
}));
|
|
1918
1877
|
//#endregion
|
|
1878
|
+
//#region src/commands/query-support.ts
|
|
1879
|
+
/** Options every read command shares: credential, decryption keys, paging, output. */
|
|
1880
|
+
const credentialOptions = {
|
|
1881
|
+
"api-token": apiTokenOption,
|
|
1882
|
+
password: passwordOption,
|
|
1883
|
+
"base-url": baseUrlOption,
|
|
1884
|
+
quiet: quietOption
|
|
1885
|
+
};
|
|
1886
|
+
const formatOption$4 = Options.choice("format", ["json", "pretty"]).pipe(Options.withDefault("json"), Options.withDescription("json = one JSON object per line (agents, jq); pretty = human-readable lines."));
|
|
1887
|
+
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
|
+
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
|
+
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."));
|
|
1891
|
+
const cursorOption = Options.text("cursor").pipe(Options.optional, Options.withDescription("Continue a previous page: pass the cursor it reported."));
|
|
1892
|
+
const allOption = Options.boolean("all").pipe(Options.withDescription("Follow cursors until the last page instead of printing one page and a cursor."));
|
|
1893
|
+
const STATUSES = [
|
|
1894
|
+
"pending",
|
|
1895
|
+
"completed",
|
|
1896
|
+
"canceled",
|
|
1897
|
+
"declined",
|
|
1898
|
+
"expired"
|
|
1899
|
+
];
|
|
1900
|
+
/** `--status` accepts repeats and comma lists: `--status expired --status declined` or `--status expired,declined`. */
|
|
1901
|
+
const statusOption = Options.text("status").pipe(Options.repeated, Options.mapTryCatch((values) => {
|
|
1902
|
+
const flat = values.flatMap((v) => v.split(",")).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1903
|
+
const bad = flat.filter((s) => !STATUSES.includes(s));
|
|
1904
|
+
if (bad.length > 0) throw new Error(`unknown status ${bad.join(", ")} — use ${STATUSES.join(", ")}`);
|
|
1905
|
+
return flat;
|
|
1906
|
+
}, (e) => HelpDoc.p(e instanceof Error ? e.message : String(e))), Options.withDescription("Only tasks in these states (repeatable or comma-separated)."));
|
|
1907
|
+
/** Resolves the credential (personal API-Token or the saved org session),
|
|
1908
|
+
* builds the matching client and — when keys are available — a keyring, and
|
|
1909
|
+
* runs `body` inside the client's scope. Mirrors what `sp events` does. */
|
|
1910
|
+
const withQueryClient = (args, body) => Effect.gen(function* () {
|
|
1911
|
+
const out = yield* CliOutput;
|
|
1912
|
+
yield* out.setQuiet(args.quiet);
|
|
1913
|
+
const cred = yield* resolveCredential(args["api-token"], args["base-url"]);
|
|
1914
|
+
const orgKeys = cred.kind === "org" ? yield* loadOrgMasterKeys : void 0;
|
|
1915
|
+
return yield* Effect.scoped(Effect.gen(function* () {
|
|
1916
|
+
const client = cred.kind === "personal" ? yield* acquireClient({
|
|
1917
|
+
baseUrl: cred.baseUrl,
|
|
1918
|
+
apiToken: cred.apiToken,
|
|
1919
|
+
passwords: args.password
|
|
1920
|
+
}) : yield* acquireOrgClient({
|
|
1921
|
+
baseUrl: cred.baseUrl,
|
|
1922
|
+
bearerToken: cred.bearer,
|
|
1923
|
+
...orgKeys !== void 0 ? { orgMasterKeys: orgKeys } : {}
|
|
1924
|
+
});
|
|
1925
|
+
return yield* body({
|
|
1926
|
+
client,
|
|
1927
|
+
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,
|
|
1928
|
+
org: cred.kind === "org",
|
|
1929
|
+
out
|
|
1930
|
+
});
|
|
1931
|
+
}));
|
|
1932
|
+
});
|
|
1933
|
+
/** Decrypts what the keyring can and counts what it cannot, so a command can
|
|
1934
|
+
* report "N values left as ciphertext" once instead of per row. Each method
|
|
1935
|
+
* maps one wire shape onto its SDK field-schema decryptor. */
|
|
1936
|
+
var Decryptor = class {
|
|
1937
|
+
undecryptable = 0;
|
|
1938
|
+
constructor(keyring) {
|
|
1939
|
+
this.keyring = keyring;
|
|
1940
|
+
}
|
|
1941
|
+
run(value, go) {
|
|
1942
|
+
if (this.keyring === void 0) return Effect.succeed(value);
|
|
1943
|
+
const keyring = this.keyring;
|
|
1944
|
+
return Effect.promise(() => go(keyring)).pipe(Effect.map((r) => {
|
|
1945
|
+
this.undecryptable += r.undecryptable;
|
|
1946
|
+
return r.value;
|
|
1947
|
+
}));
|
|
1948
|
+
}
|
|
1949
|
+
/** A task or subtask payload (chain reads). */
|
|
1950
|
+
payload(value) {
|
|
1951
|
+
return this.run(value, (kr) => decryptTaskPayload(value, kr));
|
|
1952
|
+
}
|
|
1953
|
+
/** A task index / roster row. */
|
|
1954
|
+
summary(value) {
|
|
1955
|
+
return this.run(value, (kr) => decryptTaskSummary(value, kr));
|
|
1956
|
+
}
|
|
1957
|
+
/** A submission under its feed entry's marker. */
|
|
1958
|
+
submission(value, marker) {
|
|
1959
|
+
return this.run(value, (kr) => decryptSubmission(value, kr, marker));
|
|
1960
|
+
}
|
|
1961
|
+
report(out) {
|
|
1962
|
+
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;
|
|
1963
|
+
}
|
|
1964
|
+
};
|
|
1965
|
+
/** Runs `page` for the first cursor, then keeps following cursors when `all`
|
|
1966
|
+
* is set; otherwise reports the cursor of the next page so the caller can
|
|
1967
|
+
* continue by hand. */
|
|
1968
|
+
const forEachPage = (ctx, format, all, first, page, handle) => Effect.gen(function* () {
|
|
1969
|
+
let cursor = first;
|
|
1970
|
+
for (;;) {
|
|
1971
|
+
const p = yield* page(cursor);
|
|
1972
|
+
yield* handle(p);
|
|
1973
|
+
if (p.nextCursor === void 0) return;
|
|
1974
|
+
if (!all) {
|
|
1975
|
+
yield* format === "json" ? ctx.out.print(JSON.stringify({
|
|
1976
|
+
type: "more",
|
|
1977
|
+
cursor: p.nextCursor
|
|
1978
|
+
})) : ctx.out.info(`more available — continue with --cursor ${p.nextCursor} (or pass --all)`);
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
cursor = p.nextCursor;
|
|
1982
|
+
}
|
|
1983
|
+
});
|
|
1984
|
+
function recipientLabels(t) {
|
|
1985
|
+
return t.recipients.map((r) => r.name ?? r.publicId);
|
|
1986
|
+
}
|
|
1987
|
+
/** One line per task: id, state, when, title, who, subtask counts, group. */
|
|
1988
|
+
function formatTaskSummary(t) {
|
|
1989
|
+
const subs = Object.entries(t.subtasks).map(([k, v]) => `${k} ${v}`).join(", ");
|
|
1990
|
+
return [
|
|
1991
|
+
`${t.taskId} [${t.status}] ${t.createdAt}`,
|
|
1992
|
+
t.title !== void 0 ? ` ${t.title}` : void 0,
|
|
1993
|
+
` to: ${recipientLabels(t).join(", ") || "-"}`,
|
|
1994
|
+
t.topic !== void 0 ? ` topic: ${t.topic}` : void 0,
|
|
1995
|
+
t.tag !== void 0 ? ` tag: ${t.tag}` : void 0,
|
|
1996
|
+
t.inputs.length > 0 ? ` inputs: ${t.inputs.join(", ")}` : void 0,
|
|
1997
|
+
t.attachments.length > 0 ? ` attachments: ${t.attachments.join(", ")}` : void 0,
|
|
1998
|
+
t.reply !== void 0 ? ` reply: ${t.reply}` : void 0,
|
|
1999
|
+
subs ? ` subtasks: ${subs}` : void 0,
|
|
2000
|
+
t.expiresAt !== void 0 ? ` expires: ${t.expiresAt}` : void 0,
|
|
2001
|
+
t.groupId !== void 0 ? ` group: ${t.groupId}` : void 0
|
|
2002
|
+
].filter((l) => l !== void 0).join("\n");
|
|
2003
|
+
}
|
|
2004
|
+
//#endregion
|
|
1919
2005
|
//#region src/commands/download.ts
|
|
1920
2006
|
const ID_GLOSSARY = "(ids: tsk_ task, sub_ subtask, sbm_ submission; files: inp_ input upload, rfl_ reply file, sbf_ submission file)";
|
|
1921
2007
|
const resolveScope = (scopeId, fileId) => {
|
|
@@ -1927,11 +2013,6 @@ const resolveScope = (scopeId, fileId) => {
|
|
|
1927
2013
|
if (wantsSubmission !== (scope === "submission")) return Effect.fail(new UserError({ message: wantsSubmission ? `${fileId} is a submission file — pass its sbm_… id, not ${scopeId} ${ID_GLOSSARY}` : `${fileId} lives on a task chain — pass the tsk_… or sub_… id it belongs to, not ${scopeId} ${ID_GLOSSARY}` }));
|
|
1928
2014
|
return Effect.succeed(scope);
|
|
1929
2015
|
};
|
|
1930
|
-
const downloadUrlPath = (scope, scopeId, fileId) => {
|
|
1931
|
-
if (scope === "submission") return `/v1/submissions/${scopeId}/files/${fileId}/download-url`;
|
|
1932
|
-
const kind = fileId.startsWith("inp_") ? "inputs" : "replies";
|
|
1933
|
-
return `/v1/${scope === "task" ? "tasks" : "subtasks"}/${scopeId}/${kind}/${fileId}/download-url`;
|
|
1934
|
-
};
|
|
1935
2016
|
/** Resolve where to write: an explicit file path, an existing directory (the
|
|
1936
2017
|
* server-declared filename inside it), or the current directory. */
|
|
1937
2018
|
const resolveTarget = async (outArg, filename) => {
|
|
@@ -1953,94 +2034,31 @@ const downloadCommand = Command.make("download", {
|
|
|
1953
2034
|
"base-url": baseUrlOption,
|
|
1954
2035
|
quiet: quietOption
|
|
1955
2036
|
}, (args) => Effect.gen(function* () {
|
|
1956
|
-
|
|
1957
|
-
yield*
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
try: async () => {
|
|
1965
|
-
const res = await fetch(`${baseUrl}${downloadUrlPath(scope, args.scopeId, args.fileId)}`, {
|
|
1966
|
-
method: "POST",
|
|
1967
|
-
headers: authHeaders
|
|
1968
|
-
});
|
|
1969
|
-
if (!res.ok) {
|
|
1970
|
-
const body = await res.text().catch(() => "");
|
|
1971
|
-
let msg = `download-url failed (${res.status})`;
|
|
1972
|
-
try {
|
|
1973
|
-
const parsed = JSON.parse(body);
|
|
1974
|
-
if (parsed.msg) msg = `${msg}: ${parsed.msg}`;
|
|
1975
|
-
} catch {
|
|
1976
|
-
if (body) msg = `${msg}: ${body.slice(0, 200)}`;
|
|
1977
|
-
}
|
|
1978
|
-
throw new Error(msg);
|
|
1979
|
-
}
|
|
1980
|
-
return await res.json();
|
|
1981
|
-
},
|
|
1982
|
-
catch: (e) => new UserError({ message: e instanceof Error ? e.message : String(e) })
|
|
1983
|
-
});
|
|
1984
|
-
const stored = yield* Effect.tryPromise({
|
|
1985
|
-
try: async () => {
|
|
1986
|
-
const res = await fetch(meta.presignedGetUrl);
|
|
1987
|
-
if (!res.ok) throw new Error(`fetching the file failed (${res.status})`);
|
|
1988
|
-
return new Uint8Array(await res.arrayBuffer());
|
|
1989
|
-
},
|
|
1990
|
-
catch: (e) => new UserError({ message: e instanceof Error ? e.message : String(e) })
|
|
1991
|
-
});
|
|
1992
|
-
if (meta.checksumSha256 !== void 0) {
|
|
1993
|
-
const actual = createHash("sha256").update(stored).digest("base64");
|
|
1994
|
-
if (actual !== meta.checksumSha256) return yield* Effect.fail(new UserError({ message: `checksum mismatch: stored blob hashes to ${actual}, server declared ${meta.checksumSha256}` }));
|
|
1995
|
-
}
|
|
1996
|
-
let bytes = stored;
|
|
1997
|
-
if (meta.encryption !== void 0) {
|
|
1998
|
-
const passwords = [...args.password];
|
|
1999
|
-
const passwordSalt = passwords.filter((p) => typeof p === "string").length > 0 && cred.kind === "personal" ? yield* Effect.tryPromise({
|
|
2000
|
-
try: async () => (await fetchUserInfo({
|
|
2001
|
-
baseUrl: new URL(baseUrl),
|
|
2002
|
-
apiToken: cred.apiToken,
|
|
2003
|
-
fetch
|
|
2004
|
-
})).passwordSalt,
|
|
2005
|
-
catch: (e) => new UserError({ message: `fetching the password salt failed: ${e instanceof Error ? e.message : String(e)}` })
|
|
2006
|
-
}) : void 0;
|
|
2007
|
-
const key = yield* Effect.tryPromise({
|
|
2037
|
+
yield* resolveScope(args.scopeId, args.fileId);
|
|
2038
|
+
return yield* withQueryClient(args, (ctx) => Effect.gen(function* () {
|
|
2039
|
+
const file = yield* Effect.tryPromise({
|
|
2040
|
+
try: () => ctx.client.downloadFile(args.scopeId, args.fileId),
|
|
2041
|
+
catch: (e) => new UserError({ message: e instanceof DownloadError && e.message.includes("no matching key") ? ctx.org ? "the file is sealed under an org master key — unlock the vault (org session) to decrypt" : "the file is encrypted — pass the matching -p/--password (`pw@topic`, or a bare account password)" : e instanceof Error ? e.message : String(e) })
|
|
2042
|
+
});
|
|
2043
|
+
const filename = file.filename ?? args.fileId;
|
|
2044
|
+
const target = yield* Effect.tryPromise({
|
|
2008
2045
|
try: async () => {
|
|
2009
|
-
const
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
...orgKeys !== void 0 ? { orgMasterKeys: orgKeys } : {}
|
|
2013
|
-
});
|
|
2014
|
-
for (const p of passwords) if (typeof p !== "string") ring.add(await deriveKey(p[0], p[1]));
|
|
2015
|
-
else if (passwordSalt !== void 0) ring.add(await deriveKey(p, passwordSalt));
|
|
2016
|
-
return ring.keyForMarker(meta.encryption);
|
|
2046
|
+
const t = await resolveTarget(Option.getOrUndefined(args.out), filename);
|
|
2047
|
+
await writeFile(t, file.bytes);
|
|
2048
|
+
return t;
|
|
2017
2049
|
},
|
|
2018
|
-
catch: (e) => new UserError({ message: `
|
|
2019
|
-
});
|
|
2020
|
-
if (key === void 0) return yield* Effect.fail(new UserError({ message: meta.encryption.type === "org" ? `the file is sealed under org master_key v${meta.encryption.v} — unlock the vault (org session) to decrypt` : "the file is encrypted — pass the matching -p/--password (`pw@topic`, or a bare account password)" }));
|
|
2021
|
-
bytes = yield* Effect.tryPromise({
|
|
2022
|
-
try: () => decryptBytes(key, stored),
|
|
2023
|
-
catch: (e) => new UserError({ message: `decryption failed: ${e instanceof Error ? e.message : String(e)}` })
|
|
2050
|
+
catch: (e) => new UserError({ message: `saving failed: ${e instanceof Error ? e.message : String(e)}` })
|
|
2024
2051
|
});
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
}
|
|
2033
|
-
|
|
2034
|
-
});
|
|
2035
|
-
if (args.format === "json") yield* out.print(JSON.stringify({
|
|
2036
|
-
type: "downloaded",
|
|
2037
|
-
id: args.fileId,
|
|
2038
|
-
path: target,
|
|
2039
|
-
filename: meta.filename ?? null,
|
|
2040
|
-
contentType: meta.contentType ?? null,
|
|
2041
|
-
size: meta.size ?? null
|
|
2052
|
+
if (args.format === "json") yield* ctx.out.print(JSON.stringify({
|
|
2053
|
+
type: "downloaded",
|
|
2054
|
+
id: args.fileId,
|
|
2055
|
+
path: target,
|
|
2056
|
+
filename: file.filename ?? null,
|
|
2057
|
+
contentType: file.contentType ?? null,
|
|
2058
|
+
size: file.size ?? null
|
|
2059
|
+
}));
|
|
2060
|
+
else yield* ctx.out.print(target);
|
|
2042
2061
|
}));
|
|
2043
|
-
else yield* out.print(target);
|
|
2044
2062
|
}));
|
|
2045
2063
|
//#endregion
|
|
2046
2064
|
//#region src/output.ts
|
|
@@ -2103,6 +2121,7 @@ const sinceOption = mappedText("since", resolveSince).pipe(Options.withDescripti
|
|
|
2103
2121
|
const untilOption = mappedText("until", resolveUntil).pipe(Options.withDescription("Stop at this timestamp. Forces a finite range, so `--follow` is ignored."), Options.optional);
|
|
2104
2122
|
const limitOption = Options.integer("limit").pipe(Options.withDescription("Maximum number of events to print, then exit."), Options.optional);
|
|
2105
2123
|
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
|
+
const memberOption$2 = Options.text("member").pipe(Options.optional, Options.withDescription("Only events by this person: a usr_ id, or the actor's name."));
|
|
2106
2125
|
const formatOption$3 = Options.choice("format", [
|
|
2107
2126
|
"json",
|
|
2108
2127
|
"pretty",
|
|
@@ -2112,13 +2131,14 @@ const directOption = Options.boolean("direct").pipe(Options.withDescription("Ope
|
|
|
2112
2131
|
const QUIET_EXIT_MS = 2e3;
|
|
2113
2132
|
const eventsCommand = Command.make("events", {
|
|
2114
2133
|
type: eventTypeOption,
|
|
2134
|
+
member: memberOption$2,
|
|
2115
2135
|
since: sinceOption,
|
|
2116
2136
|
until: untilOption,
|
|
2117
2137
|
limit: limitOption,
|
|
2118
2138
|
follow: followOption,
|
|
2119
2139
|
format: formatOption$3,
|
|
2120
2140
|
direct: directOption,
|
|
2121
|
-
topic: topicOption,
|
|
2141
|
+
topic: topicOption$1,
|
|
2122
2142
|
"api-token": apiTokenOption,
|
|
2123
2143
|
password: passwordOption,
|
|
2124
2144
|
"base-url": baseUrlOption,
|
|
@@ -2132,6 +2152,8 @@ const eventsCommand = Command.make("events", {
|
|
|
2132
2152
|
const untilDate = Option.getOrUndefined(args.until);
|
|
2133
2153
|
const limit = Option.getOrUndefined(args.limit);
|
|
2134
2154
|
const filter = new TypeFilter(args.type);
|
|
2155
|
+
const member = Option.getOrUndefined(args.member);
|
|
2156
|
+
const memberMatches = (ev) => member === void 0 || ev.actor?.publicId === member || ev.actor?.name === member;
|
|
2135
2157
|
yield* Effect.forEach(filter.unknown, (u) => out.warn(`ignoring unknown --type \`${u}\``));
|
|
2136
2158
|
const webSocketFactory = args.direct || sinceIso !== void 0 ? Option.none() : yield* sharedWebSocketFactory({
|
|
2137
2159
|
credential: cred.kind === "personal" ? {
|
|
@@ -2194,7 +2216,7 @@ const eventsCommand = Command.make("events", {
|
|
|
2194
2216
|
yield* sdkStream("events stream", (signal) => client.events({
|
|
2195
2217
|
...sinceIso !== void 0 ? { since: sinceIso } : {},
|
|
2196
2218
|
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* () {
|
|
2219
|
+
})).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
2220
|
const decrypted = keyring ? yield* sdkCall("decrypt event", () => tryDecryptEventData(ev, keyring)) : void 0;
|
|
2199
2221
|
yield* out.print(formatEvent(ev, args.format, decrypted));
|
|
2200
2222
|
const n = yield* Ref.updateAndGet(printed, (x) => x + 1);
|
|
@@ -2212,6 +2234,204 @@ const eventsCommand = Command.make("events", {
|
|
|
2212
2234
|
}));
|
|
2213
2235
|
}));
|
|
2214
2236
|
//#endregion
|
|
2237
|
+
//#region src/commands/get.ts
|
|
2238
|
+
const topicOption = Options.text("topic").pipe(Options.optional, Options.withDescription("Only tasks sent to this topic: its value (name), or its id."));
|
|
2239
|
+
const groupOption = Options.text("group").pipe(Options.optional, Options.withDescription("Only the instances of this grptsk_ group."));
|
|
2240
|
+
/** Flags that only make sense on the paged listings. Id reads reject them by
|
|
2241
|
+
* name instead of silently ignoring them. */
|
|
2242
|
+
function rejectListFlags(args, target, allowStatus) {
|
|
2243
|
+
const set = [
|
|
2244
|
+
...!allowStatus && args.status.length > 0 ? ["--status"] : [],
|
|
2245
|
+
...Option.isSome(args.since) ? ["--since"] : [],
|
|
2246
|
+
...Option.isSome(args.until) ? ["--until"] : [],
|
|
2247
|
+
...Option.isSome(args.topic) ? ["--topic"] : [],
|
|
2248
|
+
...Option.isSome(args.member) ? ["--member"] : [],
|
|
2249
|
+
...Option.isSome(args.group) ? ["--group"] : [],
|
|
2250
|
+
...Option.isSome(args.limit) ? ["--limit"] : [],
|
|
2251
|
+
...Option.isSome(args.cursor) ? ["--cursor"] : [],
|
|
2252
|
+
...args.all ? ["--all"] : []
|
|
2253
|
+
];
|
|
2254
|
+
return set.length === 0 ? Effect.void : Effect.fail(new UserError({ message: `${set.join(", ")} does not apply when reading ${target}` }));
|
|
2255
|
+
}
|
|
2256
|
+
function getChain(args, ctx) {
|
|
2257
|
+
return Effect.gen(function* () {
|
|
2258
|
+
yield* rejectListFlags(args, "one task", false);
|
|
2259
|
+
const chain = yield* sdkCall("read task chain", () => ctx.client.getTaskChain(args.what));
|
|
2260
|
+
const dec = new Decryptor(ctx.keyring);
|
|
2261
|
+
const task = yield* dec.payload(chain.task);
|
|
2262
|
+
const subtasks = [];
|
|
2263
|
+
for (const s of chain.subtasks) subtasks.push({
|
|
2264
|
+
subtask: yield* dec.payload(s.subtask),
|
|
2265
|
+
createdAt: s.createdAt
|
|
2266
|
+
});
|
|
2267
|
+
const view = {
|
|
2268
|
+
task,
|
|
2269
|
+
createdAt: chain.createdAt,
|
|
2270
|
+
subtasks
|
|
2271
|
+
};
|
|
2272
|
+
if (args.format === "json") yield* ctx.out.print(JSON.stringify(view));
|
|
2273
|
+
else {
|
|
2274
|
+
yield* ctx.out.print(`${task.taskId} [${task.status}] ${chain.createdAt}`);
|
|
2275
|
+
yield* ctx.out.print(JSON.stringify(task, null, 2));
|
|
2276
|
+
for (const s of subtasks) {
|
|
2277
|
+
yield* ctx.out.print(`\n${s.subtask.subtaskId} [${s.subtask.status}] ${s.createdAt}`);
|
|
2278
|
+
yield* ctx.out.print(JSON.stringify(s.subtask, null, 2));
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
yield* dec.report(ctx.out);
|
|
2282
|
+
});
|
|
2283
|
+
}
|
|
2284
|
+
function getSubtaskRead(args, ctx) {
|
|
2285
|
+
return Effect.gen(function* () {
|
|
2286
|
+
yield* rejectListFlags(args, "one subtask", false);
|
|
2287
|
+
const subtask = yield* sdkCall("read subtask", () => ctx.client.getSubtask(args.what));
|
|
2288
|
+
const dec = new Decryptor(ctx.keyring);
|
|
2289
|
+
const view = yield* dec.payload(subtask);
|
|
2290
|
+
if (args.format === "json") yield* ctx.out.print(JSON.stringify(view));
|
|
2291
|
+
else {
|
|
2292
|
+
yield* ctx.out.print(`${view.subtaskId} [${view.status}] parent ${String(view.parentTaskId ?? "-")}`);
|
|
2293
|
+
yield* ctx.out.print(JSON.stringify(view, null, 2));
|
|
2294
|
+
}
|
|
2295
|
+
yield* dec.report(ctx.out);
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
function getRoster(args, ctx) {
|
|
2299
|
+
return Effect.gen(function* () {
|
|
2300
|
+
yield* rejectListFlags(args, "a task group roster", true);
|
|
2301
|
+
const roster = yield* sdkCall("read task group", () => ctx.client.getTaskGroup(args.what, args.status.length > 0 ? { status: args.status } : {}));
|
|
2302
|
+
const dec = new Decryptor(ctx.keyring);
|
|
2303
|
+
const counts = {};
|
|
2304
|
+
for (const t of roster.tasks) counts[t.status] = (counts[t.status] ?? 0) + 1;
|
|
2305
|
+
if (args.format === "json") {
|
|
2306
|
+
yield* ctx.out.print(JSON.stringify({
|
|
2307
|
+
type: "group",
|
|
2308
|
+
groupId: roster.groupId,
|
|
2309
|
+
counts
|
|
2310
|
+
}));
|
|
2311
|
+
for (const t of roster.tasks) yield* ctx.out.print(JSON.stringify(yield* dec.summary(t)));
|
|
2312
|
+
} else {
|
|
2313
|
+
const summary = Object.entries(counts).map(([k, v]) => `${k} ${v}`).join(", ") || "no instances";
|
|
2314
|
+
yield* ctx.out.print(`${roster.groupId} ${summary}`);
|
|
2315
|
+
for (const t of roster.tasks) yield* ctx.out.print(formatTaskSummary(yield* dec.summary(t)));
|
|
2316
|
+
}
|
|
2317
|
+
yield* dec.report(ctx.out);
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
function getTasks(args, ctx) {
|
|
2321
|
+
return Effect.gen(function* () {
|
|
2322
|
+
const dec = new Decryptor(ctx.keyring);
|
|
2323
|
+
const opts = {
|
|
2324
|
+
...args.status.length > 0 ? { status: args.status } : {},
|
|
2325
|
+
...Option.match(args.since, {
|
|
2326
|
+
onNone: () => ({}),
|
|
2327
|
+
onSome: (since) => ({ since })
|
|
2328
|
+
}),
|
|
2329
|
+
...Option.match(args.until, {
|
|
2330
|
+
onNone: () => ({}),
|
|
2331
|
+
onSome: (until) => ({ until })
|
|
2332
|
+
}),
|
|
2333
|
+
...Option.match(args.topic, {
|
|
2334
|
+
onNone: () => ({}),
|
|
2335
|
+
onSome: (topic) => ({ topic })
|
|
2336
|
+
}),
|
|
2337
|
+
...Option.match(args.member, {
|
|
2338
|
+
onNone: () => ({}),
|
|
2339
|
+
onSome: (member) => ({ member })
|
|
2340
|
+
}),
|
|
2341
|
+
...Option.match(args.group, {
|
|
2342
|
+
onNone: () => ({}),
|
|
2343
|
+
onSome: (group) => ({ group })
|
|
2344
|
+
}),
|
|
2345
|
+
...Option.match(args.limit, {
|
|
2346
|
+
onNone: () => ({}),
|
|
2347
|
+
onSome: (limit) => ({ limit })
|
|
2348
|
+
})
|
|
2349
|
+
};
|
|
2350
|
+
yield* forEachPage(ctx, args.format, args.all, Option.getOrUndefined(args.cursor), (cursor) => sdkCall("list tasks", () => ctx.client.listTasks({
|
|
2351
|
+
...opts,
|
|
2352
|
+
...cursor !== void 0 ? { cursor } : {}
|
|
2353
|
+
})), (page) => Effect.forEach(page.tasks, (t) => Effect.gen(function* () {
|
|
2354
|
+
const row = yield* dec.summary(t);
|
|
2355
|
+
yield* ctx.out.print(args.format === "json" ? JSON.stringify(row) : formatTaskSummary(row));
|
|
2356
|
+
}), { discard: true }));
|
|
2357
|
+
yield* dec.report(ctx.out);
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
function getSubmissions(args, ctx) {
|
|
2361
|
+
return Effect.gen(function* () {
|
|
2362
|
+
const wrong = [
|
|
2363
|
+
...args.status.length > 0 ? ["--status"] : [],
|
|
2364
|
+
...Option.isSome(args.topic) ? ["--topic"] : [],
|
|
2365
|
+
...Option.isSome(args.group) ? ["--group"] : []
|
|
2366
|
+
];
|
|
2367
|
+
if (wrong.length > 0) return yield* Effect.fail(new UserError({ message: `${wrong.join(", ")} does not apply to submissions` }));
|
|
2368
|
+
const dec = new Decryptor(ctx.keyring);
|
|
2369
|
+
const opts = {
|
|
2370
|
+
...Option.match(args.since, {
|
|
2371
|
+
onNone: () => ({}),
|
|
2372
|
+
onSome: (since) => ({ since })
|
|
2373
|
+
}),
|
|
2374
|
+
...Option.match(args.until, {
|
|
2375
|
+
onNone: () => ({}),
|
|
2376
|
+
onSome: (until) => ({ until })
|
|
2377
|
+
}),
|
|
2378
|
+
...Option.match(args.member, {
|
|
2379
|
+
onNone: () => ({}),
|
|
2380
|
+
onSome: (member) => ({ member })
|
|
2381
|
+
}),
|
|
2382
|
+
...Option.match(args.limit, {
|
|
2383
|
+
onNone: () => ({}),
|
|
2384
|
+
onSome: (limit) => ({ limit })
|
|
2385
|
+
})
|
|
2386
|
+
};
|
|
2387
|
+
yield* forEachPage(ctx, args.format, args.all, Option.getOrUndefined(args.cursor), (cursor) => sdkCall("list submissions", () => ctx.client.listSubmissions({
|
|
2388
|
+
...opts,
|
|
2389
|
+
...cursor !== void 0 ? { cursor } : {}
|
|
2390
|
+
})), (page) => Effect.forEach(page.submissions, (entry) => Effect.gen(function* () {
|
|
2391
|
+
const submission = yield* dec.submission(entry.submission, entry.encryption);
|
|
2392
|
+
const by = entry.actor?.name ?? entry.actor?.publicId;
|
|
2393
|
+
if (args.format === "json") yield* ctx.out.print(JSON.stringify({
|
|
2394
|
+
...submission,
|
|
2395
|
+
...entry.actor !== void 0 ? { actor: entry.actor } : {}
|
|
2396
|
+
}));
|
|
2397
|
+
else {
|
|
2398
|
+
const body = submission.body;
|
|
2399
|
+
const parts = [
|
|
2400
|
+
body?.value !== void 0 ? body.value : void 0,
|
|
2401
|
+
submission.photo !== void 0 ? "[photo]" : void 0,
|
|
2402
|
+
submission.file !== void 0 ? "[file]" : void 0,
|
|
2403
|
+
submission.audio !== void 0 ? "[audio]" : void 0,
|
|
2404
|
+
submission.location !== void 0 ? "[location]" : void 0
|
|
2405
|
+
].filter((p) => p !== void 0);
|
|
2406
|
+
yield* ctx.out.print(`${submission.id} ${submission.createdAt} ${by ?? "-"}: ${parts.join(" ") || "(empty)"}`);
|
|
2407
|
+
}
|
|
2408
|
+
}), { discard: true }));
|
|
2409
|
+
yield* dec.report(ctx.out);
|
|
2410
|
+
});
|
|
2411
|
+
}
|
|
2412
|
+
const getCommand = Command.make("get", {
|
|
2413
|
+
what: Args.text({ name: "what" }).pipe(Args.withDescription("What to read: tasks, submissions, a tsk_ id, a sub_ id, or a grptsk_ id.")),
|
|
2414
|
+
status: statusOption,
|
|
2415
|
+
since: sinceOption$1,
|
|
2416
|
+
until: untilOption$1,
|
|
2417
|
+
topic: topicOption,
|
|
2418
|
+
member: memberOption$3,
|
|
2419
|
+
group: groupOption,
|
|
2420
|
+
limit: limitOption$1,
|
|
2421
|
+
cursor: cursorOption,
|
|
2422
|
+
all: allOption,
|
|
2423
|
+
format: formatOption$4,
|
|
2424
|
+
...credentialOptions
|
|
2425
|
+
}, (args) => withQueryClient(args, (ctx) => {
|
|
2426
|
+
if (args.what === "tasks") return getTasks(args, ctx);
|
|
2427
|
+
if (args.what === "submissions") return getSubmissions(args, ctx);
|
|
2428
|
+
if (args.what.startsWith("tsk_")) return getChain(args, ctx);
|
|
2429
|
+
if (args.what.startsWith("sub_")) return getSubtaskRead(args, ctx);
|
|
2430
|
+
if (args.what.startsWith("grptsk_")) return getRoster(args, ctx);
|
|
2431
|
+
const hint = 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, a sub_ id, or a grptsk_ id";
|
|
2432
|
+
return Effect.fail(new UserError({ message: `cannot read '${args.what}': ${hint}` }));
|
|
2433
|
+
})).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
|
+
//#endregion
|
|
2215
2435
|
//#region src/input-spec.ts
|
|
2216
2436
|
const SETTING_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
2217
2437
|
function looksLikeSetting(seg) {
|
|
@@ -2621,7 +2841,7 @@ const notifyCommand = Command.make("notify", {
|
|
|
2621
2841
|
shared: sharedOption$1,
|
|
2622
2842
|
format: formatOption$2,
|
|
2623
2843
|
noEncrypt: noEncryptOption$2,
|
|
2624
|
-
topic: topicOption,
|
|
2844
|
+
topic: topicOption$1,
|
|
2625
2845
|
"api-token": apiTokenOption,
|
|
2626
2846
|
password: passwordOption,
|
|
2627
2847
|
"base-url": baseUrlOption,
|
|
@@ -2812,7 +3032,7 @@ const putIntegrationWraps = (id, wraps) => Effect.gen(function* () {
|
|
|
2812
3032
|
yield* (yield* Api).put("integration wrap", `/v1/org/integrations/${encodeURIComponent(id)}/wraps`, { wraps });
|
|
2813
3033
|
});
|
|
2814
3034
|
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,
|
|
3035
|
+
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
3036
|
const createCommand = Command.make("create", {
|
|
2817
3037
|
name: nameOption,
|
|
2818
3038
|
scopes: scopesOption,
|
|
@@ -3535,7 +3755,7 @@ const taskCommand = Command.make("task", {
|
|
|
3535
3755
|
shared: sharedOption,
|
|
3536
3756
|
expires: expiresOption,
|
|
3537
3757
|
format: formatOption$1,
|
|
3538
|
-
topic: topicOption,
|
|
3758
|
+
topic: topicOption$1,
|
|
3539
3759
|
"api-token": apiTokenOption,
|
|
3540
3760
|
password: passwordOption,
|
|
3541
3761
|
"base-url": baseUrlOption,
|
|
@@ -3841,7 +4061,7 @@ const subtaskCommand = Command.make("subtask", {
|
|
|
3841
4061
|
markdown: markdownOption,
|
|
3842
4062
|
"no-encrypt": noEncryptOption,
|
|
3843
4063
|
instance: instanceOption,
|
|
3844
|
-
topic: topicOption,
|
|
4064
|
+
topic: topicOption$1,
|
|
3845
4065
|
"api-token": apiTokenOption,
|
|
3846
4066
|
password: passwordOption,
|
|
3847
4067
|
"base-url": baseUrlOption,
|
|
@@ -4022,7 +4242,8 @@ const root = Command.make("simplepush").pipe(Command.withSubcommands([
|
|
|
4022
4242
|
notifyCommand,
|
|
4023
4243
|
taskCommand,
|
|
4024
4244
|
subtaskCommand,
|
|
4025
|
-
cancelCommand
|
|
4245
|
+
cancelCommand,
|
|
4246
|
+
getCommand
|
|
4026
4247
|
]));
|
|
4027
4248
|
const cli = Command.run(root, {
|
|
4028
4249
|
name: "Simplepush CLI",
|