@ozzylabs/feedradar 0.1.3 → 0.1.5

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.
Files changed (144) hide show
  1. package/README.ja.md +31 -6
  2. package/README.md +31 -6
  3. package/dist/agents/claude-code.d.ts +12 -1
  4. package/dist/agents/claude-code.d.ts.map +1 -1
  5. package/dist/agents/claude-code.js +9 -5
  6. package/dist/agents/claude-code.js.map +1 -1
  7. package/dist/agents/codex-cli.d.ts +7 -1
  8. package/dist/agents/codex-cli.d.ts.map +1 -1
  9. package/dist/agents/codex-cli.js +9 -5
  10. package/dist/agents/codex-cli.js.map +1 -1
  11. package/dist/agents/copilot.d.ts +7 -1
  12. package/dist/agents/copilot.d.ts.map +1 -1
  13. package/dist/agents/copilot.js +9 -5
  14. package/dist/agents/copilot.js.map +1 -1
  15. package/dist/agents/gemini-cli.d.ts +7 -1
  16. package/dist/agents/gemini-cli.d.ts.map +1 -1
  17. package/dist/agents/gemini-cli.js +9 -5
  18. package/dist/agents/gemini-cli.js.map +1 -1
  19. package/dist/agents/index.d.ts +1 -1
  20. package/dist/agents/index.d.ts.map +1 -1
  21. package/dist/agents/types.d.ts +33 -0
  22. package/dist/agents/types.d.ts.map +1 -1
  23. package/dist/cli/_progress.d.ts +138 -0
  24. package/dist/cli/_progress.d.ts.map +1 -0
  25. package/dist/cli/_progress.js +176 -0
  26. package/dist/cli/_progress.js.map +1 -0
  27. package/dist/cli/doctor.d.ts +20 -0
  28. package/dist/cli/doctor.d.ts.map +1 -1
  29. package/dist/cli/doctor.js +291 -2
  30. package/dist/cli/doctor.js.map +1 -1
  31. package/dist/cli/index.d.ts.map +1 -1
  32. package/dist/cli/index.js +2 -0
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/cli/research.d.ts +18 -20
  35. package/dist/cli/research.d.ts.map +1 -1
  36. package/dist/cli/research.js +318 -203
  37. package/dist/cli/research.js.map +1 -1
  38. package/dist/cli/respawn.d.ts +53 -0
  39. package/dist/cli/respawn.d.ts.map +1 -0
  40. package/dist/cli/respawn.js +120 -0
  41. package/dist/cli/respawn.js.map +1 -0
  42. package/dist/cli/review.d.ts +7 -0
  43. package/dist/cli/review.d.ts.map +1 -1
  44. package/dist/cli/review.js +46 -1
  45. package/dist/cli/review.js.map +1 -1
  46. package/dist/cli/source.d.ts +23 -2
  47. package/dist/cli/source.d.ts.map +1 -1
  48. package/dist/cli/source.js +425 -7
  49. package/dist/cli/source.js.map +1 -1
  50. package/dist/cli/update.d.ts +7 -0
  51. package/dist/cli/update.d.ts.map +1 -1
  52. package/dist/cli/update.js +41 -1
  53. package/dist/cli/update.js.map +1 -1
  54. package/dist/cli/watch.d.ts.map +1 -1
  55. package/dist/cli/watch.js +65 -3
  56. package/dist/cli/watch.js.map +1 -1
  57. package/dist/cli/workflow/generate-combined.d.ts +100 -0
  58. package/dist/cli/workflow/generate-combined.d.ts.map +1 -0
  59. package/dist/cli/workflow/generate-combined.js +387 -0
  60. package/dist/cli/workflow/generate-combined.js.map +1 -0
  61. package/dist/cli/workflow/generate-watch.d.ts +142 -0
  62. package/dist/cli/workflow/generate-watch.d.ts.map +1 -0
  63. package/dist/cli/workflow/generate-watch.js +338 -0
  64. package/dist/cli/workflow/generate-watch.js.map +1 -0
  65. package/dist/cli/workflow.d.ts +29 -0
  66. package/dist/cli/workflow.d.ts.map +1 -0
  67. package/dist/cli/workflow.js +66 -0
  68. package/dist/cli/workflow.js.map +1 -0
  69. package/dist/core/feeds/_fetch.d.ts +103 -0
  70. package/dist/core/feeds/_fetch.d.ts.map +1 -0
  71. package/dist/core/feeds/_fetch.js +364 -0
  72. package/dist/core/feeds/_fetch.js.map +1 -0
  73. package/dist/core/feeds/_jsonpath.d.ts +57 -0
  74. package/dist/core/feeds/_jsonpath.d.ts.map +1 -0
  75. package/dist/core/feeds/_jsonpath.js +207 -0
  76. package/dist/core/feeds/_jsonpath.js.map +1 -0
  77. package/dist/core/feeds/github-api.d.ts.map +1 -1
  78. package/dist/core/feeds/github-api.js +2 -1
  79. package/dist/core/feeds/github-api.js.map +1 -1
  80. package/dist/core/feeds/html-js.d.ts +29 -0
  81. package/dist/core/feeds/html-js.d.ts.map +1 -1
  82. package/dist/core/feeds/html-js.js +86 -2
  83. package/dist/core/feeds/html-js.js.map +1 -1
  84. package/dist/core/feeds/html.d.ts.map +1 -1
  85. package/dist/core/feeds/html.js +2 -1
  86. package/dist/core/feeds/html.js.map +1 -1
  87. package/dist/core/feeds/index.d.ts +1 -1
  88. package/dist/core/feeds/index.d.ts.map +1 -1
  89. package/dist/core/feeds/index.js +4 -0
  90. package/dist/core/feeds/index.js.map +1 -1
  91. package/dist/core/feeds/json-api.d.ts +3 -0
  92. package/dist/core/feeds/json-api.d.ts.map +1 -0
  93. package/dist/core/feeds/json-api.js +723 -0
  94. package/dist/core/feeds/json-api.js.map +1 -0
  95. package/dist/core/feeds/json-feed.d.ts +11 -0
  96. package/dist/core/feeds/json-feed.d.ts.map +1 -0
  97. package/dist/core/feeds/json-feed.js +242 -0
  98. package/dist/core/feeds/json-feed.js.map +1 -0
  99. package/dist/core/feeds/npm-registry.d.ts.map +1 -1
  100. package/dist/core/feeds/npm-registry.js +2 -1
  101. package/dist/core/feeds/npm-registry.js.map +1 -1
  102. package/dist/core/feeds/rss.d.ts.map +1 -1
  103. package/dist/core/feeds/rss.js +2 -1
  104. package/dist/core/feeds/rss.js.map +1 -1
  105. package/dist/core/feeds/types.d.ts +123 -0
  106. package/dist/core/feeds/types.d.ts.map +1 -1
  107. package/dist/core/progress.d.ts +101 -0
  108. package/dist/core/progress.d.ts.map +1 -0
  109. package/dist/core/progress.js +212 -0
  110. package/dist/core/progress.js.map +1 -0
  111. package/dist/core/proxy.d.ts +87 -0
  112. package/dist/core/proxy.d.ts.map +1 -0
  113. package/dist/core/proxy.js +146 -0
  114. package/dist/core/proxy.js.map +1 -0
  115. package/dist/core/recipes.d.ts +138 -0
  116. package/dist/core/recipes.d.ts.map +1 -0
  117. package/dist/core/recipes.js +238 -0
  118. package/dist/core/recipes.js.map +1 -0
  119. package/dist/core/watcher.d.ts +61 -1
  120. package/dist/core/watcher.d.ts.map +1 -1
  121. package/dist/core/watcher.js +99 -2
  122. package/dist/core/watcher.js.map +1 -1
  123. package/dist/index.js +17 -4
  124. package/dist/index.js.map +1 -1
  125. package/dist/recipes/aws-whats-new.yaml +61 -0
  126. package/dist/recipes/dev-to.yaml +40 -0
  127. package/dist/schemas/index.d.ts +1 -0
  128. package/dist/schemas/index.d.ts.map +1 -1
  129. package/dist/schemas/index.js +1 -0
  130. package/dist/schemas/index.js.map +1 -1
  131. package/dist/schemas/recipe.d.ts +115 -0
  132. package/dist/schemas/recipe.d.ts.map +1 -0
  133. package/dist/schemas/recipe.js +54 -0
  134. package/dist/schemas/recipe.js.map +1 -0
  135. package/dist/schemas/source.d.ts +130 -0
  136. package/dist/schemas/source.d.ts.map +1 -1
  137. package/dist/schemas/source.js +130 -0
  138. package/dist/schemas/source.js.map +1 -1
  139. package/dist/templates/agents/AGENTS.md +31 -3
  140. package/dist/templates/feedradar.md +23 -8
  141. package/dist/templates/workflows/combined.template.yaml.tmpl +110 -0
  142. package/dist/templates/workflows/watch.template.yaml.tmpl +103 -0
  143. package/dist/templates/workflows/watch.yaml +5 -1
  144. package/package.json +2 -3
