@ozzylabs/feedradar 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/LICENSE +21 -0
- package/README.md +104 -0
- package/dist/agents/_boundary.d.ts +44 -0
- package/dist/agents/_boundary.d.ts.map +1 -0
- package/dist/agents/_boundary.js +59 -0
- package/dist/agents/_boundary.js.map +1 -0
- package/dist/agents/claude-code.d.ts +32 -0
- package/dist/agents/claude-code.d.ts.map +1 -0
- package/dist/agents/claude-code.js +256 -0
- package/dist/agents/claude-code.js.map +1 -0
- package/dist/agents/codex-cli.d.ts +31 -0
- package/dist/agents/codex-cli.d.ts.map +1 -0
- package/dist/agents/codex-cli.js +303 -0
- package/dist/agents/codex-cli.js.map +1 -0
- package/dist/agents/copilot.d.ts +29 -0
- package/dist/agents/copilot.d.ts.map +1 -0
- package/dist/agents/copilot.js +282 -0
- package/dist/agents/copilot.js.map +1 -0
- package/dist/agents/gemini-cli.d.ts +30 -0
- package/dist/agents/gemini-cli.d.ts.map +1 -0
- package/dist/agents/gemini-cli.js +316 -0
- package/dist/agents/gemini-cli.js.map +1 -0
- package/dist/agents/index.d.ts +12 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +33 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/types.d.ts +103 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +2 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/claude-skills/dismiss/SKILL.md +41 -0
- package/dist/claude-skills/research/SKILL.md +45 -0
- package/dist/claude-skills/review/SKILL.md +45 -0
- package/dist/claude-skills/update/SKILL.md +49 -0
- package/dist/cli/dismiss.d.ts +28 -0
- package/dist/cli/dismiss.d.ts.map +1 -0
- package/dist/cli/dismiss.js +122 -0
- package/dist/cli/dismiss.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +64 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +148 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +578 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/research.d.ts +30 -0
- package/dist/cli/research.d.ts.map +1 -0
- package/dist/cli/research.js +313 -0
- package/dist/cli/research.js.map +1 -0
- package/dist/cli/review.d.ts +34 -0
- package/dist/cli/review.d.ts.map +1 -0
- package/dist/cli/review.js +418 -0
- package/dist/cli/review.js.map +1 -0
- package/dist/cli/source.d.ts +57 -0
- package/dist/cli/source.d.ts.map +1 -0
- package/dist/cli/source.js +511 -0
- package/dist/cli/source.js.map +1 -0
- package/dist/cli/update.d.ts +43 -0
- package/dist/cli/update.d.ts.map +1 -0
- package/dist/cli/update.js +429 -0
- package/dist/cli/update.js.map +1 -0
- package/dist/cli/watch.d.ts +22 -0
- package/dist/cli/watch.d.ts.map +1 -0
- package/dist/cli/watch.js +101 -0
- package/dist/cli/watch.js.map +1 -0
- package/dist/core/config.d.ts +60 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +101 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/feeds/derive-id.d.ts +43 -0
- package/dist/core/feeds/derive-id.d.ts.map +1 -0
- package/dist/core/feeds/derive-id.js +66 -0
- package/dist/core/feeds/derive-id.js.map +1 -0
- package/dist/core/feeds/github-api.d.ts +69 -0
- package/dist/core/feeds/github-api.d.ts.map +1 -0
- package/dist/core/feeds/github-api.js +161 -0
- package/dist/core/feeds/github-api.js.map +1 -0
- package/dist/core/feeds/github-releases.d.ts +3 -0
- package/dist/core/feeds/github-releases.d.ts.map +1 -0
- package/dist/core/feeds/github-releases.js +85 -0
- package/dist/core/feeds/github-releases.js.map +1 -0
- package/dist/core/feeds/html.d.ts +10 -0
- package/dist/core/feeds/html.d.ts.map +1 -0
- package/dist/core/feeds/html.js +263 -0
- package/dist/core/feeds/html.js.map +1 -0
- package/dist/core/feeds/index.d.ts +5 -0
- package/dist/core/feeds/index.d.ts.map +1 -0
- package/dist/core/feeds/index.js +18 -0
- package/dist/core/feeds/index.js.map +1 -0
- package/dist/core/feeds/npm-registry.d.ts +36 -0
- package/dist/core/feeds/npm-registry.d.ts.map +1 -0
- package/dist/core/feeds/npm-registry.js +200 -0
- package/dist/core/feeds/npm-registry.js.map +1 -0
- package/dist/core/feeds/rss.d.ts +12 -0
- package/dist/core/feeds/rss.d.ts.map +1 -0
- package/dist/core/feeds/rss.js +222 -0
- package/dist/core/feeds/rss.js.map +1 -0
- package/dist/core/feeds/types.d.ts +45 -0
- package/dist/core/feeds/types.d.ts.map +1 -0
- package/dist/core/feeds/types.js +2 -0
- package/dist/core/feeds/types.js.map +1 -0
- package/dist/core/filter.d.ts +25 -0
- package/dist/core/filter.d.ts.map +1 -0
- package/dist/core/filter.js +123 -0
- package/dist/core/filter.js.map +1 -0
- package/dist/core/injection-detector.d.ts +57 -0
- package/dist/core/injection-detector.d.ts.map +1 -0
- package/dist/core/injection-detector.js +109 -0
- package/dist/core/injection-detector.js.map +1 -0
- package/dist/core/items.d.ts +20 -0
- package/dist/core/items.d.ts.map +1 -0
- package/dist/core/items.js +105 -0
- package/dist/core/items.js.map +1 -0
- package/dist/core/state.d.ts +12 -0
- package/dist/core/state.d.ts.map +1 -0
- package/dist/core/state.js +42 -0
- package/dist/core/state.js.map +1 -0
- package/dist/core/templates.d.ts +21 -0
- package/dist/core/templates.d.ts.map +1 -0
- package/dist/core/templates.js +52 -0
- package/dist/core/templates.js.map +1 -0
- package/dist/core/watcher.d.ts +72 -0
- package/dist/core/watcher.d.ts.map +1 -0
- package/dist/core/watcher.js +240 -0
- package/dist/core/watcher.js.map +1 -0
- package/dist/gemini-commands/dismiss.toml +2 -0
- package/dist/gemini-commands/research.toml +2 -0
- package/dist/gemini-commands/review.toml +2 -0
- package/dist/gemini-commands/update.toml +2 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas/config.d.ts +39 -0
- package/dist/schemas/config.d.ts.map +1 -0
- package/dist/schemas/config.js +23 -0
- package/dist/schemas/config.js.map +1 -0
- package/dist/schemas/index.d.ts +6 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +6 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/item.d.ts +38 -0
- package/dist/schemas/item.d.ts.map +1 -0
- package/dist/schemas/item.js +34 -0
- package/dist/schemas/item.js.map +1 -0
- package/dist/schemas/research.d.ts +82 -0
- package/dist/schemas/research.d.ts.map +1 -0
- package/dist/schemas/research.js +45 -0
- package/dist/schemas/research.js.map +1 -0
- package/dist/schemas/source.d.ts +139 -0
- package/dist/schemas/source.d.ts.map +1 -0
- package/dist/schemas/source.js +127 -0
- package/dist/schemas/source.js.map +1 -0
- package/dist/schemas/state.d.ts +19 -0
- package/dist/schemas/state.d.ts.map +1 -0
- package/dist/schemas/state.js +12 -0
- package/dist/schemas/state.js.map +1 -0
- package/dist/skills/research/SKILL.md +156 -0
- package/dist/skills/review/SKILL.md +173 -0
- package/dist/skills/update/SKILL.md +200 -0
- package/dist/templates/agents/AGENTS.md +161 -0
- package/dist/templates/claude/CLAUDE.md +5 -0
- package/dist/templates/default.md +16 -0
- package/dist/templates/feedradar.md +165 -0
- package/dist/templates/routines/watch-daily.md +42 -0
- package/dist/templates/workflows/watch.yaml +70 -0
- package/package.json +73 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { parse as parseYaml } from "yaml";
|
|
4
|
+
import { RadarConfigSchema } from "../schemas/index.js";
|
|
5
|
+
/** File name of the workspace config, located at the workspace root. */
|
|
6
|
+
export const CONFIG_FILENAME = "radar.config.yaml";
|
|
7
|
+
/**
|
|
8
|
+
* Hard-coded fallback when neither `--agent` nor `radar.config.yaml` sets a
|
|
9
|
+
* default. Documented in user-guide.md and architecture.md (ADR-0001).
|
|
10
|
+
*/
|
|
11
|
+
export const HARDCODED_DEFAULT_AGENT = "claude-code";
|
|
12
|
+
async function pathExists(p) {
|
|
13
|
+
try {
|
|
14
|
+
await access(p);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Error raised when `radar.config.yaml` exists but fails to parse or violates
|
|
23
|
+
* `RadarConfigSchema`. Throws (rather than fall-back to defaults) because the
|
|
24
|
+
* user explicitly authored a config and silently ignoring it would surprise
|
|
25
|
+
* them and mask typos like `defaultResarchAgent`.
|
|
26
|
+
*/
|
|
27
|
+
export class RadarConfigError extends Error {
|
|
28
|
+
constructor(message) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = "RadarConfigError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Load `radar.config.yaml` from a workspace root.
|
|
35
|
+
*
|
|
36
|
+
* Behavior:
|
|
37
|
+
* - File missing -> returns an empty config (`{}`).
|
|
38
|
+
* - File present and valid -> returns the parsed `RadarConfig`.
|
|
39
|
+
* - File present but malformed (YAML parse error / schema violation)
|
|
40
|
+
* -> throws `RadarConfigError` with a contextual message.
|
|
41
|
+
*
|
|
42
|
+
* Returning `{}` for the missing-file case lets callers treat absence as
|
|
43
|
+
* "use the hard-coded default" without an extra `await pathExists` round-trip.
|
|
44
|
+
*/
|
|
45
|
+
export async function loadRadarConfig(cwd) {
|
|
46
|
+
const file = join(cwd, CONFIG_FILENAME);
|
|
47
|
+
if (!(await pathExists(file))) {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
let raw;
|
|
51
|
+
try {
|
|
52
|
+
raw = await readFile(file, "utf8");
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
throw new RadarConfigError(`failed to read ${CONFIG_FILENAME}: ${e instanceof Error ? e.message : String(e)}`);
|
|
56
|
+
}
|
|
57
|
+
let parsed;
|
|
58
|
+
try {
|
|
59
|
+
parsed = parseYaml(raw);
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
throw new RadarConfigError(`failed to parse ${CONFIG_FILENAME} as YAML: ${e instanceof Error ? e.message : String(e)}`);
|
|
63
|
+
}
|
|
64
|
+
// An empty file parses to `null` / `undefined`; normalize to `{}` so the
|
|
65
|
+
// schema validation treats it the same as an absent file.
|
|
66
|
+
const candidate = parsed ?? {};
|
|
67
|
+
const result = RadarConfigSchema.safeParse(candidate);
|
|
68
|
+
if (!result.success) {
|
|
69
|
+
const issues = result.error.issues
|
|
70
|
+
.map((i) => ` - ${i.path.join(".") || "<root>"}: ${i.message}`)
|
|
71
|
+
.join("\n");
|
|
72
|
+
throw new RadarConfigError(`${CONFIG_FILENAME} schema violation:\n${issues}`);
|
|
73
|
+
}
|
|
74
|
+
return result.data;
|
|
75
|
+
}
|
|
76
|
+
export async function getDefaultAgent(command, options = {}) {
|
|
77
|
+
if (options.explicit) {
|
|
78
|
+
return options.explicit;
|
|
79
|
+
}
|
|
80
|
+
const config = options.configOverride ?? (await loadRadarConfig(options.cwd ?? process.cwd()));
|
|
81
|
+
const fromConfig = pickConfigDefault(command, config);
|
|
82
|
+
if (fromConfig) {
|
|
83
|
+
return fromConfig;
|
|
84
|
+
}
|
|
85
|
+
return HARDCODED_DEFAULT_AGENT;
|
|
86
|
+
}
|
|
87
|
+
function pickConfigDefault(command, config) {
|
|
88
|
+
switch (command) {
|
|
89
|
+
case "research":
|
|
90
|
+
return config.defaultResearchAgent;
|
|
91
|
+
case "review":
|
|
92
|
+
return config.defaultReviewAgent;
|
|
93
|
+
default: {
|
|
94
|
+
// Exhaustiveness check — adding a new `ConfigurableCommand` without
|
|
95
|
+
// updating this switch becomes a compile error.
|
|
96
|
+
const _exhaustive = command;
|
|
97
|
+
return _exhaustive;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Candidate inputs an adapter passes to `deriveStableKey()`. The adapter
|
|
3
|
+
* ranks its candidates in preferred order (most-stable first); the helper
|
|
4
|
+
* returns the first non-empty value, falling back to a `sha1:`-prefixed
|
|
5
|
+
* content hash so the result is **always** defined.
|
|
6
|
+
*
|
|
7
|
+
* - `publisherId`: a stable id the publisher itself declares (RSS `guid`,
|
|
8
|
+
* GitHub release id, npm `<pkg>@<version>`, …). Preferred when present.
|
|
9
|
+
* - `url`: the canonical URL of the entity. Preferred fallback because URLs
|
|
10
|
+
* are usually stable across re-fetches even when no explicit id is given.
|
|
11
|
+
* - `fallbackHashInputs`: free-form strings (title, pubDate, …) hashed only
|
|
12
|
+
* when neither `publisherId` nor `url` exists. The `sha1:` prefix is
|
|
13
|
+
* retained from the legacy implementation so existing ids stay byte-stable
|
|
14
|
+
* across the refactor.
|
|
15
|
+
*/
|
|
16
|
+
export interface StableKeyCandidates {
|
|
17
|
+
publisherId?: string;
|
|
18
|
+
url?: string;
|
|
19
|
+
fallbackHashInputs?: Array<string | undefined>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Pick the most stable identifier available for an entity from a
|
|
23
|
+
* publisher-id-first fallback ladder. See ADR-0002 "Item ID 派生のコントラクト"
|
|
24
|
+
* for the contract this helper enforces.
|
|
25
|
+
*
|
|
26
|
+
* The return value is opaque — callers pass it directly to `deriveItemId()`
|
|
27
|
+
* (or hash it themselves) without inspecting the contents.
|
|
28
|
+
*/
|
|
29
|
+
export declare function deriveStableKey(candidates: StableKeyCandidates): string;
|
|
30
|
+
/**
|
|
31
|
+
* Build the canonical `Item.id` for a feed entry.
|
|
32
|
+
*
|
|
33
|
+
* Shape: `<title-slug>-<8 hex of sha256(stableKey)>` (or just the hash when
|
|
34
|
+
* the title contains no slug-friendly characters).
|
|
35
|
+
*
|
|
36
|
+
* The title slug keeps ids human-readable in shell args and log lines; the
|
|
37
|
+
* hash suffix makes the id stable across re-fetches and avoids collisions
|
|
38
|
+
* between entries with identical titles within the same source. We hash the
|
|
39
|
+
* adapter-selected `stableKey`, not the raw title, so two posts with the
|
|
40
|
+
* same title still get distinct ids when their publisher ids differ.
|
|
41
|
+
*/
|
|
42
|
+
export declare function deriveItemId(title: string | undefined, stableKey: string): string;
|
|
43
|
+
//# sourceMappingURL=derive-id.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derive-id.d.ts","sourceRoot":"","sources":["../../../src/core/feeds/derive-id.ts"],"names":[],"mappings":"AAiBA;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,mBAAmB;IAClC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kBAAkB,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CAChD;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,mBAAmB,GAAG,MAAM,CAOvE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAIjF"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Maximum number of characters kept from the title slug before the hash
|
|
4
|
+
* suffix. 40 keeps `Item.id` short enough for shell args and file names while
|
|
5
|
+
* still leaving room for human-readable context.
|
|
6
|
+
*/
|
|
7
|
+
const SLUG_MAX_LENGTH = 40;
|
|
8
|
+
/**
|
|
9
|
+
* Length of the hash suffix appended to the slug. 8 hex chars (32 bits) is
|
|
10
|
+
* enough to disambiguate same-title entries within a single source without
|
|
11
|
+
* bloating ids — feeds have at most a few thousand active items, far below
|
|
12
|
+
* the birthday-collision threshold.
|
|
13
|
+
*/
|
|
14
|
+
const HASH_SUFFIX_LENGTH = 8;
|
|
15
|
+
/**
|
|
16
|
+
* Pick the most stable identifier available for an entity from a
|
|
17
|
+
* publisher-id-first fallback ladder. See ADR-0002 "Item ID 派生のコントラクト"
|
|
18
|
+
* for the contract this helper enforces.
|
|
19
|
+
*
|
|
20
|
+
* The return value is opaque — callers pass it directly to `deriveItemId()`
|
|
21
|
+
* (or hash it themselves) without inspecting the contents.
|
|
22
|
+
*/
|
|
23
|
+
export function deriveStableKey(candidates) {
|
|
24
|
+
const publisherId = trimToValue(candidates.publisherId);
|
|
25
|
+
if (publisherId)
|
|
26
|
+
return publisherId;
|
|
27
|
+
const url = trimToValue(candidates.url);
|
|
28
|
+
if (url)
|
|
29
|
+
return url;
|
|
30
|
+
const joined = (candidates.fallbackHashInputs ?? []).map((v) => v ?? "").join("|");
|
|
31
|
+
return `sha1:${createHash("sha1").update(joined).digest("hex")}`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build the canonical `Item.id` for a feed entry.
|
|
35
|
+
*
|
|
36
|
+
* Shape: `<title-slug>-<8 hex of sha256(stableKey)>` (or just the hash when
|
|
37
|
+
* the title contains no slug-friendly characters).
|
|
38
|
+
*
|
|
39
|
+
* The title slug keeps ids human-readable in shell args and log lines; the
|
|
40
|
+
* hash suffix makes the id stable across re-fetches and avoids collisions
|
|
41
|
+
* between entries with identical titles within the same source. We hash the
|
|
42
|
+
* adapter-selected `stableKey`, not the raw title, so two posts with the
|
|
43
|
+
* same title still get distinct ids when their publisher ids differ.
|
|
44
|
+
*/
|
|
45
|
+
export function deriveItemId(title, stableKey) {
|
|
46
|
+
const hash = createHash("sha256").update(stableKey).digest("hex").slice(0, HASH_SUFFIX_LENGTH);
|
|
47
|
+
const slug = slugifyTitle(title);
|
|
48
|
+
return slug ? `${slug}-${hash}` : hash;
|
|
49
|
+
}
|
|
50
|
+
/** Title → kebab-case, lowercase, ASCII-only, capped at SLUG_MAX_LENGTH. */
|
|
51
|
+
function slugifyTitle(title) {
|
|
52
|
+
return (title ?? "")
|
|
53
|
+
.toLowerCase()
|
|
54
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
55
|
+
.replace(/^-+|-+$/g, "")
|
|
56
|
+
.slice(0, SLUG_MAX_LENGTH)
|
|
57
|
+
.replace(/-+$/g, "");
|
|
58
|
+
}
|
|
59
|
+
/** Trim a candidate and return `undefined` for empty strings. */
|
|
60
|
+
function trimToValue(value) {
|
|
61
|
+
if (value == null)
|
|
62
|
+
return undefined;
|
|
63
|
+
const trimmed = value.trim();
|
|
64
|
+
return trimmed.length === 0 ? undefined : trimmed;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=derive-id.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derive-id.js","sourceRoot":"","sources":["../../../src/core/feeds/derive-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC;;;;GAIG;AACH,MAAM,eAAe,GAAG,EAAE,CAAC;AAE3B;;;;;GAKG;AACH,MAAM,kBAAkB,GAAG,CAAC,CAAC;AAuB7B;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,UAA+B;IAC7D,MAAM,WAAW,GAAG,WAAW,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;IACxD,IAAI,WAAW;QAAE,OAAO,WAAW,CAAC;IACpC,MAAM,GAAG,GAAG,WAAW,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;IACxC,IAAI,GAAG;QAAE,OAAO,GAAG,CAAC;IACpB,MAAM,MAAM,GAAG,CAAC,UAAU,CAAC,kBAAkB,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACnF,OAAO,QAAQ,UAAU,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;AACnE,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,YAAY,CAAC,KAAyB,EAAE,SAAiB;IACvE,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,kBAAkB,CAAC,CAAC;IAC/F,MAAM,IAAI,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;IACjC,OAAO,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AACzC,CAAC;AAED,4EAA4E;AAC5E,SAAS,YAAY,CAAC,KAAyB;IAC7C,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC;SACjB,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;SACvB,KAAK,CAAC,CAAC,EAAE,eAAe,CAAC;SACzB,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AACzB,CAAC;AAED,iEAAiE;AACjE,SAAS,WAAW,CAAC,KAAyB;IAC5C,IAAI,KAAK,IAAI,IAAI;QAAE,OAAO,SAAS,CAAC;IACpC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,OAAO,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;AACpD,CAAC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { FetchLike } from "./types.js";
|
|
2
|
+
/** A single GitHub Release as returned by the REST API. Only fields we touch are typed. */
|
|
3
|
+
export interface GitHubRelease {
|
|
4
|
+
id: number;
|
|
5
|
+
tag_name: string;
|
|
6
|
+
name: string | null;
|
|
7
|
+
body: string | null;
|
|
8
|
+
draft: boolean;
|
|
9
|
+
prerelease: boolean;
|
|
10
|
+
html_url: string;
|
|
11
|
+
published_at: string | null;
|
|
12
|
+
created_at: string;
|
|
13
|
+
}
|
|
14
|
+
/** Parsed `owner/repo` extracted from various URL/shorthand inputs. */
|
|
15
|
+
export interface OwnerRepo {
|
|
16
|
+
owner: string;
|
|
17
|
+
repo: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Accept both `https://github.com/<owner>/<repo>` and shorthand `<owner>/<repo>`.
|
|
21
|
+
*
|
|
22
|
+
* We intentionally allow trailing path segments (`.../tree/main`, `.git`) so
|
|
23
|
+
* users can paste any GitHub URL without trimming first.
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseOwnerRepo(input: string): OwnerRepo;
|
|
26
|
+
/** Build the canonical Releases API URL for an `owner/repo`. */
|
|
27
|
+
export declare function buildReleasesUrl(owner: string, repo: string): string;
|
|
28
|
+
/** Inputs to `fetchReleases()`. `fetch` and `token` are injectable for tests. */
|
|
29
|
+
export interface FetchReleasesOptions {
|
|
30
|
+
fetch?: FetchLike;
|
|
31
|
+
/** GitHub PAT (or any token accepted by GitHub). Defaults to `process.env.GITHUB_TOKEN`. */
|
|
32
|
+
token?: string;
|
|
33
|
+
/** `If-None-Match` value from a previous response. */
|
|
34
|
+
etag?: string;
|
|
35
|
+
/** Logger for rate-limit warnings; defaults to `console.warn`. */
|
|
36
|
+
warn?: (message: string) => void;
|
|
37
|
+
signal?: AbortSignal;
|
|
38
|
+
}
|
|
39
|
+
/** Result shape returned by `fetchReleases()`. */
|
|
40
|
+
export interface FetchReleasesResult {
|
|
41
|
+
status: number;
|
|
42
|
+
releases: GitHubRelease[];
|
|
43
|
+
etag: string | null;
|
|
44
|
+
/** `true` when the server responded 304 Not Modified. */
|
|
45
|
+
notModified: boolean;
|
|
46
|
+
rateLimit: RateLimitInfo;
|
|
47
|
+
}
|
|
48
|
+
/** Subset of GitHub rate-limit headers we care about. */
|
|
49
|
+
export interface RateLimitInfo {
|
|
50
|
+
/** Requests remaining in the current window (`X-RateLimit-Remaining`). */
|
|
51
|
+
remaining: number | null;
|
|
52
|
+
/** Total quota for the current window (`X-RateLimit-Limit`). */
|
|
53
|
+
limit: number | null;
|
|
54
|
+
/** Unix epoch seconds when the window resets (`X-RateLimit-Reset`). */
|
|
55
|
+
resetAt: number | null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Fetch GitHub Releases for an `owner/repo`.
|
|
59
|
+
*
|
|
60
|
+
* Why we hit the REST API directly instead of bringing in `@octokit/rest`:
|
|
61
|
+
* the adapter only needs one endpoint, and dropping the dep keeps the
|
|
62
|
+
* published bundle small (see ADR-0002 / issue #37 commit 1 rationale).
|
|
63
|
+
*
|
|
64
|
+
* Authentication is opportunistic — without a token we still work, just at
|
|
65
|
+
* the much lower 60 req/h anonymous rate. The caller (CLI) is expected to
|
|
66
|
+
* surface `GITHUB_TOKEN` in user-facing docs.
|
|
67
|
+
*/
|
|
68
|
+
export declare function fetchReleases(owner: string, repo: string, options?: FetchReleasesOptions): Promise<FetchReleasesResult>;
|
|
69
|
+
//# sourceMappingURL=github-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github-api.d.ts","sourceRoot":"","sources":["../../../src/core/feeds/github-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAY5C,2FAA2F;AAC3F,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,uEAAuE;AACvE,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CA+BvD;AAED,gEAAgE;AAChE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpE;AAED,iFAAiF;AACjF,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,4FAA4F;IAC5F,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,kDAAkD;AAClD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,yDAAyD;IACzD,WAAW,EAAE,OAAO,CAAC;IACrB,SAAS,EAAE,aAAa,CAAC;CAC1B;AAED,yDAAyD;AACzD,MAAM,WAAW,aAAa;IAC5B,0EAA0E;IAC1E,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,gEAAgE;IAChE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,uEAAuE;IACvE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,aAAa,CACjC,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,mBAAmB,CAAC,CA+E9B"}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
const USER_AGENT = "feedradar/0.0.0 (+https://github.com/ozzy-labs/feedradar)";
|
|
2
|
+
const GITHUB_API_BASE = "https://api.github.com";
|
|
3
|
+
/**
|
|
4
|
+
* Threshold under which we surface a rate-limit warning. GitHub resets the
|
|
5
|
+
* counter every hour, so 10 leftover requests is the smallest cushion that
|
|
6
|
+
* still lets a typical run (1–2 sources) complete before reset.
|
|
7
|
+
*/
|
|
8
|
+
const RATE_LIMIT_WARNING_THRESHOLD = 10;
|
|
9
|
+
/**
|
|
10
|
+
* Accept both `https://github.com/<owner>/<repo>` and shorthand `<owner>/<repo>`.
|
|
11
|
+
*
|
|
12
|
+
* We intentionally allow trailing path segments (`.../tree/main`, `.git`) so
|
|
13
|
+
* users can paste any GitHub URL without trimming first.
|
|
14
|
+
*/
|
|
15
|
+
export function parseOwnerRepo(input) {
|
|
16
|
+
const trimmed = input.trim();
|
|
17
|
+
if (!trimmed)
|
|
18
|
+
throw new Error("github-releases adapter: empty source URL");
|
|
19
|
+
let candidate = trimmed;
|
|
20
|
+
// URL form — strip protocol/host and any trailing path.
|
|
21
|
+
if (/^https?:\/\//i.test(candidate)) {
|
|
22
|
+
let url;
|
|
23
|
+
try {
|
|
24
|
+
url = new URL(candidate);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
throw new Error(`github-releases adapter: invalid URL: ${input}`);
|
|
28
|
+
}
|
|
29
|
+
// Accept api.github.com/repos/<owner>/<repo> too, in case someone pastes an API URL.
|
|
30
|
+
const path = url.pathname.replace(/^\/repos\//, "/").replace(/^\/+|\/+$/g, "");
|
|
31
|
+
candidate = path;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
candidate = candidate.replace(/^\/+|\/+$/g, "");
|
|
35
|
+
}
|
|
36
|
+
// Strip a `.git` suffix and anything past the second segment (`tree/main`, etc.).
|
|
37
|
+
const segments = candidate.split("/").filter(Boolean);
|
|
38
|
+
const [ownerSegment, repoSegment] = segments;
|
|
39
|
+
if (!ownerSegment || !repoSegment) {
|
|
40
|
+
throw new Error(`github-releases adapter: expected <owner>/<repo>, got: ${input}`);
|
|
41
|
+
}
|
|
42
|
+
const repo = repoSegment.replace(/\.git$/i, "");
|
|
43
|
+
if (!repo) {
|
|
44
|
+
throw new Error(`github-releases adapter: expected <owner>/<repo>, got: ${input}`);
|
|
45
|
+
}
|
|
46
|
+
return { owner: ownerSegment, repo };
|
|
47
|
+
}
|
|
48
|
+
/** Build the canonical Releases API URL for an `owner/repo`. */
|
|
49
|
+
export function buildReleasesUrl(owner, repo) {
|
|
50
|
+
return `${GITHUB_API_BASE}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/releases`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Fetch GitHub Releases for an `owner/repo`.
|
|
54
|
+
*
|
|
55
|
+
* Why we hit the REST API directly instead of bringing in `@octokit/rest`:
|
|
56
|
+
* the adapter only needs one endpoint, and dropping the dep keeps the
|
|
57
|
+
* published bundle small (see ADR-0002 / issue #37 commit 1 rationale).
|
|
58
|
+
*
|
|
59
|
+
* Authentication is opportunistic — without a token we still work, just at
|
|
60
|
+
* the much lower 60 req/h anonymous rate. The caller (CLI) is expected to
|
|
61
|
+
* surface `GITHUB_TOKEN` in user-facing docs.
|
|
62
|
+
*/
|
|
63
|
+
export async function fetchReleases(owner, repo, options = {}) {
|
|
64
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
65
|
+
if (typeof fetchImpl !== "function") {
|
|
66
|
+
throw new Error("github-releases adapter: no fetch implementation available (Node 22+ required)");
|
|
67
|
+
}
|
|
68
|
+
const token = options.token ?? process.env.GITHUB_TOKEN;
|
|
69
|
+
const warn = options.warn ?? ((m) => console.warn(m));
|
|
70
|
+
const headers = {
|
|
71
|
+
accept: "application/vnd.github+json",
|
|
72
|
+
"user-agent": USER_AGENT,
|
|
73
|
+
// Pinning the API version protects us from breaking schema changes on
|
|
74
|
+
// the GitHub side without an explicit migration step.
|
|
75
|
+
"x-github-api-version": "2022-11-28",
|
|
76
|
+
};
|
|
77
|
+
if (token)
|
|
78
|
+
headers.authorization = `Bearer ${token}`;
|
|
79
|
+
if (options.etag)
|
|
80
|
+
headers["if-none-match"] = options.etag;
|
|
81
|
+
const url = buildReleasesUrl(owner, repo);
|
|
82
|
+
const response = await fetchImpl(url, { headers, signal: options.signal });
|
|
83
|
+
const rateLimit = {
|
|
84
|
+
remaining: parseIntHeader(response.headers.get("x-ratelimit-remaining")),
|
|
85
|
+
limit: parseIntHeader(response.headers.get("x-ratelimit-limit")),
|
|
86
|
+
resetAt: parseIntHeader(response.headers.get("x-ratelimit-reset")),
|
|
87
|
+
};
|
|
88
|
+
emitRateLimitWarning(rateLimit, owner, repo, token != null, warn);
|
|
89
|
+
const etag = response.headers.get("etag");
|
|
90
|
+
if (response.status === 304) {
|
|
91
|
+
return { status: 304, releases: [], etag, notModified: true, rateLimit };
|
|
92
|
+
}
|
|
93
|
+
if (response.status === 403 && rateLimit.remaining === 0) {
|
|
94
|
+
// Distinct error message so the CLI / docs can guide users to set
|
|
95
|
+
// `GITHUB_TOKEN`. The bare HTTP 403 message is ambiguous (could be auth).
|
|
96
|
+
const resetHint = formatResetHint(rateLimit.resetAt);
|
|
97
|
+
throw new Error(`github-releases adapter: rate limit exhausted for ${owner}/${repo}${resetHint}. ` +
|
|
98
|
+
(token
|
|
99
|
+
? "Authenticated quota is 5000 req/h."
|
|
100
|
+
: "Set GITHUB_TOKEN to raise the quota from 60 to 5000 req/h."));
|
|
101
|
+
}
|
|
102
|
+
if (response.status === 404) {
|
|
103
|
+
throw new Error(`github-releases adapter: repository not found: ${owner}/${repo}`);
|
|
104
|
+
}
|
|
105
|
+
if (response.status === 401) {
|
|
106
|
+
throw new Error(`github-releases adapter: authentication failed for ${owner}/${repo} (check GITHUB_TOKEN)`);
|
|
107
|
+
}
|
|
108
|
+
if (response.status < 200 || response.status >= 300) {
|
|
109
|
+
throw new Error(`github-releases adapter: HTTP ${response.status} from ${url}`);
|
|
110
|
+
}
|
|
111
|
+
const body = await response.text();
|
|
112
|
+
let parsed;
|
|
113
|
+
try {
|
|
114
|
+
parsed = JSON.parse(body);
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
throw new Error(`github-releases adapter: failed to parse JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
118
|
+
}
|
|
119
|
+
if (!Array.isArray(parsed)) {
|
|
120
|
+
throw new Error(`github-releases adapter: expected JSON array from ${url}, got ${typeof parsed}`);
|
|
121
|
+
}
|
|
122
|
+
const releases = parsed.filter(isGitHubRelease);
|
|
123
|
+
return { status: response.status, releases, etag, notModified: false, rateLimit };
|
|
124
|
+
}
|
|
125
|
+
/** Narrow `unknown` to `GitHubRelease`. Defensive — drops malformed entries silently. */
|
|
126
|
+
function isGitHubRelease(value) {
|
|
127
|
+
if (value == null || typeof value !== "object")
|
|
128
|
+
return false;
|
|
129
|
+
const v = value;
|
|
130
|
+
return (typeof v.id === "number" &&
|
|
131
|
+
typeof v.tag_name === "string" &&
|
|
132
|
+
typeof v.html_url === "string" &&
|
|
133
|
+
typeof v.draft === "boolean" &&
|
|
134
|
+
typeof v.prerelease === "boolean");
|
|
135
|
+
}
|
|
136
|
+
function parseIntHeader(value) {
|
|
137
|
+
if (value == null)
|
|
138
|
+
return null;
|
|
139
|
+
const n = Number.parseInt(value, 10);
|
|
140
|
+
return Number.isFinite(n) ? n : null;
|
|
141
|
+
}
|
|
142
|
+
function emitRateLimitWarning(rateLimit, owner, repo, authenticated, warn) {
|
|
143
|
+
const { remaining, limit } = rateLimit;
|
|
144
|
+
if (remaining == null || remaining > RATE_LIMIT_WARNING_THRESHOLD)
|
|
145
|
+
return;
|
|
146
|
+
const limitHint = limit != null ? `/${limit}` : "";
|
|
147
|
+
const authHint = authenticated
|
|
148
|
+
? ""
|
|
149
|
+
: " Set GITHUB_TOKEN to raise the quota from 60 to 5000 req/h.";
|
|
150
|
+
const resetHint = formatResetHint(rateLimit.resetAt);
|
|
151
|
+
warn(`github-releases: rate limit low (${remaining}${limitHint} remaining) for ${owner}/${repo}${resetHint}.${authHint}`);
|
|
152
|
+
}
|
|
153
|
+
function formatResetHint(resetAt) {
|
|
154
|
+
if (resetAt == null)
|
|
155
|
+
return "";
|
|
156
|
+
const date = new Date(resetAt * 1000);
|
|
157
|
+
if (Number.isNaN(date.getTime()))
|
|
158
|
+
return "";
|
|
159
|
+
return ` (resets at ${date.toISOString()})`;
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=github-api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github-api.js","sourceRoot":"","sources":["../../../src/core/feeds/github-api.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,GAAG,2DAA2D,CAAC;AAC/E,MAAM,eAAe,GAAG,wBAAwB,CAAC;AAEjD;;;;GAIG;AACH,MAAM,4BAA4B,GAAG,EAAE,CAAC;AAqBxC;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;IAE3E,IAAI,SAAS,GAAG,OAAO,CAAC;IACxB,wDAAwD;IACxD,IAAI,eAAe,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QACpC,IAAI,GAAQ,CAAC;QACb,IAAI,CAAC;YACH,GAAG,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC;QAC3B,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,KAAK,CAAC,yCAAyC,KAAK,EAAE,CAAC,CAAC;QACpE,CAAC;QACD,qFAAqF;QACrF,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;QAC/E,SAAS,GAAG,IAAI,CAAC;IACnB,CAAC;SAAM,CAAC;QACN,SAAS,GAAG,SAAS,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;IAClD,CAAC;IAED,kFAAkF;IAClF,MAAM,QAAQ,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACtD,MAAM,CAAC,YAAY,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC;IAC7C,IAAI,CAAC,YAAY,IAAI,CAAC,WAAW,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,0DAA0D,KAAK,EAAE,CAAC,CAAC;IACrF,CAAC;IACD,MAAM,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IAChD,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CAAC,0DAA0D,KAAK,EAAE,CAAC,CAAC;IACrF,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;AACvC,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,gBAAgB,CAAC,KAAa,EAAE,IAAY;IAC1D,OAAO,GAAG,eAAe,UAAU,kBAAkB,CAAC,KAAK,CAAC,IAAI,kBAAkB,CAAC,IAAI,CAAC,WAAW,CAAC;AACtG,CAAC;AAkCD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,KAAa,EACb,IAAY,EACZ,UAAgC,EAAE;IAElC,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,IAAK,UAAU,CAAC,KAA8B,CAAC;IAC9E,IAAI,OAAO,SAAS,KAAK,UAAU,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CACb,gFAAgF,CACjF,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;IACxD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAEtD,MAAM,OAAO,GAA2B;QACtC,MAAM,EAAE,6BAA6B;QACrC,YAAY,EAAE,UAAU;QACxB,sEAAsE;QACtE,sDAAsD;QACtD,sBAAsB,EAAE,YAAY;KACrC,CAAC;IACF,IAAI,KAAK;QAAE,OAAO,CAAC,aAAa,GAAG,UAAU,KAAK,EAAE,CAAC;IACrD,IAAI,OAAO,CAAC,IAAI;QAAE,OAAO,CAAC,eAAe,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAE1D,MAAM,GAAG,GAAG,gBAAgB,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAC1C,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAE3E,MAAM,SAAS,GAAkB;QAC/B,SAAS,EAAE,cAAc,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;QACxE,KAAK,EAAE,cAAc,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;QAChE,OAAO,EAAE,cAAc,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;KACnE,CAAC;IACF,oBAAoB,CAAC,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,IAAI,EAAE,IAAI,CAAC,CAAC;IAElE,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAE1C,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAC3E,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,SAAS,CAAC,SAAS,KAAK,CAAC,EAAE,CAAC;QACzD,kEAAkE;QAClE,0EAA0E;QAC1E,MAAM,SAAS,GAAG,eAAe,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QACrD,MAAM,IAAI,KAAK,CACb,qDAAqD,KAAK,IAAI,IAAI,GAAG,SAAS,IAAI;YAChF,CAAC,KAAK;gBACJ,CAAC,CAAC,oCAAoC;gBACtC,CAAC,CAAC,4DAA4D,CAAC,CACpE,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,kDAAkD,KAAK,IAAI,IAAI,EAAE,CAAC,CAAC;IACrF,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CACb,sDAAsD,KAAK,IAAI,IAAI,uBAAuB,CAC3F,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;QACpD,MAAM,IAAI,KAAK,CAAC,iCAAiC,QAAQ,CAAC,MAAM,SAAS,GAAG,EAAE,CAAC,CAAC;IAClF,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IACnC,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,kDAAkD,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAC/F,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CACb,qDAAqD,GAAG,SAAS,OAAO,MAAM,EAAE,CACjF,CAAC;IACJ,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;IAChD,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;AACpF,CAAC;AAED,yFAAyF;AACzF,SAAS,eAAe,CAAC,KAAc;IACrC,IAAI,KAAK,IAAI,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC7D,MAAM,CAAC,GAAG,KAAgC,CAAC;IAC3C,OAAO,CACL,OAAO,CAAC,CAAC,EAAE,KAAK,QAAQ;QACxB,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ;QAC9B,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ;QAC9B,OAAO,CAAC,CAAC,KAAK,KAAK,SAAS;QAC5B,OAAO,CAAC,CAAC,UAAU,KAAK,SAAS,CAClC,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CAAC,KAAoB;IAC1C,IAAI,KAAK,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IAC/B,MAAM,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACrC,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACvC,CAAC;AAED,SAAS,oBAAoB,CAC3B,SAAwB,EACxB,KAAa,EACb,IAAY,EACZ,aAAsB,EACtB,IAA+B;IAE/B,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,SAAS,CAAC;IACvC,IAAI,SAAS,IAAI,IAAI,IAAI,SAAS,GAAG,4BAA4B;QAAE,OAAO;IAC1E,MAAM,SAAS,GAAG,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACnD,MAAM,QAAQ,GAAG,aAAa;QAC5B,CAAC,CAAC,EAAE;QACJ,CAAC,CAAC,6DAA6D,CAAC;IAClE,MAAM,SAAS,GAAG,eAAe,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IACrD,IAAI,CACF,oCAAoC,SAAS,GAAG,SAAS,mBAAmB,KAAK,IAAI,IAAI,GAAG,SAAS,IAAI,QAAQ,EAAE,CACpH,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CAAC,OAAsB;IAC7C,IAAI,OAAO,IAAI,IAAI;QAAE,OAAO,EAAE,CAAC;IAC/B,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;IACtC,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;QAAE,OAAO,EAAE,CAAC;IAC5C,OAAO,eAAe,IAAI,CAAC,WAAW,EAAE,GAAG,CAAC;AAC9C,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github-releases.d.ts","sourceRoot":"","sources":["../../../src/core/feeds/github-releases.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAsB,MAAM,YAAY,CAAC;AAqDlE,eAAO,MAAM,qBAAqB,EAAE,WAmCnC,CAAC"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { ItemSchema } from "../../schemas/index.js";
|
|
2
|
+
import { deriveItemId, deriveStableKey } from "./derive-id.js";
|
|
3
|
+
import { fetchReleases, parseOwnerRepo } from "./github-api.js";
|
|
4
|
+
/**
|
|
5
|
+
* Normalize one GitHub Release into our canonical `Item` shape.
|
|
6
|
+
*
|
|
7
|
+
* Stable id derivation follows ADR-0002 §"Item ID 派生のコントラクト":
|
|
8
|
+
* - `publisherId`: `<tag_name>#<id>` — both fields can change independently
|
|
9
|
+
* (re-tags rewrite `tag_name` but keep `id`; deleted-and-recreated releases
|
|
10
|
+
* keep `tag_name` but get a new `id`). Combining them gives the strongest
|
|
11
|
+
* "same entity" signal available, matching the issue spec
|
|
12
|
+
* ("stableKey: release tag_name + id").
|
|
13
|
+
* - `url`: the release HTML URL — kept as a secondary fallback even though
|
|
14
|
+
* GitHub guarantees `id`, so the contract behaves uniformly across adapters.
|
|
15
|
+
* - Title slug: prefer the release `name`; fall back to `tag_name` when the
|
|
16
|
+
* maintainer left `name` blank (common on auto-cut releases).
|
|
17
|
+
*/
|
|
18
|
+
function releaseToItem(release, source, fetchedAt) {
|
|
19
|
+
const title = release.name?.trim() || release.tag_name;
|
|
20
|
+
const url = release.html_url;
|
|
21
|
+
const publishedAt = toIsoDate(release.published_at ?? release.created_at);
|
|
22
|
+
const summary = release.body?.trim() || undefined;
|
|
23
|
+
const stableKey = deriveStableKey({
|
|
24
|
+
publisherId: `${release.tag_name}#${release.id}`,
|
|
25
|
+
url,
|
|
26
|
+
fallbackHashInputs: [title, publishedAt],
|
|
27
|
+
});
|
|
28
|
+
const id = deriveItemId(title, stableKey);
|
|
29
|
+
const candidate = {
|
|
30
|
+
id,
|
|
31
|
+
sourceId: source.id,
|
|
32
|
+
title,
|
|
33
|
+
url,
|
|
34
|
+
summary,
|
|
35
|
+
publishedAt,
|
|
36
|
+
fetchedAt,
|
|
37
|
+
raw: release,
|
|
38
|
+
};
|
|
39
|
+
const result = ItemSchema.safeParse(candidate);
|
|
40
|
+
// Drop malformed entries silently — one broken release should not poison the
|
|
41
|
+
// whole feed (mirrors the RSS adapter's policy).
|
|
42
|
+
return result.success ? result.data : null;
|
|
43
|
+
}
|
|
44
|
+
/** Convert a GitHub timestamp to ISO 8601, returning `undefined` for invalid input. */
|
|
45
|
+
function toIsoDate(value) {
|
|
46
|
+
if (!value)
|
|
47
|
+
return undefined;
|
|
48
|
+
const date = new Date(value);
|
|
49
|
+
if (Number.isNaN(date.getTime()))
|
|
50
|
+
return undefined;
|
|
51
|
+
return date.toISOString();
|
|
52
|
+
}
|
|
53
|
+
export const githubReleasesAdapter = {
|
|
54
|
+
kind: "github-releases",
|
|
55
|
+
fetch: async (source, options = {}) => {
|
|
56
|
+
const { owner, repo } = parseOwnerRepo(source.url);
|
|
57
|
+
const previous = options.state;
|
|
58
|
+
const fetchedAt = new Date().toISOString();
|
|
59
|
+
const response = await fetchReleases(owner, repo, {
|
|
60
|
+
fetch: options.fetch,
|
|
61
|
+
etag: previous?.lastEtag,
|
|
62
|
+
});
|
|
63
|
+
if (response.notModified) {
|
|
64
|
+
return {
|
|
65
|
+
items: [],
|
|
66
|
+
notModified: true,
|
|
67
|
+
state: {
|
|
68
|
+
lastFetchedAt: fetchedAt,
|
|
69
|
+
lastEtag: response.etag ?? previous?.lastEtag,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const items = response.releases
|
|
74
|
+
.map((release) => releaseToItem(release, source, fetchedAt))
|
|
75
|
+
.filter((i) => i !== null);
|
|
76
|
+
return {
|
|
77
|
+
items,
|
|
78
|
+
state: {
|
|
79
|
+
lastFetchedAt: fetchedAt,
|
|
80
|
+
lastEtag: response.etag ?? previous?.lastEtag,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
//# sourceMappingURL=github-releases.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github-releases.js","sourceRoot":"","sources":["../../../src/core/feeds/github-releases.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Item, Source } from "../../schemas/index.js";
|
|
2
|
+
import type { FeedAdapter } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Parse an HTML document into validated `Item[]` using the source's
|
|
5
|
+
* `selectors`. Exported so tests can drive the parser directly without
|
|
6
|
+
* needing a fake HTTP layer.
|
|
7
|
+
*/
|
|
8
|
+
export declare function parseHtmlDocument(html: string, source: Source, fetchedAt: string): Item[];
|
|
9
|
+
export declare const htmlAdapter: FeedAdapter;
|
|
10
|
+
//# sourceMappingURL=html.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../../src/core/feeds/html.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAmB,MAAM,wBAAwB,CAAC;AAG5E,OAAO,KAAK,EAAE,WAAW,EAAiC,MAAM,YAAY,CAAC;AA6J7E;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,CAiBzF;AA+CD,eAAO,MAAM,WAAW,EAAE,WAuDzB,CAAC"}
|