@ozzylabs/feedradar 0.1.4 → 0.1.6
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 +12 -6
- package/README.md +11 -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/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/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 +428 -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 +67 -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 +10 -0
- package/dist/core/feeds/_fetch.d.ts.map +1 -1
- package/dist/core/feeds/_fetch.js +182 -0
- package/dist/core/feeds/_fetch.js.map +1 -1
- 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/html-js.d.ts +8 -0
- package/dist/core/feeds/html-js.d.ts.map +1 -1
- package/dist/core/feeds/html-js.js +47 -1
- package/dist/core/feeds/html-js.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 +29 -0
- package/dist/core/feeds/json-api.d.ts.map +1 -0
- package/dist/core/feeds/json-api.js +860 -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/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/recipes.d.ts +138 -0
- package/dist/core/recipes.d.ts.map +1 -0
- package/dist/core/recipes.js +242 -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/recipes/aws-whats-new.yaml +87 -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 +127 -0
- package/dist/schemas/recipe.d.ts.map +1 -0
- package/dist/schemas/recipe.js +57 -0
- package/dist/schemas/recipe.js.map +1 -0
- package/dist/schemas/source.d.ts +222 -0
- package/dist/schemas/source.d.ts.map +1 -1
- package/dist/schemas/source.js +234 -0
- package/dist/schemas/source.js.map +1 -1
- package/dist/templates/agents/AGENTS.md +33 -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/package.json +1 -2
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProgressReporter — UX abstraction for long-running CLI operations.
|
|
3
|
+
*
|
|
4
|
+
* Implements [ADR-0015 Progress Reporting UX](../../docs/adr/0015-progress-reporting-ux.md):
|
|
5
|
+
*
|
|
6
|
+
* - 3-layer model (phase markers / heartbeat spinner / side metrics)
|
|
7
|
+
* - TTY auto-detection with env / flag overrides (`RADAR_NO_PROGRESS=1`,
|
|
8
|
+
* `--quiet`, `--verbose`)
|
|
9
|
+
* - CI / non-TTY safe degradation: spinner becomes plain text and `\r`
|
|
10
|
+
* same-line updates are disabled
|
|
11
|
+
* - Zero new runtime dependencies — the spinner frame set
|
|
12
|
+
* (`⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏`) is rotated by an internal `setInterval` and rendered via
|
|
13
|
+
* `process.stderr.write` + ANSI escape `\x1b[K\r`
|
|
14
|
+
*
|
|
15
|
+
* This is the **base** (#196) — only the interface, the factory, and the
|
|
16
|
+
* default reporter implementation. CLI integration (`research` / `review` /
|
|
17
|
+
* `update`) ships in #197, feed adapter integration in #198. Adapter call
|
|
18
|
+
* sites in `src/agents/*.ts` consume the optional `onProgress` callback
|
|
19
|
+
* defined alongside this module (see `src/agents/types.ts`).
|
|
20
|
+
*/
|
|
21
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
22
|
+
const ANSI_CLEAR_LINE = "\x1b[K";
|
|
23
|
+
/**
|
|
24
|
+
* No-op reporter. Used by:
|
|
25
|
+
*
|
|
26
|
+
* - tests that do not care about progress output
|
|
27
|
+
* - `--quiet` / `RADAR_NO_PROGRESS=1` paths (see `createProgressReporter`)
|
|
28
|
+
* - adapter call sites where the caller did not opt into progress
|
|
29
|
+
*
|
|
30
|
+
* All six methods return immediately so wiring a `noopProgressReporter()` to
|
|
31
|
+
* an existing adapter is byte-equivalent to passing `undefined`.
|
|
32
|
+
*/
|
|
33
|
+
export function noopProgressReporter() {
|
|
34
|
+
return {
|
|
35
|
+
phase() { },
|
|
36
|
+
start() { },
|
|
37
|
+
update() { },
|
|
38
|
+
succeed() { },
|
|
39
|
+
fail() { },
|
|
40
|
+
raw() { },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Construct a `ProgressReporter` honouring the ADR-0015 D2 priority table:
|
|
45
|
+
*
|
|
46
|
+
* env (`RADAR_NO_PROGRESS=1`) > flag (`level`) > TTY auto-detect
|
|
47
|
+
*
|
|
48
|
+
* - `level: "quiet"` → no-op (callers want zero output)
|
|
49
|
+
* - `RADAR_NO_PROGRESS=1` → no-op (CI escape hatch)
|
|
50
|
+
* - non-TTY + `level: "normal"` → plain text (phase markers + completion
|
|
51
|
+
* lines, NO spinner animation, NO `\r` overwrite)
|
|
52
|
+
* - TTY + `level: "normal"` → phase markers + spinner + same-line update
|
|
53
|
+
* - `level: "verbose"` → phase markers + spinner (if TTY) + `raw()` pass-
|
|
54
|
+
* through enabled (regardless of TTY)
|
|
55
|
+
*
|
|
56
|
+
* The reporter is self-contained: callers should not depend on internal
|
|
57
|
+
* state (e.g. the active spinner timer). The contract is the 6-method
|
|
58
|
+
* interface above.
|
|
59
|
+
*/
|
|
60
|
+
export function createProgressReporter(opts) {
|
|
61
|
+
// Env escape hatch (D2 table row 1). Honoured even at `level: "verbose"`
|
|
62
|
+
// — CI environments must be able to opt out unconditionally.
|
|
63
|
+
if (process.env.RADAR_NO_PROGRESS === "1") {
|
|
64
|
+
return noopProgressReporter();
|
|
65
|
+
}
|
|
66
|
+
if (opts.level === "quiet") {
|
|
67
|
+
return noopProgressReporter();
|
|
68
|
+
}
|
|
69
|
+
const tty = opts.tty ?? Boolean(process.stderr.isTTY);
|
|
70
|
+
const stream = opts.stream ?? process.stderr;
|
|
71
|
+
const heartbeatMs = opts.heartbeatMs ?? 1000;
|
|
72
|
+
const now = opts.now ?? Date.now;
|
|
73
|
+
const verbose = opts.level === "verbose";
|
|
74
|
+
// Active spinner state. `null` means no spinner is running.
|
|
75
|
+
let active = null;
|
|
76
|
+
function renderSpinnerRow() {
|
|
77
|
+
if (!active)
|
|
78
|
+
return "";
|
|
79
|
+
const elapsedSec = Math.max(0, Math.floor((now() - active.startedAt) / 1000));
|
|
80
|
+
const mm = String(Math.floor(elapsedSec / 60)).padStart(2, "0");
|
|
81
|
+
const ss = String(elapsedSec % 60).padStart(2, "0");
|
|
82
|
+
const frame = SPINNER_FRAMES[active.frame % SPINNER_FRAMES.length];
|
|
83
|
+
const metricEntries = Object.entries(active.metrics);
|
|
84
|
+
const metricsSuffix = metricEntries.length > 0 ? ` ${metricEntries.map(([k, v]) => `${k}: ${v}`).join(" ")}` : "";
|
|
85
|
+
return `${frame} ${active.label} [${mm}:${ss}]${metricsSuffix}`;
|
|
86
|
+
}
|
|
87
|
+
function repaint() {
|
|
88
|
+
if (!active)
|
|
89
|
+
return;
|
|
90
|
+
if (tty) {
|
|
91
|
+
stream.write(`\r${ANSI_CLEAR_LINE}${renderSpinnerRow()}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function clearSpinnerLine() {
|
|
95
|
+
if (tty && active) {
|
|
96
|
+
stream.write(`\r${ANSI_CLEAR_LINE}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function stopSpinner() {
|
|
100
|
+
if (active?.timer) {
|
|
101
|
+
clearInterval(active.timer);
|
|
102
|
+
}
|
|
103
|
+
active = null;
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
phase(name, info) {
|
|
107
|
+
// Phase markers always claim their own line; clear any active spinner
|
|
108
|
+
// overwrite first so we don't leave a half-painted row in scrollback.
|
|
109
|
+
clearSpinnerLine();
|
|
110
|
+
const suffix = info ? ` (${info})` : "";
|
|
111
|
+
stream.write(`${name}${suffix}\n`);
|
|
112
|
+
// Re-paint the spinner row on the new line so it continues to update.
|
|
113
|
+
repaint();
|
|
114
|
+
},
|
|
115
|
+
start(label) {
|
|
116
|
+
// If a previous spinner was still active, drop it silently — the new
|
|
117
|
+
// start supersedes it (caller bug, but we don't want to crash).
|
|
118
|
+
clearSpinnerLine();
|
|
119
|
+
stopSpinner();
|
|
120
|
+
active = {
|
|
121
|
+
label,
|
|
122
|
+
startedAt: now(),
|
|
123
|
+
frame: 0,
|
|
124
|
+
metrics: {},
|
|
125
|
+
timer: null,
|
|
126
|
+
};
|
|
127
|
+
if (tty) {
|
|
128
|
+
// Paint frame 0 immediately so the user sees something before the
|
|
129
|
+
// first heartbeat tick fires.
|
|
130
|
+
repaint();
|
|
131
|
+
if (heartbeatMs > 0) {
|
|
132
|
+
const timer = setInterval(() => {
|
|
133
|
+
if (!active)
|
|
134
|
+
return;
|
|
135
|
+
active.frame += 1;
|
|
136
|
+
repaint();
|
|
137
|
+
}, heartbeatMs);
|
|
138
|
+
// Don't keep the event loop alive just for the spinner — if the
|
|
139
|
+
// host process is otherwise idle (e.g. awaiting a child), we still
|
|
140
|
+
// want it to exit when the work is done.
|
|
141
|
+
if (typeof timer.unref === "function") {
|
|
142
|
+
timer.unref();
|
|
143
|
+
}
|
|
144
|
+
active.timer = timer;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
// Non-TTY plain-text degrade: one line per state transition, no `\r`.
|
|
149
|
+
stream.write(`${label}\n`);
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
update(metrics) {
|
|
153
|
+
if (!active)
|
|
154
|
+
return;
|
|
155
|
+
// Merge so successive `update` calls only need to pass the changed
|
|
156
|
+
// metric (e.g. page index ticks per fetch).
|
|
157
|
+
active.metrics = { ...active.metrics, ...metrics };
|
|
158
|
+
if (tty) {
|
|
159
|
+
repaint();
|
|
160
|
+
}
|
|
161
|
+
// On non-TTY we intentionally drop tick updates to avoid spamming the
|
|
162
|
+
// log with one line per page. The next phase / succeed / fail line
|
|
163
|
+
// re-states the final metric set.
|
|
164
|
+
},
|
|
165
|
+
succeed(label, duration) {
|
|
166
|
+
const elapsedMs = duration ?? (active ? now() - active.startedAt : 0);
|
|
167
|
+
const formatted = formatDuration(elapsedMs);
|
|
168
|
+
clearSpinnerLine();
|
|
169
|
+
stopSpinner();
|
|
170
|
+
stream.write(`${label} (${formatted})\n`);
|
|
171
|
+
},
|
|
172
|
+
fail(label, reason) {
|
|
173
|
+
clearSpinnerLine();
|
|
174
|
+
stopSpinner();
|
|
175
|
+
stream.write(`${label} — ${reason}\n`);
|
|
176
|
+
},
|
|
177
|
+
raw(text) {
|
|
178
|
+
if (!verbose)
|
|
179
|
+
return;
|
|
180
|
+
// Clear the spinner row before flushing pass-through so the agent's
|
|
181
|
+
// stdout / stderr line doesn't end up appended to the spinner frame.
|
|
182
|
+
// On non-TTY, the spinner doesn't share a line so clearing is a no-op.
|
|
183
|
+
clearSpinnerLine();
|
|
184
|
+
stream.write(text);
|
|
185
|
+
// If the chunk does not end in a newline, the spinner repaint would
|
|
186
|
+
// overwrite the tail of the chunk. Insert a soft newline before
|
|
187
|
+
// re-rendering so the chunk is preserved verbatim in scrollback.
|
|
188
|
+
if (active && tty && !text.endsWith("\n")) {
|
|
189
|
+
stream.write("\n");
|
|
190
|
+
}
|
|
191
|
+
repaint();
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Format an elapsed-time in milliseconds as either `Nms`, `N.Ns`, or
|
|
197
|
+
* `Nm Ns`. Used by `succeed()` so the completion line carries the actual
|
|
198
|
+
* sub-task duration without the caller having to compute it.
|
|
199
|
+
*/
|
|
200
|
+
function formatDuration(ms) {
|
|
201
|
+
if (ms < 1000) {
|
|
202
|
+
return `${ms}ms`;
|
|
203
|
+
}
|
|
204
|
+
if (ms < 60_000) {
|
|
205
|
+
const seconds = (ms / 1000).toFixed(1);
|
|
206
|
+
return `${seconds}s`;
|
|
207
|
+
}
|
|
208
|
+
const minutes = Math.floor(ms / 60_000);
|
|
209
|
+
const seconds = Math.floor((ms % 60_000) / 1000);
|
|
210
|
+
return `${minutes}m ${seconds}s`;
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=progress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"progress.js","sourceRoot":"","sources":["../../src/core/progress.ts"],"names":[],"mappings":"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"}
|
|
@@ -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,CAkDzB"}
|
|
@@ -0,0 +1,242 @@
|
|
|
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
|
+
// Facet sweep (ADR-0017). Recipe-only structural field — caller cannot
|
|
236
|
+
// override via flags in Phase 1 (see `printAddHelp` for the rationale).
|
|
237
|
+
if (recipe.facets !== undefined)
|
|
238
|
+
candidate.facets = recipe.facets;
|
|
239
|
+
candidate.trustLevel = recipe.trustLevel;
|
|
240
|
+
return candidate;
|
|
241
|
+
}
|
|
242
|
+
//# 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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"}
|