@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,101 @@
|
|
|
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
|
+
export type ProgressLevel = "quiet" | "normal" | "verbose";
|
|
22
|
+
/**
|
|
23
|
+
* Public API surface used by CLI / adapter integrations. Six methods cover
|
|
24
|
+
* the three UX layers from ADR-0015:
|
|
25
|
+
*
|
|
26
|
+
* - `phase`: structured milestone, always rendered (TTY and non-TTY)
|
|
27
|
+
* - `start` / `succeed` / `fail`: spinner lifecycle for a single sub-task.
|
|
28
|
+
* On TTY, `start` begins same-line spinner animation; `succeed` / `fail`
|
|
29
|
+
* stop it and emit a single completion line.
|
|
30
|
+
* - `update`: side metrics (page x/N, stdout bytes, ...) — merged into the
|
|
31
|
+
* spinner row on TTY, suppressed or printed as plain text on non-TTY
|
|
32
|
+
* - `raw`: agent stdout / stderr pass-through. Only printed in `verbose`
|
|
33
|
+
* level. Default reporter pipes the chunk to `process.stderr` verbatim.
|
|
34
|
+
*/
|
|
35
|
+
export interface ProgressReporter {
|
|
36
|
+
/** Structured milestone. e.g. `"Spawning claude-code"` */
|
|
37
|
+
phase(name: string, info?: string): void;
|
|
38
|
+
/** Begin a spinner-tracked sub-task. e.g. `"Agent running…"` */
|
|
39
|
+
start(label: string): void;
|
|
40
|
+
/** Update side metrics for the active spinner row. */
|
|
41
|
+
update(metrics: Record<string, string>): void;
|
|
42
|
+
/** Stop the spinner and emit a success line. `duration` is milliseconds. */
|
|
43
|
+
succeed(label: string, duration?: number): void;
|
|
44
|
+
/** Stop the spinner and emit a failure line. */
|
|
45
|
+
fail(label: string, reason: string): void;
|
|
46
|
+
/** Pass-through agent stdout / stderr text (verbose only). */
|
|
47
|
+
raw(text: string): void;
|
|
48
|
+
}
|
|
49
|
+
export interface CreateProgressReporterOptions {
|
|
50
|
+
/**
|
|
51
|
+
* TTY override. If unspecified, falls back to `process.stderr.isTTY`.
|
|
52
|
+
* Always overridable so tests can pin the value.
|
|
53
|
+
*/
|
|
54
|
+
tty?: boolean;
|
|
55
|
+
/** Verbosity. `quiet` = phase markers off; `verbose` = `raw()` enabled. */
|
|
56
|
+
level: ProgressLevel;
|
|
57
|
+
/**
|
|
58
|
+
* Output stream. Defaults to `process.stderr` so progress does not pollute
|
|
59
|
+
* piped stdout (`radar research > out.md`). Tests pass an in-memory stream.
|
|
60
|
+
*/
|
|
61
|
+
stream?: NodeJS.WritableStream;
|
|
62
|
+
/**
|
|
63
|
+
* Heartbeat interval in milliseconds (default 1000ms / 1s). Tests can
|
|
64
|
+
* shorten this to verify spinner rotation without burning real time.
|
|
65
|
+
* Negative or zero disables the heartbeat (still renders a static first
|
|
66
|
+
* frame on `start`).
|
|
67
|
+
*/
|
|
68
|
+
heartbeatMs?: number;
|
|
69
|
+
/** `Date.now()` override for deterministic elapsed-time rendering. */
|
|
70
|
+
now?: () => number;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* No-op reporter. Used by:
|
|
74
|
+
*
|
|
75
|
+
* - tests that do not care about progress output
|
|
76
|
+
* - `--quiet` / `RADAR_NO_PROGRESS=1` paths (see `createProgressReporter`)
|
|
77
|
+
* - adapter call sites where the caller did not opt into progress
|
|
78
|
+
*
|
|
79
|
+
* All six methods return immediately so wiring a `noopProgressReporter()` to
|
|
80
|
+
* an existing adapter is byte-equivalent to passing `undefined`.
|
|
81
|
+
*/
|
|
82
|
+
export declare function noopProgressReporter(): ProgressReporter;
|
|
83
|
+
/**
|
|
84
|
+
* Construct a `ProgressReporter` honouring the ADR-0015 D2 priority table:
|
|
85
|
+
*
|
|
86
|
+
* env (`RADAR_NO_PROGRESS=1`) > flag (`level`) > TTY auto-detect
|
|
87
|
+
*
|
|
88
|
+
* - `level: "quiet"` → no-op (callers want zero output)
|
|
89
|
+
* - `RADAR_NO_PROGRESS=1` → no-op (CI escape hatch)
|
|
90
|
+
* - non-TTY + `level: "normal"` → plain text (phase markers + completion
|
|
91
|
+
* lines, NO spinner animation, NO `\r` overwrite)
|
|
92
|
+
* - TTY + `level: "normal"` → phase markers + spinner + same-line update
|
|
93
|
+
* - `level: "verbose"` → phase markers + spinner (if TTY) + `raw()` pass-
|
|
94
|
+
* through enabled (regardless of TTY)
|
|
95
|
+
*
|
|
96
|
+
* The reporter is self-contained: callers should not depend on internal
|
|
97
|
+
* state (e.g. the active spinner timer). The contract is the 6-method
|
|
98
|
+
* interface above.
|
|
99
|
+
*/
|
|
100
|
+
export declare function createProgressReporter(opts: CreateProgressReporterOptions): ProgressReporter;
|
|
101
|
+
//# sourceMappingURL=progress.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"progress.d.ts","sourceRoot":"","sources":["../../src/core/progress.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE3D;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,gBAAgB;IAC/B,0DAA0D;IAC1D,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,gEAAgE;IAChE,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,sDAAsD;IACtD,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;IAC9C,4EAA4E;IAC5E,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,gDAAgD;IAChD,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,8DAA8D;IAC9D,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,6BAA6B;IAC5C;;;OAGG;IACH,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,2EAA2E;IAC3E,KAAK,EAAE,aAAa,CAAC;IACrB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC/B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sEAAsE;IACtE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAKD;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,IAAI,gBAAgB,CASvD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,6BAA6B,GAAG,gBAAgB,CA8I5F"}
|
|
@@ -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,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy environment detection & NODE_OPTIONS merging helpers.
|
|
3
|
+
*
|
|
4
|
+
* `radar` runs `fetch()` on Node's built-in `undici`. Honoring `HTTPS_PROXY` /
|
|
5
|
+
* `HTTP_PROXY` requires `NODE_OPTIONS=--use-env-proxy` (Node 22.21+ / 24.5+).
|
|
6
|
+
* Rather than asking users to set it manually, the `bin` self-respawns with
|
|
7
|
+
* the flag injected when a proxy env var is present.
|
|
8
|
+
*
|
|
9
|
+
* This module is intentionally pure (no I/O, no `process` access) so the
|
|
10
|
+
* detection / merge logic can be unit tested with arbitrary env objects.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Result of probing the environment for a proxy URL. `source` records which
|
|
14
|
+
* variable supplied the URL so the CLI can warn when only `ALL_PROXY` is set
|
|
15
|
+
* (`ALL_PROXY` does not engage `--use-env-proxy`; users must also set
|
|
16
|
+
* `HTTPS_PROXY` or `HTTP_PROXY`).
|
|
17
|
+
*/
|
|
18
|
+
export interface ProxyDetection {
|
|
19
|
+
/** The proxy URL string as found in env (no normalization). */
|
|
20
|
+
url: string;
|
|
21
|
+
/** Variable name the URL was sourced from. */
|
|
22
|
+
source: "HTTPS_PROXY" | "HTTP_PROXY" | "ALL_PROXY";
|
|
23
|
+
/**
|
|
24
|
+
* True when the URL came **only** from `ALL_PROXY` and no `HTTPS_PROXY` /
|
|
25
|
+
* `HTTP_PROXY` is set. `--use-env-proxy` ignores `ALL_PROXY`, so the CLI
|
|
26
|
+
* should warn the user before respawning would otherwise be a no-op.
|
|
27
|
+
*/
|
|
28
|
+
allProxyOnly: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Lookup order: HTTPS_PROXY → HTTP_PROXY → ALL_PROXY, each tried in upper-case
|
|
32
|
+
* then lower-case form (POSIX convention, matches what `undici` itself checks).
|
|
33
|
+
* Empty-string values are treated as unset, matching `curl` / `wget` behavior
|
|
34
|
+
* where `HTTPS_PROXY=""` disables the proxy rather than configures it.
|
|
35
|
+
*/
|
|
36
|
+
export declare function detectProxyUrl(env: NodeJS.ProcessEnv): ProxyDetection | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Append `flag` to an existing `NODE_OPTIONS` string, preserving user-supplied
|
|
39
|
+
* options. Skips the append when the exact flag is already present (whitespace
|
|
40
|
+
* separated) so re-spawn chains don't keep growing the env var.
|
|
41
|
+
*
|
|
42
|
+
* `existing` may be `undefined` (env var unset) or empty string — both yield
|
|
43
|
+
* just `flag` with no leading whitespace.
|
|
44
|
+
*/
|
|
45
|
+
export declare function mergeNodeOptions(existing: string | undefined, flag: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* Convert a `NO_PROXY` env value (Node / curl / wget convention) to Playwright's
|
|
48
|
+
* `proxy.bypass` form.
|
|
49
|
+
*
|
|
50
|
+
* Differences between the two formats:
|
|
51
|
+
*
|
|
52
|
+
* | Concern | Node `NO_PROXY` | Playwright `bypass` |
|
|
53
|
+
* |----------------|------------------------------|---------------------------------|
|
|
54
|
+
* | Separator | `,` | `;` |
|
|
55
|
+
* | Domain suffix | `.example.com` (leading dot) | `*.example.com` (glob wildcard) |
|
|
56
|
+
* | Bare host | `example.com` | `example.com` |
|
|
57
|
+
*
|
|
58
|
+
* Empty / whitespace-only entries are dropped so trailing commas or accidental
|
|
59
|
+
* double commas don't produce empty rules (which Playwright would treat as
|
|
60
|
+
* "bypass nothing"). Returns `undefined` when the input is unset / empty so
|
|
61
|
+
* callers can pass through "no bypass list" to Playwright without an empty
|
|
62
|
+
* string (Playwright treats `""` as a valid empty bypass list — slightly
|
|
63
|
+
* different intent than "not specified").
|
|
64
|
+
*/
|
|
65
|
+
export declare function noProxyToPlaywrightBypass(noProxy: string | undefined): string | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* Redact basic-auth credentials from a proxy URL before printing.
|
|
68
|
+
*
|
|
69
|
+
* Proxy URLs like `http://user:pass@proxy:8080` are routinely set in env vars
|
|
70
|
+
* (`HTTPS_PROXY`, `HTTP_PROXY`) at corporate networks. `radar doctor` prints
|
|
71
|
+
* the detected URL so users can confirm radar is honoring the right env var,
|
|
72
|
+
* but the credentials must never reach stdout — terminal scrollback, CI
|
|
73
|
+
* artifact logs, and shared screenshots are all common leak vectors.
|
|
74
|
+
*
|
|
75
|
+
* Strategy:
|
|
76
|
+
* - Userinfo is replaced with `***:***` when **both** username and password
|
|
77
|
+
* are present. When only a username is set, it is still replaced (treating
|
|
78
|
+
* it as a credential because some proxies encode tokens in the userinfo
|
|
79
|
+
* slot without a separate password).
|
|
80
|
+
* - All other URL components (scheme / host / port / path / query) are
|
|
81
|
+
* preserved verbatim so the user can still verify the proxy address.
|
|
82
|
+
* - Invalid URLs that can't be parsed are returned as `***` rather than the
|
|
83
|
+
* original string — better to over-redact than risk leaking malformed
|
|
84
|
+
* credentials we couldn't classify.
|
|
85
|
+
*/
|
|
86
|
+
export declare function maskProxyUrl(url: string): string;
|
|
87
|
+
//# sourceMappingURL=proxy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/core/proxy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,+DAA+D;IAC/D,GAAG,EAAE,MAAM,CAAC;IACZ,8CAA8C;IAC9C,MAAM,EAAE,aAAa,GAAG,YAAY,GAAG,WAAW,CAAC;IACnD;;;;OAIG;IACH,YAAY,EAAE,OAAO,CAAC;CACvB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,cAAc,GAAG,SAAS,CAmBjF;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAOnF;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAiBzF;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA2BhD"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy environment detection & NODE_OPTIONS merging helpers.
|
|
3
|
+
*
|
|
4
|
+
* `radar` runs `fetch()` on Node's built-in `undici`. Honoring `HTTPS_PROXY` /
|
|
5
|
+
* `HTTP_PROXY` requires `NODE_OPTIONS=--use-env-proxy` (Node 22.21+ / 24.5+).
|
|
6
|
+
* Rather than asking users to set it manually, the `bin` self-respawns with
|
|
7
|
+
* the flag injected when a proxy env var is present.
|
|
8
|
+
*
|
|
9
|
+
* This module is intentionally pure (no I/O, no `process` access) so the
|
|
10
|
+
* detection / merge logic can be unit tested with arbitrary env objects.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Lookup order: HTTPS_PROXY → HTTP_PROXY → ALL_PROXY, each tried in upper-case
|
|
14
|
+
* then lower-case form (POSIX convention, matches what `undici` itself checks).
|
|
15
|
+
* Empty-string values are treated as unset, matching `curl` / `wget` behavior
|
|
16
|
+
* where `HTTPS_PROXY=""` disables the proxy rather than configures it.
|
|
17
|
+
*/
|
|
18
|
+
export function detectProxyUrl(env) {
|
|
19
|
+
const pick = (name) => {
|
|
20
|
+
const upper = env[name];
|
|
21
|
+
if (upper && upper.length > 0)
|
|
22
|
+
return upper;
|
|
23
|
+
const lower = env[name.toLowerCase()];
|
|
24
|
+
if (lower && lower.length > 0)
|
|
25
|
+
return lower;
|
|
26
|
+
return undefined;
|
|
27
|
+
};
|
|
28
|
+
const https = pick("HTTPS_PROXY");
|
|
29
|
+
if (https)
|
|
30
|
+
return { url: https, source: "HTTPS_PROXY", allProxyOnly: false };
|
|
31
|
+
const http = pick("HTTP_PROXY");
|
|
32
|
+
if (http)
|
|
33
|
+
return { url: http, source: "HTTP_PROXY", allProxyOnly: false };
|
|
34
|
+
const all = pick("ALL_PROXY");
|
|
35
|
+
if (all)
|
|
36
|
+
return { url: all, source: "ALL_PROXY", allProxyOnly: true };
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Append `flag` to an existing `NODE_OPTIONS` string, preserving user-supplied
|
|
41
|
+
* options. Skips the append when the exact flag is already present (whitespace
|
|
42
|
+
* separated) so re-spawn chains don't keep growing the env var.
|
|
43
|
+
*
|
|
44
|
+
* `existing` may be `undefined` (env var unset) or empty string — both yield
|
|
45
|
+
* just `flag` with no leading whitespace.
|
|
46
|
+
*/
|
|
47
|
+
export function mergeNodeOptions(existing, flag) {
|
|
48
|
+
if (!existing || existing.length === 0)
|
|
49
|
+
return flag;
|
|
50
|
+
// Whitespace-split avoids false positives like matching `--use-env-proxy`
|
|
51
|
+
// against an unrelated flag that contains the same substring.
|
|
52
|
+
const tokens = existing.split(/\s+/).filter((t) => t.length > 0);
|
|
53
|
+
if (tokens.includes(flag))
|
|
54
|
+
return existing;
|
|
55
|
+
return `${existing} ${flag}`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Convert a `NO_PROXY` env value (Node / curl / wget convention) to Playwright's
|
|
59
|
+
* `proxy.bypass` form.
|
|
60
|
+
*
|
|
61
|
+
* Differences between the two formats:
|
|
62
|
+
*
|
|
63
|
+
* | Concern | Node `NO_PROXY` | Playwright `bypass` |
|
|
64
|
+
* |----------------|------------------------------|---------------------------------|
|
|
65
|
+
* | Separator | `,` | `;` |
|
|
66
|
+
* | Domain suffix | `.example.com` (leading dot) | `*.example.com` (glob wildcard) |
|
|
67
|
+
* | Bare host | `example.com` | `example.com` |
|
|
68
|
+
*
|
|
69
|
+
* Empty / whitespace-only entries are dropped so trailing commas or accidental
|
|
70
|
+
* double commas don't produce empty rules (which Playwright would treat as
|
|
71
|
+
* "bypass nothing"). Returns `undefined` when the input is unset / empty so
|
|
72
|
+
* callers can pass through "no bypass list" to Playwright without an empty
|
|
73
|
+
* string (Playwright treats `""` as a valid empty bypass list — slightly
|
|
74
|
+
* different intent than "not specified").
|
|
75
|
+
*/
|
|
76
|
+
export function noProxyToPlaywrightBypass(noProxy) {
|
|
77
|
+
if (!noProxy)
|
|
78
|
+
return undefined;
|
|
79
|
+
const entries = noProxy
|
|
80
|
+
.split(",")
|
|
81
|
+
.map((s) => s.trim())
|
|
82
|
+
.filter((s) => s.length > 0)
|
|
83
|
+
.map((entry) => {
|
|
84
|
+
// Node convention: a leading dot means "match this domain and all
|
|
85
|
+
// subdomains" (`.example.com` matches `api.example.com`). Playwright
|
|
86
|
+
// expresses the same intent with a glob wildcard: `*.example.com`.
|
|
87
|
+
// Bare hosts (no leading dot) are passed through untouched — both
|
|
88
|
+
// formats agree on the exact-match semantics there.
|
|
89
|
+
if (entry.startsWith("."))
|
|
90
|
+
return `*${entry}`;
|
|
91
|
+
return entry;
|
|
92
|
+
});
|
|
93
|
+
if (entries.length === 0)
|
|
94
|
+
return undefined;
|
|
95
|
+
return entries.join(";");
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Redact basic-auth credentials from a proxy URL before printing.
|
|
99
|
+
*
|
|
100
|
+
* Proxy URLs like `http://user:pass@proxy:8080` are routinely set in env vars
|
|
101
|
+
* (`HTTPS_PROXY`, `HTTP_PROXY`) at corporate networks. `radar doctor` prints
|
|
102
|
+
* the detected URL so users can confirm radar is honoring the right env var,
|
|
103
|
+
* but the credentials must never reach stdout — terminal scrollback, CI
|
|
104
|
+
* artifact logs, and shared screenshots are all common leak vectors.
|
|
105
|
+
*
|
|
106
|
+
* Strategy:
|
|
107
|
+
* - Userinfo is replaced with `***:***` when **both** username and password
|
|
108
|
+
* are present. When only a username is set, it is still replaced (treating
|
|
109
|
+
* it as a credential because some proxies encode tokens in the userinfo
|
|
110
|
+
* slot without a separate password).
|
|
111
|
+
* - All other URL components (scheme / host / port / path / query) are
|
|
112
|
+
* preserved verbatim so the user can still verify the proxy address.
|
|
113
|
+
* - Invalid URLs that can't be parsed are returned as `***` rather than the
|
|
114
|
+
* original string — better to over-redact than risk leaking malformed
|
|
115
|
+
* credentials we couldn't classify.
|
|
116
|
+
*/
|
|
117
|
+
export function maskProxyUrl(url) {
|
|
118
|
+
let parsed;
|
|
119
|
+
try {
|
|
120
|
+
parsed = new URL(url);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// We can't tell where the credentials are if parsing failed; treat the
|
|
124
|
+
// whole string as sensitive. The doctor still tells the user *which*
|
|
125
|
+
// env var the URL came from, so total redaction is acceptable here.
|
|
126
|
+
return "***";
|
|
127
|
+
}
|
|
128
|
+
if (parsed.password) {
|
|
129
|
+
// Both halves present (or password-only — unusual but spec-valid). Mask
|
|
130
|
+
// both with `***` so neither leaks into terminal output. We intentionally
|
|
131
|
+
// don't keep the username "for debugging" because admins often use the
|
|
132
|
+
// username slot for tokens or service-account names that are themselves
|
|
133
|
+
// sensitive.
|
|
134
|
+
parsed.username = "***";
|
|
135
|
+
parsed.password = "***";
|
|
136
|
+
}
|
|
137
|
+
else if (parsed.username) {
|
|
138
|
+
// Userinfo with no password: some proxies encode service-account tokens
|
|
139
|
+
// or PATs in the username slot. Replace just the username; keep the URL
|
|
140
|
+
// shape (no spurious `:` separator) so the output matches the user's env
|
|
141
|
+
// var format.
|
|
142
|
+
parsed.username = "***";
|
|
143
|
+
}
|
|
144
|
+
return parsed.toString();
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/core/proxy.ts"],"names":[],"mappings":"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"}
|