@singlefax/cli 1.0.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 +62 -0
- package/dist/cli.mjs +507 -0
- package/dist/cli.mjs.map +1 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# SingleFax CLI
|
|
2
|
+
|
|
3
|
+
Official CLI for [SingleFax](https://singlefax.com): send a fax with an API key and prepaid credits.
|
|
4
|
+
|
|
5
|
+
Chat apps should use MCP (`https://singlefax.com/mcp`). Autonomous Stripe MPP payments use Stripe Link CLI, not this tool.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i -g @singlefax/cli
|
|
11
|
+
# or
|
|
12
|
+
npx @singlefax/cli --help
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Create a scoped key at [https://singlefax.com/dashboard/api-keys](https://singlefax.com/dashboard/api-keys) (`fax:send`, `fax:read`, `credits:read`). Prepay credits in the dashboard, or run `singlefax credits buy`.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
export SINGLEFAX_API_KEY=sf_live_…
|
|
19
|
+
# or
|
|
20
|
+
singlefax config set api-key sf_live_…
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Send
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx @singlefax/cli send ./document.pdf --to +14155552671
|
|
27
|
+
npx @singlefax/cli send ./document.pdf --to +14155552671 --wait --json
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`--help` and `send --help` work without a key.
|
|
31
|
+
|
|
32
|
+
## Commands
|
|
33
|
+
|
|
34
|
+
| Command | API |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| `send FILE --to +E164 [--cover] [--wait]` | `POST /uploads` → `PUT` → `POST /faxes` |
|
|
37
|
+
| `status <id>` | `GET /api/v1/faxes/:id` |
|
|
38
|
+
| `list` | `GET /api/v1/faxes` |
|
|
39
|
+
| `credits` | `GET /api/v1/credits` |
|
|
40
|
+
| `credits buy [--dollars 20]` | `POST /api/v1/credits/checkout` (prints Checkout URL) |
|
|
41
|
+
| `config set api-key sf_live_…` | writes `~/.config/singlefax/config.json` |
|
|
42
|
+
|
|
43
|
+
Flags: `--json`, `--api-key`, `--api-url` (default `https://api.singlefax.com`).
|
|
44
|
+
|
|
45
|
+
Do not send PHI through this CLI. Covered Entities use the website HIPAA Secure path with an electronic BAA.
|
|
46
|
+
|
|
47
|
+
## Publish (maintainers)
|
|
48
|
+
|
|
49
|
+
Releases use [npm trusted publishing](https://docs.npmjs.com/trusted-publishers) (OIDC). There is no `NPM_TOKEN`.
|
|
50
|
+
|
|
51
|
+
On [npmjs.com](https://www.npmjs.com) → `@singlefax/cli` → Settings → Trusted Publisher → GitHub Actions:
|
|
52
|
+
|
|
53
|
+
| Field | Value |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| Organization or user | `SingleFax` |
|
|
56
|
+
| Repository | `singlefax-app` |
|
|
57
|
+
| Workflow filename | `ci-cd-cli.yml` |
|
|
58
|
+
| Environment name | leave empty |
|
|
59
|
+
| Allowed actions | `npm publish` |
|
|
60
|
+
|
|
61
|
+
Configure this on the package **before** the first Actions publish (npm does not validate the form until publish). Then tag `cli-v1.0.0` or run the CLI workflow with **Publish**. Provenance is generated automatically. The `repository.url` in `package.json` must stay `https://github.com/SingleFax/singlefax-app.git`.
|
|
62
|
+
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { runMain } from "citty";
|
|
5
|
+
|
|
6
|
+
// src/program.ts
|
|
7
|
+
import { defineCommand, renderUsage } from "citty";
|
|
8
|
+
|
|
9
|
+
// src/client.ts
|
|
10
|
+
var ApiError = class extends Error {
|
|
11
|
+
constructor(message, status, body) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.body = body;
|
|
15
|
+
this.name = "ApiError";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
function sleep(ms) {
|
|
19
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
20
|
+
}
|
|
21
|
+
function formatError(status, body) {
|
|
22
|
+
if (body && typeof body === "object") {
|
|
23
|
+
const row = body;
|
|
24
|
+
const headline = row.statusMessage || row.message || `HTTP ${status}`;
|
|
25
|
+
const resolution = row.data?.resolution;
|
|
26
|
+
return resolution ? `${headline}
|
|
27
|
+
${resolution}` : headline;
|
|
28
|
+
}
|
|
29
|
+
return `HTTP ${status}`;
|
|
30
|
+
}
|
|
31
|
+
function retryAfterMs(res) {
|
|
32
|
+
const header = res.headers.get("retry-after");
|
|
33
|
+
const seconds = header ? Number(header) : 1;
|
|
34
|
+
if (!Number.isFinite(seconds) || seconds < 1) return 1e3;
|
|
35
|
+
return Math.min(seconds, 60) * 1e3;
|
|
36
|
+
}
|
|
37
|
+
function createClient(opts) {
|
|
38
|
+
const doFetch = opts.fetch ?? globalThis.fetch;
|
|
39
|
+
const doSleep = opts.sleep ?? sleep;
|
|
40
|
+
const apiUrl = opts.apiUrl.replace(/\/$/, "");
|
|
41
|
+
async function request(input) {
|
|
42
|
+
const url = input.path.startsWith("http") ? input.path : `${apiUrl}${input.path}`;
|
|
43
|
+
const headers = {
|
|
44
|
+
authorization: `Bearer ${opts.apiKey}`,
|
|
45
|
+
accept: "application/json"
|
|
46
|
+
};
|
|
47
|
+
let body;
|
|
48
|
+
if (input.raw) {
|
|
49
|
+
headers["content-type"] = input.contentType || "application/octet-stream";
|
|
50
|
+
body = input.raw;
|
|
51
|
+
} else if (input.json !== void 0) {
|
|
52
|
+
headers["content-type"] = "application/json";
|
|
53
|
+
body = JSON.stringify(input.json);
|
|
54
|
+
}
|
|
55
|
+
if (input.idempotencyKey) headers["idempotency-key"] = input.idempotencyKey;
|
|
56
|
+
const send = () => doFetch(url, {
|
|
57
|
+
method: input.method,
|
|
58
|
+
headers,
|
|
59
|
+
body
|
|
60
|
+
});
|
|
61
|
+
let res = await send();
|
|
62
|
+
if (res.status === 429) {
|
|
63
|
+
await doSleep(retryAfterMs(res));
|
|
64
|
+
res = await send();
|
|
65
|
+
}
|
|
66
|
+
const text = await res.text();
|
|
67
|
+
let parsed = text;
|
|
68
|
+
if (text) {
|
|
69
|
+
try {
|
|
70
|
+
parsed = JSON.parse(text);
|
|
71
|
+
} catch {
|
|
72
|
+
parsed = { message: text };
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
parsed = {};
|
|
76
|
+
}
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
throw new ApiError(formatError(res.status, parsed), res.status, parsed);
|
|
79
|
+
}
|
|
80
|
+
return parsed;
|
|
81
|
+
}
|
|
82
|
+
return { apiUrl, request };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/config.ts
|
|
86
|
+
import { chmod, mkdir, readFile, writeFile } from "fs/promises";
|
|
87
|
+
import { homedir } from "os";
|
|
88
|
+
import { dirname, join } from "path";
|
|
89
|
+
var DEFAULT_API_URL = "https://api.singlefax.com";
|
|
90
|
+
function configFilePath() {
|
|
91
|
+
if (process.env.SINGLEFAX_CONFIG_PATH) return process.env.SINGLEFAX_CONFIG_PATH;
|
|
92
|
+
const xdg = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
93
|
+
return join(xdg, "singlefax", "config.json");
|
|
94
|
+
}
|
|
95
|
+
async function readConfigFile() {
|
|
96
|
+
try {
|
|
97
|
+
const raw = await readFile(configFilePath(), "utf8");
|
|
98
|
+
const parsed = JSON.parse(raw);
|
|
99
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
100
|
+
} catch {
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function writeConfigFile(next) {
|
|
105
|
+
const path = configFilePath();
|
|
106
|
+
await mkdir(dirname(path), { recursive: true, mode: 448 });
|
|
107
|
+
await writeFile(path, `${JSON.stringify(next, null, 2)}
|
|
108
|
+
`, { mode: 384 });
|
|
109
|
+
await chmod(path, 384);
|
|
110
|
+
return path;
|
|
111
|
+
}
|
|
112
|
+
function asString(value) {
|
|
113
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
114
|
+
}
|
|
115
|
+
async function resolveConfig(args) {
|
|
116
|
+
const file = await readConfigFile();
|
|
117
|
+
const apiUrl = asString(args["api-url"]) || asString(args.apiUrl) || asString(process.env.SINGLEFAX_API_URL) || file.apiUrl || DEFAULT_API_URL;
|
|
118
|
+
const apiKey = asString(args["api-key"]) || asString(args.apiKey) || asString(process.env.SINGLEFAX_API_KEY) || file.apiKey || "";
|
|
119
|
+
return {
|
|
120
|
+
apiUrl: apiUrl.replace(/\/$/, ""),
|
|
121
|
+
apiKey,
|
|
122
|
+
json: Boolean(args.json)
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function requireApiKey(config) {
|
|
126
|
+
if (config.apiKey) return config.apiKey;
|
|
127
|
+
throw new Error(
|
|
128
|
+
"Missing API key. Set SINGLEFAX_API_KEY, pass --api-key, or run: singlefax config set api-key sf_live_\u2026"
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/output.ts
|
|
133
|
+
function printJson(value) {
|
|
134
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
135
|
+
`);
|
|
136
|
+
}
|
|
137
|
+
function printLine(line) {
|
|
138
|
+
process.stdout.write(`${line}
|
|
139
|
+
`);
|
|
140
|
+
}
|
|
141
|
+
function cents(value) {
|
|
142
|
+
const n = Number(value);
|
|
143
|
+
if (!Number.isFinite(n)) return "\u2014";
|
|
144
|
+
return `$${(n / 100).toFixed(2)}`;
|
|
145
|
+
}
|
|
146
|
+
function printSendHuman(row) {
|
|
147
|
+
printLine(
|
|
148
|
+
[
|
|
149
|
+
row.fulfillmentStatus || "queued",
|
|
150
|
+
row.id || "",
|
|
151
|
+
row.billablePages != null ? `${row.billablePages} pages` : "",
|
|
152
|
+
cents(row.totalCents ?? row.subtotalCents)
|
|
153
|
+
].filter(Boolean).join(" ")
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
function printStatusHuman(row) {
|
|
157
|
+
const parts = [
|
|
158
|
+
row.fulfillmentStatus || "unknown",
|
|
159
|
+
row.id || "",
|
|
160
|
+
row.billablePages != null ? `${row.billablePages} pages` : "",
|
|
161
|
+
cents(row.totalCents ?? row.subtotalCents)
|
|
162
|
+
];
|
|
163
|
+
if (row.failureClass) parts.push(row.failureClass);
|
|
164
|
+
printLine(parts.filter(Boolean).join(" "));
|
|
165
|
+
}
|
|
166
|
+
function printListHuman(payload) {
|
|
167
|
+
const rows = payload.data ?? [];
|
|
168
|
+
if (!rows.length) {
|
|
169
|
+
printLine("No faxes.");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
for (const row of rows) {
|
|
173
|
+
printLine(
|
|
174
|
+
[
|
|
175
|
+
row.id || "",
|
|
176
|
+
row.fulfillmentStatus || "",
|
|
177
|
+
row.billablePages != null ? `${row.billablePages}p` : "",
|
|
178
|
+
cents(row.totalCents),
|
|
179
|
+
row.destinationCountry || ""
|
|
180
|
+
].filter((part) => part !== "").join(" ")
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function printCreditsHuman(row) {
|
|
185
|
+
printLine(
|
|
186
|
+
`available ${cents(row.availableCents)} reserved ${cents(row.reservedCents)}`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/send.ts
|
|
191
|
+
import { randomUUID } from "crypto";
|
|
192
|
+
import { readFile as readFile2, stat } from "fs/promises";
|
|
193
|
+
import { extname } from "path";
|
|
194
|
+
var TERMINAL = /* @__PURE__ */ new Set(["delivered", "failed_final"]);
|
|
195
|
+
function contentTypeFor(file) {
|
|
196
|
+
switch (extname(file).toLowerCase()) {
|
|
197
|
+
case ".pdf":
|
|
198
|
+
return "application/pdf";
|
|
199
|
+
case ".png":
|
|
200
|
+
return "image/png";
|
|
201
|
+
case ".jpg":
|
|
202
|
+
case ".jpeg":
|
|
203
|
+
return "image/jpeg";
|
|
204
|
+
case ".tif":
|
|
205
|
+
case ".tiff":
|
|
206
|
+
return "image/tiff";
|
|
207
|
+
case ".doc":
|
|
208
|
+
return "application/msword";
|
|
209
|
+
case ".docx":
|
|
210
|
+
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
211
|
+
default:
|
|
212
|
+
return "application/pdf";
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function sendFax(client, opts) {
|
|
216
|
+
const filePath = opts.file;
|
|
217
|
+
const info = await stat(filePath);
|
|
218
|
+
if (!info.isFile()) {
|
|
219
|
+
throw new Error("FILE must be a regular file");
|
|
220
|
+
}
|
|
221
|
+
const contentType = contentTypeFor(filePath);
|
|
222
|
+
const bytes = await readFile2(filePath);
|
|
223
|
+
const upload = await client.request({
|
|
224
|
+
method: "POST",
|
|
225
|
+
path: "/api/v1/uploads",
|
|
226
|
+
json: { contentType, maxBytes: bytes.byteLength }
|
|
227
|
+
});
|
|
228
|
+
await client.request({
|
|
229
|
+
method: "PUT",
|
|
230
|
+
path: upload.uploadUrl,
|
|
231
|
+
raw: bytes,
|
|
232
|
+
contentType
|
|
233
|
+
});
|
|
234
|
+
const fax = await client.request({
|
|
235
|
+
method: "POST",
|
|
236
|
+
path: "/api/v1/faxes",
|
|
237
|
+
json: { to: opts.to, uploadToken: upload.uploadToken, cover: Boolean(opts.cover) },
|
|
238
|
+
idempotencyKey: randomUUID()
|
|
239
|
+
});
|
|
240
|
+
if (!opts.wait) return fax;
|
|
241
|
+
const doSleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
242
|
+
const started = (opts.now ?? Date.now)();
|
|
243
|
+
const deadline = started + 18e4;
|
|
244
|
+
let latest = fax;
|
|
245
|
+
while ((opts.now ?? Date.now)() < deadline) {
|
|
246
|
+
if (TERMINAL.has(latest.fulfillmentStatus)) return latest;
|
|
247
|
+
await doSleep(2e3);
|
|
248
|
+
latest = await client.request({
|
|
249
|
+
method: "GET",
|
|
250
|
+
path: `/api/v1/faxes/${encodeURIComponent(fax.id)}`
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return latest;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// src/program.ts
|
|
257
|
+
var sharedArgs = {
|
|
258
|
+
json: {
|
|
259
|
+
type: "boolean",
|
|
260
|
+
description: "Print the API JSON body"
|
|
261
|
+
},
|
|
262
|
+
"api-key": {
|
|
263
|
+
type: "string",
|
|
264
|
+
description: "API key (sf_live_\u2026). Overrides SINGLEFAX_API_KEY"
|
|
265
|
+
},
|
|
266
|
+
"api-url": {
|
|
267
|
+
type: "string",
|
|
268
|
+
description: "API base URL (default https://api.singlefax.com)"
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
function fail(error) {
|
|
272
|
+
if (error instanceof ApiError) {
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
276
|
+
}
|
|
277
|
+
var sendCommand = defineCommand({
|
|
278
|
+
meta: {
|
|
279
|
+
name: "send",
|
|
280
|
+
description: "Upload a document and send it as a fax"
|
|
281
|
+
},
|
|
282
|
+
args: {
|
|
283
|
+
file: {
|
|
284
|
+
type: "positional",
|
|
285
|
+
description: "Path to the PDF or office document",
|
|
286
|
+
required: true
|
|
287
|
+
},
|
|
288
|
+
to: {
|
|
289
|
+
type: "string",
|
|
290
|
+
description: "Destination fax number in E.164",
|
|
291
|
+
required: true
|
|
292
|
+
},
|
|
293
|
+
cover: {
|
|
294
|
+
type: "boolean",
|
|
295
|
+
description: "Prepend a cover page"
|
|
296
|
+
},
|
|
297
|
+
wait: {
|
|
298
|
+
type: "boolean",
|
|
299
|
+
description: "Poll until delivered or failed"
|
|
300
|
+
},
|
|
301
|
+
...sharedArgs
|
|
302
|
+
},
|
|
303
|
+
async run({ args }) {
|
|
304
|
+
const config = await resolveConfig(args);
|
|
305
|
+
const client = createClient({
|
|
306
|
+
apiUrl: config.apiUrl,
|
|
307
|
+
apiKey: requireApiKey(config)
|
|
308
|
+
});
|
|
309
|
+
try {
|
|
310
|
+
const row = await sendFax(client, {
|
|
311
|
+
file: String(args.file),
|
|
312
|
+
to: String(args.to),
|
|
313
|
+
cover: Boolean(args.cover),
|
|
314
|
+
wait: Boolean(args.wait)
|
|
315
|
+
});
|
|
316
|
+
if (config.json) printJson(row);
|
|
317
|
+
else printSendHuman(row);
|
|
318
|
+
} catch (error) {
|
|
319
|
+
fail(error);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
var statusCommand = defineCommand({
|
|
324
|
+
meta: {
|
|
325
|
+
name: "status",
|
|
326
|
+
description: "Poll delivery status for an order you own"
|
|
327
|
+
},
|
|
328
|
+
args: {
|
|
329
|
+
id: {
|
|
330
|
+
type: "positional",
|
|
331
|
+
description: "Fax order id",
|
|
332
|
+
required: true
|
|
333
|
+
},
|
|
334
|
+
...sharedArgs
|
|
335
|
+
},
|
|
336
|
+
async run({ args }) {
|
|
337
|
+
const config = await resolveConfig(args);
|
|
338
|
+
const client = createClient({
|
|
339
|
+
apiUrl: config.apiUrl,
|
|
340
|
+
apiKey: requireApiKey(config)
|
|
341
|
+
});
|
|
342
|
+
try {
|
|
343
|
+
const row = await client.request({
|
|
344
|
+
method: "GET",
|
|
345
|
+
path: `/api/v1/faxes/${encodeURIComponent(String(args.id))}`
|
|
346
|
+
});
|
|
347
|
+
if (config.json) printJson(row);
|
|
348
|
+
else printStatusHuman(row);
|
|
349
|
+
} catch (error) {
|
|
350
|
+
fail(error);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
var listCommand = defineCommand({
|
|
355
|
+
meta: {
|
|
356
|
+
name: "list",
|
|
357
|
+
description: "List recent fax orders"
|
|
358
|
+
},
|
|
359
|
+
args: {
|
|
360
|
+
limit: {
|
|
361
|
+
type: "string",
|
|
362
|
+
description: "Page size (max 100)",
|
|
363
|
+
default: "20"
|
|
364
|
+
},
|
|
365
|
+
offset: {
|
|
366
|
+
type: "string",
|
|
367
|
+
description: "Offset",
|
|
368
|
+
default: "0"
|
|
369
|
+
},
|
|
370
|
+
...sharedArgs
|
|
371
|
+
},
|
|
372
|
+
async run({ args }) {
|
|
373
|
+
const config = await resolveConfig(args);
|
|
374
|
+
const client = createClient({
|
|
375
|
+
apiUrl: config.apiUrl,
|
|
376
|
+
apiKey: requireApiKey(config)
|
|
377
|
+
});
|
|
378
|
+
const limit = Math.min(Number(args.limit) || 20, 100);
|
|
379
|
+
const offset = Number(args.offset) || 0;
|
|
380
|
+
try {
|
|
381
|
+
const row = await client.request({
|
|
382
|
+
method: "GET",
|
|
383
|
+
path: `/api/v1/faxes?limit=${limit}&offset=${offset}`
|
|
384
|
+
});
|
|
385
|
+
if (config.json) printJson(row);
|
|
386
|
+
else printListHuman(row);
|
|
387
|
+
} catch (error) {
|
|
388
|
+
fail(error);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
var creditsBuyCommand = defineCommand({
|
|
393
|
+
meta: {
|
|
394
|
+
name: "buy",
|
|
395
|
+
description: "Create a Stripe Checkout session for prepaid credits"
|
|
396
|
+
},
|
|
397
|
+
args: {
|
|
398
|
+
dollars: {
|
|
399
|
+
type: "string",
|
|
400
|
+
description: "Credit pack in dollars (default 20)",
|
|
401
|
+
default: "20"
|
|
402
|
+
},
|
|
403
|
+
...sharedArgs
|
|
404
|
+
},
|
|
405
|
+
async run({ args }) {
|
|
406
|
+
const config = await resolveConfig(args);
|
|
407
|
+
const client = createClient({
|
|
408
|
+
apiUrl: config.apiUrl,
|
|
409
|
+
apiKey: requireApiKey(config)
|
|
410
|
+
});
|
|
411
|
+
const dollars = Number(args.dollars);
|
|
412
|
+
const amountCents = Number.isFinite(dollars) && dollars > 0 ? Math.round(dollars * 100) : 2e3;
|
|
413
|
+
try {
|
|
414
|
+
const row = await client.request({
|
|
415
|
+
method: "POST",
|
|
416
|
+
path: "/api/v1/credits/checkout",
|
|
417
|
+
json: { amountCents }
|
|
418
|
+
});
|
|
419
|
+
if (config.json) printJson(row);
|
|
420
|
+
else printLine(row.checkoutUrl || "Checkout URL missing");
|
|
421
|
+
} catch (error) {
|
|
422
|
+
fail(error);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
var creditsCommand = defineCommand({
|
|
427
|
+
meta: {
|
|
428
|
+
name: "credits",
|
|
429
|
+
description: "Show prepaid credit balance, or buy more"
|
|
430
|
+
},
|
|
431
|
+
args: sharedArgs,
|
|
432
|
+
subCommands: {
|
|
433
|
+
buy: creditsBuyCommand
|
|
434
|
+
},
|
|
435
|
+
async run({ args }) {
|
|
436
|
+
const config = await resolveConfig(args);
|
|
437
|
+
const client = createClient({
|
|
438
|
+
apiUrl: config.apiUrl,
|
|
439
|
+
apiKey: requireApiKey(config)
|
|
440
|
+
});
|
|
441
|
+
try {
|
|
442
|
+
const row = await client.request({
|
|
443
|
+
method: "GET",
|
|
444
|
+
path: "/api/v1/credits"
|
|
445
|
+
});
|
|
446
|
+
if (config.json) printJson(row);
|
|
447
|
+
else printCreditsHuman(row);
|
|
448
|
+
} catch (error) {
|
|
449
|
+
fail(error);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
var configSetCommand = defineCommand({
|
|
454
|
+
meta: {
|
|
455
|
+
name: "set",
|
|
456
|
+
description: "Write a local config value (api-key)"
|
|
457
|
+
},
|
|
458
|
+
args: {
|
|
459
|
+
name: {
|
|
460
|
+
type: "positional",
|
|
461
|
+
description: "Name (only api-key is supported)",
|
|
462
|
+
required: true
|
|
463
|
+
},
|
|
464
|
+
value: {
|
|
465
|
+
type: "positional",
|
|
466
|
+
description: "Value",
|
|
467
|
+
required: true
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
async run({ args }) {
|
|
471
|
+
const name = String(args.name);
|
|
472
|
+
const value = String(args.value);
|
|
473
|
+
if (name !== "api-key" && name !== "apiKey") {
|
|
474
|
+
throw new Error("Supported keys: api-key");
|
|
475
|
+
}
|
|
476
|
+
const current = await readConfigFile();
|
|
477
|
+
const path = await writeConfigFile({ ...current, apiKey: value });
|
|
478
|
+
printLine(`Saved API key to ${path}`);
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
var configCommand = defineCommand({
|
|
482
|
+
meta: {
|
|
483
|
+
name: "config",
|
|
484
|
+
description: "Store API credentials locally"
|
|
485
|
+
},
|
|
486
|
+
subCommands: {
|
|
487
|
+
set: configSetCommand
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
var main = defineCommand({
|
|
491
|
+
meta: {
|
|
492
|
+
name: "singlefax",
|
|
493
|
+
version: "1.0.0",
|
|
494
|
+
description: "SingleFax CLI \u2014 send faxes via API key and prepaid credits"
|
|
495
|
+
},
|
|
496
|
+
subCommands: {
|
|
497
|
+
send: sendCommand,
|
|
498
|
+
status: statusCommand,
|
|
499
|
+
list: listCommand,
|
|
500
|
+
credits: creditsCommand,
|
|
501
|
+
config: configCommand
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// src/cli.ts
|
|
506
|
+
await runMain(main);
|
|
507
|
+
//# sourceMappingURL=cli.mjs.map
|
package/dist/cli.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/program.ts","../src/client.ts","../src/config.ts","../src/output.ts","../src/send.ts"],"sourcesContent":["import { runMain } from 'citty'\nimport { main } from './program.ts'\n\nawait runMain(main)\n","import { defineCommand, renderUsage } from 'citty'\nimport { ApiError, createClient } from './client.ts'\nimport { readConfigFile, requireApiKey, resolveConfig, writeConfigFile } from './config.ts'\nimport {\n printCreditsHuman,\n printJson,\n printLine,\n printListHuman,\n printSendHuman,\n printStatusHuman,\n} from './output.ts'\nimport { sendFax } from './send.ts'\n\nconst sharedArgs = {\n json: {\n type: 'boolean' as const,\n description: 'Print the API JSON body',\n },\n 'api-key': {\n type: 'string' as const,\n description: 'API key (sf_live_…). Overrides SINGLEFAX_API_KEY',\n },\n 'api-url': {\n type: 'string' as const,\n description: 'API base URL (default https://api.singlefax.com)',\n },\n}\n\nfunction fail(error: unknown): never {\n if (error instanceof ApiError) {\n throw error\n }\n throw error instanceof Error ? error : new Error(String(error))\n}\n\nexport const sendCommand = defineCommand({\n meta: {\n name: 'send',\n description: 'Upload a document and send it as a fax',\n },\n args: {\n file: {\n type: 'positional',\n description: 'Path to the PDF or office document',\n required: true,\n },\n to: {\n type: 'string',\n description: 'Destination fax number in E.164',\n required: true,\n },\n cover: {\n type: 'boolean',\n description: 'Prepend a cover page',\n },\n wait: {\n type: 'boolean',\n description: 'Poll until delivered or failed',\n },\n ...sharedArgs,\n },\n async run({ args }) {\n const config = await resolveConfig(args)\n const client = createClient({\n apiUrl: config.apiUrl,\n apiKey: requireApiKey(config),\n })\n try {\n const row = await sendFax(client, {\n file: String(args.file),\n to: String(args.to),\n cover: Boolean(args.cover),\n wait: Boolean(args.wait),\n })\n if (config.json) printJson(row)\n else printSendHuman(row)\n } catch (error) {\n fail(error)\n }\n },\n})\n\nconst statusCommand = defineCommand({\n meta: {\n name: 'status',\n description: 'Poll delivery status for an order you own',\n },\n args: {\n id: {\n type: 'positional',\n description: 'Fax order id',\n required: true,\n },\n ...sharedArgs,\n },\n async run({ args }) {\n const config = await resolveConfig(args)\n const client = createClient({\n apiUrl: config.apiUrl,\n apiKey: requireApiKey(config),\n })\n try {\n const row = await client.request({\n method: 'GET',\n path: `/api/v1/faxes/${encodeURIComponent(String(args.id))}`,\n })\n if (config.json) printJson(row)\n else printStatusHuman(row as Parameters<typeof printStatusHuman>[0])\n } catch (error) {\n fail(error)\n }\n },\n})\n\nconst listCommand = defineCommand({\n meta: {\n name: 'list',\n description: 'List recent fax orders',\n },\n args: {\n limit: {\n type: 'string',\n description: 'Page size (max 100)',\n default: '20',\n },\n offset: {\n type: 'string',\n description: 'Offset',\n default: '0',\n },\n ...sharedArgs,\n },\n async run({ args }) {\n const config = await resolveConfig(args)\n const client = createClient({\n apiUrl: config.apiUrl,\n apiKey: requireApiKey(config),\n })\n const limit = Math.min(Number(args.limit) || 20, 100)\n const offset = Number(args.offset) || 0\n try {\n const row = await client.request({\n method: 'GET',\n path: `/api/v1/faxes?limit=${limit}&offset=${offset}`,\n })\n if (config.json) printJson(row)\n else printListHuman(row as Parameters<typeof printListHuman>[0])\n } catch (error) {\n fail(error)\n }\n },\n})\n\nconst creditsBuyCommand = defineCommand({\n meta: {\n name: 'buy',\n description: 'Create a Stripe Checkout session for prepaid credits',\n },\n args: {\n dollars: {\n type: 'string',\n description: 'Credit pack in dollars (default 20)',\n default: '20',\n },\n ...sharedArgs,\n },\n async run({ args }) {\n const config = await resolveConfig(args)\n const client = createClient({\n apiUrl: config.apiUrl,\n apiKey: requireApiKey(config),\n })\n const dollars = Number(args.dollars)\n const amountCents = Number.isFinite(dollars) && dollars > 0 ? Math.round(dollars * 100) : 2000\n try {\n const row = await client.request<{ checkoutUrl?: string }>({\n method: 'POST',\n path: '/api/v1/credits/checkout',\n json: { amountCents },\n })\n if (config.json) printJson(row)\n else printLine(row.checkoutUrl || 'Checkout URL missing')\n } catch (error) {\n fail(error)\n }\n },\n})\n\nconst creditsCommand = defineCommand({\n meta: {\n name: 'credits',\n description: 'Show prepaid credit balance, or buy more',\n },\n args: sharedArgs,\n subCommands: {\n buy: creditsBuyCommand,\n },\n async run({ args }) {\n const config = await resolveConfig(args)\n const client = createClient({\n apiUrl: config.apiUrl,\n apiKey: requireApiKey(config),\n })\n try {\n const row = await client.request({\n method: 'GET',\n path: '/api/v1/credits',\n })\n if (config.json) printJson(row)\n else printCreditsHuman(row as Parameters<typeof printCreditsHuman>[0])\n } catch (error) {\n fail(error)\n }\n },\n})\n\nconst configSetCommand = defineCommand({\n meta: {\n name: 'set',\n description: 'Write a local config value (api-key)',\n },\n args: {\n name: {\n type: 'positional',\n description: 'Name (only api-key is supported)',\n required: true,\n },\n value: {\n type: 'positional',\n description: 'Value',\n required: true,\n },\n },\n async run({ args }) {\n const name = String(args.name)\n const value = String(args.value)\n if (name !== 'api-key' && name !== 'apiKey') {\n throw new Error('Supported keys: api-key')\n }\n const current = await readConfigFile()\n const path = await writeConfigFile({ ...current, apiKey: value })\n printLine(`Saved API key to ${path}`)\n },\n})\n\nconst configCommand = defineCommand({\n meta: {\n name: 'config',\n description: 'Store API credentials locally',\n },\n subCommands: {\n set: configSetCommand,\n },\n})\n\nexport const main = defineCommand({\n meta: {\n name: 'singlefax',\n version: '1.0.0',\n description: 'SingleFax CLI — send faxes via API key and prepaid credits',\n },\n subCommands: {\n send: sendCommand,\n status: statusCommand,\n list: listCommand,\n credits: creditsCommand,\n config: configCommand,\n },\n})\n\nexport async function usageText(): Promise<string> {\n return renderUsage(main)\n}\n","export class ApiError extends Error {\n constructor(\n message: string,\n readonly status: number,\n readonly body: unknown,\n ) {\n super(message)\n this.name = 'ApiError'\n }\n}\n\nexport type ApiClient = {\n apiUrl: string\n request: <T>(opts: {\n method: string\n path: string\n json?: unknown\n raw?: Buffer | Uint8Array\n contentType?: string\n idempotencyKey?: string\n }) => Promise<T>\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\nfunction formatError(status: number, body: unknown): string {\n if (body && typeof body === 'object') {\n const row = body as {\n statusMessage?: string\n message?: string\n data?: { code?: string; resolution?: string }\n }\n const headline = row.statusMessage || row.message || `HTTP ${status}`\n const resolution = row.data?.resolution\n return resolution ? `${headline}\\n${resolution}` : headline\n }\n return `HTTP ${status}`\n}\n\nfunction retryAfterMs(res: Response): number {\n const header = res.headers.get('retry-after')\n const seconds = header ? Number(header) : 1\n if (!Number.isFinite(seconds) || seconds < 1) return 1000\n return Math.min(seconds, 60) * 1000\n}\n\nexport function createClient(opts: {\n apiUrl: string\n apiKey: string\n fetch?: typeof fetch\n sleep?: (ms: number) => Promise<void>\n}): ApiClient {\n const doFetch = opts.fetch ?? globalThis.fetch\n const doSleep = opts.sleep ?? sleep\n const apiUrl = opts.apiUrl.replace(/\\/$/, '')\n\n async function request<T>(input: {\n method: string\n path: string\n json?: unknown\n raw?: Buffer | Uint8Array\n contentType?: string\n idempotencyKey?: string\n }): Promise<T> {\n const url = input.path.startsWith('http') ? input.path : `${apiUrl}${input.path}`\n const headers: Record<string, string> = {\n authorization: `Bearer ${opts.apiKey}`,\n accept: 'application/json',\n }\n let body: BodyInit | undefined\n if (input.raw) {\n headers['content-type'] = input.contentType || 'application/octet-stream'\n body = input.raw as BodyInit\n } else if (input.json !== undefined) {\n headers['content-type'] = 'application/json'\n body = JSON.stringify(input.json)\n }\n if (input.idempotencyKey) headers['idempotency-key'] = input.idempotencyKey\n\n const send = () =>\n doFetch(url, {\n method: input.method,\n headers,\n body,\n })\n\n let res = await send()\n if (res.status === 429) {\n await doSleep(retryAfterMs(res))\n res = await send()\n }\n\n const text = await res.text()\n let parsed: unknown = text\n if (text) {\n try {\n parsed = JSON.parse(text)\n } catch {\n parsed = { message: text }\n }\n } else {\n parsed = {}\n }\n\n if (!res.ok) {\n throw new ApiError(formatError(res.status, parsed), res.status, parsed)\n }\n return parsed as T\n }\n\n return { apiUrl, request }\n}\n","import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'\nimport { homedir } from 'node:os'\nimport { dirname, join } from 'node:path'\n\nexport const DEFAULT_API_URL = 'https://api.singlefax.com'\n\nexport type CliConfigFile = {\n apiKey?: string\n apiUrl?: string\n}\n\nexport function configFilePath(): string {\n if (process.env.SINGLEFAX_CONFIG_PATH) return process.env.SINGLEFAX_CONFIG_PATH\n const xdg = process.env.XDG_CONFIG_HOME || join(homedir(), '.config')\n return join(xdg, 'singlefax', 'config.json')\n}\n\nexport async function readConfigFile(): Promise<CliConfigFile> {\n try {\n const raw = await readFile(configFilePath(), 'utf8')\n const parsed = JSON.parse(raw) as CliConfigFile\n return parsed && typeof parsed === 'object' ? parsed : {}\n } catch {\n return {}\n }\n}\n\nexport async function writeConfigFile(next: CliConfigFile): Promise<string> {\n const path = configFilePath()\n await mkdir(dirname(path), { recursive: true, mode: 0o700 })\n await writeFile(path, `${JSON.stringify(next, null, 2)}\\n`, { mode: 0o600 })\n await chmod(path, 0o600)\n return path\n}\n\nexport type ResolvedConfig = {\n apiUrl: string\n apiKey: string\n json: boolean\n}\n\nfunction asString(value: unknown): string | undefined {\n return typeof value === 'string' && value.trim() ? value.trim() : undefined\n}\n\nexport async function resolveConfig(args: Record<string, unknown>): Promise<ResolvedConfig> {\n const file = await readConfigFile()\n const apiUrl =\n asString(args['api-url']) ||\n asString(args.apiUrl) ||\n asString(process.env.SINGLEFAX_API_URL) ||\n file.apiUrl ||\n DEFAULT_API_URL\n const apiKey =\n asString(args['api-key']) ||\n asString(args.apiKey) ||\n asString(process.env.SINGLEFAX_API_KEY) ||\n file.apiKey ||\n ''\n return {\n apiUrl: apiUrl.replace(/\\/$/, ''),\n apiKey,\n json: Boolean(args.json),\n }\n}\n\nexport function requireApiKey(config: ResolvedConfig): string {\n if (config.apiKey) return config.apiKey\n throw new Error(\n 'Missing API key. Set SINGLEFAX_API_KEY, pass --api-key, or run: singlefax config set api-key sf_live_…',\n )\n}\n","export function printJson(value: unknown): void {\n process.stdout.write(`${JSON.stringify(value, null, 2)}\\n`)\n}\n\nexport function printLine(line: string): void {\n process.stdout.write(`${line}\\n`)\n}\n\nfunction cents(value: unknown): string {\n const n = Number(value)\n if (!Number.isFinite(n)) return '—'\n return `$${(n / 100).toFixed(2)}`\n}\n\nexport function printSendHuman(row: {\n id?: string\n fulfillmentStatus?: string\n billablePages?: number\n totalCents?: number\n subtotalCents?: number\n}): void {\n printLine(\n [\n row.fulfillmentStatus || 'queued',\n row.id || '',\n row.billablePages != null ? `${row.billablePages} pages` : '',\n cents(row.totalCents ?? row.subtotalCents),\n ]\n .filter(Boolean)\n .join(' '),\n )\n}\n\nexport function printStatusHuman(row: {\n id?: string\n fulfillmentStatus?: string\n billablePages?: number\n totalCents?: number\n subtotalCents?: number\n failureClass?: string | null\n}): void {\n const parts = [\n row.fulfillmentStatus || 'unknown',\n row.id || '',\n row.billablePages != null ? `${row.billablePages} pages` : '',\n cents(row.totalCents ?? row.subtotalCents),\n ]\n if (row.failureClass) parts.push(row.failureClass)\n printLine(parts.filter(Boolean).join(' '))\n}\n\nexport function printListHuman(payload: {\n data?: Array<{\n id?: string\n fulfillmentStatus?: string\n billablePages?: number\n totalCents?: number\n destinationCountry?: string | null\n }>\n}): void {\n const rows = payload.data ?? []\n if (!rows.length) {\n printLine('No faxes.')\n return\n }\n for (const row of rows) {\n printLine(\n [\n row.id || '',\n row.fulfillmentStatus || '',\n row.billablePages != null ? `${row.billablePages}p` : '',\n cents(row.totalCents),\n row.destinationCountry || '',\n ]\n .filter((part) => part !== '')\n .join(' '),\n )\n }\n}\n\nexport function printCreditsHuman(row: {\n availableCents?: number\n reservedCents?: number\n}): void {\n printLine(\n `available ${cents(row.availableCents)} reserved ${cents(row.reservedCents)}`,\n )\n}\n","import { randomUUID } from 'node:crypto'\nimport { readFile, stat } from 'node:fs/promises'\nimport { extname } from 'node:path'\nimport type { ApiClient } from './client.ts'\n\nconst TERMINAL = new Set(['delivered', 'failed_final'])\n\nexport type UploadCreated = {\n uploadToken: string\n uploadUrl: string\n contentType?: string\n maxBytes?: number\n}\n\nexport type FaxCreated = {\n id: string\n fulfillmentStatus: string\n billablePages?: number\n totalCents?: number\n subtotalCents?: number\n destinationCountry?: string\n}\n\nfunction contentTypeFor(file: string): string {\n switch (extname(file).toLowerCase()) {\n case '.pdf':\n return 'application/pdf'\n case '.png':\n return 'image/png'\n case '.jpg':\n case '.jpeg':\n return 'image/jpeg'\n case '.tif':\n case '.tiff':\n return 'image/tiff'\n case '.doc':\n return 'application/msword'\n case '.docx':\n return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'\n default:\n return 'application/pdf'\n }\n}\n\nexport async function sendFax(\n client: ApiClient,\n opts: {\n file: string\n to: string\n cover?: boolean\n wait?: boolean\n sleep?: (ms: number) => Promise<void>\n now?: () => number\n },\n): Promise<FaxCreated> {\n const filePath = opts.file\n const info = await stat(filePath)\n if (!info.isFile()) {\n throw new Error('FILE must be a regular file')\n }\n const contentType = contentTypeFor(filePath)\n const bytes = await readFile(filePath)\n\n const upload = await client.request<UploadCreated>({\n method: 'POST',\n path: '/api/v1/uploads',\n json: { contentType, maxBytes: bytes.byteLength },\n })\n\n await client.request({\n method: 'PUT',\n path: upload.uploadUrl,\n raw: bytes,\n contentType,\n })\n\n const fax = await client.request<FaxCreated>({\n method: 'POST',\n path: '/api/v1/faxes',\n json: { to: opts.to, uploadToken: upload.uploadToken, cover: Boolean(opts.cover) },\n idempotencyKey: randomUUID(),\n })\n\n if (!opts.wait) return fax\n\n const doSleep = opts.sleep ?? ((ms: number) => new Promise((r) => setTimeout(r, ms)))\n const started = (opts.now ?? Date.now)()\n const deadline = started + 180_000\n let latest: FaxCreated = fax\n while ((opts.now ?? Date.now)() < deadline) {\n if (TERMINAL.has(latest.fulfillmentStatus)) return latest\n await doSleep(2000)\n latest = await client.request<FaxCreated>({\n method: 'GET',\n path: `/api/v1/faxes/${encodeURIComponent(fax.id)}`,\n })\n }\n return latest\n}\n"],"mappings":"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","names":["readFile"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@singlefax/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "SingleFax CLI — send faxes with an API key and prepaid credits",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"singlefax": "./dist/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"prepublishOnly": "npm run build",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"test": "tsx --test src/*.test.ts",
|
|
24
|
+
"start": "node dist/cli.mjs"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"singlefax",
|
|
28
|
+
"fax",
|
|
29
|
+
"cli",
|
|
30
|
+
"api"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"homepage": "https://singlefax.com/agents",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/SingleFax/singlefax-app.git",
|
|
37
|
+
"directory": "packages/cli"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/SingleFax/singlefax-app/issues"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"citty": "^0.1.6"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^24.0.0",
|
|
47
|
+
"tsx": "^4.23.12",
|
|
48
|
+
"tsup": "^8.5.0",
|
|
49
|
+
"typescript": "^6.0.3"
|
|
50
|
+
}
|
|
51
|
+
}
|