@opensip-cli/output 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +8 -0
  3. package/README.md +31 -0
  4. package/dist/format/baseline-diff.d.ts +37 -0
  5. package/dist/format/baseline-diff.d.ts.map +1 -0
  6. package/dist/format/baseline-diff.js +96 -0
  7. package/dist/format/baseline-diff.js.map +1 -0
  8. package/dist/format/signal-json.d.ts +17 -0
  9. package/dist/format/signal-json.d.ts.map +1 -0
  10. package/dist/format/signal-json.js +3 -0
  11. package/dist/format/signal-json.js.map +1 -0
  12. package/dist/format/signal-sarif.d.ts +50 -0
  13. package/dist/format/signal-sarif.d.ts.map +1 -0
  14. package/dist/format/signal-sarif.js +107 -0
  15. package/dist/format/signal-sarif.js.map +1 -0
  16. package/dist/format/signal-table.d.ts +44 -0
  17. package/dist/format/signal-table.d.ts.map +1 -0
  18. package/dist/format/signal-table.js +84 -0
  19. package/dist/format/signal-table.js.map +1 -0
  20. package/dist/format/types.d.ts +20 -0
  21. package/dist/format/types.d.ts.map +1 -0
  22. package/dist/format/types.js +2 -0
  23. package/dist/format/types.js.map +1 -0
  24. package/dist/index.d.ts +19 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +19 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/sink/cloud-signal-sink.d.ts +10 -0
  29. package/dist/sink/cloud-signal-sink.d.ts.map +1 -0
  30. package/dist/sink/cloud-signal-sink.js +94 -0
  31. package/dist/sink/cloud-signal-sink.js.map +1 -0
  32. package/dist/sink/entitlement.d.ts +28 -0
  33. package/dist/sink/entitlement.d.ts.map +1 -0
  34. package/dist/sink/entitlement.js +109 -0
  35. package/dist/sink/entitlement.js.map +1 -0
  36. package/dist/sink/http-egress.d.ts +49 -0
  37. package/dist/sink/http-egress.d.ts.map +1 -0
  38. package/dist/sink/http-egress.js +158 -0
  39. package/dist/sink/http-egress.js.map +1 -0
  40. package/dist/sink/repo-identity.d.ts +4 -0
  41. package/dist/sink/repo-identity.d.ts.map +1 -0
  42. package/dist/sink/repo-identity.js +32 -0
  43. package/dist/sink/repo-identity.js.map +1 -0
  44. package/dist/sink/resolve-signal-sink.d.ts +17 -0
  45. package/dist/sink/resolve-signal-sink.d.ts.map +1 -0
  46. package/dist/sink/resolve-signal-sink.js +87 -0
  47. package/dist/sink/resolve-signal-sink.js.map +1 -0
  48. package/package.json +48 -0
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Shared chunked cloud-egress transport (ADR-0008).
3
+ *
4
+ * Both cloud paths POST through here: `reportToCloud` (SARIF, `--report-to`)
5
+ * and the OpenSIP Cloud signal sink. The transport is the *mechanism*; each
6
+ * caller supplies its own *policy* (Strategy) and applies its own failure
7
+ * semantics to the returned {@link EgressResult} — `reportToCloud` maps failure
8
+ * to exit 4, the signal sink swallows. **`postChunked` never throws.**
9
+ *
10
+ * Unlike the prior SARIF-only loop (which only retried `fetch` *throws* and
11
+ * dropped a chunk on an HTTP `429`/`5xx`), this retries `429`/`5xx` at the
12
+ * chunk level, honors `Retry-After`, bounds total work by an overall deadline,
13
+ * and sends a stable `Idempotency-Key` per chunk so a retried-but-stored chunk
14
+ * is de-duplicated server-side.
15
+ */
16
+ import { logger } from '@opensip-cli/core';
17
+ const MODULE_TAG = 'http-egress';
18
+ function isTransient(status) {
19
+ return status >= 500 || status === 429;
20
+ }
21
+ /** Parse `Retry-After` (delta-seconds or HTTP-date) into a delay in ms. */
22
+ function parseRetryAfter(headerVal, now) {
23
+ if (!headerVal)
24
+ return undefined;
25
+ const secs = Number(headerVal);
26
+ if (Number.isFinite(secs))
27
+ return Math.max(0, secs * 1000);
28
+ const when = Date.parse(headerVal);
29
+ return Number.isNaN(when) ? undefined : Math.max(0, when - now);
30
+ }
31
+ /** Exponential backoff with jitter (mirrors core's withRetry shape). */
32
+ function backoffMs(attempt) {
33
+ const base = 500 * 2 ** (attempt - 1);
34
+ return Math.min(base + Math.random() * base * 0.5, 5000);
35
+ }
36
+ const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
37
+ /**
38
+ * POST each chunk with bounded per-chunk retries on `429`/`5xx`/transport
39
+ * errors, honoring `Retry-After`, an overall deadline, and stable idempotency
40
+ * keys. Returns an {@link EgressResult}; never throws.
41
+ */
42
+ // eslint-disable-next-line sonarjs/cognitive-complexity -- network transport: per-chunk retry with status classification, Retry-After, and an overall deadline; the phases read better inline than split across helpers
43
+ export async function postChunked(args) {
44
+ const { chunks, policy, evtPrefix } = args;
45
+ const fetchImpl = args.fetchImpl ?? fetch;
46
+ const now = args.now ?? Date.now;
47
+ const sleep = args.sleep ?? defaultSleep;
48
+ const started = now();
49
+ const headersBase = { 'Content-Type': 'application/json' };
50
+ if (args.apiKey)
51
+ headersBase['X-API-Key'] = args.apiKey;
52
+ const chunkResults = Array.from({ length: chunks.length }, () => false);
53
+ const errors = [];
54
+ let acceptedChunks = 0;
55
+ let authRejected = false;
56
+ let throttled = false;
57
+ let deadlineExceeded = false;
58
+ const deadlineLeft = () => policy.overallDeadlineMs - (now() - started);
59
+ outer: for (let ci = 0; ci < chunks.length; ci++) {
60
+ for (let attempt = 1; attempt <= policy.maxAttempts; attempt++) {
61
+ if (deadlineLeft() <= 0) {
62
+ deadlineExceeded = true;
63
+ break outer;
64
+ }
65
+ let retryAfterMs;
66
+ try {
67
+ const res = await fetchImpl(args.url, {
68
+ method: 'POST',
69
+ headers: { ...headersBase, 'Idempotency-Key': args.idempotencyKeyFor(ci) },
70
+ body: JSON.stringify(chunks[ci]),
71
+ signal: AbortSignal.timeout(args.timeoutFor(chunks[ci], ci)),
72
+ });
73
+ if (res.ok) {
74
+ chunkResults[ci] = true;
75
+ acceptedChunks++;
76
+ logger.info({
77
+ evt: `${evtPrefix}.chunk`,
78
+ module: MODULE_TAG,
79
+ chunk: `${ci + 1}/${chunks.length}`,
80
+ status: res.status,
81
+ });
82
+ continue outer;
83
+ }
84
+ errors.push(`${res.status} ${res.statusText}`.trim());
85
+ if (res.status === 401 || res.status === 403) {
86
+ authRejected = true;
87
+ logger.warn({
88
+ evt: `${evtPrefix}.auth-rejected`,
89
+ module: MODULE_TAG,
90
+ status: res.status,
91
+ });
92
+ break outer; // permanent auth failure — stop everything
93
+ }
94
+ if (!isTransient(res.status)) {
95
+ logger.warn({
96
+ evt: `${evtPrefix}.abort`,
97
+ module: MODULE_TAG,
98
+ status: res.status,
99
+ remaining: chunks.length - ci - 1,
100
+ });
101
+ break outer; // other 4xx — retrying won't help; abort remaining
102
+ }
103
+ if (res.status === 429) {
104
+ throttled = true;
105
+ if (policy.honorRetryAfter)
106
+ retryAfterMs = parseRetryAfter(res.headers.get('Retry-After'), now());
107
+ }
108
+ else if (res.status === 503 && policy.honorRetryAfter) {
109
+ retryAfterMs = parseRetryAfter(res.headers.get('Retry-After'), now());
110
+ }
111
+ }
112
+ catch (error) {
113
+ // Network error / timeout — transient.
114
+ errors.push(error instanceof Error ? error.message : String(error));
115
+ }
116
+ // Transient: retry if attempts remain and the deadline allows.
117
+ if (attempt >= policy.maxAttempts) {
118
+ logger.warn({
119
+ evt: `${evtPrefix}.error`,
120
+ module: MODULE_TAG,
121
+ chunk: `${ci + 1}/${chunks.length}`,
122
+ attempts: attempt,
123
+ });
124
+ continue outer; // give up on this chunk, try the next
125
+ }
126
+ const wanted = retryAfterMs ?? backoffMs(attempt);
127
+ const delay = Math.min(wanted, Math.max(0, deadlineLeft()));
128
+ if (deadlineLeft() - delay <= 0) {
129
+ deadlineExceeded = true;
130
+ break outer;
131
+ }
132
+ logger.info({
133
+ evt: throttled ? `${evtPrefix}.throttled` : `${evtPrefix}.retry`,
134
+ module: MODULE_TAG,
135
+ chunk: `${ci + 1}/${chunks.length}`,
136
+ attempt,
137
+ delayMs: delay,
138
+ retryAfterMs,
139
+ });
140
+ await sleep(delay);
141
+ }
142
+ }
143
+ let outcome = 'partial';
144
+ if (acceptedChunks === chunks.length)
145
+ outcome = 'ok';
146
+ else if (acceptedChunks === 0)
147
+ outcome = 'failed';
148
+ return {
149
+ acceptedChunks,
150
+ chunkResults,
151
+ outcome,
152
+ authRejected,
153
+ throttled,
154
+ deadlineExceeded,
155
+ errors,
156
+ };
157
+ }
158
+ //# sourceMappingURL=http-egress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-egress.js","sourceRoot":"","sources":["../../src/sink/http-egress.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { RepoIdentity } from '@opensip-cli/core';
2
+ /** Best-effort repo identity (HEAD sha + origin remote). Never throws. */
3
+ export declare function resolveRepoIdentity(cwd: string): RepoIdentity;
4
+ //# sourceMappingURL=repo-identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo-identity.d.ts","sourceRoot":"","sources":["../../src/sink/repo-identity.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAgBtD,0EAA0E;AAC1E,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAK7D"}
@@ -0,0 +1,32 @@
1
+ // @fitness-ignore-file error-handling-quality -- best-effort repo-identity detection (ADR-0008): a missing git binary, a non-repo cwd, or a spawn failure must degrade silently to an undefined field; it never blocks, slows, or fails the user's run.
2
+ /**
3
+ * resolveRepoIdentity — best-effort git repo identity (HEAD sha + origin
4
+ * remote) for cloud signal egress (ADR-0008).
5
+ *
6
+ * The composition root resolves this once per run and threads it into
7
+ * `deliverEnvelope`, so the cloud `SignalBatch` carries provenance. Pure
8
+ * inputs in, no throw: any git failure leaves the field `undefined`.
9
+ */
10
+ import { execFileSync } from 'node:child_process';
11
+ function git(cwd, args) {
12
+ try {
13
+ const out = execFileSync('git', args, {
14
+ cwd,
15
+ stdio: ['ignore', 'pipe', 'ignore'],
16
+ encoding: 'utf8',
17
+ timeout: 2000,
18
+ }).trim();
19
+ return out || undefined;
20
+ }
21
+ catch {
22
+ return undefined; // not a git repo / git absent → leave the field undefined
23
+ }
24
+ }
25
+ /** Best-effort repo identity (HEAD sha + origin remote). Never throws. */
26
+ export function resolveRepoIdentity(cwd) {
27
+ return {
28
+ commit: git(cwd, ['rev-parse', 'HEAD']),
29
+ remoteUrl: git(cwd, ['config', '--get', 'remote.origin.url']),
30
+ };
31
+ }
32
+ //# sourceMappingURL=repo-identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo-identity.js","sourceRoot":"","sources":["../../src/sink/repo-identity.ts"],"names":[],"mappings":"AAAA,wPAAwP;AACxP;;;;;;;GAOG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAIlD,SAAS,GAAG,CAAC,GAAW,EAAE,IAAuB;IAC/C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE;YACpC,GAAG;YACH,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;YACnC,QAAQ,EAAE,MAAM;YAChB,OAAO,EAAE,IAAI;SACd,CAAC,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,GAAG,IAAI,SAAS,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC,CAAC,0DAA0D;IAC9E,CAAC;AACH,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAC7C,OAAO;QACL,MAAM,EAAE,GAAG,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QACvC,SAAS,EAAE,GAAG,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,mBAAmB,CAAC,CAAC;KAC9D,CAAC;AACJ,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { SignalSink } from '@opensip-cli/core';
2
+ /** Built-in OpenSIP Cloud base URL; the sink appends `/signals`. Overridable via config. */
3
+ export declare const DEFAULT_CLOUD_ENDPOINT = "https://opensip.ai/api";
4
+ export interface ResolveSignalSinkInput {
5
+ readonly apiKey?: string;
6
+ readonly cloud?: {
7
+ readonly sync?: boolean;
8
+ readonly endpoint?: string;
9
+ };
10
+ /** `--no-cloud` per-run opt-out. */
11
+ readonly noCloud?: boolean;
12
+ /** Directory for the entitlement cache (user-level). */
13
+ readonly cacheDir: string;
14
+ readonly fetchImpl?: typeof fetch;
15
+ }
16
+ export declare function resolveSignalSink(input: ResolveSignalSinkInput): SignalSink;
17
+ //# sourceMappingURL=resolve-signal-sink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-signal-sink.d.ts","sourceRoot":"","sources":["../../src/sink/resolve-signal-sink.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAA2B,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAE7E,4FAA4F;AAC5F,eAAO,MAAM,sBAAsB,2BAA2B,CAAC;AAI/D,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACzE,oCAAoC;IACpC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,wDAAwD;IACxD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CACnC;AA4BD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,GAAG,UAAU,CAyC3E"}
@@ -0,0 +1,87 @@
1
+ // @fitness-ignore-file error-handling-quality -- best-effort sink selection + first-run notice (ADR-0008): a failed marker read/write just re-shows the notice, and the entitlement/egress paths degrade silently. Cloud sync never blocks the user's run.
2
+ /**
3
+ * resolveSignalSink — choose the SignalSink for a run (ADR-0008).
4
+ *
5
+ * Sync and cheap: the opt-out paths (no API key, `cloud.sync: false`,
6
+ * `--no-cloud`, a non-https endpoint) return `noopSignalSink` with zero IO, so
7
+ * non-signal commands and the keyless OSS majority pay nothing. When cloud sync
8
+ * is viable it returns a **deferred** sink: the entitlement check runs lazily on
9
+ * the first `emit` (and is cached), so only signal-producing runs incur it, and
10
+ * a revoked plan (401/403) busts the entitlement cache so it stops within one run.
11
+ */
12
+ import { access, mkdir, writeFile } from 'node:fs/promises';
13
+ import { join } from 'node:path';
14
+ import { logger, noopSignalSink } from '@opensip-cli/core';
15
+ import { createCloudSignalSink } from './cloud-signal-sink.js';
16
+ import { checkEntitlement, invalidateEntitlement } from './entitlement.js';
17
+ /** Built-in OpenSIP Cloud base URL; the sink appends `/signals`. Overridable via config. */
18
+ export const DEFAULT_CLOUD_ENDPOINT = 'https://opensip.ai/api';
19
+ const MODULE_TAG = 'resolve-signal-sink';
20
+ /**
21
+ * One-time privacy notice the first time the cloud sink goes active, telling
22
+ * the user what is synced and how to opt out (ADR-0008). Marker lives in the
23
+ * entitlement cache dir; best-effort — a failure just shows it again next run.
24
+ */
25
+ async function maybeShowFirstRunNotice(cacheDir) {
26
+ const marker = join(cacheDir, 'signal-sync-notice');
27
+ try {
28
+ await access(marker);
29
+ return; // already shown
30
+ }
31
+ catch {
32
+ /* not shown yet */
33
+ }
34
+ try {
35
+ await mkdir(cacheDir, { recursive: true });
36
+ await writeFile(marker, new Date().toISOString());
37
+ process.stderr.write('OpenSIP Cloud signal sync is on: each run sends its signals (file paths,\n' +
38
+ 'messages, suggestions, code-location hints) to your OpenSIP Cloud account.\n' +
39
+ 'Local results are unaffected. Disable with --no-cloud or `cloud.sync: false`.\n');
40
+ }
41
+ catch {
42
+ /* best-effort */
43
+ }
44
+ }
45
+ export function resolveSignalSink(input) {
46
+ const { apiKey } = input;
47
+ // Cheap opt-out paths — no IO.
48
+ if (!apiKey)
49
+ return noopSignalSink;
50
+ if (input.noCloud || input.cloud?.sync === false)
51
+ return noopSignalSink;
52
+ const endpoint = input.cloud?.endpoint ?? DEFAULT_CLOUD_ENDPOINT;
53
+ if (!endpoint.startsWith('https://')) {
54
+ // Never send the credential-bearing X-API-Key over plaintext.
55
+ logger.warn({ evt: 'cli.signal-sync.insecure-endpoint', module: MODULE_TAG, endpoint });
56
+ return noopSignalSink;
57
+ }
58
+ const cloudSink = createCloudSignalSink({ endpoint, apiKey, fetchImpl: input.fetchImpl });
59
+ // Deferred sink: entitlement is checked lazily/cached on first emit.
60
+ return {
61
+ async emit(batch) {
62
+ try {
63
+ const ent = await checkEntitlement({
64
+ apiKey,
65
+ endpoint,
66
+ now: Date.now(),
67
+ cacheDir: input.cacheDir,
68
+ fetchImpl: input.fetchImpl,
69
+ });
70
+ if (!ent.entitled)
71
+ return { accepted: 0, authRejected: false, skippedReason: 'unentitled' };
72
+ // @fitness-ignore-next-line async-waterfall-detection -- the first-run notice must print BEFORE the emit; deliberately sequential, not parallelized
73
+ await maybeShowFirstRunNotice(input.cacheDir);
74
+ const result = await cloudSink.emit(batch);
75
+ if (result.authRejected) {
76
+ await invalidateEntitlement({ apiKey, cacheDir: input.cacheDir });
77
+ }
78
+ return result;
79
+ }
80
+ catch {
81
+ // Belt and suspenders — emit MUST NOT throw into the run.
82
+ return { accepted: 0, authRejected: false, skippedReason: 'error' };
83
+ }
84
+ },
85
+ };
86
+ }
87
+ //# sourceMappingURL=resolve-signal-sink.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-signal-sink.js","sourceRoot":"","sources":["../../src/sink/resolve-signal-sink.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@opensip-cli/output",
3
+ "version": "0.1.0",
4
+ "license": "Apache-2.0",
5
+ "description": "Output layer for OpenSIP CLI — pure signal-envelope formatters (json/sarif/table) and effectful sinks (file/cloud)",
6
+ "keywords": [
7
+ "opensip-cli",
8
+ "static-analysis",
9
+ "code-quality",
10
+ "sarif",
11
+ "output"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/opensip-ai/opensip-cli.git",
16
+ "directory": "packages/output"
17
+ },
18
+ "homepage": "https://github.com/opensip-ai/opensip-cli",
19
+ "bugs": {
20
+ "url": "https://github.com/opensip-ai/opensip-cli/issues"
21
+ },
22
+ "type": "module",
23
+ "main": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "exports": {
26
+ ".": "./dist/index.js"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "LICENSE",
31
+ "NOTICE"
32
+ ],
33
+ "dependencies": {
34
+ "@opensip-cli/contracts": "0.1.0",
35
+ "@opensip-cli/core": "0.1.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^24.13.2",
39
+ "typescript": "~6.0.3",
40
+ "vitest": "^4.1.8"
41
+ },
42
+ "scripts": {
43
+ "build": "tsc",
44
+ "test": "vitest run --passWithNoTests",
45
+ "typecheck": "tsc --noEmit",
46
+ "clean": "rm -rf dist"
47
+ }
48
+ }