daffy-cli 0.1.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 +106 -0
- package/dist/args.js +43 -0
- package/dist/bin.js +74 -0
- package/dist/client.js +50 -0
- package/dist/commands/balance.js +15 -0
- package/dist/commands/cancel.js +18 -0
- package/dist/commands/causes.js +12 -0
- package/dist/commands/contributions.js +21 -0
- package/dist/commands/donate.js +38 -0
- package/dist/commands/donations.js +21 -0
- package/dist/commands/gift.js +60 -0
- package/dist/commands/gifts.js +21 -0
- package/dist/commands/me.js +16 -0
- package/dist/commands/nonprofit.js +22 -0
- package/dist/commands/search.js +29 -0
- package/dist/commands/user.js +21 -0
- package/dist/format.js +59 -0
- package/dist/output.js +31 -0
- package/dist/prompt.js +16 -0
- package/dist/types.js +1 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# daffy-cli
|
|
2
|
+
|
|
3
|
+
CLI for the [Daffy Public API](https://docs.daffy.org). Search nonprofits, make donations, check balances, send gifts, and more — straight from the terminal.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd daffy-cli
|
|
9
|
+
npm install
|
|
10
|
+
npm run build
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Set your API key (get one at https://www.daffy.org/settings/api):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
export DAFFY_API_KEY="your-key-here"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Run a command:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
node dist/bin.js balance
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
### Account
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
daffy-cli me # Your profile
|
|
31
|
+
daffy-cli balance # Your fund balance
|
|
32
|
+
daffy-cli causes # Your selected causes
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Non-Profits
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
daffy-cli search "khan academy" # Search by name
|
|
39
|
+
daffy-cli search --cause 1 # Filter by cause ID
|
|
40
|
+
daffy-cli search "schools" --page 2 # Paginate results
|
|
41
|
+
daffy-cli nonprofit 261544963 # Details by EIN
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Donations
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
daffy-cli donations # List your donations
|
|
48
|
+
daffy-cli donations --page 2 # Page through results
|
|
49
|
+
daffy-cli donate --ein 261544963 --amount 25
|
|
50
|
+
daffy-cli donate --ein 261544963 --amount 25 --note "Keep it up"
|
|
51
|
+
daffy-cli cancel 12345 # Cancel a pending donation
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Contributions
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
daffy-cli contributions # List your contributions
|
|
58
|
+
daffy-cli contributions --page 3
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Gifts
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
daffy-cli gifts # List your gifts
|
|
65
|
+
daffy-cli gift <code> # Gift details by code
|
|
66
|
+
daffy-cli gift create --name "Alex" --amount 25
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## JSON Output
|
|
70
|
+
|
|
71
|
+
Append `--json` to any command to get raw API JSON instead of formatted output. Useful for scripting and piping:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
daffy-cli balance --json
|
|
75
|
+
daffy-cli search "khan academy" --json | jq '.items[].name'
|
|
76
|
+
daffy-cli donations --json --page 2
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Options
|
|
80
|
+
|
|
81
|
+
| Flag | Description |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `--help`, `-h` | Show usage |
|
|
84
|
+
| `--json` | Output raw JSON (works with every command) |
|
|
85
|
+
| `--yes`, `-y` | Skip confirmation prompts (for `donate`, `cancel`, `gift create`) |
|
|
86
|
+
| `--page N` | Page number for paginated commands |
|
|
87
|
+
|
|
88
|
+
## Environment Variables
|
|
89
|
+
|
|
90
|
+
| Variable | Description |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `DAFFY_API_KEY` | **Required.** Your Daffy API key. |
|
|
93
|
+
| `NO_COLOR` | Set to any value to disable colored output. |
|
|
94
|
+
|
|
95
|
+
## Publishing
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npm pack --dry-run # Preview what gets published (only dist/)
|
|
99
|
+
npm publish # Publish to npm
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
After publishing:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
npx daffy-cli balance
|
|
106
|
+
```
|
package/dist/args.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export function getFlag(args, name) {
|
|
2
|
+
const prefix = `--${name}`;
|
|
3
|
+
for (let i = 0; i < args.length; i++) {
|
|
4
|
+
if (args[i] === prefix && i + 1 < args.length) {
|
|
5
|
+
return args[i + 1];
|
|
6
|
+
}
|
|
7
|
+
if (args[i]?.startsWith(`${prefix}=`)) {
|
|
8
|
+
return args[i].slice(prefix.length + 1);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
export function requireFlag(args, name) {
|
|
14
|
+
const value = getFlag(args, name);
|
|
15
|
+
if (!value) {
|
|
16
|
+
console.error(`Error: --${name} is required.`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
const BOOLEAN_FLAGS = new Set(["--yes", "-y", "--json"]);
|
|
22
|
+
export function getPositional(args, index) {
|
|
23
|
+
let pos = 0;
|
|
24
|
+
for (let i = 0; i < args.length; i++) {
|
|
25
|
+
const arg = args[i];
|
|
26
|
+
if (BOOLEAN_FLAGS.has(arg))
|
|
27
|
+
continue;
|
|
28
|
+
if (arg.startsWith("--")) {
|
|
29
|
+
if (!arg.includes("="))
|
|
30
|
+
i++; // skip flag value unless --flag=value
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (arg.startsWith("-"))
|
|
34
|
+
continue;
|
|
35
|
+
if (pos === index)
|
|
36
|
+
return arg;
|
|
37
|
+
pos++;
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
export function hasFlag(args, name) {
|
|
42
|
+
return args.includes(`--${name}`);
|
|
43
|
+
}
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { ApiError } from "./client.js";
|
|
3
|
+
import { setJsonMode } from "./output.js";
|
|
4
|
+
const HELP = `
|
|
5
|
+
daffy-cli — CLI for the Daffy Public API
|
|
6
|
+
|
|
7
|
+
Usage: daffy-cli <command> [options]
|
|
8
|
+
|
|
9
|
+
Commands:
|
|
10
|
+
me Show your profile
|
|
11
|
+
user <username> Show a user's profile
|
|
12
|
+
balance Show your fund balance
|
|
13
|
+
search <query> [--cause N] Search non-profits
|
|
14
|
+
nonprofit <ein> Show non-profit details
|
|
15
|
+
donations [--page N] List your donations
|
|
16
|
+
donate --ein <ein> --amount <n> [--note "..."]
|
|
17
|
+
Create a donation
|
|
18
|
+
cancel <id> Cancel a donation
|
|
19
|
+
contributions [--page N] List your contributions
|
|
20
|
+
causes List your causes
|
|
21
|
+
gifts [--page N] List your gifts
|
|
22
|
+
gift <code> Show gift details
|
|
23
|
+
gift create --name <n> --amount <n>
|
|
24
|
+
Create a gift
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
--json Output raw JSON instead of formatted text
|
|
28
|
+
--yes, -y Skip confirmation prompts
|
|
29
|
+
|
|
30
|
+
Environment:
|
|
31
|
+
DAFFY_API_KEY Your Daffy API key (required)
|
|
32
|
+
Get one at https://www.daffy.org/settings/api
|
|
33
|
+
NO_COLOR Disable color output
|
|
34
|
+
`;
|
|
35
|
+
const commands = {
|
|
36
|
+
me: () => import("./commands/me.js"),
|
|
37
|
+
user: () => import("./commands/user.js"),
|
|
38
|
+
balance: () => import("./commands/balance.js"),
|
|
39
|
+
search: () => import("./commands/search.js"),
|
|
40
|
+
nonprofit: () => import("./commands/nonprofit.js"),
|
|
41
|
+
donations: () => import("./commands/donations.js"),
|
|
42
|
+
donate: () => import("./commands/donate.js"),
|
|
43
|
+
cancel: () => import("./commands/cancel.js"),
|
|
44
|
+
contributions: () => import("./commands/contributions.js"),
|
|
45
|
+
causes: () => import("./commands/causes.js"),
|
|
46
|
+
gifts: () => import("./commands/gifts.js"),
|
|
47
|
+
gift: () => import("./commands/gift.js"),
|
|
48
|
+
};
|
|
49
|
+
async function main() {
|
|
50
|
+
const args = process.argv.slice(2);
|
|
51
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
52
|
+
console.log(HELP);
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
const name = args[0];
|
|
56
|
+
const loader = commands[name];
|
|
57
|
+
if (!loader) {
|
|
58
|
+
console.error(`Unknown command: ${name}\nRun "daffy-cli --help" for usage.`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
if (args.includes("--json"))
|
|
62
|
+
setJsonMode(true);
|
|
63
|
+
const mod = await loader();
|
|
64
|
+
await mod.run(args.slice(1));
|
|
65
|
+
}
|
|
66
|
+
main().catch((err) => {
|
|
67
|
+
if (err instanceof ApiError) {
|
|
68
|
+
console.error(`\nAPI Error (${err.status}): ${err.body}`);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
console.error(`\nError: ${err instanceof Error ? err.message : String(err)}`);
|
|
72
|
+
}
|
|
73
|
+
process.exit(1);
|
|
74
|
+
});
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const BASE_URL = "https://public.daffy.org";
|
|
2
|
+
export class ApiError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
body;
|
|
5
|
+
constructor(status, body) {
|
|
6
|
+
super(`API error ${status}: ${body}`);
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.body = body;
|
|
9
|
+
this.name = "ApiError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function getApiKey() {
|
|
13
|
+
const key = process.env.DAFFY_API_KEY;
|
|
14
|
+
if (!key) {
|
|
15
|
+
console.error("Error: DAFFY_API_KEY environment variable is required.\n" +
|
|
16
|
+
"Get your API key at https://www.daffy.org/settings/api");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
return key;
|
|
20
|
+
}
|
|
21
|
+
async function request(method, path, body) {
|
|
22
|
+
const url = `${BASE_URL}${path}`;
|
|
23
|
+
const headers = {
|
|
24
|
+
"X-Api-Key": getApiKey(),
|
|
25
|
+
Accept: "application/json",
|
|
26
|
+
};
|
|
27
|
+
const init = { method, headers };
|
|
28
|
+
if (body) {
|
|
29
|
+
headers["Content-Type"] = "application/json";
|
|
30
|
+
init.body = JSON.stringify(body);
|
|
31
|
+
}
|
|
32
|
+
const res = await fetch(url, init);
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
const text = await res.text();
|
|
35
|
+
throw new ApiError(res.status, text);
|
|
36
|
+
}
|
|
37
|
+
const text = await res.text();
|
|
38
|
+
if (!text)
|
|
39
|
+
return {};
|
|
40
|
+
return JSON.parse(text);
|
|
41
|
+
}
|
|
42
|
+
export function get(path) {
|
|
43
|
+
return request("GET", path);
|
|
44
|
+
}
|
|
45
|
+
export function post(path, body) {
|
|
46
|
+
return request("POST", path, body);
|
|
47
|
+
}
|
|
48
|
+
export function del(path) {
|
|
49
|
+
return request("DELETE", path);
|
|
50
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { get } from "../client.js";
|
|
2
|
+
import { renderKeyValue, formatMoney } from "../output.js";
|
|
3
|
+
export async function run(_args) {
|
|
4
|
+
const bal = (await get("/v1/users/me/balance"));
|
|
5
|
+
renderKeyValue({
|
|
6
|
+
title: "Fund Balance",
|
|
7
|
+
data: bal,
|
|
8
|
+
fields: [
|
|
9
|
+
["Available", formatMoney(bal.available_balance)],
|
|
10
|
+
["Portfolio", formatMoney(bal.portfolio_balance)],
|
|
11
|
+
["Pending Deposits", formatMoney(bal.pending_deposit_balance)],
|
|
12
|
+
["Total", formatMoney(bal.amount)],
|
|
13
|
+
],
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { del } from "../client.js";
|
|
2
|
+
import { getPositional } from "../args.js";
|
|
3
|
+
import { renderMessage } from "../output.js";
|
|
4
|
+
import { confirm } from "../prompt.js";
|
|
5
|
+
export async function run(args) {
|
|
6
|
+
const id = getPositional(args, 0);
|
|
7
|
+
if (!id) {
|
|
8
|
+
console.error("Usage: daffy-cli cancel <donation_id>");
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
const ok = await confirm(`Cancel donation ${id}?`, args);
|
|
12
|
+
if (!ok) {
|
|
13
|
+
renderMessage("Cancelled.", { cancelled: false, id });
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
await del(`/v1/donations/${encodeURIComponent(id)}`);
|
|
17
|
+
renderMessage(`\nDonation ${id} cancelled.`, { cancelled: true, id });
|
|
18
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { get } from "../client.js";
|
|
2
|
+
import { renderTable } from "../output.js";
|
|
3
|
+
export async function run(_args) {
|
|
4
|
+
const user = (await get("/v1/users/me"));
|
|
5
|
+
const causes = (await get(`/v1/users/${user.id}/causes`));
|
|
6
|
+
renderTable({
|
|
7
|
+
title: "Your Causes",
|
|
8
|
+
data: causes,
|
|
9
|
+
headers: ["ID", "Name"],
|
|
10
|
+
rows: causes.map((c) => [String(c.id), c.name]),
|
|
11
|
+
});
|
|
12
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { get } from "../client.js";
|
|
2
|
+
import { getFlag } from "../args.js";
|
|
3
|
+
import { renderTable, formatStatus } from "../output.js";
|
|
4
|
+
export async function run(args) {
|
|
5
|
+
const page = getFlag(args, "page");
|
|
6
|
+
const qs = page ? `?page=${page}` : "";
|
|
7
|
+
const data = (await get(`/v1/contributions${qs}`));
|
|
8
|
+
renderTable({
|
|
9
|
+
title: "Your Contributions",
|
|
10
|
+
data,
|
|
11
|
+
headers: ["Amount", "Currency", "Type", "Status", "Date"],
|
|
12
|
+
rows: data.items.map((c) => [
|
|
13
|
+
`$${(c.units * c.valuation).toFixed(2)}`,
|
|
14
|
+
c.currency,
|
|
15
|
+
c.type.replace(/_/g, " "),
|
|
16
|
+
formatStatus(c.status),
|
|
17
|
+
c.created_at.slice(0, 10),
|
|
18
|
+
]),
|
|
19
|
+
meta: data.meta,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { get, post } from "../client.js";
|
|
2
|
+
import { requireFlag, getFlag } from "../args.js";
|
|
3
|
+
import { renderKeyValue, formatMoney, formatStatus } from "../output.js";
|
|
4
|
+
import { confirm } from "../prompt.js";
|
|
5
|
+
export async function run(args) {
|
|
6
|
+
const ein = requireFlag(args, "ein");
|
|
7
|
+
const amountStr = requireFlag(args, "amount");
|
|
8
|
+
const note = getFlag(args, "note");
|
|
9
|
+
const amount = parseFloat(amountStr);
|
|
10
|
+
if (isNaN(amount) || amount <= 0) {
|
|
11
|
+
console.error("Error: --amount must be a positive number.");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
const np = (await get(`/v1/non_profits/${encodeURIComponent(ein)}`));
|
|
15
|
+
console.log(`\nDonate ${formatMoney(amount)} to ${np.name} (${np.ein})`);
|
|
16
|
+
if (note)
|
|
17
|
+
console.log(`Note: "${note}"`);
|
|
18
|
+
const ok = await confirm("\nProceed?", args);
|
|
19
|
+
if (!ok) {
|
|
20
|
+
console.log("Cancelled.");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const body = { ein, amount };
|
|
24
|
+
if (note)
|
|
25
|
+
body.note = note;
|
|
26
|
+
const donation = (await post("/v1/donations", body));
|
|
27
|
+
renderKeyValue({
|
|
28
|
+
title: "Donation Created",
|
|
29
|
+
data: donation,
|
|
30
|
+
fields: [
|
|
31
|
+
["ID", donation.id],
|
|
32
|
+
["Amount", formatMoney(donation.amount)],
|
|
33
|
+
["Non-Profit", donation.non_profit.name],
|
|
34
|
+
["Status", formatStatus(donation.status)],
|
|
35
|
+
["Date", donation.created_at.slice(0, 10)],
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { get } from "../client.js";
|
|
2
|
+
import { getFlag } from "../args.js";
|
|
3
|
+
import { renderTable, formatMoney, formatStatus } from "../output.js";
|
|
4
|
+
export async function run(args) {
|
|
5
|
+
const page = getFlag(args, "page");
|
|
6
|
+
const qs = page ? `?page=${page}` : "";
|
|
7
|
+
const data = (await get(`/v1/donations${qs}`));
|
|
8
|
+
renderTable({
|
|
9
|
+
title: "Your Donations",
|
|
10
|
+
data,
|
|
11
|
+
headers: ["ID", "Amount", "Non-Profit", "Status", "Date"],
|
|
12
|
+
rows: data.items.map((d) => [
|
|
13
|
+
String(d.id),
|
|
14
|
+
formatMoney(d.amount),
|
|
15
|
+
d.non_profit.name.slice(0, 30),
|
|
16
|
+
formatStatus(d.status),
|
|
17
|
+
d.created_at.slice(0, 10),
|
|
18
|
+
]),
|
|
19
|
+
meta: data.meta,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { get, post } from "../client.js";
|
|
2
|
+
import { getPositional, requireFlag } from "../args.js";
|
|
3
|
+
import { renderKeyValue, formatMoney, formatStatus } from "../output.js";
|
|
4
|
+
import { confirm } from "../prompt.js";
|
|
5
|
+
export async function run(args) {
|
|
6
|
+
const first = getPositional(args, 0);
|
|
7
|
+
if (first === "create") {
|
|
8
|
+
await createGift(args);
|
|
9
|
+
}
|
|
10
|
+
else if (first) {
|
|
11
|
+
await showGift(first);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
console.error("Usage: daffy-cli gift <code> | daffy-cli gift create --name <n> --amount <n>");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function showGift(code) {
|
|
19
|
+
const gift = (await get(`/v1/gifts/${encodeURIComponent(code)}`));
|
|
20
|
+
renderKeyValue({
|
|
21
|
+
title: "Gift Details",
|
|
22
|
+
data: gift,
|
|
23
|
+
fields: [
|
|
24
|
+
["Code", gift.code],
|
|
25
|
+
["Name", gift.name],
|
|
26
|
+
["Amount", formatMoney(gift.amount)],
|
|
27
|
+
["Status", formatStatus(gift.status)],
|
|
28
|
+
["Claimed", gift.claimed ? "Yes" : "No"],
|
|
29
|
+
["URL", gift.url],
|
|
30
|
+
["Created", gift.created_at.slice(0, 10)],
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async function createGift(args) {
|
|
35
|
+
const name = requireFlag(args, "name");
|
|
36
|
+
const amountStr = requireFlag(args, "amount");
|
|
37
|
+
const amount = parseFloat(amountStr);
|
|
38
|
+
if (isNaN(amount) || amount < 18) {
|
|
39
|
+
console.error("Error: --amount must be at least 18.");
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
console.log(`\nCreate gift "${name}" for ${formatMoney(amount)}`);
|
|
43
|
+
const ok = await confirm("\nProceed?", args);
|
|
44
|
+
if (!ok) {
|
|
45
|
+
console.log("Cancelled.");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const gift = (await post("/v1/gifts", { name, amount }));
|
|
49
|
+
renderKeyValue({
|
|
50
|
+
title: "Gift Created",
|
|
51
|
+
data: gift,
|
|
52
|
+
fields: [
|
|
53
|
+
["Code", gift.code],
|
|
54
|
+
["Name", gift.name],
|
|
55
|
+
["Amount", formatMoney(gift.amount)],
|
|
56
|
+
["Status", formatStatus(gift.status)],
|
|
57
|
+
["URL", gift.url],
|
|
58
|
+
],
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { get } from "../client.js";
|
|
2
|
+
import { getFlag } from "../args.js";
|
|
3
|
+
import { renderTable, formatMoney, formatStatus } from "../output.js";
|
|
4
|
+
export async function run(args) {
|
|
5
|
+
const page = getFlag(args, "page");
|
|
6
|
+
const qs = page ? `?page=${page}` : "";
|
|
7
|
+
const data = (await get(`/v1/gifts${qs}`));
|
|
8
|
+
renderTable({
|
|
9
|
+
title: "Your Gifts",
|
|
10
|
+
data,
|
|
11
|
+
headers: ["Code", "Name", "Amount", "Status", "Date"],
|
|
12
|
+
rows: data.items.map((g) => [
|
|
13
|
+
g.code.slice(0, 8) + "...",
|
|
14
|
+
g.name.slice(0, 25),
|
|
15
|
+
formatMoney(g.amount),
|
|
16
|
+
formatStatus(g.status),
|
|
17
|
+
g.created_at.slice(0, 10),
|
|
18
|
+
]),
|
|
19
|
+
meta: data.meta,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { get } from "../client.js";
|
|
2
|
+
import { renderKeyValue } from "../output.js";
|
|
3
|
+
export async function run(_args) {
|
|
4
|
+
const user = (await get("/v1/users/me"));
|
|
5
|
+
renderKeyValue({
|
|
6
|
+
title: "Your Profile",
|
|
7
|
+
data: user,
|
|
8
|
+
fields: [
|
|
9
|
+
["Name", user.name],
|
|
10
|
+
["Username", user.slug],
|
|
11
|
+
["Fund", user.fund_name],
|
|
12
|
+
["Onboarding", user.onboarding_status],
|
|
13
|
+
["Avatar", user.avatar],
|
|
14
|
+
],
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { get } from "../client.js";
|
|
2
|
+
import { getPositional } from "../args.js";
|
|
3
|
+
import { renderKeyValue } from "../output.js";
|
|
4
|
+
export async function run(args) {
|
|
5
|
+
const ein = getPositional(args, 0);
|
|
6
|
+
if (!ein) {
|
|
7
|
+
console.error("Usage: daffy-cli nonprofit <ein>");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
const np = (await get(`/v1/non_profits/${encodeURIComponent(ein)}`));
|
|
11
|
+
renderKeyValue({
|
|
12
|
+
title: np.name,
|
|
13
|
+
data: np,
|
|
14
|
+
fields: [
|
|
15
|
+
["EIN", np.ein],
|
|
16
|
+
["Website", np.website],
|
|
17
|
+
["Location", [np.city, np.state].filter(Boolean).join(", ") || null],
|
|
18
|
+
["Cause", np.cause?.name],
|
|
19
|
+
["URL", np.public_url],
|
|
20
|
+
],
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { get } from "../client.js";
|
|
2
|
+
import { getPositional, getFlag } from "../args.js";
|
|
3
|
+
import { renderTable } from "../output.js";
|
|
4
|
+
export async function run(args) {
|
|
5
|
+
const query = getPositional(args, 0);
|
|
6
|
+
const causeId = getFlag(args, "cause");
|
|
7
|
+
const page = getFlag(args, "page");
|
|
8
|
+
const params = new URLSearchParams();
|
|
9
|
+
if (query)
|
|
10
|
+
params.set("query", query);
|
|
11
|
+
if (causeId)
|
|
12
|
+
params.set("cause_id", causeId);
|
|
13
|
+
if (page)
|
|
14
|
+
params.set("page", page);
|
|
15
|
+
const qs = params.toString();
|
|
16
|
+
const data = (await get(`/v1/non_profits${qs ? `?${qs}` : ""}`));
|
|
17
|
+
renderTable({
|
|
18
|
+
title: "Non-Profit Search Results",
|
|
19
|
+
data,
|
|
20
|
+
headers: ["EIN", "Name", "City", "State"],
|
|
21
|
+
rows: data.items.map((np) => [
|
|
22
|
+
np.ein,
|
|
23
|
+
np.name.slice(0, 40),
|
|
24
|
+
np.city || "—",
|
|
25
|
+
np.state || "—",
|
|
26
|
+
]),
|
|
27
|
+
meta: data.meta,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { get } from "../client.js";
|
|
2
|
+
import { getPositional } from "../args.js";
|
|
3
|
+
import { renderKeyValue } from "../output.js";
|
|
4
|
+
export async function run(args) {
|
|
5
|
+
const username = getPositional(args, 0);
|
|
6
|
+
if (!username) {
|
|
7
|
+
console.error("Usage: daffy-cli user <username>");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
const user = (await get(`/v1/users/${encodeURIComponent(username)}`));
|
|
11
|
+
renderKeyValue({
|
|
12
|
+
title: user.name,
|
|
13
|
+
data: user,
|
|
14
|
+
fields: [
|
|
15
|
+
["Username", user.slug],
|
|
16
|
+
["Fund", user.fund_name],
|
|
17
|
+
["Onboarding", user.onboarding_status],
|
|
18
|
+
["Avatar", user.avatar],
|
|
19
|
+
],
|
|
20
|
+
});
|
|
21
|
+
}
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const NO_COLOR = "NO_COLOR" in process.env;
|
|
2
|
+
const BOLD = NO_COLOR ? "" : "\x1b[1m";
|
|
3
|
+
const DIM = NO_COLOR ? "" : "\x1b[2m";
|
|
4
|
+
const GREEN = NO_COLOR ? "" : "\x1b[32m";
|
|
5
|
+
const YELLOW = NO_COLOR ? "" : "\x1b[33m";
|
|
6
|
+
const CYAN = NO_COLOR ? "" : "\x1b[36m";
|
|
7
|
+
const RED = NO_COLOR ? "" : "\x1b[31m";
|
|
8
|
+
const RESET = NO_COLOR ? "" : "\x1b[0m";
|
|
9
|
+
export function printKeyValue(pairs) {
|
|
10
|
+
const maxKey = Math.max(...pairs.map(([k]) => k.length));
|
|
11
|
+
for (const [key, value] of pairs) {
|
|
12
|
+
const display = value === null || value === undefined ? `${DIM}—${RESET}` : String(value);
|
|
13
|
+
console.log(` ${BOLD}${key.padEnd(maxKey)}${RESET} ${display}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function printTable(headers, rows) {
|
|
17
|
+
if (rows.length === 0) {
|
|
18
|
+
console.log(`${DIM} No results.${RESET}`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
|
|
22
|
+
const header = headers.map((h, i) => h.padEnd(widths[i])).join(" ");
|
|
23
|
+
console.log(` ${DIM}${header}${RESET}`);
|
|
24
|
+
console.log(` ${DIM}${widths.map((w) => "─".repeat(w)).join("──")}${RESET}`);
|
|
25
|
+
for (const row of rows) {
|
|
26
|
+
const line = row.map((cell, i) => cell.padEnd(widths[i])).join(" ");
|
|
27
|
+
console.log(` ${line}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function printMeta(meta) {
|
|
31
|
+
console.log(`\n ${DIM}Page ${meta.page} of ${meta.last} (${meta.count} total)${RESET}`);
|
|
32
|
+
}
|
|
33
|
+
export function formatMoney(amount) {
|
|
34
|
+
return `${GREEN}$${amount.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}${RESET}`;
|
|
35
|
+
}
|
|
36
|
+
export function formatStatus(status) {
|
|
37
|
+
switch (status) {
|
|
38
|
+
case "completed":
|
|
39
|
+
case "success":
|
|
40
|
+
case "approved":
|
|
41
|
+
case "claimed":
|
|
42
|
+
return `${GREEN}${status}${RESET}`;
|
|
43
|
+
case "pending":
|
|
44
|
+
case "scheduled":
|
|
45
|
+
case "waiting_for_funds":
|
|
46
|
+
case "new":
|
|
47
|
+
return `${YELLOW}${status}${RESET}`;
|
|
48
|
+
case "failed":
|
|
49
|
+
case "rejected":
|
|
50
|
+
case "not_completed":
|
|
51
|
+
case "denied":
|
|
52
|
+
return `${RED}${status}${RESET}`;
|
|
53
|
+
default:
|
|
54
|
+
return `${CYAN}${status}${RESET}`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function heading(text) {
|
|
58
|
+
console.log(`\n${BOLD}${text}${RESET}\n`);
|
|
59
|
+
}
|
package/dist/output.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { heading, printKeyValue, printTable, printMeta, formatMoney, formatStatus } from "./format.js";
|
|
2
|
+
let jsonMode = false;
|
|
3
|
+
export function setJsonMode(enabled) {
|
|
4
|
+
jsonMode = enabled;
|
|
5
|
+
}
|
|
6
|
+
export function renderKeyValue({ title, data, fields }) {
|
|
7
|
+
if (jsonMode) {
|
|
8
|
+
console.log(JSON.stringify(data, null, 2));
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
heading(title);
|
|
12
|
+
printKeyValue(fields);
|
|
13
|
+
}
|
|
14
|
+
export function renderTable({ title, data, headers, rows, meta }) {
|
|
15
|
+
if (jsonMode) {
|
|
16
|
+
console.log(JSON.stringify(data, null, 2));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
heading(title);
|
|
20
|
+
printTable(headers, rows);
|
|
21
|
+
if (meta)
|
|
22
|
+
printMeta(meta);
|
|
23
|
+
}
|
|
24
|
+
export function renderMessage(message, data) {
|
|
25
|
+
if (jsonMode) {
|
|
26
|
+
console.log(JSON.stringify(data, null, 2));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
console.log(message);
|
|
30
|
+
}
|
|
31
|
+
export { formatMoney, formatStatus };
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as readline from "node:readline";
|
|
2
|
+
export function confirm(message, args) {
|
|
3
|
+
if (args.includes("--yes") || args.includes("-y")) {
|
|
4
|
+
return Promise.resolve(true);
|
|
5
|
+
}
|
|
6
|
+
const rl = readline.createInterface({
|
|
7
|
+
input: process.stdin,
|
|
8
|
+
output: process.stdout,
|
|
9
|
+
});
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
rl.question(`${message} [y/N] `, (answer) => {
|
|
12
|
+
rl.close();
|
|
13
|
+
resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "daffy-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for the Daffy Public API — search nonprofits, donate, check balances, and more",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"daffy-cli": "./dist/bin.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"start": "node dist/bin.js"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"daffy",
|
|
21
|
+
"donations",
|
|
22
|
+
"charity",
|
|
23
|
+
"nonprofit",
|
|
24
|
+
"cli"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"typescript": "^5.3.0",
|
|
29
|
+
"@types/node": "^20.11.0"
|
|
30
|
+
}
|
|
31
|
+
}
|