@@ -0,0 +1,138 @@
1
+ import { type RecipeFile } from "../schemas/recipe.js";
2
+ /**
3
+ * Recipe loader and CLI-args merger for `radar source recipes` /
4
+ * `radar source add --recipe <name>` (ADR-0012 §D3, strategy A — リポ同梱).
5
+ *
6
+ * Design notes:
7
+ *
8
+ * - Recipes live in `recipes/*.yaml` at the package root and are bundled
9
+ * into npm publish via the package.json `files` allowlist plus a copy
10
+ * step in `scripts/copy-skills.mjs` (`recipes/` → `dist/recipes/`). The
11
+ * resolver tries the compiled location first, then falls back to the
12
+ * source tree (used by the test suite and `pnpm test` runs that have
13
+ * not built `dist/` yet).
14
+ *
15
+ * - The directory is allowed to be empty (#178 adds the actual bundled
16
+ * recipes). When the directory is missing entirely the loader behaves
17
+ * the same as "no recipes" — the bundle is optional at the schema
18
+ * level. The CLI surfaces a friendly "no recipes" message instead of
19
+ * an error in that case.
20
+ *
21
+ * - Each recipe is independently parse-and-validate so one malformed
22
+ * YAML does not prevent the rest from being listed. `listRecipes`
23
+ * returns per-recipe `error` strings; `loadRecipe(name)` throws so
24
+ * `--recipe <name>` can hard-fail at `source add` time.
25
+ *
26
+ * - The recipe's identifier ( `--recipe <name>` match key) is the YAML
27
+ * filename stem. There is no inner "name" field that doubles as the
28
+ * match key — recipe authors rename the file to rename the recipe.
29
+ * `RecipeFile.name` is the *display name* (mirrors `Source.name`).
30
+ */
31
+ /** A recipe loaded from disk, paired with its filename-derived identifier. */
32
+ export interface LoadedRecipe {
33
+ /** Filename stem (e.g. `aws-whats-new` for `aws-whats-new.yaml`). */
34
+ name: string;
35
+ /** Absolute path of the recipe YAML, useful for error messages. */
36
+ path: string;
37
+ recipe: RecipeFile;
38
+ }
39
+ /** Entry returned by `listRecipes`, including malformed recipes (with `error`). */
40
+ export interface RecipeListEntry {
41
+ name: string;
42
+ path: string;
43
+ /** Parsed recipe, or `null` when this entry failed to load (see `error`). */
44
+ recipe: RecipeFile | null;
45
+ /** Human-readable error string when the entry could not be loaded. */
46
+ error?: string;
47
+ }
48
+ /** Options for the loader/lister to override the recipes directory (used by tests). */
49
+ export interface RecipeLoaderOptions {
50
+ recipesRoot?: string;
51
+ }
52
+ /**
53
+ * Resolve the directory holding the bundled recipes.
54
+ *
55
+ * Compiled layout (npm install): `dist/core/recipes.js` → `../recipes`.
56
+ * Source layout (tests / `pnpm test`): `src/core/recipes.ts` → `../../recipes`.
57
+ *
58
+ * We probe the compiled location first because that is the path users
59
+ * hit at runtime. Both paths can be present during local development
60
+ * (after `pnpm run build`); preferring compiled keeps the source tree
61
+ * from being the active asset directory by accident.
62
+ */
63
+ export declare function resolveRecipesRoot(): Promise<string>;
64
+ /**
65
+ * List all bundled recipes by reading every `*.yaml` file in the recipes
66
+ * directory.
67
+ *
68
+ * Behaviour:
69
+ *
70
+ * - Missing recipes directory → returns `[]` (treated as "no recipes",
71
+ * not an error). This matches the bootstrap state where #178 has not
72
+ * yet shipped the actual recipe files.
73
+ * - Each `.yaml` is independently parsed and Zod-validated. Failures are
74
+ * captured in the per-entry `error` field so partial corruption never
75
+ * prevents the rest from rendering.
76
+ * - Entries are sorted by `name` for deterministic output (tests rely on
77
+ * this; users get a stable display order).
78
+ */
79
+ export declare function listRecipes(opts?: RecipeLoaderOptions): Promise<RecipeListEntry[]>;
80
+ /**
81
+ * Load a single recipe by its filename stem (e.g. `aws-whats-new`).
82
+ *
83
+ * Throws on:
84
+ *
85
+ * - missing recipes directory (the bundle is absent)
86
+ * - unknown recipe name (the file does not exist)
87
+ * - malformed YAML or Zod-schema violation
88
+ *
89
+ * The error messages are user-facing — `source add --recipe` surfaces
90
+ * them via the CLI `error()` sink without further wrapping.
91
+ */
92
+ export declare function loadRecipe(name: string, opts?: RecipeLoaderOptions): Promise<LoadedRecipe>;
93
+ /**
94
+ * CLI overrides applied on top of a recipe when generating a Source.
95
+ *
96
+ * The whitelist is intentionally narrow:
97
+ *
98
+ * - `id` (required) — recipes never carry an `id`; the caller picks one
99
+ * per workspace
100
+ * - `name` (display name) — useful when a single recipe is applied
101
+ * multiple times to differentiate, or to localize
102
+ * - `tags` — workspace-level taxonomy that varies per install
103
+ * - `filters.keywords` / `filters.excludeKeywords` — the only fields a
104
+ * user is reliably expected to override; "what counts as a hit" is
105
+ * per-workspace
106
+ *
107
+ * Other fields (`pagination`, `jsonSelectors`, `selectors`, `js`,
108
+ * `http`, `url`, `kind`, `trustLevel`) are NOT overridable from the
109
+ * CLI. Recipe authors own these "structural" fields. Users edit the
110
+ * generated `sources/<id>.yaml` if they need to deviate further.
111
+ */
112
+ export interface RecipeOverrides {
113
+ /** Required — the source id chosen by the caller. */
114
+ id: string;
115
+ /** Optional override for `Source.name` (display name). */
116
+ name?: string;
117
+ /** Optional override for `Source.tags` (replaces, does not merge). */
118
+ tags?: string[];
119
+ /** Optional override for `filters.keywords` (replaces, does not merge). */
120
+ keywords?: string[];
121
+ /** Optional override for `filters.excludeKeywords` (replaces, does not merge). */
122
+ excludeKeywords?: string[];
123
+ }
124
+ /**
125
+ * Merge a recipe with CLI overrides to produce a plain object suitable
126
+ * for `SourceSchema.safeParse`.
127
+ *
128
+ * Override semantics: each field is *replaced* (not merged) when the
129
+ * override is present. This mirrors `source add` flag semantics
130
+ * (`--keywords a,b` replaces, never appends) and keeps the mental model
131
+ * uniform across `add` and `add --recipe`.
132
+ *
133
+ * `description` from the recipe is dropped — it is recipe metadata,
134
+ * not Source metadata. Strip it explicitly so the generated YAML does
135
+ * not carry a stray field that fails downstream schema validation.
136
+ */
137
+ export declare function mergeRecipeWithOverrides(recipe: RecipeFile, overrides: RecipeOverrides): Record<string, unknown>;
138
+ //# sourceMappingURL=recipes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recipes.d.ts","sourceRoot":"","sources":["../../src/core/recipes.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,UAAU,EAAoB,MAAM,sBAAsB,CAAC;AAEzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,8EAA8E;AAC9E,MAAM,WAAW,YAAY;IAC3B,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;CACpB;AAED,mFAAmF;AACnF,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,MAAM,EAAE,UAAU,GAAG,IAAI,CAAC;IAC1B,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,uFAAuF;AACvF,MAAM,WAAW,mBAAmB;IAClC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC,CAS1D;AAWD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,WAAW,CAAC,IAAI,GAAE,mBAAwB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CA8D5F;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,mBAAwB,GAC7B,OAAO,CAAC,YAAY,CAAC,CAyDvB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,WAAW,eAAe;IAC9B,qDAAqD;IACrD,EAAE,EAAE,MAAM,CAAC;IACX,0DAA0D;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,kFAAkF;IAClF,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,UAAU,EAClB,SAAS,EAAE,eAAe,GACzB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA+CzB"}
@@ -0,0 +1,238 @@
1
+ import { access, readdir, readFile } from "node:fs/promises";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { parse as parseYaml } from "yaml";
5
+ import { RecipeFileSchema } from "../schemas/recipe.js";
6
+ /**
7
+ * Resolve the directory holding the bundled recipes.
8
+ *
9
+ * Compiled layout (npm install): `dist/core/recipes.js` → `../recipes`.
10
+ * Source layout (tests / `pnpm test`): `src/core/recipes.ts` → `../../recipes`.
11
+ *
12
+ * We probe the compiled location first because that is the path users
13
+ * hit at runtime. Both paths can be present during local development
14
+ * (after `pnpm run build`); preferring compiled keeps the source tree
15
+ * from being the active asset directory by accident.
16
+ */
17
+ export async function resolveRecipesRoot() {
18
+ const here = dirname(fileURLToPath(import.meta.url));
19
+ const compiled = resolve(here, "../recipes");
20
+ if (await pathExists(compiled)) {
21
+ return compiled;
22
+ }
23
+ // Source layout fallback. We walk two levels up from src/core/ to find
24
+ // the package root, then descend into `recipes/`.
25
+ return resolve(here, "../../recipes");
26
+ }
27
+ async function pathExists(p) {
28
+ try {
29
+ await access(p);
30
+ return true;
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ }
36
+ /**
37
+ * List all bundled recipes by reading every `*.yaml` file in the recipes
38
+ * directory.
39
+ *
40
+ * Behaviour:
41
+ *
42
+ * - Missing recipes directory → returns `[]` (treated as "no recipes",
43
+ * not an error). This matches the bootstrap state where #178 has not
44
+ * yet shipped the actual recipe files.
45
+ * - Each `.yaml` is independently parsed and Zod-validated. Failures are
46
+ * captured in the per-entry `error` field so partial corruption never
47
+ * prevents the rest from rendering.
48
+ * - Entries are sorted by `name` for deterministic output (tests rely on
49
+ * this; users get a stable display order).
50
+ */
51
+ export async function listRecipes(opts = {}) {
52
+ const root = opts.recipesRoot ?? (await resolveRecipesRoot());
53
+ if (!(await pathExists(root))) {
54
+ return [];
55
+ }
56
+ let entries;
57
+ try {
58
+ entries = await readdir(root);
59
+ }
60
+ catch {
61
+ return [];
62
+ }
63
+ // `.gitkeep` (or any other dotfile) must not be picked up as a recipe;
64
+ // the `*.yaml` glob is enforced by suffix rather than a separate
65
+ // exclude list.
66
+ const yamlFiles = entries.filter((f) => f.endsWith(".yaml")).sort();
67
+ const results = [];
68
+ for (const filename of yamlFiles) {
69
+ const path = join(root, filename);
70
+ const name = filename.slice(0, -".yaml".length);
71
+ let raw;
72
+ try {
73
+ raw = await readFile(path, "utf8");
74
+ }
75
+ catch (e) {
76
+ results.push({
77
+ name,
78
+ path,
79
+ recipe: null,
80
+ error: `failed to read: ${e instanceof Error ? e.message : String(e)}`,
81
+ });
82
+ continue;
83
+ }
84
+ let parsed;
85
+ try {
86
+ parsed = parseYaml(raw);
87
+ }
88
+ catch (e) {
89
+ results.push({
90
+ name,
91
+ path,
92
+ recipe: null,
93
+ error: `invalid YAML: ${e instanceof Error ? e.message : String(e)}`,
94
+ });
95
+ continue;
96
+ }
97
+ const result = RecipeFileSchema.safeParse(parsed);
98
+ if (!result.success) {
99
+ const issues = result.error.issues
100
+ .map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`)
101
+ .join("; ");
102
+ results.push({
103
+ name,
104
+ path,
105
+ recipe: null,
106
+ error: `schema validation failed: ${issues}`,
107
+ });
108
+ continue;
109
+ }
110
+ results.push({ name, path, recipe: result.data });
111
+ }
112
+ return results;
113
+ }
114
+ /**
115
+ * Load a single recipe by its filename stem (e.g. `aws-whats-new`).
116
+ *
117
+ * Throws on:
118
+ *
119
+ * - missing recipes directory (the bundle is absent)
120
+ * - unknown recipe name (the file does not exist)
121
+ * - malformed YAML or Zod-schema violation
122
+ *
123
+ * The error messages are user-facing — `source add --recipe` surfaces
124
+ * them via the CLI `error()` sink without further wrapping.
125
+ */
126
+ export async function loadRecipe(name, opts = {}) {
127
+ // Reject path-separator / traversal characters defensively. `--recipe`
128
+ // is positional CLI input and could be copied from arbitrary sources;
129
+ // the same posture as `isSafeSourceId` keeps the lookup confined to
130
+ // the recipes directory.
131
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) || name.includes("..")) {
132
+ throw new Error(`invalid recipe name '${name}' (must match [A-Za-z0-9][A-Za-z0-9._-]*)`);
133
+ }
134
+ const root = opts.recipesRoot ?? (await resolveRecipesRoot());
135
+ if (!(await pathExists(root))) {
136
+ throw new Error(`no bundled recipes available (recipes/ not found at ${root}); recipe '${name}' cannot be resolved`);
137
+ }
138
+ const path = join(root, `${name}.yaml`);
139
+ if (!(await pathExists(path))) {
140
+ // Surface available names so the user can self-correct without having
141
+ // to run a second command. List failures are swallowed here (best
142
+ // effort) so the primary error message is the one the user sees.
143
+ let available = [];
144
+ try {
145
+ const all = await listRecipes({ recipesRoot: root });
146
+ available = all.map((r) => r.name);
147
+ }
148
+ catch {
149
+ // ignore — we already have the primary error to report
150
+ }
151
+ const hint = available.length === 0 ? "" : ` (available: ${available.join(", ")})`;
152
+ throw new Error(`recipe '${name}' not found${hint}`);
153
+ }
154
+ let raw;
155
+ try {
156
+ raw = await readFile(path, "utf8");
157
+ }
158
+ catch (e) {
159
+ throw new Error(`failed to read recipe '${name}': ${e instanceof Error ? e.message : String(e)}`);
160
+ }
161
+ let parsed;
162
+ try {
163
+ parsed = parseYaml(raw);
164
+ }
165
+ catch (e) {
166
+ throw new Error(`invalid YAML in recipe '${name}': ${e instanceof Error ? e.message : String(e)}`);
167
+ }
168
+ const result = RecipeFileSchema.safeParse(parsed);
169
+ if (!result.success) {
170
+ const issues = result.error.issues
171
+ .map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`)
172
+ .join("; ");
173
+ throw new Error(`recipe '${name}' failed schema validation: ${issues}`);
174
+ }
175
+ return { name, path, recipe: result.data };
176
+ }
177
+ /**
178
+ * Merge a recipe with CLI overrides to produce a plain object suitable
179
+ * for `SourceSchema.safeParse`.
180
+ *
181
+ * Override semantics: each field is *replaced* (not merged) when the
182
+ * override is present. This mirrors `source add` flag semantics
183
+ * (`--keywords a,b` replaces, never appends) and keeps the mental model
184
+ * uniform across `add` and `add --recipe`.
185
+ *
186
+ * `description` from the recipe is dropped — it is recipe metadata,
187
+ * not Source metadata. Strip it explicitly so the generated YAML does
188
+ * not carry a stray field that fails downstream schema validation.
189
+ */
190
+ export function mergeRecipeWithOverrides(recipe, overrides) {
191
+ // Build the candidate as a fresh object so the recipe object on disk
192
+ // is not mutated and we get control over field ordering in the output
193
+ // YAML (id first → kind → url → ...).
194
+ const candidate = {
195
+ id: overrides.id,
196
+ kind: recipe.kind,
197
+ url: recipe.url,
198
+ };
199
+ // Display name: caller override wins, then recipe.name, then nothing.
200
+ if (overrides.name !== undefined) {
201
+ candidate.name = overrides.name;
202
+ }
203
+ else if (recipe.name !== undefined) {
204
+ candidate.name = recipe.name;
205
+ }
206
+ // Tags: override replaces; otherwise inherit recipe tags (which defaults
207
+ // to []). Emit only when non-empty so the YAML stays minimal for the
208
+ // common case "no tags in either place".
209
+ const tags = overrides.tags ?? recipe.tags;
210
+ if (tags.length > 0) {
211
+ candidate.tags = tags;
212
+ }
213
+ // Filters: override the include/exclude keyword arrays; preserve the
214
+ // recipe's other filter knobs (matchMode / matchFields / caseSensitive)
215
+ // because those reflect adapter-specific tuning that the recipe author
216
+ // already picked.
217
+ const mergedFilters = {
218
+ ...recipe.filters,
219
+ keywords: overrides.keywords ?? recipe.filters.keywords,
220
+ excludeKeywords: overrides.excludeKeywords ?? recipe.filters.excludeKeywords,
221
+ };
222
+ candidate.filters = mergedFilters;
223
+ // Structural fields that the recipe owns. Drop undefined entries to
224
+ // keep the generated YAML free of explicit nulls.
225
+ if (recipe.selectors !== undefined)
226
+ candidate.selectors = recipe.selectors;
227
+ if (recipe.js !== undefined)
228
+ candidate.js = recipe.js;
229
+ if (recipe.http !== undefined)
230
+ candidate.http = recipe.http;
231
+ if (recipe.pagination !== undefined)
232
+ candidate.pagination = recipe.pagination;
233
+ if (recipe.jsonSelectors !== undefined)
234
+ candidate.jsonSelectors = recipe.jsonSelectors;
235
+ candidate.trustLevel = recipe.trustLevel;
236
+ return candidate;
237
+ }
238
+ //# sourceMappingURL=recipes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recipes.js","sourceRoot":"","sources":["../../src/core/recipes.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import type { Item, Source, SourceState } from "../schemas/index.js";
2
- import type { FeedAdapter, FetchLike } from "./feeds/index.js";
2
+ import type { FeedAdapter, FeedFetchDiag, FetchLike } from "./feeds/index.js";
3
3
  import { installChromium, type ProbeOptions } from "./playwright-check.js";
4
+ import type { ProgressReporter } from "./progress.js";
4
5
  export interface WorkspacePaths {
5
6
  /** Workspace root; defaults to process.cwd() at the CLI layer. */
6
7
  cwd: string;
@@ -17,6 +18,20 @@ export interface WatchRunOptions extends WorkspacePaths {
17
18
  * backlogs (ADR-0008 §運用: 初回ノイズ抑制).
18
19
  */
19
20
  bootstrap?: boolean;
21
+ /**
22
+ * Backfill mode (ADR-0012 §D4): walk paginated sources to ingest all
23
+ * available history into items/. Emits items AND updates state, unlike
24
+ * `bootstrap` which only updates state. Mutually exclusive with
25
+ * `bootstrap`; the CLI layer enforces the exclusivity, the watcher accepts
26
+ * whichever flag was passed.
27
+ */
28
+ backfill?: boolean;
29
+ /**
30
+ * Override the per-source `pagination.maxPages` cap. Threaded straight
31
+ * through to adapters that paginate (json-api / github-releases /
32
+ * npm-registry). Only honored when `backfill` is true.
33
+ */
34
+ maxPagesOverride?: number;
20
35
  /**
21
36
  * Dry-run mode: run the full fetch + filter pipeline but do not persist
22
37
  * anything to disk — neither item YAMLs under `items/` nor the updated
@@ -52,6 +67,17 @@ export interface WatchRunOptions extends WorkspacePaths {
52
67
  * records invocation without actually spawning `npx playwright install`.
53
68
  */
54
69
  installChromiumImpl?: typeof installChromium;
70
+ /**
71
+ * Optional progress reporter for per-source fetch phases (#198 /
72
+ * ADR-0015). The watcher gates wiring on a heuristic
73
+ * ({@link shouldEnableProgress}) so the typical small / fast workspace
74
+ * never sees flicker: progress is enabled only when at least 3 sources
75
+ * run together OR any source uses a slow kind (`html-js` / `json-api`).
76
+ *
77
+ * When the reporter is unset (or the heuristic is off) every source runs
78
+ * with no progress wiring — byte-equivalent to the pre-#198 behaviour.
79
+ */
80
+ progress?: ProgressReporter;
55
81
  }
56
82
  export interface WatchRunResult {
57
83
  /** Map of sourceId → detected (filter-passing, not previously seen) items. */
@@ -78,6 +104,17 @@ export interface WatchRunResult {
78
104
  fetched: number;
79
105
  filtered: number;
80
106
  }>;
107
+ /**
108
+ * Per-source diagnostic payload returned by adapters that produce one
109
+ * (currently only `kind: json-api` — see `FeedFetchDiag`). Populated for
110
+ * every source whose fetch returned a `diag` field; missing entries are
111
+ * legal and indicate the adapter does not surface diagnostics.
112
+ *
113
+ * Consumers (`radar source test --show-content`) render this alongside
114
+ * the matched-items preview so users can audit which default selector
115
+ * chain candidate was adopted and how pagination would advance.
116
+ */
117
+ diag: Record<string, FeedFetchDiag>;
81
118
  }
82
119
  /**
83
120
  * Load all enabled sources from `sources/*.yaml`.
@@ -87,6 +124,29 @@ export interface WatchRunResult {
87
124
  * behaves (see `src/cli/source.ts`).
88
125
  */
89
126
  export declare function loadSources(sourcesDir: string, onError: (message: string) => void): Promise<Source[]>;
127
+ /**
128
+ * Heuristic: should the watcher actively report per-source progress to the
129
+ * supplied {@link ProgressReporter}?
130
+ *
131
+ * The typical small workspace (1-2 RSS sources, ~3 seconds end-to-end)
132
+ * gains nothing from a spinner that flashes in and out faster than the eye
133
+ * can track. We therefore enable progress only when:
134
+ *
135
+ * 1. There are 3 or more sources to fetch in this run — at that scale the
136
+ * user wants per-source orientation as the loop iterates, OR
137
+ * 2. Any source uses a slow kind — `html-js` (Playwright launch + render =
138
+ * seconds-to-tens-of-seconds) or `json-api` in `--backfill` mode
139
+ * (~80 page traversal). Even a single one of these makes the per-source
140
+ * indicator worth the noise.
141
+ *
142
+ * The heuristic is intentionally NOT user-configurable (ADR-0015 D5 /
143
+ * issue #198 note). `--quiet` / `RADAR_NO_PROGRESS=1` are the documented
144
+ * escape hatches when even the heuristic-on output is undesirable.
145
+ *
146
+ * Exported so the watcher / CLI can share one definition; tests pin
147
+ * behaviour against this single source of truth.
148
+ */
149
+ export declare function shouldEnableProgress(sources: Source[], backfill: boolean): boolean;
90
150
  /**
91
151
  * Execute one full watch cycle.
92
152
  *
@@ -1 +1 @@
1
- {"version":3,"file":"watcher.d.ts","sourceRoot":"","sources":["../../src/core/watcher.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAErE,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAK/D,OAAO,EAEL,eAAe,EAGf,KAAK,YAAY,EAElB,MAAM,uBAAuB,CAAC;AA2C/B,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAgB,SAAQ,cAAc;IACrD,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,6CAA6C;IAC7C,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,WAAW,CAAC;IACnD,yCAAyC;IACzC,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,2DAA2D;IAC3D,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,YAAY,CAAC;IACtC;;;OAGG;IACH,mBAAmB,CAAC,EAAE,OAAO,eAAe,CAAC;CAC9C;AAED,MAAM,WAAW,cAAc;IAC7B,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IACjC,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACpC,6EAA6E;IAC7E,MAAM,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrD;;;;;;;;;;OAUG;IACH,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC9D;AAWD;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GACjC,OAAO,CAAC,MAAM,EAAE,CAAC,CA6BnB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAoMhF;AAED;;;;;;GAMG;AACH,wBAAsB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAQ9D"}
1
+ {"version":3,"file":"watcher.d.ts","sourceRoot":"","sources":["../../src/core/watcher.ts"],"names":[],"mappings":"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"}
@@ -97,6 +97,39 @@ export async function loadSources(sourcesDir, onError) {
97
97
  }
98
98
  return sources;
99
99
  }
100
+ /**
101
+ * Heuristic: should the watcher actively report per-source progress to the
102
+ * supplied {@link ProgressReporter}?
103
+ *
104
+ * The typical small workspace (1-2 RSS sources, ~3 seconds end-to-end)
105
+ * gains nothing from a spinner that flashes in and out faster than the eye
106
+ * can track. We therefore enable progress only when:
107
+ *
108
+ * 1. There are 3 or more sources to fetch in this run — at that scale the
109
+ * user wants per-source orientation as the loop iterates, OR
110
+ * 2. Any source uses a slow kind — `html-js` (Playwright launch + render =
111
+ * seconds-to-tens-of-seconds) or `json-api` in `--backfill` mode
112
+ * (~80 page traversal). Even a single one of these makes the per-source
113
+ * indicator worth the noise.
114
+ *
115
+ * The heuristic is intentionally NOT user-configurable (ADR-0015 D5 /
116
+ * issue #198 note). `--quiet` / `RADAR_NO_PROGRESS=1` are the documented
117
+ * escape hatches when even the heuristic-on output is undesirable.
118
+ *
119
+ * Exported so the watcher / CLI can share one definition; tests pin
120
+ * behaviour against this single source of truth.
121
+ */
122
+ export function shouldEnableProgress(sources, backfill) {
123
+ if (sources.length >= 3)
124
+ return true;
125
+ for (const s of sources) {
126
+ if (s.kind === "html-js")
127
+ return true;
128
+ if (s.kind === "json-api" && backfill)
129
+ return true;
130
+ }
131
+ return false;
132
+ }
100
133
  /**
101
134
  * Execute one full watch cycle.
102
135
  *
@@ -130,9 +163,22 @@ export async function watchRun(options) {
130
163
  else {
131
164
  log("watch run: no sources defined (use `radar source add ...`)");
132
165
  }
133
- return { detected: {}, states: {}, errors: [], stats: {} };
166
+ return { detected: {}, states: {}, errors: [], stats: {}, diag: {} };
134
167
  }
135
- const result = { detected: {}, states: {}, errors: [], stats: {} };
168
+ const result = {
169
+ detected: {},
170
+ states: {},
171
+ errors: [],
172
+ stats: {},
173
+ diag: {},
174
+ };
175
+ // Heuristic gate: only wire the reporter when the run is worth narrating.
176
+ // Without this guard, `radar watch run` on a 1-source RSS workspace would
177
+ // spam the spinner for ~3 seconds straight with no informational value.
178
+ // `options.progress` being unset already means no output regardless.
179
+ const progress = options.progress && shouldEnableProgress(filtered, options.backfill === true)
180
+ ? options.progress
181
+ : undefined;
136
182
  // Lazy Playwright probe cache. We only run the probe when the first
137
183
  // `html-js` source comes up so RSS / GitHub / npm-only workspaces never pay
138
184
  // for the dynamic import. The result is reused across every subsequent
@@ -204,17 +250,58 @@ export async function watchRun(options) {
204
250
  let fetched;
205
251
  let nextStatePatch;
206
252
  let notModified = false;
253
+ // Per-source phase markers (#198). `Fetching…` is the start-of-source
254
+ // boundary the user sees in the spinner; the adapter may emit its own
255
+ // sub-phases (html-js Chromium lifecycle, json-api page x/n) between
256
+ // here and `Completed`. Side metrics for the spinner row default to
257
+ // the source kind so even non-paginating adapters surface useful info.
258
+ progress?.phase(`[${source.id}] Fetching…`, `kind: ${source.kind}`);
259
+ progress?.start(`[${source.id}] ${source.kind}`);
260
+ const sourceStartedAt = Date.now();
207
261
  try {
208
262
  const fetchResult = await adapter.fetch(source, {
209
263
  fetch: options.fetch,
210
264
  state: previousState,
265
+ backfill: options.backfill,
266
+ maxPagesOverride: options.maxPagesOverride,
267
+ env: options.env,
268
+ // In dry-run mode (`radar source test`), paginating adapters fetch
269
+ // only page 0 so the preview never walks past the recipe's first
270
+ // window. Non-paginating adapters ignore the flag.
271
+ dryRun: options.dryRun,
272
+ // Surface adapter-level non-fatal hints (default-selector misses on
273
+ // json-api, etc.) through the same warn sink we use for
274
+ // schema-mismatch / playwright-skip messages.
275
+ warn: (m) => warn(`watch run: ${m}`),
276
+ // Forward the source-scoped reporter so adapter phases nest under
277
+ // the parent `[<source-id>] …` marker. html-js uses it for the
278
+ // Chromium lifecycle; other kinds currently ignore it.
279
+ onProgress: progress,
280
+ // Per-page hook for paginating adapters (json-api). We translate
281
+ // each page event into a phase marker so non-TTY logs preserve the
282
+ // narrative ("Page 3/80: 100 items") and TTY rows pick up the
283
+ // metric on the spinner.
284
+ onPage: progress
285
+ ? ({ pageIndex, pageTotal, items: pageItems }) => {
286
+ const human = `Page ${pageIndex + 1}/${pageTotal}: ${pageItems} items fetched`;
287
+ progress.phase(`[${source.id}] ${human}`);
288
+ progress.update({
289
+ page: `${pageIndex + 1}/${pageTotal}`,
290
+ items: String(pageItems),
291
+ });
292
+ }
293
+ : undefined,
211
294
  });
212
295
  fetched = fetchResult.items;
213
296
  nextStatePatch = fetchResult.state;
214
297
  notModified = fetchResult.notModified ?? false;
298
+ if (fetchResult.diag) {
299
+ result.diag[source.id] = fetchResult.diag;
300
+ }
215
301
  }
216
302
  catch (e) {
217
303
  const message = e instanceof Error ? e.message : String(e);
304
+ progress?.fail(`[${source.id}] Failed`, message);
218
305
  error(`watch run: '${source.id}' fetch failed: ${message}`);
219
306
  result.errors.push({ sourceId: source.id, message });
220
307
  continue;
@@ -298,6 +385,16 @@ export async function watchRun(options) {
298
385
  result.detected[source.id] = detectedItems;
299
386
  result.states[source.id] = nextState;
300
387
  result.stats[source.id] = { fetched: fetched.length, filtered: filteredCount };
388
+ // Per-source completion phase. We use the reporter's `succeed()` so the
389
+ // spinner row stops cleanly and the user gets a single summary line
390
+ // (`[<source-id>] Completed: <items> total, <new> new (<duration>)`).
391
+ // The legacy plain log lines further up the loop are intentionally
392
+ // preserved (acceptance criterion #7) so scripts that grep stdout
393
+ // continue to work.
394
+ if (progress) {
395
+ const duration = Date.now() - sourceStartedAt;
396
+ progress.succeed(`[${source.id}] Completed: ${fetched.length} total, ${detectedItems.length} new`, duration);
397
+ }
301
398
  }
302
399
  return result;
303
400
  }