newsnow 1.0.0 → 1.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/LICENSE +21 -0
- package/README.md +18 -31
- package/dist/src/cli.js +180 -28
- package/dist/src/types.js +1 -1
- package/dist/test/cli.test.js +99 -19
- package/package.json +4 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-present ChenCheng (sorrycc@gmail.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# newsnow
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/newsnow)
|
|
4
|
+
[](https://www.npmjs.com/package/newsnow)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://clawhub.ai/sorrycc/newsnow)
|
|
7
|
+
|
|
3
8
|
A command-line tool to fetch trending news and hot topics from 66 sources across 44 platforms. Built with TypeScript, runs on Bun.
|
|
4
9
|
|
|
5
10
|
Ported from [ourongxing/newsnow](https://github.com/ourongxing/newsnow) server sources.
|
|
@@ -7,26 +12,32 @@ Ported from [ourongxing/newsnow](https://github.com/ourongxing/newsnow) server s
|
|
|
7
12
|
## Install
|
|
8
13
|
|
|
9
14
|
```bash
|
|
10
|
-
|
|
15
|
+
npm install -g newsnow
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or use directly with npx:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx newsnow
|
|
11
22
|
```
|
|
12
23
|
|
|
13
24
|
## Usage
|
|
14
25
|
|
|
15
26
|
```bash
|
|
16
27
|
# Show help
|
|
17
|
-
|
|
28
|
+
newsnow --help
|
|
18
29
|
|
|
19
30
|
# List all available sources
|
|
20
|
-
|
|
31
|
+
newsnow list
|
|
21
32
|
|
|
22
33
|
# Fetch news from a source
|
|
23
|
-
|
|
34
|
+
newsnow hackernews
|
|
24
35
|
|
|
25
36
|
# Output as JSON (pipeable to jq, etc.)
|
|
26
|
-
|
|
37
|
+
newsnow hackernews --json
|
|
27
38
|
|
|
28
39
|
# List sources as JSON
|
|
29
|
-
|
|
40
|
+
newsnow list --json
|
|
30
41
|
```
|
|
31
42
|
|
|
32
43
|
## Sources
|
|
@@ -94,30 +105,6 @@ Some sources may be blocked by Cloudflare or require authentication:
|
|
|
94
105
|
bun test
|
|
95
106
|
```
|
|
96
107
|
|
|
97
|
-
## Project Structure
|
|
98
|
-
|
|
99
|
-
```
|
|
100
|
-
src/
|
|
101
|
-
cli.ts # CLI entry point (raw process.argv)
|
|
102
|
-
types.ts # NewsItem type definitions
|
|
103
|
-
fetch.ts # HTTP client (ofetch wrapper)
|
|
104
|
-
crypto.ts # md5, SHA-1, base64 helpers
|
|
105
|
-
utils.ts # Date parsing utilities
|
|
106
|
-
rss.ts # RSS feed parser
|
|
107
|
-
sources/
|
|
108
|
-
index.ts # Source registry (all sources merged)
|
|
109
|
-
baidu.ts # One file per platform
|
|
110
|
-
bilibili.ts
|
|
111
|
-
cls/ # Multi-file sources
|
|
112
|
-
index.ts
|
|
113
|
-
utils.ts
|
|
114
|
-
coolapk/
|
|
115
|
-
index.ts
|
|
116
|
-
...
|
|
117
|
-
test/
|
|
118
|
-
cli.test.ts
|
|
119
|
-
```
|
|
120
|
-
|
|
121
108
|
## Dependencies
|
|
122
109
|
|
|
123
110
|
- [cheerio](https://github.com/cheeriojs/cheerio) - HTML parsing
|
|
@@ -127,4 +114,4 @@ test/
|
|
|
127
114
|
|
|
128
115
|
## License
|
|
129
116
|
|
|
130
|
-
|
|
117
|
+
MIT
|
package/dist/src/cli.js
CHANGED
|
@@ -1,35 +1,98 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { sources } from "./sources/index.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
3
|
+
import { NEWS_ITEM_FIELDS } from "./types.js";
|
|
4
|
+
function parseArgs(argv) {
|
|
5
|
+
const raw = argv.slice(2);
|
|
6
|
+
let json = false;
|
|
7
|
+
let pretty = false;
|
|
8
|
+
const fields = [];
|
|
9
|
+
let limit;
|
|
10
|
+
let command;
|
|
11
|
+
for (let i = 0; i < raw.length; i++) {
|
|
12
|
+
const arg = raw[i];
|
|
13
|
+
if (arg === "--json") {
|
|
14
|
+
json = true;
|
|
15
|
+
}
|
|
16
|
+
else if (arg === "--output" && raw[i + 1] === "json") {
|
|
17
|
+
json = true;
|
|
18
|
+
i++;
|
|
19
|
+
}
|
|
20
|
+
else if (arg === "--pretty") {
|
|
21
|
+
pretty = true;
|
|
22
|
+
}
|
|
23
|
+
else if (arg === "--fields" && raw[i + 1]) {
|
|
24
|
+
fields.push(...raw[++i].split(",").map(f => f.trim()).filter(Boolean));
|
|
25
|
+
}
|
|
26
|
+
else if (arg === "--limit" && raw[i + 1]) {
|
|
27
|
+
limit = parseInt(raw[++i], 10);
|
|
28
|
+
}
|
|
29
|
+
else if (!arg.startsWith("-") && command === undefined) {
|
|
30
|
+
command = arg;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { command, json, pretty, fields, limit };
|
|
34
|
+
}
|
|
35
|
+
const opts = parseArgs(process.argv);
|
|
36
|
+
function jsonOut(data) {
|
|
37
|
+
console.log(opts.pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data));
|
|
38
|
+
}
|
|
39
|
+
function emitError(msg, code, exitCode, suggestions) {
|
|
40
|
+
if (opts.json) {
|
|
41
|
+
const envelope = { error: msg, code };
|
|
42
|
+
if (suggestions?.length)
|
|
43
|
+
envelope.suggestions = suggestions;
|
|
44
|
+
process.stderr.write(JSON.stringify(envelope) + "\n");
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
console.error(`Error: ${msg}`);
|
|
48
|
+
if (suggestions?.length) {
|
|
49
|
+
console.error(`Did you mean: ${suggestions.join(", ")}?`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
process.exit(exitCode);
|
|
53
|
+
}
|
|
7
54
|
function printHelp() {
|
|
8
|
-
console.log(`Usage: newsnow <source> [
|
|
9
|
-
newsnow list [--json]
|
|
55
|
+
console.log(`Usage: newsnow <source> [options]
|
|
56
|
+
newsnow list [--json | --output json] [--pretty]
|
|
57
|
+
newsnow schema [--pretty]
|
|
10
58
|
|
|
11
59
|
Commands:
|
|
12
60
|
list List all available sources
|
|
61
|
+
schema Print machine-readable JSON Schema for this CLI
|
|
13
62
|
<source> Fetch news from the given source
|
|
14
63
|
|
|
15
64
|
Options:
|
|
16
|
-
--json
|
|
65
|
+
--json Output as JSON
|
|
66
|
+
--output json Alias for --json
|
|
67
|
+
--pretty Pretty-print JSON output
|
|
68
|
+
--fields f1,f2 Filter output fields (JSON mode only)
|
|
69
|
+
--limit N Limit number of items returned
|
|
17
70
|
|
|
18
71
|
Sources: ${Object.keys(sources).length} available. Run "newsnow list" to see all.`);
|
|
19
72
|
}
|
|
73
|
+
function buildSourceMeta() {
|
|
74
|
+
const envVarMap = {
|
|
75
|
+
producthunt: ["PRODUCTHUNT_API_TOKEN"],
|
|
76
|
+
};
|
|
77
|
+
return Object.keys(sources).sort().map(name => ({
|
|
78
|
+
name,
|
|
79
|
+
category: name.split("-")[0],
|
|
80
|
+
envVars: envVarMap[name] || [],
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
20
83
|
function printList() {
|
|
21
|
-
const
|
|
22
|
-
if (
|
|
23
|
-
|
|
84
|
+
const metas = buildSourceMeta();
|
|
85
|
+
if (opts.json) {
|
|
86
|
+
const envelope = { count: metas.length, sources: metas };
|
|
87
|
+
jsonOut(envelope);
|
|
24
88
|
}
|
|
25
89
|
else {
|
|
26
|
-
console.log(`Available sources (${
|
|
90
|
+
console.log(`Available sources (${metas.length}):\n`);
|
|
27
91
|
const groups = {};
|
|
28
|
-
for (const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
groups[base].push(name);
|
|
92
|
+
for (const m of metas) {
|
|
93
|
+
if (!groups[m.category])
|
|
94
|
+
groups[m.category] = [];
|
|
95
|
+
groups[m.category].push(m.name);
|
|
33
96
|
}
|
|
34
97
|
for (const [base, items] of Object.entries(groups).sort(([a], [b]) => a.localeCompare(b))) {
|
|
35
98
|
if (items.length === 1) {
|
|
@@ -41,6 +104,74 @@ function printList() {
|
|
|
41
104
|
}
|
|
42
105
|
}
|
|
43
106
|
}
|
|
107
|
+
function printSchema() {
|
|
108
|
+
const metas = buildSourceMeta();
|
|
109
|
+
const categories = {};
|
|
110
|
+
for (const m of metas) {
|
|
111
|
+
if (!categories[m.category])
|
|
112
|
+
categories[m.category] = [];
|
|
113
|
+
categories[m.category].push(m.name);
|
|
114
|
+
}
|
|
115
|
+
const schema = {
|
|
116
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
117
|
+
name: "newsnow",
|
|
118
|
+
commands: {
|
|
119
|
+
list: { description: "List all available news sources" },
|
|
120
|
+
schema: { description: "Print this JSON Schema" },
|
|
121
|
+
"<source>": { description: "Fetch news items from the named source" },
|
|
122
|
+
},
|
|
123
|
+
flags: {
|
|
124
|
+
"--json": { description: "Output as JSON" },
|
|
125
|
+
"--output": { description: "Output format", values: ["json"] },
|
|
126
|
+
"--pretty": { description: "Pretty-print JSON output" },
|
|
127
|
+
"--fields": { description: "Comma-separated list of fields to include", values: [...NEWS_ITEM_FIELDS] },
|
|
128
|
+
"--limit": { description: "Maximum number of items to return", type: "integer" },
|
|
129
|
+
},
|
|
130
|
+
categories,
|
|
131
|
+
$defs: {
|
|
132
|
+
NewsItem: {
|
|
133
|
+
type: "object",
|
|
134
|
+
properties: {
|
|
135
|
+
id: { type: ["string", "number"] },
|
|
136
|
+
title: { type: "string" },
|
|
137
|
+
url: { type: "string" },
|
|
138
|
+
mobileUrl: { type: "string" },
|
|
139
|
+
pubDate: { type: ["string", "number"] },
|
|
140
|
+
extra: { type: "object" },
|
|
141
|
+
},
|
|
142
|
+
required: ["id", "title"],
|
|
143
|
+
},
|
|
144
|
+
FetchEnvelope: {
|
|
145
|
+
type: "object",
|
|
146
|
+
properties: {
|
|
147
|
+
source: { type: "string" },
|
|
148
|
+
count: { type: "integer" },
|
|
149
|
+
items: { type: "array", items: { $ref: "#/$defs/NewsItem" } },
|
|
150
|
+
},
|
|
151
|
+
required: ["source", "count", "items"],
|
|
152
|
+
},
|
|
153
|
+
ListEnvelope: {
|
|
154
|
+
type: "object",
|
|
155
|
+
properties: {
|
|
156
|
+
count: { type: "integer" },
|
|
157
|
+
sources: {
|
|
158
|
+
type: "array",
|
|
159
|
+
items: {
|
|
160
|
+
type: "object",
|
|
161
|
+
properties: {
|
|
162
|
+
name: { type: "string" },
|
|
163
|
+
category: { type: "string" },
|
|
164
|
+
envVars: { type: "array", items: { type: "string" } },
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
required: ["count", "sources"],
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
console.log(opts.pretty ? JSON.stringify(schema, null, 2) : JSON.stringify(schema));
|
|
174
|
+
}
|
|
44
175
|
function suggestSimilar(input) {
|
|
45
176
|
const names = Object.keys(sources);
|
|
46
177
|
return names.filter(n => n.includes(input) || input.includes(n) || levenshtein(n, input) <= 3).slice(0, 5);
|
|
@@ -61,16 +192,35 @@ async function fetchSource(name) {
|
|
|
61
192
|
const handler = sources[name];
|
|
62
193
|
if (!handler) {
|
|
63
194
|
const similar = suggestSimilar(name);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
195
|
+
emitError(`Unknown source "${name}"`, "UNKNOWN_SOURCE", 1, similar.length ? similar : undefined);
|
|
196
|
+
}
|
|
197
|
+
// Validate --fields before fetching
|
|
198
|
+
if (opts.fields.length) {
|
|
199
|
+
const validFields = new Set(NEWS_ITEM_FIELDS);
|
|
200
|
+
const invalid = opts.fields.filter(f => !validFields.has(f));
|
|
201
|
+
if (invalid.length) {
|
|
202
|
+
emitError(`Invalid field(s): ${invalid.join(", ")}. Valid fields: ${NEWS_ITEM_FIELDS.join(", ")}`, "INVALID_FIELD", 1);
|
|
67
203
|
}
|
|
68
|
-
process.exit(1);
|
|
69
204
|
}
|
|
70
205
|
try {
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
|
|
206
|
+
let items = await handler();
|
|
207
|
+
if (opts.limit !== undefined && opts.limit > 0) {
|
|
208
|
+
items = items.slice(0, opts.limit);
|
|
209
|
+
}
|
|
210
|
+
if (opts.json) {
|
|
211
|
+
let projected = items;
|
|
212
|
+
if (opts.fields.length) {
|
|
213
|
+
projected = items.map(item => {
|
|
214
|
+
const picked = {};
|
|
215
|
+
for (const f of opts.fields) {
|
|
216
|
+
if (f in item)
|
|
217
|
+
picked[f] = item[f];
|
|
218
|
+
}
|
|
219
|
+
return picked;
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
const envelope = { source: name, count: projected.length, items: projected };
|
|
223
|
+
jsonOut(envelope);
|
|
74
224
|
}
|
|
75
225
|
else {
|
|
76
226
|
if (!items.length) {
|
|
@@ -89,16 +239,18 @@ async function fetchSource(name) {
|
|
|
89
239
|
}
|
|
90
240
|
}
|
|
91
241
|
catch (err) {
|
|
92
|
-
|
|
93
|
-
process.exit(1);
|
|
242
|
+
emitError(`Fetch failed for "${name}": ${err.message}`, "FETCH_ERROR", 2);
|
|
94
243
|
}
|
|
95
244
|
}
|
|
96
|
-
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
245
|
+
if (!opts.command || opts.command === "help" || opts.command === "--help" || opts.command === "-h") {
|
|
97
246
|
printHelp();
|
|
98
247
|
}
|
|
99
|
-
else if (command === "list") {
|
|
248
|
+
else if (opts.command === "list") {
|
|
100
249
|
printList();
|
|
101
250
|
}
|
|
251
|
+
else if (opts.command === "schema") {
|
|
252
|
+
printSchema();
|
|
253
|
+
}
|
|
102
254
|
else {
|
|
103
|
-
fetchSource(command);
|
|
255
|
+
fetchSource(opts.command);
|
|
104
256
|
}
|
package/dist/src/types.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export const NEWS_ITEM_FIELDS = ["id", "title", "url", "mobileUrl", "pubDate", "extra"];
|
package/dist/test/cli.test.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { describe, test, expect } from "bun:test";
|
|
2
2
|
import { sources } from "../src/sources/index.js";
|
|
3
|
+
const cwd = import.meta.dir + "/..";
|
|
4
|
+
function run(args) {
|
|
5
|
+
return Bun.spawn(["bun", "src/cli.ts", ...args], {
|
|
6
|
+
cwd,
|
|
7
|
+
stdout: "pipe",
|
|
8
|
+
stderr: "pipe",
|
|
9
|
+
});
|
|
10
|
+
}
|
|
3
11
|
describe("registry", () => {
|
|
4
12
|
test("has sources registered", () => {
|
|
5
13
|
const names = Object.keys(sources);
|
|
@@ -21,32 +29,85 @@ describe("registry", () => {
|
|
|
21
29
|
});
|
|
22
30
|
});
|
|
23
31
|
describe("cli", () => {
|
|
24
|
-
test("list
|
|
25
|
-
const proc =
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
expect(
|
|
32
|
-
expect(
|
|
33
|
-
expect(
|
|
32
|
+
test("list --json returns ListEnvelope", async () => {
|
|
33
|
+
const proc = run(["list", "--json"]);
|
|
34
|
+
const output = await new Response(proc.stdout).text();
|
|
35
|
+
const envelope = JSON.parse(output);
|
|
36
|
+
expect(envelope).toHaveProperty("count");
|
|
37
|
+
expect(envelope).toHaveProperty("sources");
|
|
38
|
+
expect(Array.isArray(envelope.sources)).toBe(true);
|
|
39
|
+
expect(envelope.count).toBeGreaterThan(40);
|
|
40
|
+
expect(envelope.sources[0]).toHaveProperty("name");
|
|
41
|
+
expect(envelope.sources[0]).toHaveProperty("category");
|
|
42
|
+
expect(envelope.sources[0]).toHaveProperty("envVars");
|
|
43
|
+
});
|
|
44
|
+
test("--output json alias works", async () => {
|
|
45
|
+
const proc = run(["list", "--output", "json"]);
|
|
46
|
+
const output = await new Response(proc.stdout).text();
|
|
47
|
+
const envelope = JSON.parse(output);
|
|
48
|
+
expect(envelope).toHaveProperty("count");
|
|
49
|
+
expect(envelope).toHaveProperty("sources");
|
|
34
50
|
});
|
|
35
51
|
test("help command works", async () => {
|
|
36
|
-
const proc =
|
|
37
|
-
cwd: import.meta.dir + "/..",
|
|
38
|
-
stdout: "pipe",
|
|
39
|
-
});
|
|
52
|
+
const proc = run(["--help"]);
|
|
40
53
|
const output = await new Response(proc.stdout).text();
|
|
41
54
|
expect(output).toContain("Usage:");
|
|
55
|
+
expect(output).toContain("--fields");
|
|
56
|
+
expect(output).toContain("--limit");
|
|
57
|
+
expect(output).toContain("schema");
|
|
42
58
|
});
|
|
43
|
-
test("
|
|
44
|
-
const proc =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
59
|
+
test("schema command returns valid JSON Schema", async () => {
|
|
60
|
+
const proc = run(["schema"]);
|
|
61
|
+
const output = await new Response(proc.stdout).text();
|
|
62
|
+
const schema = JSON.parse(output);
|
|
63
|
+
expect(schema).toHaveProperty("$schema");
|
|
64
|
+
expect(schema).toHaveProperty("commands");
|
|
65
|
+
expect(schema).toHaveProperty("flags");
|
|
66
|
+
expect(schema).toHaveProperty("categories");
|
|
67
|
+
expect(schema).toHaveProperty("$defs");
|
|
68
|
+
expect(schema.$defs).toHaveProperty("NewsItem");
|
|
69
|
+
expect(schema.$defs).toHaveProperty("FetchEnvelope");
|
|
70
|
+
expect(schema.$defs).toHaveProperty("ListEnvelope");
|
|
71
|
+
});
|
|
72
|
+
test("unknown source shows error with exitCode", async () => {
|
|
73
|
+
const proc = run(["nonexistent_xyz"]);
|
|
48
74
|
const err = await new Response(proc.stderr).text();
|
|
49
75
|
expect(err).toContain("Unknown source");
|
|
76
|
+
const code = await proc.exited;
|
|
77
|
+
expect(code).toBe(1);
|
|
78
|
+
});
|
|
79
|
+
test("unknown source --json emits ErrorEnvelope to stderr", async () => {
|
|
80
|
+
const proc = run(["nonexistent_xyz", "--json"]);
|
|
81
|
+
const err = await new Response(proc.stderr).text();
|
|
82
|
+
const envelope = JSON.parse(err);
|
|
83
|
+
expect(envelope).toHaveProperty("error");
|
|
84
|
+
expect(envelope.code).toBe("UNKNOWN_SOURCE");
|
|
85
|
+
const code = await proc.exited;
|
|
86
|
+
expect(code).toBe(1);
|
|
87
|
+
});
|
|
88
|
+
test("--fields with invalid field returns INVALID_FIELD error", async () => {
|
|
89
|
+
const proc = run(["hackernews", "--json", "--fields", "bogus"]);
|
|
90
|
+
const err = await new Response(proc.stderr).text();
|
|
91
|
+
const envelope = JSON.parse(err);
|
|
92
|
+
expect(envelope.code).toBe("INVALID_FIELD");
|
|
93
|
+
expect(envelope.error).toContain("bogus");
|
|
94
|
+
const code = await proc.exited;
|
|
95
|
+
expect(code).toBe(1);
|
|
96
|
+
});
|
|
97
|
+
test("--pretty produces indented output", async () => {
|
|
98
|
+
const proc = run(["list", "--json", "--pretty"]);
|
|
99
|
+
const output = await new Response(proc.stdout).text();
|
|
100
|
+
expect(output).toContain("\n ");
|
|
101
|
+
// Should still parse as valid JSON
|
|
102
|
+
const parsed = JSON.parse(output);
|
|
103
|
+
expect(parsed).toHaveProperty("count");
|
|
104
|
+
});
|
|
105
|
+
test("compact JSON is default (no indentation)", async () => {
|
|
106
|
+
const proc = run(["list", "--json"]);
|
|
107
|
+
const output = await new Response(proc.stdout).text();
|
|
108
|
+
// Compact JSON is a single line
|
|
109
|
+
const lines = output.trim().split("\n");
|
|
110
|
+
expect(lines.length).toBe(1);
|
|
50
111
|
});
|
|
51
112
|
});
|
|
52
113
|
describe("fetch source", () => {
|
|
@@ -58,4 +119,23 @@ describe("fetch source", () => {
|
|
|
58
119
|
expect(items[0]).toHaveProperty("id");
|
|
59
120
|
expect(items[0]).toHaveProperty("url");
|
|
60
121
|
}, 15000);
|
|
122
|
+
test("--fields filters output fields", async () => {
|
|
123
|
+
const proc = run(["hackernews", "--json", "--fields", "id,title", "--limit", "3"]);
|
|
124
|
+
const output = await new Response(proc.stdout).text();
|
|
125
|
+
const envelope = JSON.parse(output);
|
|
126
|
+
expect(envelope.source).toBe("hackernews");
|
|
127
|
+
expect(envelope.count).toBeLessThanOrEqual(3);
|
|
128
|
+
for (const item of envelope.items) {
|
|
129
|
+
expect(item).toHaveProperty("id");
|
|
130
|
+
expect(item).toHaveProperty("title");
|
|
131
|
+
expect(item).not.toHaveProperty("url");
|
|
132
|
+
}
|
|
133
|
+
}, 15000);
|
|
134
|
+
test("--limit caps item count", async () => {
|
|
135
|
+
const proc = run(["hackernews", "--json", "--limit", "2"]);
|
|
136
|
+
const output = await new Response(proc.stdout).text();
|
|
137
|
+
const envelope = JSON.parse(output);
|
|
138
|
+
expect(envelope.count).toBeLessThanOrEqual(2);
|
|
139
|
+
expect(envelope.items.length).toBeLessThanOrEqual(2);
|
|
140
|
+
}, 15000);
|
|
61
141
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "newsnow",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"newsnow": "./dist/src/cli.js"
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"start": "bun src/cli.ts",
|
|
13
13
|
"test": "bun test",
|
|
14
14
|
"build": "tsc",
|
|
15
|
-
"release": "bun
|
|
15
|
+
"release": "bun scripts/release.ts"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"cheerio": "^1.0.0",
|
|
@@ -24,5 +24,6 @@
|
|
|
24
24
|
"@types/bun": "latest",
|
|
25
25
|
"@types/node": "^22.0.0",
|
|
26
26
|
"typescript": "^5.7.0"
|
|
27
|
-
}
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT"
|
|
28
29
|
}
|