@congressio/cli 0.0.0-stage → 0.2.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 +84 -2
- package/bin/congressio.mjs +110 -0
- package/lib/args.mjs +113 -0
- package/lib/commands.mjs +318 -0
- package/lib/format.mjs +73 -0
- package/lib/help.mjs +66 -0
- package/lib/http.mjs +156 -0
- package/package.json +42 -4
package/README.md
CHANGED
|
@@ -1,3 +1,85 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @congressio/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Command-line access to [Congressio](https://congressio.net), a conference
|
|
4
|
+
management platform for call for papers, peer review, program scheduling and
|
|
5
|
+
attendee registration.
|
|
6
|
+
|
|
7
|
+
Zero dependencies, no build step — it is plain ESM on Node's built-in `fetch`,
|
|
8
|
+
so `npx` resolves and runs it in one download.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npx @congressio/cli events list
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Commands
|
|
15
|
+
|
|
16
|
+
Commands marked **[key]** need an organization API key. The rest need no
|
|
17
|
+
credentials at all.
|
|
18
|
+
|
|
19
|
+
| Command | What it does |
|
|
20
|
+
| -------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
|
21
|
+
| `congressio events list [--all]` | Public conference directory |
|
|
22
|
+
| `congressio events get <slug>` | One public conference |
|
|
23
|
+
| `congressio my events [--all]` | **[key]** Your organization's events, drafts included |
|
|
24
|
+
| `congressio submissions list --event <id> [--all]` | **[key]** Submissions with status and track |
|
|
25
|
+
| `congressio registrations list --event <id> [--all]` | **[key]** Confirmed registrations |
|
|
26
|
+
| `congressio registrations create --event <id> --name <name> --email <email> [--tier <tier>]` | **[key]** Register an attendee (comped — takes no payment) |
|
|
27
|
+
| `congressio docs [slug]` | Documentation as Markdown |
|
|
28
|
+
| `congressio openapi` | The OpenAPI 3.1 document |
|
|
29
|
+
|
|
30
|
+
## Options
|
|
31
|
+
|
|
32
|
+
| Option | Effect |
|
|
33
|
+
| ------------------ | -------------------------------------------------------- |
|
|
34
|
+
| `--json` | Print the API response verbatim — for scripts and agents |
|
|
35
|
+
| `--all` | Follow pagination and return every item |
|
|
36
|
+
| `--retry` | On a 429, wait `Retry-After` and try again |
|
|
37
|
+
| `--api-key <key>` | Override `CONGRESSIO_API_KEY` |
|
|
38
|
+
| `--base-url <url>` | Point at another deployment |
|
|
39
|
+
| `--quiet` | Suppress progress and quota notes |
|
|
40
|
+
|
|
41
|
+
## Authentication
|
|
42
|
+
|
|
43
|
+
Create a key in Congressio under **Organization settings → API**, pick its
|
|
44
|
+
scopes, then:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
export CONGRESSIO_API_KEY=cong_live_…
|
|
48
|
+
congressio my events
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The secret is shown once at creation and stored only as a hash, so keep it
|
|
52
|
+
somewhere safe — to rotate, create a new key and revoke the old one. A key is
|
|
53
|
+
scoped to one organization and to the scopes you granted it; an operation
|
|
54
|
+
missing its scope is refused with `403 forbidden`.
|
|
55
|
+
|
|
56
|
+
## Designed to be scripted
|
|
57
|
+
|
|
58
|
+
- `--json` writes the API payload **unchanged** to stdout. Nothing is reshaped,
|
|
59
|
+
so the [OpenAPI schema](https://congressio.net/openapi.json) describes exactly
|
|
60
|
+
what you get.
|
|
61
|
+
- Progress, warnings and quota notes always go to **stderr**, so `--json`
|
|
62
|
+
output pipes straight into `jq`.
|
|
63
|
+
- Exit codes: `0` success, `1` API error, `2` usage error.
|
|
64
|
+
- The CLI reads the API's `RateLimit-*` headers and reports what is left. With
|
|
65
|
+
`--retry` it honours `Retry-After` rather than guessing a backoff.
|
|
66
|
+
- If an endpoint is ever deprecated, its `Deprecation`/`Sunset` headers are
|
|
67
|
+
surfaced as a warning on stderr — a scripted integration finds out before it
|
|
68
|
+
breaks.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Every conference opening a call for papers, as JSON
|
|
72
|
+
npx @congressio/cli events list --all --json | jq '.data[] | .slug'
|
|
73
|
+
|
|
74
|
+
# Import attendees from a CSV
|
|
75
|
+
while IFS=, read -r name email; do
|
|
76
|
+
congressio registrations create --event "$EVENT_ID" --name "$name" --email "$email"
|
|
77
|
+
done < attendees.csv
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Links
|
|
81
|
+
|
|
82
|
+
- API reference — <https://congressio.net/developers>
|
|
83
|
+
- OpenAPI 3.1 — <https://congressio.net/openapi.json>
|
|
84
|
+
- Guidance for AI agents — <https://congressio.net/llms.txt>
|
|
85
|
+
- Documentation — <https://congressio.net/docs>
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Entry point for the Congressio CLI.
|
|
5
|
+
*
|
|
6
|
+
* Keeps all the I/O and process concerns here — argument parsing, command
|
|
7
|
+
* dispatch, formatting and HTTP are pure modules under `lib/`, so they can be
|
|
8
|
+
* unit-tested without spawning a process or touching the network.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { parseArgs, resolveConfig } from "../lib/args.mjs";
|
|
12
|
+
import { COMMANDS, UsageError, resolveCommand } from "../lib/commands.mjs";
|
|
13
|
+
import { helpText } from "../lib/help.mjs";
|
|
14
|
+
import { quotaLine } from "../lib/format.mjs";
|
|
15
|
+
import { ApiError } from "../lib/http.mjs";
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
|
+
|
|
20
|
+
function version() {
|
|
21
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8"))
|
|
24
|
+
.version;
|
|
25
|
+
} catch {
|
|
26
|
+
return "0.0.0";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function main(argv) {
|
|
31
|
+
const parsed = parseArgs(argv);
|
|
32
|
+
const config = resolveConfig(parsed.flags, process.env);
|
|
33
|
+
|
|
34
|
+
if (parsed.flags.version) {
|
|
35
|
+
process.stdout.write(`${version()}\n`);
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const resolved = resolveCommand(
|
|
40
|
+
parsed.command,
|
|
41
|
+
parsed.subcommand,
|
|
42
|
+
parsed.positionals,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
if (parsed.flags.help || !parsed.command) {
|
|
46
|
+
process.stdout.write(`${helpText(version())}\n`);
|
|
47
|
+
return parsed.command || parsed.flags.help ? 0 : 2;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!resolved) {
|
|
51
|
+
process.stderr.write(
|
|
52
|
+
`congressio: unknown command "${[parsed.command, parsed.subcommand]
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
.join(" ")}"\nRun \`congressio --help\` for the command list.\n`,
|
|
55
|
+
);
|
|
56
|
+
return 2;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const command = COMMANDS[resolved.key];
|
|
60
|
+
|
|
61
|
+
if (command.needsKey && !config.apiKey) {
|
|
62
|
+
process.stderr.write(
|
|
63
|
+
`congressio: ${resolved.key.replace(":", " ")} needs an API key.\n` +
|
|
64
|
+
"Set CONGRESSIO_API_KEY (Organization settings → API), or pass --api-key.\n",
|
|
65
|
+
);
|
|
66
|
+
return 2;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// stderr for everything advisory, so --json stdout stays parseable.
|
|
70
|
+
const io = {
|
|
71
|
+
warn: (message) => {
|
|
72
|
+
if (!config.quiet) process.stderr.write(`${message}\n`);
|
|
73
|
+
},
|
|
74
|
+
quota: (rateLimit) => {
|
|
75
|
+
if (config.quiet) return;
|
|
76
|
+
const line = quotaLine(rateLimit);
|
|
77
|
+
if (line) process.stderr.write(`${line}\n`);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const output = await command.run({
|
|
82
|
+
config,
|
|
83
|
+
flags: parsed.flags,
|
|
84
|
+
subject: resolved.subject,
|
|
85
|
+
io,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (output) process.stdout.write(`${output}\n`);
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
main(process.argv.slice(2))
|
|
93
|
+
.then((code) => process.exit(code))
|
|
94
|
+
.catch((error) => {
|
|
95
|
+
if (error instanceof UsageError) {
|
|
96
|
+
process.stderr.write(`congressio: ${error.message}\n`);
|
|
97
|
+
process.exit(2);
|
|
98
|
+
}
|
|
99
|
+
if (error instanceof ApiError) {
|
|
100
|
+
process.stderr.write(`congressio: ${error.message}\n`);
|
|
101
|
+
if (error.code) process.stderr.write(` code: ${error.code}\n`);
|
|
102
|
+
if (error.hint) process.stderr.write(` hint: ${error.hint}\n`);
|
|
103
|
+
if (error.docs) process.stderr.write(` docs: ${error.docs}\n`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
process.stderr.write(
|
|
107
|
+
`congressio: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
108
|
+
);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
});
|
package/lib/args.mjs
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argument parsing for the Congressio CLI.
|
|
3
|
+
*
|
|
4
|
+
* Hand-rolled on purpose. The whole package ships with zero dependencies so
|
|
5
|
+
* `npx @congressio/cli` is a single download with nothing to audit and no
|
|
6
|
+
* build step — which matters when the caller is an agent resolving the tool
|
|
7
|
+
* on the fly. The grammar is small enough that a parser library would be more
|
|
8
|
+
* code than this.
|
|
9
|
+
*
|
|
10
|
+
* Grammar: `congressio <command> [subcommand] [positional] [--flag value]`
|
|
11
|
+
* --flag value | --flag=value | --boolean-flag
|
|
12
|
+
* -h / --help, -v / --version
|
|
13
|
+
* Everything after a bare `--` is left untouched in `rest`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Flags that are booleans, so `--json next-arg` does not swallow `next-arg`. */
|
|
17
|
+
export const BOOLEAN_FLAGS = new Set([
|
|
18
|
+
"json",
|
|
19
|
+
"help",
|
|
20
|
+
"version",
|
|
21
|
+
"retry",
|
|
22
|
+
"no-color",
|
|
23
|
+
"quiet",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse argv (without `node` and the script path).
|
|
28
|
+
*
|
|
29
|
+
* Returns `{ command, subcommand, positionals, flags, rest }`. Unknown flags
|
|
30
|
+
* are kept rather than rejected here — the command decides what it accepts, so
|
|
31
|
+
* a typo produces one clear error from one place.
|
|
32
|
+
*/
|
|
33
|
+
export function parseArgs(argv) {
|
|
34
|
+
const out = {
|
|
35
|
+
command: null,
|
|
36
|
+
subcommand: null,
|
|
37
|
+
positionals: [],
|
|
38
|
+
flags: {},
|
|
39
|
+
rest: [],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const words = [];
|
|
43
|
+
let i = 0;
|
|
44
|
+
for (; i < argv.length; i++) {
|
|
45
|
+
const arg = argv[i];
|
|
46
|
+
|
|
47
|
+
if (arg === "--") {
|
|
48
|
+
out.rest = argv.slice(i + 1);
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (arg === "-h") {
|
|
53
|
+
out.flags.help = true;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (arg === "-v") {
|
|
57
|
+
out.flags.version = true;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (arg.startsWith("--")) {
|
|
62
|
+
const body = arg.slice(2);
|
|
63
|
+
const eq = body.indexOf("=");
|
|
64
|
+
if (eq !== -1) {
|
|
65
|
+
out.flags[body.slice(0, eq)] = body.slice(eq + 1);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (BOOLEAN_FLAGS.has(body)) {
|
|
69
|
+
out.flags[body] = true;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const next = argv[i + 1];
|
|
73
|
+
if (next === undefined || next.startsWith("--")) {
|
|
74
|
+
// A value-taking flag with nothing after it. Record it as present and
|
|
75
|
+
// empty; the command reports the missing value with its own wording.
|
|
76
|
+
out.flags[body] = "";
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
out.flags[body] = next;
|
|
80
|
+
i++;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
words.push(arg);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
out.command = words[0] ?? null;
|
|
88
|
+
out.subcommand = words[1] ?? null;
|
|
89
|
+
out.positionals = words.slice(2);
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resolve configuration from flags then environment, in that order.
|
|
95
|
+
*
|
|
96
|
+
* `CONGRESSIO_API_KEY` is read from the environment so a key never has to
|
|
97
|
+
* appear in a shell history or a process list.
|
|
98
|
+
*/
|
|
99
|
+
export function resolveConfig(flags, env = {}) {
|
|
100
|
+
const baseUrl = (
|
|
101
|
+
flags["base-url"] ||
|
|
102
|
+
env.CONGRESSIO_BASE_URL ||
|
|
103
|
+
"https://congressio.net"
|
|
104
|
+
).replace(/\/+$/, "");
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
baseUrl,
|
|
108
|
+
apiKey: flags["api-key"] || env.CONGRESSIO_API_KEY || null,
|
|
109
|
+
json: flags.json === true,
|
|
110
|
+
retry: flags.retry === true,
|
|
111
|
+
quiet: flags.quiet === true,
|
|
112
|
+
};
|
|
113
|
+
}
|
package/lib/commands.mjs
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { ApiError, request } from "./http.mjs";
|
|
2
|
+
import { asJson, dateRange, quotaLine, table } from "./format.mjs";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The command table.
|
|
6
|
+
*
|
|
7
|
+
* Each entry declares what it needs (`needsKey`, `needsEvent`) so the entry
|
|
8
|
+
* point can refuse early with a precise message instead of making a request
|
|
9
|
+
* that is certain to 401. `run` returns the text for stdout; anything advisory
|
|
10
|
+
* goes through `io.warn` to stderr.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Thrown for a usage mistake — the caller's input, not the API's fault. */
|
|
14
|
+
export class UsageError extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "UsageError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function requireFlag(flags, name, hint) {
|
|
22
|
+
const value = flags[name];
|
|
23
|
+
if (!value || value === true) {
|
|
24
|
+
throw new UsageError(`--${name} is required${hint ? ` (${hint})` : ""}`);
|
|
25
|
+
}
|
|
26
|
+
return String(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Page through a collection until the API returns a short page. */
|
|
30
|
+
async function collectAll(path, config, io, { limit = 100, max = 1000 } = {}) {
|
|
31
|
+
const items = [];
|
|
32
|
+
let offset = 0;
|
|
33
|
+
let rateLimit = null;
|
|
34
|
+
for (;;) {
|
|
35
|
+
const sep = path.includes("?") ? "&" : "?";
|
|
36
|
+
const result = await request(
|
|
37
|
+
`${path}${sep}limit=${limit}&offset=${offset}`,
|
|
38
|
+
{ ...config, onNotice: io.warn },
|
|
39
|
+
);
|
|
40
|
+
rateLimit = result.rateLimit;
|
|
41
|
+
const page = result.data?.data ?? [];
|
|
42
|
+
items.push(...page);
|
|
43
|
+
if (page.length < limit || items.length >= max) break;
|
|
44
|
+
offset += limit;
|
|
45
|
+
}
|
|
46
|
+
return { items, rateLimit };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const EVENT_COLUMNS = [
|
|
50
|
+
{ key: "slug", label: "slug" },
|
|
51
|
+
{ key: "name", label: "name" },
|
|
52
|
+
{ label: "dates", value: (e) => dateRange(e.startDate, e.endDate) },
|
|
53
|
+
{ label: "venue", value: (e) => e.venue?.city ?? null },
|
|
54
|
+
{
|
|
55
|
+
label: "program",
|
|
56
|
+
value: (e) => (e.programPublished ? "published" : "draft"),
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
export const COMMANDS = {
|
|
61
|
+
"events:list": {
|
|
62
|
+
summary: "List public conferences (no API key needed)",
|
|
63
|
+
usage: "congressio events list [--all] [--json]",
|
|
64
|
+
needsKey: false,
|
|
65
|
+
async run({ config, flags, io }) {
|
|
66
|
+
const { items, rateLimit } =
|
|
67
|
+
flags.all === true || flags.all === "true"
|
|
68
|
+
? await collectAll("/api/v1/public/events", config, io)
|
|
69
|
+
: await request("/api/v1/public/events", {
|
|
70
|
+
...config,
|
|
71
|
+
onNotice: io.warn,
|
|
72
|
+
}).then((r) => ({ items: r.data.data, rateLimit: r.rateLimit }));
|
|
73
|
+
|
|
74
|
+
io.quota(rateLimit);
|
|
75
|
+
if (config.json) return asJson({ data: items });
|
|
76
|
+
if (items.length === 0) return "No public conferences are published.";
|
|
77
|
+
return table(items, EVENT_COLUMNS);
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
"events:get": {
|
|
82
|
+
summary: "Show one public conference by slug (no API key needed)",
|
|
83
|
+
usage: "congressio events get <slug> [--json]",
|
|
84
|
+
needsKey: false,
|
|
85
|
+
async run({ config, subject, io }) {
|
|
86
|
+
if (!subject) throw new UsageError("a conference slug is required");
|
|
87
|
+
const { data, rateLimit } = await request(
|
|
88
|
+
`/api/v1/public/events/${encodeURIComponent(subject)}`,
|
|
89
|
+
{ ...config, onNotice: io.warn },
|
|
90
|
+
);
|
|
91
|
+
io.quota(rateLimit);
|
|
92
|
+
if (config.json) return asJson(data);
|
|
93
|
+
|
|
94
|
+
const e = data.data;
|
|
95
|
+
const rows = [
|
|
96
|
+
["name", e.name],
|
|
97
|
+
["slug", e.slug],
|
|
98
|
+
["dates", dateRange(e.startDate, e.endDate)],
|
|
99
|
+
["timezone", e.timezone],
|
|
100
|
+
[
|
|
101
|
+
"venue",
|
|
102
|
+
[e.venue?.name, e.venue?.city, e.venue?.country]
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.join(", "),
|
|
105
|
+
],
|
|
106
|
+
["organizer", e.organization?.name],
|
|
107
|
+
["program", e.programPublished ? "published" : "not published"],
|
|
108
|
+
["url", e.url],
|
|
109
|
+
];
|
|
110
|
+
return table(
|
|
111
|
+
rows.map(([field, value]) => ({ field, value })),
|
|
112
|
+
[
|
|
113
|
+
{ key: "field", label: "field" },
|
|
114
|
+
{ key: "value", label: "value" },
|
|
115
|
+
],
|
|
116
|
+
);
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
"my:events": {
|
|
121
|
+
summary: "List your organization's events, including drafts",
|
|
122
|
+
usage: "congressio my events [--all] [--json]",
|
|
123
|
+
needsKey: true,
|
|
124
|
+
async run({ config, flags, io }) {
|
|
125
|
+
const { items, rateLimit } =
|
|
126
|
+
flags.all === true
|
|
127
|
+
? await collectAll("/api/v1/events", config, io)
|
|
128
|
+
: await request("/api/v1/events", {
|
|
129
|
+
...config,
|
|
130
|
+
onNotice: io.warn,
|
|
131
|
+
}).then((r) => ({ items: r.data.data, rateLimit: r.rateLimit }));
|
|
132
|
+
|
|
133
|
+
io.quota(rateLimit);
|
|
134
|
+
if (config.json) return asJson({ data: items });
|
|
135
|
+
if (items.length === 0) return "This organization has no events yet.";
|
|
136
|
+
return table(items, [
|
|
137
|
+
{ key: "id", label: "id" },
|
|
138
|
+
{ key: "name", label: "name" },
|
|
139
|
+
{ key: "status", label: "status" },
|
|
140
|
+
{ label: "dates", value: (e) => dateRange(e.startDate, e.endDate) },
|
|
141
|
+
]);
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
"submissions:list": {
|
|
146
|
+
summary: "List an event's submissions",
|
|
147
|
+
usage: "congressio submissions list --event <eventId> [--all] [--json]",
|
|
148
|
+
needsKey: true,
|
|
149
|
+
async run({ config, flags, io }) {
|
|
150
|
+
const eventId = requireFlag(
|
|
151
|
+
flags,
|
|
152
|
+
"event",
|
|
153
|
+
"an event UUID from `my events`",
|
|
154
|
+
);
|
|
155
|
+
const path = `/api/v1/events/${encodeURIComponent(eventId)}/submissions`;
|
|
156
|
+
const { items, rateLimit } =
|
|
157
|
+
flags.all === true
|
|
158
|
+
? await collectAll(path, config, io)
|
|
159
|
+
: await request(path, { ...config, onNotice: io.warn }).then((r) => ({
|
|
160
|
+
items: r.data.data,
|
|
161
|
+
rateLimit: r.rateLimit,
|
|
162
|
+
}));
|
|
163
|
+
|
|
164
|
+
io.quota(rateLimit);
|
|
165
|
+
if (config.json) return asJson({ data: items });
|
|
166
|
+
if (items.length === 0) return "No submissions yet.";
|
|
167
|
+
return table(items, [
|
|
168
|
+
{ key: "id", label: "id" },
|
|
169
|
+
{ key: "title", label: "title" },
|
|
170
|
+
{ key: "status", label: "status" },
|
|
171
|
+
{ key: "track", label: "track" },
|
|
172
|
+
]);
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
"registrations:list": {
|
|
177
|
+
summary: "List an event's confirmed registrations",
|
|
178
|
+
usage: "congressio registrations list --event <eventId> [--all] [--json]",
|
|
179
|
+
needsKey: true,
|
|
180
|
+
async run({ config, flags, io }) {
|
|
181
|
+
const eventId = requireFlag(
|
|
182
|
+
flags,
|
|
183
|
+
"event",
|
|
184
|
+
"an event UUID from `my events`",
|
|
185
|
+
);
|
|
186
|
+
const path = `/api/v1/events/${encodeURIComponent(eventId)}/registrations`;
|
|
187
|
+
const { items, rateLimit } =
|
|
188
|
+
flags.all === true
|
|
189
|
+
? await collectAll(path, config, io)
|
|
190
|
+
: await request(path, { ...config, onNotice: io.warn }).then((r) => ({
|
|
191
|
+
items: r.data.data,
|
|
192
|
+
rateLimit: r.rateLimit,
|
|
193
|
+
}));
|
|
194
|
+
|
|
195
|
+
io.quota(rateLimit);
|
|
196
|
+
if (config.json) return asJson({ data: items });
|
|
197
|
+
if (items.length === 0) return "No confirmed registrations yet.";
|
|
198
|
+
return table(items, [
|
|
199
|
+
{ key: "name", label: "name" },
|
|
200
|
+
{ key: "email", label: "email" },
|
|
201
|
+
{ key: "tier", label: "tier" },
|
|
202
|
+
{ label: "paid", value: (r) => (r.paid ? "yes" : "no") },
|
|
203
|
+
]);
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
"registrations:create": {
|
|
208
|
+
summary: "Register an attendee (comped — takes no payment)",
|
|
209
|
+
usage:
|
|
210
|
+
"congressio registrations create --event <eventId> --name <name> --email <email> [--tier <tier>]",
|
|
211
|
+
needsKey: true,
|
|
212
|
+
async run({ config, flags, io }) {
|
|
213
|
+
const eventId = requireFlag(
|
|
214
|
+
flags,
|
|
215
|
+
"event",
|
|
216
|
+
"an event UUID from `my events`",
|
|
217
|
+
);
|
|
218
|
+
const body = {
|
|
219
|
+
name: requireFlag(flags, "name"),
|
|
220
|
+
email: requireFlag(flags, "email"),
|
|
221
|
+
};
|
|
222
|
+
if (flags.tier && flags.tier !== true) body.tier = String(flags.tier);
|
|
223
|
+
|
|
224
|
+
const { data, rateLimit } = await request(
|
|
225
|
+
`/api/v1/events/${encodeURIComponent(eventId)}/registrations`,
|
|
226
|
+
{ ...config, method: "POST", body, onNotice: io.warn },
|
|
227
|
+
);
|
|
228
|
+
io.quota(rateLimit);
|
|
229
|
+
if (config.json) return asJson(data);
|
|
230
|
+
const r = data.data;
|
|
231
|
+
return `Registered ${r.name} <${r.email}> as #${r.registrationNumber}${
|
|
232
|
+
r.tier ? ` (${r.tier})` : ""
|
|
233
|
+
}. Pass serial ${r.passSerial ?? "—"}.`;
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
"docs:list": {
|
|
238
|
+
summary: "List the documentation pages",
|
|
239
|
+
usage: "congressio docs list",
|
|
240
|
+
needsKey: false,
|
|
241
|
+
async run({ config, io }) {
|
|
242
|
+
const body = await fetchMarkdown("/docs", config, io);
|
|
243
|
+
return body;
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
"docs:show": {
|
|
248
|
+
summary: "Print a documentation page as Markdown",
|
|
249
|
+
usage: "congressio docs show <slug>",
|
|
250
|
+
needsKey: false,
|
|
251
|
+
async run({ config, subject, io }) {
|
|
252
|
+
if (!subject) throw new UsageError("a documentation slug is required");
|
|
253
|
+
return fetchMarkdown(`/docs/${encodeURIComponent(subject)}`, config, io);
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
openapi: {
|
|
258
|
+
summary: "Print the OpenAPI document",
|
|
259
|
+
usage: "congressio openapi",
|
|
260
|
+
needsKey: false,
|
|
261
|
+
async run({ config, io }) {
|
|
262
|
+
const { data } = await request("/openapi.json", {
|
|
263
|
+
...config,
|
|
264
|
+
onNotice: io.warn,
|
|
265
|
+
});
|
|
266
|
+
return asJson(data);
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Fetch a page as Markdown rather than HTML.
|
|
273
|
+
*
|
|
274
|
+
* The site negotiates on `Accept`, so the CLI asks for `text/markdown` and
|
|
275
|
+
* gets the documentation as its authored source instead of scraping a rendered
|
|
276
|
+
* React page.
|
|
277
|
+
*/
|
|
278
|
+
async function fetchMarkdown(path, config, io) {
|
|
279
|
+
const response = await fetch(`${config.baseUrl}${path}`, {
|
|
280
|
+
headers: { Accept: "text/markdown", "User-Agent": "congressio-cli" },
|
|
281
|
+
});
|
|
282
|
+
const text = await response.text();
|
|
283
|
+
if (!response.ok) {
|
|
284
|
+
throw new ApiError(response.status, { message: text.split("\n")[0] }, null);
|
|
285
|
+
}
|
|
286
|
+
const type = response.headers.get("content-type") ?? "";
|
|
287
|
+
if (!type.includes("markdown")) {
|
|
288
|
+
io.warn(
|
|
289
|
+
`warning: ${path} answered ${type || "an unknown type"}, not Markdown`,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
return text.trimEnd();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Resolve the words before the flags to a command and its subject.
|
|
297
|
+
*
|
|
298
|
+
* Three shapes, so the obvious spelling works in each case:
|
|
299
|
+
* `events get ecsc-2026` → events:get, subject from the positional
|
|
300
|
+
* `docs api-webhooks` → docs:show, subject from the subcommand slot
|
|
301
|
+
* `openapi` → a bare top-level command
|
|
302
|
+
*/
|
|
303
|
+
export function resolveCommand(command, subcommand, positionals = []) {
|
|
304
|
+
if (!command) return null;
|
|
305
|
+
|
|
306
|
+
if (subcommand && COMMANDS[`${command}:${subcommand}`]) {
|
|
307
|
+
return { key: `${command}:${subcommand}`, subject: positionals[0] ?? null };
|
|
308
|
+
}
|
|
309
|
+
if (command === "docs") {
|
|
310
|
+
return subcommand
|
|
311
|
+
? { key: "docs:show", subject: subcommand }
|
|
312
|
+
: { key: "docs:list", subject: null };
|
|
313
|
+
}
|
|
314
|
+
if (COMMANDS[command]) {
|
|
315
|
+
return { key: command, subject: subcommand ?? null };
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
package/lib/format.mjs
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output formatting for the Congressio CLI.
|
|
3
|
+
*
|
|
4
|
+
* Two modes, and the distinction matters for the tool's main audience:
|
|
5
|
+
* - default: a column-aligned table for a person reading a terminal;
|
|
6
|
+
* - `--json`: the API payload, verbatim and unreshaped, for a script or an
|
|
7
|
+
* agent to parse. Never pretty, never truncated, never annotated — if the
|
|
8
|
+
* CLI reshaped it, a caller could not rely on the OpenAPI schema.
|
|
9
|
+
*
|
|
10
|
+
* Progress and warnings always go to stderr, so `--json` stdout stays a single
|
|
11
|
+
* valid JSON document that can be piped straight into `jq`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Visible width, ignoring the escape sequences we might have added. */
|
|
15
|
+
function width(value) {
|
|
16
|
+
return String(value).replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Render rows as an aligned table.
|
|
21
|
+
*
|
|
22
|
+
* `columns` is `[{ key, label, value? }]`; `value` maps a row to its cell so a
|
|
23
|
+
* nested field can be flattened without mutating the API payload.
|
|
24
|
+
*/
|
|
25
|
+
export function table(rows, columns) {
|
|
26
|
+
if (rows.length === 0) return "";
|
|
27
|
+
|
|
28
|
+
const cells = rows.map((row) =>
|
|
29
|
+
columns.map((column) => {
|
|
30
|
+
const raw = column.value ? column.value(row) : row[column.key];
|
|
31
|
+
if (raw === null || raw === undefined || raw === "") return "—";
|
|
32
|
+
return String(raw);
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const widths = columns.map((column, i) =>
|
|
37
|
+
Math.max(width(column.label), ...cells.map((row) => width(row[i]))),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const pad = (value, i) =>
|
|
41
|
+
value + " ".repeat(Math.max(0, widths[i] - width(value)));
|
|
42
|
+
|
|
43
|
+
const lines = [
|
|
44
|
+
columns.map((c, i) => pad(c.label.toUpperCase(), i)).join(" "),
|
|
45
|
+
widths.map((w) => "─".repeat(w)).join(" "),
|
|
46
|
+
...cells.map((row) => row.map(pad).join(" ")),
|
|
47
|
+
];
|
|
48
|
+
return lines.join("\n").replace(/ +$/gm, "");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** `2026-09-14 → 2026-09-17`, or a single date, or an em dash. */
|
|
52
|
+
export function dateRange(start, end) {
|
|
53
|
+
const day = (value) => (value ? String(value).slice(0, 10) : null);
|
|
54
|
+
const a = day(start);
|
|
55
|
+
const b = day(end);
|
|
56
|
+
if (a && b) return a === b ? a : `${a} → ${b}`;
|
|
57
|
+
return a ?? b ?? "—";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** One-line quota summary for stderr, or null when the API reported none. */
|
|
61
|
+
export function quotaLine(rateLimit) {
|
|
62
|
+
if (!rateLimit || rateLimit.remaining === null) return null;
|
|
63
|
+
const reset =
|
|
64
|
+
rateLimit.resetSeconds === null
|
|
65
|
+
? ""
|
|
66
|
+
: `, resets in ${rateLimit.resetSeconds}s`;
|
|
67
|
+
return `${rateLimit.remaining}/${rateLimit.limit ?? "?"} requests left${reset}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The API payload, unreshaped, as a single JSON document. */
|
|
71
|
+
export function asJson(payload) {
|
|
72
|
+
return JSON.stringify(payload, null, 2);
|
|
73
|
+
}
|
package/lib/help.mjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { COMMANDS } from "./commands.mjs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `--help` output.
|
|
5
|
+
*
|
|
6
|
+
* Written to be useful to an agent reading it once: every command with its
|
|
7
|
+
* exact usage line, whether it needs a key, and how to supply one. No colour,
|
|
8
|
+
* no boxes — the text has to survive being pasted into a model's context.
|
|
9
|
+
*/
|
|
10
|
+
export function helpText(version) {
|
|
11
|
+
const rows = Object.entries(COMMANDS).map(([key, command]) => ({
|
|
12
|
+
// `events:list` → `events list`. Keeping the name column narrow leaves the
|
|
13
|
+
// summaries readable; the full argument lists go in their own block below.
|
|
14
|
+
name: key.replace(":", " "),
|
|
15
|
+
usage: command.usage.replace(/^congressio /, ""),
|
|
16
|
+
needsKey: command.needsKey,
|
|
17
|
+
summary: command.summary,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
const width = Math.max(...rows.map((r) => r.name.length));
|
|
21
|
+
const lines = [
|
|
22
|
+
`congressio ${version} — command-line access to the Congressio conference platform`,
|
|
23
|
+
"",
|
|
24
|
+
"USAGE",
|
|
25
|
+
" congressio <command> [options]",
|
|
26
|
+
"",
|
|
27
|
+
"COMMANDS",
|
|
28
|
+
...rows.map(
|
|
29
|
+
(r) =>
|
|
30
|
+
` ${r.name.padEnd(width)} ${r.summary}${r.needsKey ? " [key]" : ""}`,
|
|
31
|
+
),
|
|
32
|
+
"",
|
|
33
|
+
"ARGUMENTS",
|
|
34
|
+
...rows.map((r) => ` ${r.usage}`),
|
|
35
|
+
"",
|
|
36
|
+
"OPTIONS",
|
|
37
|
+
" --json Print the API response verbatim, for scripts and agents",
|
|
38
|
+
" --all Follow pagination and return every item",
|
|
39
|
+
" --retry On a 429, wait Retry-After and try again",
|
|
40
|
+
" --api-key <key> Override CONGRESSIO_API_KEY",
|
|
41
|
+
" --base-url <url> Point at another deployment (default https://congressio.net)",
|
|
42
|
+
" --quiet Suppress progress and quota notes on stderr",
|
|
43
|
+
" -h, --help Show this help",
|
|
44
|
+
" -v, --version Show the version",
|
|
45
|
+
"",
|
|
46
|
+
"AUTHENTICATION",
|
|
47
|
+
" Commands marked [key] need an organization API key. Create one in",
|
|
48
|
+
" Congressio under Organization settings → API, then export it:",
|
|
49
|
+
"",
|
|
50
|
+
" export CONGRESSIO_API_KEY=cong_live_…",
|
|
51
|
+
"",
|
|
52
|
+
" Commands without [key] need no credentials at all.",
|
|
53
|
+
"",
|
|
54
|
+
"NOTES",
|
|
55
|
+
" Output: --json writes the API payload unchanged to stdout; progress,",
|
|
56
|
+
" warnings and rate-limit notes always go to stderr, so `--json` output",
|
|
57
|
+
" pipes cleanly into jq.",
|
|
58
|
+
" Exit codes: 0 success, 1 API error, 2 usage error.",
|
|
59
|
+
"",
|
|
60
|
+
"DOCUMENTATION",
|
|
61
|
+
" https://congressio.net/developers API reference and scopes",
|
|
62
|
+
" https://congressio.net/openapi.json OpenAPI 3.1 description",
|
|
63
|
+
" https://congressio.net/llms.txt Guidance for AI agents",
|
|
64
|
+
];
|
|
65
|
+
return lines.join("\n");
|
|
66
|
+
}
|
package/lib/http.mjs
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP layer for the Congressio CLI: one request helper that understands the
|
|
3
|
+
* API's error envelope and its rate-limit headers.
|
|
4
|
+
*
|
|
5
|
+
* Built on global `fetch` (Node 20+), so there is no HTTP dependency to ship.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Thrown for any non-2xx response, carrying the parsed error envelope. */
|
|
9
|
+
export class ApiError extends Error {
|
|
10
|
+
constructor(status, body, retryAfterSeconds) {
|
|
11
|
+
const message = (body && (body.message || body.error)) || `HTTP ${status}`;
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "ApiError";
|
|
14
|
+
this.status = status;
|
|
15
|
+
/** Stable machine-readable code from the API (`not_found`, `forbidden`, …). */
|
|
16
|
+
this.code = (body && body.code) || null;
|
|
17
|
+
/** What the API says to do about it — worth printing verbatim. */
|
|
18
|
+
this.hint = (body && body.hint) || null;
|
|
19
|
+
this.docs = (body && body.docs) || null;
|
|
20
|
+
this.retryAfterSeconds = retryAfterSeconds ?? null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Read the quota state the API publishes.
|
|
26
|
+
*
|
|
27
|
+
* Prefers the structured `RateLimit` field and falls back to the dashed
|
|
28
|
+
* triple, so the CLI keeps working whichever spelling the server emits.
|
|
29
|
+
* Returns nulls rather than guesses when a field is absent — the API omits
|
|
30
|
+
* live state when its counter is unknown, and inventing a number here would
|
|
31
|
+
* defeat the point.
|
|
32
|
+
*/
|
|
33
|
+
export function readRateLimit(headers) {
|
|
34
|
+
const structured = headers.get("ratelimit");
|
|
35
|
+
if (structured) {
|
|
36
|
+
const remaining = structured.match(/[;\s]r=(\d+)/);
|
|
37
|
+
const reset = structured.match(/[;\s]t=(\d+)/);
|
|
38
|
+
const policy = headers.get("ratelimit-policy") ?? "";
|
|
39
|
+
const limit = policy.match(/[;\s]q=(\d+)/);
|
|
40
|
+
return {
|
|
41
|
+
limit: limit ? Number(limit[1]) : null,
|
|
42
|
+
remaining: remaining ? Number(remaining[1]) : null,
|
|
43
|
+
resetSeconds: reset ? Number(reset[1]) : null,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const num = (name) => {
|
|
48
|
+
const raw = headers.get(name);
|
|
49
|
+
return raw === null || raw === "" ? null : Number(raw);
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
limit: num("ratelimit-limit"),
|
|
53
|
+
remaining: num("ratelimit-remaining"),
|
|
54
|
+
resetSeconds: num("ratelimit-reset"),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** `Sunset` / `Deprecation`, so a scripted caller is warned before a break. */
|
|
59
|
+
export function readDeprecation(headers) {
|
|
60
|
+
const deprecation = headers.get("deprecation");
|
|
61
|
+
const sunset = headers.get("sunset");
|
|
62
|
+
if (!deprecation && !sunset) return null;
|
|
63
|
+
return { deprecation, sunset };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Seconds to wait, from `Retry-After`. */
|
|
67
|
+
function retryAfter(headers) {
|
|
68
|
+
const raw = headers.get("retry-after");
|
|
69
|
+
if (!raw) return null;
|
|
70
|
+
const seconds = Number(raw);
|
|
71
|
+
if (Number.isFinite(seconds)) return seconds;
|
|
72
|
+
// RFC 9110 permits an HTTP-date instead of a delta.
|
|
73
|
+
const at = Date.parse(raw);
|
|
74
|
+
return Number.isNaN(at)
|
|
75
|
+
? null
|
|
76
|
+
: Math.max(0, Math.ceil((at - Date.now()) / 1000));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const sleep = (seconds) =>
|
|
80
|
+
new Promise((resolve) => setTimeout(resolve, seconds * 1000));
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Perform one API request.
|
|
84
|
+
*
|
|
85
|
+
* With `retry`, a 429 is honoured exactly as instructed — wait `Retry-After`,
|
|
86
|
+
* then try again, at most `maxRetries` times. That is the whole reason the
|
|
87
|
+
* server publishes the header, and it keeps an agent from hammering a limit
|
|
88
|
+
* it has already been told about.
|
|
89
|
+
*/
|
|
90
|
+
export async function request(
|
|
91
|
+
path,
|
|
92
|
+
{
|
|
93
|
+
baseUrl,
|
|
94
|
+
apiKey,
|
|
95
|
+
method = "GET",
|
|
96
|
+
body,
|
|
97
|
+
retry = false,
|
|
98
|
+
maxRetries = 3,
|
|
99
|
+
fetchImpl = fetch,
|
|
100
|
+
onNotice = () => {},
|
|
101
|
+
},
|
|
102
|
+
) {
|
|
103
|
+
const url = `${baseUrl}${path}`;
|
|
104
|
+
const headers = {
|
|
105
|
+
Accept: "application/json",
|
|
106
|
+
"User-Agent": "congressio-cli",
|
|
107
|
+
};
|
|
108
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
109
|
+
if (body !== undefined) headers["Content-Type"] = "application/json";
|
|
110
|
+
|
|
111
|
+
for (let attempt = 0; ; attempt++) {
|
|
112
|
+
const response = await fetchImpl(url, {
|
|
113
|
+
method,
|
|
114
|
+
headers,
|
|
115
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const deprecated = readDeprecation(response.headers);
|
|
119
|
+
if (deprecated) {
|
|
120
|
+
onNotice(
|
|
121
|
+
`warning: ${method} ${path} is deprecated` +
|
|
122
|
+
(deprecated.sunset
|
|
123
|
+
? ` and stops responding after ${deprecated.sunset}`
|
|
124
|
+
: ""),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let payload = null;
|
|
129
|
+
const text = await response.text();
|
|
130
|
+
if (text) {
|
|
131
|
+
try {
|
|
132
|
+
payload = JSON.parse(text);
|
|
133
|
+
} catch {
|
|
134
|
+
payload = null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (response.ok) {
|
|
139
|
+
return { data: payload, rateLimit: readRateLimit(response.headers) };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const wait = retryAfter(response.headers);
|
|
143
|
+
if (
|
|
144
|
+
response.status === 429 &&
|
|
145
|
+
retry &&
|
|
146
|
+
attempt < maxRetries &&
|
|
147
|
+
wait !== null
|
|
148
|
+
) {
|
|
149
|
+
onNotice(`rate limited; waiting ${wait}s as instructed by Retry-After`);
|
|
150
|
+
await sleep(wait);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
throw new ApiError(response.status, payload, wait);
|
|
155
|
+
}
|
|
156
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@congressio/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Command-line access to the Congressio conference management platform: public conference directory, submissions, peer review and attendee registrations.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"congressio",
|
|
7
|
+
"cli",
|
|
8
|
+
"conference",
|
|
9
|
+
"call-for-papers",
|
|
10
|
+
"peer-review",
|
|
11
|
+
"abstracts",
|
|
12
|
+
"registration",
|
|
13
|
+
"api"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://congressio.net/cli",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://congressio.net/contact"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "RivTech SAS",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"bin": {
|
|
23
|
+
"congressio": "bin/congressio.mjs"
|
|
24
|
+
},
|
|
25
|
+
"exports": {
|
|
26
|
+
".": "./lib/commands.mjs",
|
|
27
|
+
"./http": "./lib/http.mjs",
|
|
28
|
+
"./format": "./lib/format.mjs"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"bin",
|
|
32
|
+
"lib",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=20"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"congressio": "node bin/congressio.mjs"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
}
|
|
44
|
+
}
|