@netgreener/runtime 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 (138) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +28 -0
  3. package/TUTORIAL.md +495 -0
  4. package/dist/aggregator.d.ts +43 -0
  5. package/dist/aggregator.js +270 -0
  6. package/dist/analyze/analyzeGate.d.ts +44 -0
  7. package/dist/analyze/analyzeGate.js +114 -0
  8. package/dist/analyze/apiOverconsumptionDetector.d.ts +13 -0
  9. package/dist/analyze/apiOverconsumptionDetector.js +75 -0
  10. package/dist/analyze/astDetectors.d.ts +38 -0
  11. package/dist/analyze/astDetectors.js +566 -0
  12. package/dist/analyze/buildResourceFindings.d.ts +34 -0
  13. package/dist/analyze/buildResourceFindings.js +86 -0
  14. package/dist/analyze/cli.d.ts +20 -0
  15. package/dist/analyze/cli.js +186 -0
  16. package/dist/analyze/cpuResourceDetectors.d.ts +35 -0
  17. package/dist/analyze/cpuResourceDetectors.js +163 -0
  18. package/dist/analyze/heuristicScan.d.ts +61 -0
  19. package/dist/analyze/heuristicScan.js +91 -0
  20. package/dist/analyze/index.d.ts +15 -0
  21. package/dist/analyze/index.js +15 -0
  22. package/dist/analyze/loadTypescript.d.ts +8 -0
  23. package/dist/analyze/loadTypescript.js +23 -0
  24. package/dist/analyze/memoryIoDetectors.d.ts +15 -0
  25. package/dist/analyze/memoryIoDetectors.js +68 -0
  26. package/dist/analyze/preferAstDetectors.d.ts +18 -0
  27. package/dist/analyze/preferAstDetectors.js +91 -0
  28. package/dist/analyze/projectScan.d.ts +19 -0
  29. package/dist/analyze/projectScan.js +115 -0
  30. package/dist/analyze/resourceAdmission.d.ts +33 -0
  31. package/dist/analyze/resourceAdmission.js +91 -0
  32. package/dist/analyze/resourceFindingCandidate.d.ts +61 -0
  33. package/dist/analyze/resourceFindingCandidate.js +75 -0
  34. package/dist/analyze/retryAmplificationDetector.d.ts +11 -0
  35. package/dist/analyze/retryAmplificationDetector.js +55 -0
  36. package/dist/analyze/schemas/resource-finding.schema.json +352 -0
  37. package/dist/analyze/serviceDiscovery.d.ts +66 -0
  38. package/dist/analyze/serviceDiscovery.js +210 -0
  39. package/dist/analyze/unboundedParallelismDetector.d.ts +11 -0
  40. package/dist/analyze/unboundedParallelismDetector.js +57 -0
  41. package/dist/analyze/validateResourceFinding.d.ts +19 -0
  42. package/dist/analyze/validateResourceFinding.js +46 -0
  43. package/dist/bullmq.d.ts +33 -0
  44. package/dist/bullmq.js +106 -0
  45. package/dist/cli/helpText.d.ts +8 -0
  46. package/dist/cli/helpText.js +99 -0
  47. package/dist/cli/netgreener.d.ts +12 -0
  48. package/dist/cli/netgreener.js +65 -0
  49. package/dist/collectorIpc.d.ts +28 -0
  50. package/dist/collectorIpc.js +189 -0
  51. package/dist/collectorMetadata.d.ts +15 -0
  52. package/dist/collectorMetadata.js +33 -0
  53. package/dist/config.d.ts +41 -0
  54. package/dist/config.js +97 -0
  55. package/dist/contract/index.d.ts +1 -0
  56. package/dist/contract/index.js +1 -0
  57. package/dist/contract/observation-envelope.schema.json +331 -0
  58. package/dist/contract/observation-protobuf-view.schema.json +773 -0
  59. package/dist/contract/observation-v1.d.mts +42 -0
  60. package/dist/contract/observation-v1.mjs +555 -0
  61. package/dist/contract/observationIdentity.d.ts +11 -0
  62. package/dist/contract/observationIdentity.js +48 -0
  63. package/dist/contract/observationProjection.d.ts +3 -0
  64. package/dist/contract/observationProjection.js +29 -0
  65. package/dist/contract/validate.d.ts +20 -0
  66. package/dist/contract/validate.js +227 -0
  67. package/dist/dogfood/mp3WorkerGates.d.ts +54 -0
  68. package/dist/dogfood/mp3WorkerGates.js +86 -0
  69. package/dist/dogfood/retainDryRunArtifact.d.ts +45 -0
  70. package/dist/dogfood/retainDryRunArtifact.js +103 -0
  71. package/dist/dogfood/tenantDogfoodGates.d.ts +76 -0
  72. package/dist/dogfood/tenantDogfoodGates.js +206 -0
  73. package/dist/exporter.d.ts +56 -0
  74. package/dist/exporter.js +304 -0
  75. package/dist/express.d.ts +44 -0
  76. package/dist/express.js +118 -0
  77. package/dist/externalApiMeter.d.ts +72 -0
  78. package/dist/externalApiMeter.js +1167 -0
  79. package/dist/fastify.d.ts +53 -0
  80. package/dist/fastify.js +148 -0
  81. package/dist/index.d.ts +32 -0
  82. package/dist/index.js +31 -0
  83. package/dist/n4/deploymentMatrix.d.ts +22 -0
  84. package/dist/n4/deploymentMatrix.js +57 -0
  85. package/dist/n4/missingnessInventory.d.ts +22 -0
  86. package/dist/n4/missingnessInventory.js +72 -0
  87. package/dist/nest.d.ts +42 -0
  88. package/dist/nest.js +87 -0
  89. package/dist/processResources.d.ts +28 -0
  90. package/dist/processResources.js +31 -0
  91. package/dist/processRuntime.d.ts +35 -0
  92. package/dist/processRuntime.js +96 -0
  93. package/dist/requestContext.d.ts +21 -0
  94. package/dist/requestContext.js +33 -0
  95. package/dist/runtime.d.ts +60 -0
  96. package/dist/runtime.js +289 -0
  97. package/dist/runtimeHealth.d.ts +35 -0
  98. package/dist/runtimeHealth.js +121 -0
  99. package/dist/serverlessHints.d.ts +26 -0
  100. package/dist/serverlessHints.js +52 -0
  101. package/dist/tenantContext.d.ts +78 -0
  102. package/dist/tenantContext.js +226 -0
  103. package/dist/tenantDefaults.d.ts +9 -0
  104. package/dist/tenantDefaults.js +9 -0
  105. package/dist/types.d.ts +131 -0
  106. package/dist/types.js +2 -0
  107. package/dist/uploader.d.ts +23 -0
  108. package/dist/uploader.js +52 -0
  109. package/dist/windowId.d.ts +2 -0
  110. package/dist/windowId.js +7 -0
  111. package/examples/analyze-manifest.mjs +30 -0
  112. package/examples/bullmq-live-smoke.mjs +178 -0
  113. package/examples/bullmq-mp3-worker-gate.mjs +237 -0
  114. package/examples/express-dry-run.mjs +72 -0
  115. package/examples/process-mp3-restart-gate.mjs +135 -0
  116. package/examples/retain-dry-run.mjs +110 -0
  117. package/examples/tenant-dogfood.mjs +269 -0
  118. package/fixtures/analyze-sample/architectureNoise.ts +13 -0
  119. package/fixtures/analyze-sample/cpuHotPaths.ts +27 -0
  120. package/fixtures/analyze-sample/fanOutClient.ts +15 -0
  121. package/fixtures/analyze-sample/inferenceHotPaths.ts +22 -0
  122. package/fixtures/analyze-sample/memoryIoHotPaths.ts +21 -0
  123. package/fixtures/analyze-sample/modelLoadHotPaths.ts +11 -0
  124. package/fixtures/analyze-sample/nestedLookup.ts +15 -0
  125. package/fixtures/analyze-sample/retryClient.ts +10 -0
  126. package/fixtures/analyze-sample/securitySmell.ts +8 -0
  127. package/fixtures/analyze-sample/server.ts +11 -0
  128. package/fixtures/analyze-sample/styleOnly.ts +7 -0
  129. package/fixtures/analyze-sample/worker.ts +4 -0
  130. package/fixtures/dual-view-projection.v1.json +53 -0
  131. package/fixtures/identity-preimages.v1.json +42 -0
  132. package/fixtures/mp2_shadow_digest_golden.v1.json +47 -0
  133. package/fixtures/projection-batch-document.v1.json +170 -0
  134. package/fixtures/projection-batch.v1.json +9 -0
  135. package/fixtures/python_reference_shapes.md +24 -0
  136. package/fixtures/session_metadata_runtime_v0.json +120 -0
  137. package/fixtures/session_metadata_with_tenant.json +85 -0
  138. package/package.json +137 -0
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Heuristic JS/TS twin of Python ``unbounded_asyncio_gather_with_io``.
3
+ *
4
+ * RD0 disposition → mechanism ``unbounded_parallelism`` / ``network.io``.
5
+ * Flags ``Promise.all(…map…fetch)`` style fan-out without an obvious concurrency cap.
6
+ */
7
+ import { hasApiCall, PROMISE_ALL_RE, stripLineComment, } from './heuristicScan.js';
8
+ import { languageForPath, } from './resourceFindingCandidate.js';
9
+ const DETECTOR_ID = 'js.static.unbounded_promise_all';
10
+ const DETECTOR_VERSION = '0.1.0';
11
+ const LEGACY_ISSUE = 'unbounded_asyncio_gather_with_io';
12
+ const MECHANISM = 'unbounded_parallelism';
13
+ const TITLE = 'Unbounded Promise.all fan-out may spike concurrent I/O cost';
14
+ /** Obvious concurrency caps we treat as not-unbounded (heuristic). */
15
+ const CAP_HINT_RE = /\b(?:pLimit|p-limit|bottleneck|Semaphore|mapLimit|eachLimit|PromisePool|concurrency\s*:)\b/i;
16
+ /**
17
+ * Detect unbounded Promise.all + API fan-out in one source file.
18
+ */
19
+ export function detectUnboundedParallelismHits(source, relPath) {
20
+ const lines = source.split(/\r?\n/);
21
+ const hits = [];
22
+ const language = languageForPath(relPath);
23
+ const path = relPath.replace(/\\/g, '/');
24
+ for (let i = 0; i < lines.length; i += 1) {
25
+ const line = stripLineComment(lines[i] ?? '');
26
+ const lineNo = i + 1;
27
+ if (!PROMISE_ALL_RE.test(line))
28
+ continue;
29
+ // Look at this line + next 2 for map/fetch / API signals and cap hints.
30
+ const window = [line, stripLineComment(lines[i + 1] ?? ''), stripLineComment(lines[i + 2] ?? '')]
31
+ .join(' ');
32
+ if (CAP_HINT_RE.test(window))
33
+ continue;
34
+ const hasFanOut = /\.map\s*\(/.test(window) || /\.flatMap\s*\(/.test(window) || hasApiCall(window);
35
+ if (!hasFanOut)
36
+ continue;
37
+ if (!hasApiCall(window) && !/\.map\s*\(/.test(window))
38
+ continue;
39
+ // Require API somewhere in the window (avoid Promise.all(localCpuWork))
40
+ if (!hasApiCall(window))
41
+ continue;
42
+ hits.push({
43
+ mechanism_id: MECHANISM,
44
+ primary_domain: 'network.io',
45
+ admission_domain: 'network.io',
46
+ title: TITLE,
47
+ rel_path: path,
48
+ start_line: lineNo,
49
+ end_line: lineNo,
50
+ language,
51
+ detector_id: DETECTOR_ID,
52
+ detector_version: DETECTOR_VERSION,
53
+ legacy_issue_type: LEGACY_ISSUE,
54
+ });
55
+ }
56
+ return hits;
57
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Validate Analyze candidates against vendored ``resource_finding_v1`` JSON Schema.
3
+ * Mirrors observation-v1 Ajv usage; does not upload or change RunSession.
4
+ */
5
+ import type { ResourceFindingCandidateV1 } from './resourceFindingCandidate.js';
6
+ export type ResourceFindingValidationError = {
7
+ instancePath: string;
8
+ message: string;
9
+ };
10
+ export type ResourceFindingValidation = {
11
+ ok: boolean;
12
+ errors: ResourceFindingValidationError[];
13
+ };
14
+ /** Shape-check a single ``resource_finding_v1`` document. */
15
+ export declare function validateResourceFindingCandidate(document: unknown): ResourceFindingValidation;
16
+ /** Keep only schema-valid candidates (fail-closed for malformed emitter output). */
17
+ export declare function filterValidResourceFindings(findings: ResourceFindingCandidateV1[]): ResourceFindingCandidateV1[];
18
+ /** True when every finding validates. */
19
+ export declare function allResourceFindingsValid(findings: Iterable<ResourceFindingCandidateV1>): boolean;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Validate Analyze candidates against vendored ``resource_finding_v1`` JSON Schema.
3
+ * Mirrors observation-v1 Ajv usage; does not upload or change RunSession.
4
+ */
5
+ import { readFileSync } from 'node:fs';
6
+ import { createRequire } from 'node:module';
7
+ const require = createRequire(import.meta.url);
8
+ let shapeValidator = null;
9
+ function getValidator() {
10
+ if (!shapeValidator) {
11
+ const Ajv2020 = require('ajv/dist/2020.js').default;
12
+ const addFormats = require('ajv-formats').default;
13
+ const schema = JSON.parse(readFileSync(new URL('./schemas/resource-finding.schema.json', import.meta.url), 'utf8'));
14
+ const ajv = new Ajv2020({
15
+ strict: false,
16
+ strictNumbers: true,
17
+ allErrors: true,
18
+ ownProperties: true,
19
+ });
20
+ addFormats(ajv, ['date-time']);
21
+ shapeValidator = ajv.compile(schema);
22
+ }
23
+ return shapeValidator;
24
+ }
25
+ /** Shape-check a single ``resource_finding_v1`` document. */
26
+ export function validateResourceFindingCandidate(document) {
27
+ const validate = getValidator();
28
+ const ok = Boolean(validate(document));
29
+ const errors = (validate.errors || []).map((e) => ({
30
+ instancePath: e.instancePath || '',
31
+ message: e.message || 'invalid',
32
+ }));
33
+ return { ok, errors };
34
+ }
35
+ /** Keep only schema-valid candidates (fail-closed for malformed emitter output). */
36
+ export function filterValidResourceFindings(findings) {
37
+ return findings.filter((f) => validateResourceFindingCandidate(f).ok);
38
+ }
39
+ /** True when every finding validates. */
40
+ export function allResourceFindingsValid(findings) {
41
+ for (const f of findings) {
42
+ if (!validateResourceFindingCandidate(f).ok)
43
+ return false;
44
+ }
45
+ return true;
46
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * BullMQ worker binding (N3 / T2 Node analogue of Celery task_kwarg).
3
+ *
4
+ * No hard dependency on ``bullmq`` — wrap any processor that receives a job-like
5
+ * object with ``name`` + ``data``. Enqueue with organization_id (or configured
6
+ * kwarg) in ``job.data``.
7
+ */
8
+ import { type NetGreenerRuntime, type NetGreenerRuntimeOptions } from './runtime.js';
9
+ import { type TenantContext } from './tenantContext.js';
10
+ /** Minimal BullMQ Job shape (avoid hard dependency). */
11
+ export type BullMqJobLike = {
12
+ id?: string;
13
+ name?: string;
14
+ data?: Record<string, unknown>;
15
+ };
16
+ export type BullMqProcessorOptions = NetGreenerRuntimeOptions & {
17
+ /** Fallback service unit name when job.name is missing. */
18
+ defaultJobName?: string;
19
+ };
20
+ /**
21
+ * Wrap a BullMQ (or compatible) processor so each job:
22
+ * - binds tenant from ``job.data[NETGREENER_TENANT_TASK_KWARG]`` when configured
23
+ * - attributes outbound ``fetch`` to ``task:<job.name>``
24
+ * - records a runtime task sample for flush
25
+ */
26
+ export declare function netgreenerBullMqProcessor<Job extends BullMqJobLike, Result>(processor: (job: Job) => Promise<Result> | Result, opts?: BullMqProcessorOptions): (job: Job) => Promise<Result>;
27
+ /**
28
+ * Bind tenant + request attribution for one job without wrapping the whole Worker.
29
+ * Useful when you already own the processor loop.
30
+ */
31
+ export declare function runBullMqJobWithNetGreener<Job extends BullMqJobLike, Result>(job: Job, fn: () => Promise<Result> | Result, opts?: BullMqProcessorOptions): Promise<Result>;
32
+ export declare function getBullMqRuntime(): NetGreenerRuntime;
33
+ export type { TenantContext };
package/dist/bullmq.js ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * BullMQ worker binding (N3 / T2 Node analogue of Celery task_kwarg).
3
+ *
4
+ * No hard dependency on ``bullmq`` — wrap any processor that receives a job-like
5
+ * object with ``name`` + ``data``. Enqueue with organization_id (or configured
6
+ * kwarg) in ``job.data``.
7
+ */
8
+ import { getRuntime, } from './runtime.js';
9
+ import { loadTenantConfig, resolveTenantFromTaskData, runWithTenantContext, } from './tenantContext.js';
10
+ import { runWithRequestAttribution } from './requestContext.js';
11
+ import { beginProcessResourceSample, endProcessResourceSample, } from './processResources.js';
12
+ function serviceUnitForJob(job, fallback) {
13
+ const name = String(job.name || fallback || 'job').trim() || 'job';
14
+ return `task:${name.slice(0, 240)}`;
15
+ }
16
+ /**
17
+ * Wrap a BullMQ (or compatible) processor so each job:
18
+ * - binds tenant from ``job.data[NETGREENER_TENANT_TASK_KWARG]`` when configured
19
+ * - attributes outbound ``fetch`` to ``task:<job.name>``
20
+ * - records a runtime task sample for flush
21
+ */
22
+ export function netgreenerBullMqProcessor(processor, opts = {}) {
23
+ const runtime = getRuntime({
24
+ collector: 'bullmq_worker',
25
+ framework: 'bullmq',
26
+ ...opts,
27
+ });
28
+ runtime.start();
29
+ const tenantConfig = loadTenantConfig();
30
+ const fallbackName = opts.defaultJobName || 'job';
31
+ return async function netgreenerInstrumentedProcessor(job) {
32
+ if (!runtime.enabled) {
33
+ return await processor(job);
34
+ }
35
+ const tenant = resolveTenantFromTaskData(job?.data && typeof job.data === 'object' ? job.data : null, tenantConfig);
36
+ const serviceUnit = serviceUnitForJob(job, fallbackName);
37
+ const started = performance.now();
38
+ const resourceMark = beginProcessResourceSample();
39
+ let error = false;
40
+ const run = async () => {
41
+ try {
42
+ return await processor(job);
43
+ }
44
+ catch (err) {
45
+ error = true;
46
+ throw err;
47
+ }
48
+ finally {
49
+ const resources = endProcessResourceSample(resourceMark);
50
+ runtime.recordTask({
51
+ serviceUnit,
52
+ durationMs: performance.now() - started,
53
+ error,
54
+ tenantId: tenant?.tenantId ?? null,
55
+ cpuTimeMs: resources.cpuTimeMs,
56
+ peakRssKb: resources.rssKb,
57
+ });
58
+ }
59
+ };
60
+ return runWithTenantContext(tenant, () => runWithRequestAttribution({ serviceUnit }, () => run()));
61
+ };
62
+ }
63
+ /**
64
+ * Bind tenant + request attribution for one job without wrapping the whole Worker.
65
+ * Useful when you already own the processor loop.
66
+ */
67
+ export function runBullMqJobWithNetGreener(job, fn, opts = {}) {
68
+ const runtime = getRuntime({
69
+ collector: 'bullmq_worker',
70
+ framework: 'bullmq',
71
+ ...opts,
72
+ });
73
+ runtime.start();
74
+ const tenantConfig = loadTenantConfig();
75
+ const tenant = resolveTenantFromTaskData(job?.data && typeof job.data === 'object' ? job.data : null, tenantConfig);
76
+ const serviceUnit = serviceUnitForJob(job, opts.defaultJobName || 'job');
77
+ const started = performance.now();
78
+ const resourceMark = beginProcessResourceSample();
79
+ let error = false;
80
+ const run = async () => {
81
+ try {
82
+ return await fn();
83
+ }
84
+ catch (err) {
85
+ error = true;
86
+ throw err;
87
+ }
88
+ finally {
89
+ if (runtime.enabled) {
90
+ const resources = endProcessResourceSample(resourceMark);
91
+ runtime.recordTask({
92
+ serviceUnit,
93
+ durationMs: performance.now() - started,
94
+ error,
95
+ tenantId: tenant?.tenantId ?? null,
96
+ cpuTimeMs: resources.cpuTimeMs,
97
+ peakRssKb: resources.rssKb,
98
+ });
99
+ }
100
+ }
101
+ };
102
+ return runWithTenantContext(tenant, () => runWithRequestAttribution({ serviceUnit }, () => run()));
103
+ }
104
+ export function getBullMqRuntime() {
105
+ return getRuntime({ collector: 'bullmq_worker', framework: 'bullmq' });
106
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Shared CLI help / version copy for the Node ``netgreener`` scaffold.
3
+ * Upload remains off; run/optimize stay stubs.
4
+ */
5
+ export declare const CLI_VERSION = "0.1.0";
6
+ export declare function formatRootHelp(): string;
7
+ export declare function formatAnalyzeHelp(): string;
8
+ export declare function formatStubHelp(command: 'run' | 'optimize'): string;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Shared CLI help / version copy for the Node ``netgreener`` scaffold.
3
+ * Upload remains off; run/optimize stay stubs.
4
+ */
5
+ export const CLI_VERSION = '0.1.0';
6
+ export function formatRootHelp() {
7
+ return `NetGreener Node CLI ${CLI_VERSION} (scaffold)
8
+
9
+ Usage:
10
+ netgreener <command> [options]
11
+ netgreener help [command]
12
+
13
+ Commands:
14
+ analyze <projectDir> Local resource Analyze (manifest + resource_finding_v1)
15
+ run (stub) Not implemented — will not upload
16
+ optimize (stub) Not implemented — will not upload
17
+ help [command] Show this help or a command's help
18
+ version Show version
19
+
20
+ Global:
21
+ -h, --help Show this help
22
+ -V, --version Show version
23
+
24
+ Examples:
25
+ netgreener analyze ./my-app
26
+ netgreener analyze ./my-app --json-summary
27
+ netgreener analyze ./my-app --fail-on any --max-at-or-above 0
28
+ netgreener analyze ./my-app --paths src/hot.ts,src/retry.ts --findings-only
29
+ netgreener-analyze ./my-app # alias bin (same Analyze path)
30
+
31
+ Notes:
32
+ • Analyze prints local candidates only (upload=never).
33
+ • optimize / run are stubs (exit 2) — see OPTIMIZE_STUB.md; no upload / no patches.
34
+ • Does not modify RunSession / metering pipelines.
35
+ • VS Code: Node-first folders use this local Analyze path (same guarantee).
36
+
37
+ Analyze options: netgreener help analyze
38
+ Optimize stub: netgreener help optimize
39
+ `;
40
+ }
41
+ export function formatAnalyzeHelp() {
42
+ return `Usage: netgreener analyze <projectDir> [options]
43
+ (alias) netgreener-analyze <projectDir> [options]
44
+
45
+ Print NetGreener Analyze scaffold output:
46
+ service_manifest_v0 + resource_finding_v1 candidates (JSON on stdout).
47
+
48
+ Does not upload and does not modify RunSession / metering pipelines.
49
+
50
+ Options:
51
+ --project-id <id> Stamp project_id on candidates (default: local)
52
+ --paths a,b Explicit repo-relative source paths (comma-separated)
53
+ --findings-only Emit findings JSON array only
54
+ --manifest-only Emit service_manifest_v0 JSON only
55
+ --json-summary Emit netgreener_analyze_gate_summary_v0 only (CI)
56
+ --fail-on any|<mechanism>[,…] Fail (exit 1) when matching candidates exceed cap
57
+ --max-at-or-above <n> With --fail-on: allow up to N matches (default 0)
58
+ --quiet No stderr status lines
59
+ -h, --help Show this help
60
+
61
+ Examples:
62
+ netgreener analyze ./fixtures/analyze-sample
63
+ netgreener analyze ./app --project-id 42 --quiet
64
+ netgreener analyze ./app --json-summary
65
+ netgreener analyze ./app --fail-on retry_amplification,external_api_overconsumption
66
+ netgreener analyze ./app --fail-on any --max-at-or-above 5
67
+ netgreener analyze ./app --paths src/server.ts --findings-only
68
+ netgreener-analyze ./app --manifest-only
69
+
70
+ CI tip: pair --json-summary with --fail-on for gate annotations without upload.
71
+ `;
72
+ }
73
+ export function formatStubHelp(command) {
74
+ if (command === 'optimize') {
75
+ return `netgreener optimize: not implemented yet in the Node CLI scaffold.
76
+
77
+ This is an intentional stub (MP4 / N5), not a silent no-op:
78
+ • exit code 2 when invoked as a command
79
+ • no upload, no LLM call, no patch apply, no tree writes
80
+ • no api_server /optimize/* wiring
81
+
82
+ Until a real JS/TS OptimizerProvider + VerificationProvider land behind the
83
+ shared MP4 interfaces, use local Analyze only:
84
+
85
+ netgreener analyze <projectDir>
86
+ netgreener analyze <projectDir> --fail-on any --json-summary
87
+ netgreener help analyze
88
+
89
+ Boundary doc: OPTIMIZE_STUB.md (in the @netgreener/runtime package root).
90
+ `;
91
+ }
92
+ return `netgreener ${command}: not implemented yet in the Node CLI scaffold.
93
+
94
+ No upload is performed. Use local Analyze instead:
95
+
96
+ netgreener analyze <projectDir>
97
+ netgreener help analyze
98
+ `;
99
+ }
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * NetGreener Node CLI entry (scaffold).
4
+ *
5
+ * netgreener analyze <dir> — resource Analyze candidates (implemented)
6
+ * netgreener run … — stub (not implemented; no upload)
7
+ * netgreener optimize … — stub (not implemented; no upload)
8
+ *
9
+ * Does not change RunSession / metering pipelines. Upload remains off.
10
+ */
11
+ /** Dispatch top-level ``netgreener`` commands. */
12
+ export declare function runNetgreenerCli(argv: string[]): number;
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * NetGreener Node CLI entry (scaffold).
4
+ *
5
+ * netgreener analyze <dir> — resource Analyze candidates (implemented)
6
+ * netgreener run … — stub (not implemented; no upload)
7
+ * netgreener optimize … — stub (not implemented; no upload)
8
+ *
9
+ * Does not change RunSession / metering pipelines. Upload remains off.
10
+ */
11
+ import { resolve } from 'node:path';
12
+ import { pathToFileURL } from 'node:url';
13
+ import { runAnalyzeCli } from '../analyze/cli.js';
14
+ import { CLI_VERSION, formatAnalyzeHelp, formatRootHelp, formatStubHelp, } from './helpText.js';
15
+ function stubCommand(name) {
16
+ process.stderr.write(formatStubHelp(name));
17
+ return 2;
18
+ }
19
+ function printHelpFor(topic) {
20
+ const key = (topic || '').trim().toLowerCase();
21
+ if (!key || key === 'help') {
22
+ process.stdout.write(formatRootHelp());
23
+ return 0;
24
+ }
25
+ if (key === 'analyze') {
26
+ process.stdout.write(formatAnalyzeHelp());
27
+ return 0;
28
+ }
29
+ if (key === 'run' || key === 'optimize') {
30
+ process.stderr.write(formatStubHelp(key));
31
+ return 0;
32
+ }
33
+ process.stderr.write(`Unknown help topic: ${topic}\n\n`);
34
+ process.stdout.write(formatRootHelp());
35
+ return 2;
36
+ }
37
+ /** Dispatch top-level ``netgreener`` commands. */
38
+ export function runNetgreenerCli(argv) {
39
+ const [cmd, ...rest] = argv;
40
+ if (!cmd || cmd === '-h' || cmd === '--help') {
41
+ process.stdout.write(formatRootHelp());
42
+ return 0;
43
+ }
44
+ if (cmd === '-V' || cmd === '--version' || cmd === 'version') {
45
+ process.stdout.write(`${CLI_VERSION}\n`);
46
+ return 0;
47
+ }
48
+ if (cmd === 'help') {
49
+ return printHelpFor(rest[0]);
50
+ }
51
+ if (cmd === 'analyze') {
52
+ return runAnalyzeCli(rest);
53
+ }
54
+ if (cmd === 'run' || cmd === 'optimize') {
55
+ return stubCommand(cmd);
56
+ }
57
+ process.stderr.write(`Unknown command: ${cmd}\n\n`);
58
+ process.stdout.write(formatRootHelp());
59
+ return 2;
60
+ }
61
+ const invokedDirectly = typeof process.argv[1] === 'string' &&
62
+ pathToFileURL(resolve(process.argv[1])).href === import.meta.url;
63
+ if (invokedDirectly) {
64
+ process.exitCode = runNetgreenerCli(process.argv.slice(2));
65
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * NetGreener collector IPC client (MP2).
3
+ *
4
+ * Wire-compatible with Python ``collector_protocol``:
5
+ * 4-byte big-endian length + UTF-8 JSON object, max 256 KiB.
6
+ * Endpoint schemes: ``tcp://host:port`` (Windows/Linux) or ``unix:///abs/path`` (non-Windows).
7
+ */
8
+ import { type Socket } from 'node:net';
9
+ export declare const COLLECTOR_MAX_FRAME_BYTES: number;
10
+ export type CollectorIpcAck = {
11
+ ok: boolean;
12
+ event_id?: string;
13
+ status?: string;
14
+ error?: string;
15
+ };
16
+ export declare class CollectorIpcError extends Error {
17
+ constructor(message: string);
18
+ }
19
+ export declare function normalizeCollectorEndpoint(endpoint: string): string;
20
+ export declare function encodeCollectorFrame(payload: Record<string, unknown>): Buffer;
21
+ export declare function recvCollectorFrame(socket: Socket): Promise<Record<string, unknown>>;
22
+ export type SendCollectorEventOptions = {
23
+ endpoint: string;
24
+ payload: Record<string, unknown>;
25
+ timeoutMs?: number;
26
+ };
27
+ export declare function sendCollectorEvent(options: SendCollectorEventOptions): Promise<CollectorIpcAck>;
28
+ export declare function newCollectorEventId(): string;
@@ -0,0 +1,189 @@
1
+ /**
2
+ * NetGreener collector IPC client (MP2).
3
+ *
4
+ * Wire-compatible with Python ``collector_protocol``:
5
+ * 4-byte big-endian length + UTF-8 JSON object, max 256 KiB.
6
+ * Endpoint schemes: ``tcp://host:port`` (Windows/Linux) or ``unix:///abs/path`` (non-Windows).
7
+ */
8
+ import { createConnection } from 'node:net';
9
+ import { randomUUID } from 'node:crypto';
10
+ import { URL } from 'node:url';
11
+ export const COLLECTOR_MAX_FRAME_BYTES = 256 * 1024;
12
+ export class CollectorIpcError extends Error {
13
+ constructor(message) {
14
+ super(message);
15
+ this.name = 'CollectorIpcError';
16
+ }
17
+ }
18
+ export function normalizeCollectorEndpoint(endpoint) {
19
+ const raw = endpoint.trim();
20
+ if (!raw)
21
+ throw new CollectorIpcError('collector endpoint is empty');
22
+ if (/^(tcp|unix):\/\//i.test(raw))
23
+ return raw;
24
+ // Allow host:port convenience form used in some local notes.
25
+ if (/^[^/]+:\d+$/.test(raw))
26
+ return `tcp://${raw}`;
27
+ throw new CollectorIpcError('Collector endpoint must use tcp:// or unix://');
28
+ }
29
+ export function encodeCollectorFrame(payload) {
30
+ const body = Buffer.from(JSON.stringify(payload), 'utf8');
31
+ if (body.length < 1 || body.length > COLLECTOR_MAX_FRAME_BYTES) {
32
+ throw new CollectorIpcError(`Collector frame must be 1..${COLLECTOR_MAX_FRAME_BYTES} bytes`);
33
+ }
34
+ const header = Buffer.alloc(4);
35
+ header.writeUInt32BE(body.length, 0);
36
+ return Buffer.concat([header, body]);
37
+ }
38
+ async function readExact(socket, size) {
39
+ if (size === 0)
40
+ return Buffer.alloc(0);
41
+ // Paused-mode reads on an exclusively owned ACK socket.
42
+ // Resolve readable/close wakes on setImmediate so a sync 'readable'
43
+ // re-entry cannot microtask-spin and starve response timers (Windows).
44
+ socket.pause();
45
+ const chunks = [];
46
+ let received = 0;
47
+ while (received < size) {
48
+ const chunk = socket.read(size - received);
49
+ if (chunk !== null && chunk.length > 0) {
50
+ chunks.push(chunk);
51
+ received += chunk.length;
52
+ continue;
53
+ }
54
+ if (socket.readableEnded || socket.destroyed) {
55
+ throw new CollectorIpcError('Collector peer closed the connection mid-frame');
56
+ }
57
+ await new Promise((resolve, reject) => {
58
+ let settled = false;
59
+ const finish = (fn) => {
60
+ if (settled)
61
+ return;
62
+ settled = true;
63
+ socket.off('readable', onReadable);
64
+ socket.off('end', onClosed);
65
+ socket.off('close', onClosed);
66
+ socket.off('error', onError);
67
+ setImmediate(fn);
68
+ };
69
+ const onReadable = () => finish(() => resolve());
70
+ const onClosed = () => finish(() => reject(new CollectorIpcError('Collector peer closed the connection mid-frame')));
71
+ const onError = (err) => finish(() => reject(err));
72
+ socket.once('readable', onReadable);
73
+ socket.once('end', onClosed);
74
+ socket.once('close', onClosed);
75
+ socket.once('error', onError);
76
+ if (socket.readableEnded || socket.destroyed) {
77
+ onClosed();
78
+ return;
79
+ }
80
+ if (socket.readableLength > 0) {
81
+ onReadable();
82
+ }
83
+ });
84
+ }
85
+ return Buffer.concat(chunks);
86
+ }
87
+ export async function recvCollectorFrame(socket) {
88
+ const header = await readExact(socket, 4);
89
+ const length = header.readUInt32BE(0);
90
+ if (length < 1 || length > COLLECTOR_MAX_FRAME_BYTES) {
91
+ throw new CollectorIpcError(`Collector frame must be 1..${COLLECTOR_MAX_FRAME_BYTES} bytes`);
92
+ }
93
+ const body = await readExact(socket, length);
94
+ let parsed;
95
+ try {
96
+ parsed = JSON.parse(body.toString('utf8'));
97
+ }
98
+ catch {
99
+ throw new CollectorIpcError('Collector frame contains invalid JSON');
100
+ }
101
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
102
+ throw new CollectorIpcError('Collector frame must contain a JSON object');
103
+ }
104
+ return parsed;
105
+ }
106
+ function connectCollector(endpoint, timeoutMs) {
107
+ const normalized = normalizeCollectorEndpoint(endpoint);
108
+ return new Promise((resolve, reject) => {
109
+ const url = new URL(normalized);
110
+ let socket;
111
+ const onError = (err) => {
112
+ socket.destroy();
113
+ reject(err);
114
+ };
115
+ if (url.protocol === 'tcp:') {
116
+ const host = url.hostname;
117
+ const port = Number(url.port);
118
+ if (!host || !Number.isFinite(port) || port <= 0) {
119
+ reject(new CollectorIpcError('TCP collector endpoint must include host and port'));
120
+ return;
121
+ }
122
+ socket = createConnection({ host, port });
123
+ }
124
+ else if (url.protocol === 'unix:') {
125
+ if (process.platform === 'win32') {
126
+ reject(new CollectorIpcError('Unix collector endpoints are unavailable on Windows'));
127
+ return;
128
+ }
129
+ const path = url.pathname;
130
+ if (!path || !path.startsWith('/')) {
131
+ reject(new CollectorIpcError('Unix collector endpoint must use an absolute path'));
132
+ return;
133
+ }
134
+ socket = createConnection({ path });
135
+ }
136
+ else {
137
+ reject(new CollectorIpcError('Collector endpoint must use tcp:// or unix://'));
138
+ return;
139
+ }
140
+ socket.setTimeout(timeoutMs);
141
+ socket.once('connect', () => {
142
+ socket.setTimeout(0);
143
+ socket.off('error', onError);
144
+ resolve(socket);
145
+ });
146
+ socket.once('timeout', () => {
147
+ socket.destroy();
148
+ reject(new CollectorIpcError('Collector connection timed out'));
149
+ });
150
+ socket.once('error', onError);
151
+ });
152
+ }
153
+ export async function sendCollectorEvent(options) {
154
+ const timeoutMs = Math.max(1, options.timeoutMs ?? 100);
155
+ const socket = await connectCollector(options.endpoint, timeoutMs);
156
+ try {
157
+ const frame = encodeCollectorFrame(options.payload);
158
+ await new Promise((resolve, reject) => {
159
+ socket.write(frame, (err) => (err ? reject(err) : resolve()));
160
+ });
161
+ const ack = await new Promise((resolve, reject) => {
162
+ const timer = setTimeout(() => {
163
+ socket.destroy();
164
+ reject(new CollectorIpcError('Collector response timed out'));
165
+ }, timeoutMs);
166
+ recvCollectorFrame(socket)
167
+ .then((frame) => {
168
+ clearTimeout(timer);
169
+ resolve(frame);
170
+ })
171
+ .catch((err) => {
172
+ clearTimeout(timer);
173
+ reject(err);
174
+ });
175
+ });
176
+ return {
177
+ ok: ack.ok === true,
178
+ event_id: typeof ack.event_id === 'string' ? ack.event_id : undefined,
179
+ status: typeof ack.status === 'string' ? ack.status : undefined,
180
+ error: typeof ack.error === 'string' ? ack.error : undefined,
181
+ };
182
+ }
183
+ finally {
184
+ socket.destroy();
185
+ }
186
+ }
187
+ export function newCollectorEventId() {
188
+ return randomUUID();
189
+ }