@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.
- package/README.ja.md +31 -6
- package/README.md +31 -6
- package/dist/agents/claude-code.d.ts +12 -1
- package/dist/agents/claude-code.d.ts.map +1 -1
- package/dist/agents/claude-code.js +9 -5
- package/dist/agents/claude-code.js.map +1 -1
- package/dist/agents/codex-cli.d.ts +7 -1
- package/dist/agents/codex-cli.d.ts.map +1 -1
- package/dist/agents/codex-cli.js +9 -5
- package/dist/agents/codex-cli.js.map +1 -1
- package/dist/agents/copilot.d.ts +7 -1
- package/dist/agents/copilot.d.ts.map +1 -1
- package/dist/agents/copilot.js +9 -5
- package/dist/agents/copilot.js.map +1 -1
- package/dist/agents/gemini-cli.d.ts +7 -1
- package/dist/agents/gemini-cli.d.ts.map +1 -1
- package/dist/agents/gemini-cli.js +9 -5
- package/dist/agents/gemini-cli.js.map +1 -1
- package/dist/agents/index.d.ts +1 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/types.d.ts +33 -0
- package/dist/agents/types.d.ts.map +1 -1
- package/dist/cli/_progress.d.ts +138 -0
- package/dist/cli/_progress.d.ts.map +1 -0
- package/dist/cli/_progress.js +176 -0
- package/dist/cli/_progress.js.map +1 -0
- package/dist/cli/doctor.d.ts +20 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +291 -2
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/research.d.ts +18 -20
- package/dist/cli/research.d.ts.map +1 -1
- package/dist/cli/research.js +318 -203
- package/dist/cli/research.js.map +1 -1
- package/dist/cli/respawn.d.ts +53 -0
- package/dist/cli/respawn.d.ts.map +1 -0
- package/dist/cli/respawn.js +120 -0
- package/dist/cli/respawn.js.map +1 -0
- package/dist/cli/review.d.ts +7 -0
- package/dist/cli/review.d.ts.map +1 -1
- package/dist/cli/review.js +46 -1
- package/dist/cli/review.js.map +1 -1
- package/dist/cli/source.d.ts +23 -2
- package/dist/cli/source.d.ts.map +1 -1
- package/dist/cli/source.js +425 -7
- package/dist/cli/source.js.map +1 -1
- package/dist/cli/update.d.ts +7 -0
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +41 -1
- package/dist/cli/update.js.map +1 -1
- package/dist/cli/watch.d.ts.map +1 -1
- package/dist/cli/watch.js +65 -3
- package/dist/cli/watch.js.map +1 -1
- package/dist/cli/workflow/generate-combined.d.ts +100 -0
- package/dist/cli/workflow/generate-combined.d.ts.map +1 -0
- package/dist/cli/workflow/generate-combined.js +387 -0
- package/dist/cli/workflow/generate-combined.js.map +1 -0
- package/dist/cli/workflow/generate-watch.d.ts +142 -0
- package/dist/cli/workflow/generate-watch.d.ts.map +1 -0
- package/dist/cli/workflow/generate-watch.js +338 -0
- package/dist/cli/workflow/generate-watch.js.map +1 -0
- package/dist/cli/workflow.d.ts +29 -0
- package/dist/cli/workflow.d.ts.map +1 -0
- package/dist/cli/workflow.js +66 -0
- package/dist/cli/workflow.js.map +1 -0
- package/dist/core/feeds/_fetch.d.ts +103 -0
- package/dist/core/feeds/_fetch.d.ts.map +1 -0
- package/dist/core/feeds/_fetch.js +364 -0
- package/dist/core/feeds/_fetch.js.map +1 -0
- package/dist/core/feeds/_jsonpath.d.ts +57 -0
- package/dist/core/feeds/_jsonpath.d.ts.map +1 -0
- package/dist/core/feeds/_jsonpath.js +207 -0
- package/dist/core/feeds/_jsonpath.js.map +1 -0
- package/dist/core/feeds/github-api.d.ts.map +1 -1
- package/dist/core/feeds/github-api.js +2 -1
- package/dist/core/feeds/github-api.js.map +1 -1
- package/dist/core/feeds/html-js.d.ts +29 -0
- package/dist/core/feeds/html-js.d.ts.map +1 -1
- package/dist/core/feeds/html-js.js +86 -2
- package/dist/core/feeds/html-js.js.map +1 -1
- package/dist/core/feeds/html.d.ts.map +1 -1
- package/dist/core/feeds/html.js +2 -1
- package/dist/core/feeds/html.js.map +1 -1
- package/dist/core/feeds/index.d.ts +1 -1
- package/dist/core/feeds/index.d.ts.map +1 -1
- package/dist/core/feeds/index.js +4 -0
- package/dist/core/feeds/index.js.map +1 -1
- package/dist/core/feeds/json-api.d.ts +3 -0
- package/dist/core/feeds/json-api.d.ts.map +1 -0
- package/dist/core/feeds/json-api.js +723 -0
- package/dist/core/feeds/json-api.js.map +1 -0
- package/dist/core/feeds/json-feed.d.ts +11 -0
- package/dist/core/feeds/json-feed.d.ts.map +1 -0
- package/dist/core/feeds/json-feed.js +242 -0
- package/dist/core/feeds/json-feed.js.map +1 -0
- package/dist/core/feeds/npm-registry.d.ts.map +1 -1
- package/dist/core/feeds/npm-registry.js +2 -1
- package/dist/core/feeds/npm-registry.js.map +1 -1
- package/dist/core/feeds/rss.d.ts.map +1 -1
- package/dist/core/feeds/rss.js +2 -1
- package/dist/core/feeds/rss.js.map +1 -1
- package/dist/core/feeds/types.d.ts +123 -0
- package/dist/core/feeds/types.d.ts.map +1 -1
- package/dist/core/progress.d.ts +101 -0
- package/dist/core/progress.d.ts.map +1 -0
- package/dist/core/progress.js +212 -0
- package/dist/core/progress.js.map +1 -0
- package/dist/core/proxy.d.ts +87 -0
- package/dist/core/proxy.d.ts.map +1 -0
- package/dist/core/proxy.js +146 -0
- package/dist/core/proxy.js.map +1 -0
- package/dist/core/recipes.d.ts +138 -0
- package/dist/core/recipes.d.ts.map +1 -0
- package/dist/core/recipes.js +238 -0
- package/dist/core/recipes.js.map +1 -0
- package/dist/core/watcher.d.ts +61 -1
- package/dist/core/watcher.d.ts.map +1 -1
- package/dist/core/watcher.js +99 -2
- package/dist/core/watcher.js.map +1 -1
- package/dist/index.js +17 -4
- package/dist/index.js.map +1 -1
- package/dist/recipes/aws-whats-new.yaml +61 -0
- package/dist/recipes/dev-to.yaml +40 -0
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +1 -0
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/recipe.d.ts +115 -0
- package/dist/schemas/recipe.d.ts.map +1 -0
- package/dist/schemas/recipe.js +54 -0
- package/dist/schemas/recipe.js.map +1 -0
- package/dist/schemas/source.d.ts +130 -0
- package/dist/schemas/source.d.ts.map +1 -1
- package/dist/schemas/source.js +130 -0
- package/dist/schemas/source.js.map +1 -1
- package/dist/templates/agents/AGENTS.md +31 -3
- package/dist/templates/feedradar.md +23 -8
- package/dist/templates/workflows/combined.template.yaml.tmpl +110 -0
- package/dist/templates/workflows/watch.template.yaml.tmpl +103 -0
- package/dist/templates/workflows/watch.yaml +5 -1
- 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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"}
|
package/dist/core/watcher.d.ts
CHANGED
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"watcher.d.ts","sourceRoot":"","sources":["../../src/core/watcher.ts"],"names":[],"mappings":"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"}
|
package/dist/core/watcher.js
CHANGED
|
@@ -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 = {
|
|
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
|
}
|