@neat.is/core 0.3.5 → 0.3.7
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/dist/{chunk-4ASCXBZF.js → chunk-75IBA4UL.js} +34 -5
- package/dist/chunk-75IBA4UL.js.map +1 -0
- package/dist/{chunk-G3PDTGOW.js → chunk-CY67YKNO.js} +2 -2
- package/dist/{chunk-KCEZSFU2.js → chunk-EDHOCFOG.js} +87 -15
- package/dist/chunk-EDHOCFOG.js.map +1 -0
- package/dist/{chunk-BUB3ASD5.js → chunk-ZU2RQRCN.js} +3 -3
- package/dist/chunk-ZU2RQRCN.js.map +1 -0
- package/dist/cli.cjs +433 -65
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +400 -62
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +119 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +200 -18
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +83 -3
- package/dist/neatd.js.map +1 -1
- package/dist/{otel-grpc-J4O2SIBZ.js → otel-grpc-PM4SWPZE.js} +3 -3
- package/dist/server.cjs +34 -5
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +3 -3
- package/package.json +2 -2
- package/dist/chunk-4ASCXBZF.js.map +0 -1
- package/dist/chunk-BUB3ASD5.js.map +0 -1
- package/dist/chunk-KCEZSFU2.js.map +0 -1
- /package/dist/{chunk-G3PDTGOW.js.map → chunk-CY67YKNO.js.map} +0 -0
- /package/dist/{otel-grpc-J4O2SIBZ.js.map → otel-grpc-PM4SWPZE.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -39,13 +39,13 @@ import {
|
|
|
39
39
|
setStatus,
|
|
40
40
|
startPersistLoop,
|
|
41
41
|
startStalenessLoop
|
|
42
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-ZU2RQRCN.js";
|
|
43
43
|
import {
|
|
44
44
|
startOtelGrpcReceiver
|
|
45
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-CY67YKNO.js";
|
|
46
46
|
import {
|
|
47
47
|
buildOtelReceiver
|
|
48
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-75IBA4UL.js";
|
|
49
49
|
|
|
50
50
|
// src/cli.ts
|
|
51
51
|
import path4 from "path";
|
|
@@ -398,15 +398,68 @@ async function startWatch(graph, opts) {
|
|
|
398
398
|
// src/installers/javascript.ts
|
|
399
399
|
import { promises as fs2 } from "fs";
|
|
400
400
|
import path2 from "path";
|
|
401
|
+
|
|
402
|
+
// src/installers/templates.ts
|
|
403
|
+
var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
|
|
404
|
+
var OTEL_INIT_CJS = `${OTEL_INIT_HEADER}
|
|
405
|
+
// Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
|
|
406
|
+
// the auto-instrumentation hook attaches. Configure via the env file.
|
|
407
|
+
const path = require('node:path')
|
|
408
|
+
try {
|
|
409
|
+
require('dotenv').config({ path: path.join(__dirname, '.env.neat') })
|
|
410
|
+
} catch (err) {
|
|
411
|
+
// dotenv unavailable \u2014 fall through to process.env.
|
|
412
|
+
}
|
|
413
|
+
require('@opentelemetry/auto-instrumentations-node/register')
|
|
414
|
+
`;
|
|
415
|
+
var OTEL_INIT_ESM = `${OTEL_INIT_HEADER}
|
|
416
|
+
// Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
|
|
417
|
+
// the auto-instrumentation hook attaches. Configure via the env file.
|
|
418
|
+
import { fileURLToPath } from 'node:url'
|
|
419
|
+
import path from 'node:path'
|
|
420
|
+
import dotenv from 'dotenv'
|
|
421
|
+
|
|
422
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
423
|
+
dotenv.config({ path: path.join(here, '.env.neat') })
|
|
424
|
+
|
|
425
|
+
await import('@opentelemetry/auto-instrumentations-node/register')
|
|
426
|
+
`;
|
|
427
|
+
var OTEL_INIT_TS = `${OTEL_INIT_HEADER}
|
|
428
|
+
// Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
|
|
429
|
+
// the auto-instrumentation hook attaches. Configure via the env file.
|
|
430
|
+
import { fileURLToPath } from 'node:url'
|
|
431
|
+
import path from 'node:path'
|
|
432
|
+
import dotenv from 'dotenv'
|
|
433
|
+
|
|
434
|
+
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
435
|
+
dotenv.config({ path: path.join(here, '.env.neat') })
|
|
436
|
+
|
|
437
|
+
await import('@opentelemetry/auto-instrumentations-node/register')
|
|
438
|
+
`;
|
|
439
|
+
function renderEnvNeat(serviceName) {
|
|
440
|
+
return [
|
|
441
|
+
"# Generated by `neat init --apply` (ADR-069).",
|
|
442
|
+
`OTEL_SERVICE_NAME=${serviceName}`,
|
|
443
|
+
"OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318",
|
|
444
|
+
""
|
|
445
|
+
].join("\n");
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// src/installers/javascript.ts
|
|
401
449
|
var SDK_PACKAGES = [
|
|
402
450
|
{ name: "@opentelemetry/api", version: "^1.9.0" },
|
|
403
451
|
{ name: "@opentelemetry/sdk-node", version: "^0.57.0" },
|
|
404
|
-
{ name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" }
|
|
452
|
+
{ name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" },
|
|
453
|
+
// ADR-069 §5 — dotenv is the fourth dep. The generated otel-init loads
|
|
454
|
+
// .env.neat through it so OTEL_SERVICE_NAME and the endpoint are in scope
|
|
455
|
+
// before the auto-instrumentation hook attaches.
|
|
456
|
+
{ name: "dotenv", version: "^16.4.5" }
|
|
405
457
|
];
|
|
406
|
-
var AUTO_INSTRUMENT_REQUIRE = "--require @opentelemetry/auto-instrumentations-node/register";
|
|
407
458
|
var OTEL_ENV = {
|
|
408
|
-
//
|
|
409
|
-
//
|
|
459
|
+
// ADR-069 §4 — endpoint moves into the per-package .env.neat (written
|
|
460
|
+
// by the apply phase). The envEdits surface stays for the dry-run
|
|
461
|
+
// patch render: it documents the key/value the user can inspect in the
|
|
462
|
+
// generated .env.neat.
|
|
410
463
|
file: null,
|
|
411
464
|
key: "OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
412
465
|
value: "http://localhost:4318"
|
|
@@ -419,16 +472,134 @@ async function readPackageJson(serviceDir) {
|
|
|
419
472
|
return null;
|
|
420
473
|
}
|
|
421
474
|
}
|
|
475
|
+
async function exists(p) {
|
|
476
|
+
try {
|
|
477
|
+
await fs2.stat(p);
|
|
478
|
+
return true;
|
|
479
|
+
} catch {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
422
483
|
async function detect(serviceDir) {
|
|
423
484
|
const pkg = await readPackageJson(serviceDir);
|
|
424
485
|
return pkg !== null && typeof pkg.name === "string";
|
|
425
486
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
487
|
+
var INDEX_EXTENSIONS = [".ts", ".tsx", ".js", ".mjs", ".cjs"];
|
|
488
|
+
var INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `index${ext}`);
|
|
489
|
+
var SRC_INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `src/index${ext}`);
|
|
490
|
+
var SRC_NAMED_CANDIDATES = ["server", "main", "app"].flatMap(
|
|
491
|
+
(name) => INDEX_EXTENSIONS.map((ext) => `src/${name}${ext}`)
|
|
492
|
+
);
|
|
493
|
+
var SCRIPT_LAUNCHERS = /* @__PURE__ */ new Set([
|
|
494
|
+
"node",
|
|
495
|
+
"ts-node",
|
|
496
|
+
"tsx",
|
|
497
|
+
"ts-node-dev",
|
|
498
|
+
"nodemon",
|
|
499
|
+
"npx",
|
|
500
|
+
"pnpm",
|
|
501
|
+
"yarn",
|
|
502
|
+
"npm",
|
|
503
|
+
"cross-env",
|
|
504
|
+
"dotenv",
|
|
505
|
+
"--"
|
|
506
|
+
]);
|
|
507
|
+
function looksLikeEntryPath(token) {
|
|
508
|
+
if (token.length === 0) return false;
|
|
509
|
+
if (token.startsWith("-")) return false;
|
|
510
|
+
if (token.includes("=")) return false;
|
|
511
|
+
if (token.includes("/")) return true;
|
|
512
|
+
return /\.(?:m?[jt]sx?|c[jt]s)$/.test(token);
|
|
513
|
+
}
|
|
514
|
+
function scriptHasShellChain(script) {
|
|
515
|
+
return /(?:&&|\|\||;|\|(?!\|))/.test(script);
|
|
516
|
+
}
|
|
517
|
+
function entryFromScript(script) {
|
|
518
|
+
if (!script) return void 0;
|
|
519
|
+
if (scriptHasShellChain(script)) return void 0;
|
|
520
|
+
const tokens = script.split(/\s+/).filter((t) => t.length > 0);
|
|
521
|
+
for (const token of tokens) {
|
|
522
|
+
const lower = token.toLowerCase();
|
|
523
|
+
if (SCRIPT_LAUNCHERS.has(lower)) continue;
|
|
524
|
+
const cleaned = token.startsWith("./") ? token.slice(2) : token;
|
|
525
|
+
if (looksLikeEntryPath(cleaned)) return cleaned;
|
|
430
526
|
}
|
|
431
|
-
return
|
|
527
|
+
return void 0;
|
|
528
|
+
}
|
|
529
|
+
async function resolveEntry(serviceDir, pkg) {
|
|
530
|
+
if (typeof pkg.main === "string" && pkg.main.length > 0) {
|
|
531
|
+
const candidate = path2.resolve(serviceDir, pkg.main);
|
|
532
|
+
if (await exists(candidate)) return candidate;
|
|
533
|
+
}
|
|
534
|
+
if (pkg.bin) {
|
|
535
|
+
let binEntry;
|
|
536
|
+
if (typeof pkg.bin === "string") {
|
|
537
|
+
binEntry = pkg.bin;
|
|
538
|
+
} else if (pkg.name && typeof pkg.bin[pkg.name] === "string") {
|
|
539
|
+
binEntry = pkg.bin[pkg.name];
|
|
540
|
+
} else {
|
|
541
|
+
const first = Object.values(pkg.bin)[0];
|
|
542
|
+
if (typeof first === "string") binEntry = first;
|
|
543
|
+
}
|
|
544
|
+
if (binEntry) {
|
|
545
|
+
const candidate = path2.resolve(serviceDir, binEntry);
|
|
546
|
+
if (await exists(candidate)) return candidate;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
const startEntry = entryFromScript(pkg.scripts?.start);
|
|
550
|
+
if (startEntry) {
|
|
551
|
+
const candidate = path2.resolve(serviceDir, startEntry);
|
|
552
|
+
if (await exists(candidate)) return candidate;
|
|
553
|
+
}
|
|
554
|
+
const devEntry = entryFromScript(pkg.scripts?.dev);
|
|
555
|
+
if (devEntry) {
|
|
556
|
+
const candidate = path2.resolve(serviceDir, devEntry);
|
|
557
|
+
if (await exists(candidate)) return candidate;
|
|
558
|
+
}
|
|
559
|
+
for (const rel of SRC_INDEX_CANDIDATES) {
|
|
560
|
+
const candidate = path2.join(serviceDir, rel);
|
|
561
|
+
if (await exists(candidate)) return candidate;
|
|
562
|
+
}
|
|
563
|
+
for (const rel of SRC_NAMED_CANDIDATES) {
|
|
564
|
+
const candidate = path2.join(serviceDir, rel);
|
|
565
|
+
if (await exists(candidate)) return candidate;
|
|
566
|
+
}
|
|
567
|
+
for (const name of INDEX_CANDIDATES) {
|
|
568
|
+
const candidate = path2.join(serviceDir, name);
|
|
569
|
+
if (await exists(candidate)) return candidate;
|
|
570
|
+
}
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
function dispatchEntry(entryFile, pkg) {
|
|
574
|
+
const ext = path2.extname(entryFile).toLowerCase();
|
|
575
|
+
if (ext === ".ts" || ext === ".tsx") return "ts";
|
|
576
|
+
if (ext === ".mjs") return "esm";
|
|
577
|
+
if (ext === ".cjs") return "cjs";
|
|
578
|
+
return pkg.type === "module" ? "esm" : "cjs";
|
|
579
|
+
}
|
|
580
|
+
function otelInitFilename(flavor) {
|
|
581
|
+
if (flavor === "ts") return "otel-init.ts";
|
|
582
|
+
if (flavor === "esm") return "otel-init.mjs";
|
|
583
|
+
return "otel-init.cjs";
|
|
584
|
+
}
|
|
585
|
+
function otelInitContents(flavor) {
|
|
586
|
+
if (flavor === "ts") return OTEL_INIT_TS;
|
|
587
|
+
if (flavor === "esm") return OTEL_INIT_ESM;
|
|
588
|
+
return OTEL_INIT_CJS;
|
|
589
|
+
}
|
|
590
|
+
function injectionLine(flavor, entryFile, otelInitFile) {
|
|
591
|
+
let rel = path2.relative(path2.dirname(entryFile), otelInitFile);
|
|
592
|
+
if (!rel.startsWith(".")) rel = `./${rel}`;
|
|
593
|
+
rel = rel.split(path2.sep).join("/");
|
|
594
|
+
if (flavor === "cjs") return `require('${rel}')`;
|
|
595
|
+
if (flavor === "esm") return `import '${rel}'`;
|
|
596
|
+
const tsRel = rel.replace(/\.ts$/, "");
|
|
597
|
+
return `import '${tsRel}'`;
|
|
598
|
+
}
|
|
599
|
+
function lineIsOtelInjection(line) {
|
|
600
|
+
const trimmed = line.trim();
|
|
601
|
+
if (trimmed.length === 0) return false;
|
|
602
|
+
return /(?:require\(|import\s+)['"]\.\/otel-init[^'"]*['"]/.test(trimmed);
|
|
432
603
|
}
|
|
433
604
|
async function plan(serviceDir) {
|
|
434
605
|
const pkg = await readPackageJson(serviceDir);
|
|
@@ -438,9 +609,17 @@ async function plan(serviceDir) {
|
|
|
438
609
|
serviceDir,
|
|
439
610
|
dependencyEdits: [],
|
|
440
611
|
entrypointEdits: [],
|
|
441
|
-
envEdits: []
|
|
612
|
+
envEdits: [],
|
|
613
|
+
generatedFiles: []
|
|
442
614
|
};
|
|
443
615
|
if (!pkg) return empty;
|
|
616
|
+
const entryFile = await resolveEntry(serviceDir, pkg);
|
|
617
|
+
if (!entryFile) {
|
|
618
|
+
return { ...empty, libOnly: true };
|
|
619
|
+
}
|
|
620
|
+
const flavor = dispatchEntry(entryFile, pkg);
|
|
621
|
+
const otelInitFile = path2.join(path2.dirname(entryFile), otelInitFilename(flavor));
|
|
622
|
+
const envNeatFile = path2.join(serviceDir, ".env.neat");
|
|
444
623
|
const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
445
624
|
const dependencyEdits = [];
|
|
446
625
|
for (const sdk of SDK_PACKAGES) {
|
|
@@ -453,14 +632,37 @@ async function plan(serviceDir) {
|
|
|
453
632
|
});
|
|
454
633
|
}
|
|
455
634
|
const entrypointEdits = [];
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
635
|
+
try {
|
|
636
|
+
const raw = await fs2.readFile(entryFile, "utf8");
|
|
637
|
+
const lines = raw.split(/\r?\n/);
|
|
638
|
+
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
639
|
+
if (!lineIsOtelInjection(firstReal)) {
|
|
640
|
+
const inject = injectionLine(flavor, entryFile, otelInitFile);
|
|
641
|
+
entrypointEdits.push({
|
|
642
|
+
file: entryFile,
|
|
643
|
+
before: firstReal,
|
|
644
|
+
after: inject
|
|
645
|
+
});
|
|
461
646
|
}
|
|
647
|
+
} catch {
|
|
648
|
+
return { ...empty, libOnly: true };
|
|
649
|
+
}
|
|
650
|
+
const generatedFiles = [];
|
|
651
|
+
if (!await exists(otelInitFile)) {
|
|
652
|
+
generatedFiles.push({
|
|
653
|
+
file: otelInitFile,
|
|
654
|
+
contents: otelInitContents(flavor),
|
|
655
|
+
skipIfExists: true
|
|
656
|
+
});
|
|
462
657
|
}
|
|
463
|
-
if (
|
|
658
|
+
if (!await exists(envNeatFile)) {
|
|
659
|
+
generatedFiles.push({
|
|
660
|
+
file: envNeatFile,
|
|
661
|
+
contents: renderEnvNeat(pkg.name ?? path2.basename(serviceDir)),
|
|
662
|
+
skipIfExists: true
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
if (dependencyEdits.length === 0 && entrypointEdits.length === 0 && generatedFiles.length === 0) {
|
|
464
666
|
return empty;
|
|
465
667
|
}
|
|
466
668
|
return {
|
|
@@ -468,53 +670,129 @@ async function plan(serviceDir) {
|
|
|
468
670
|
serviceDir,
|
|
469
671
|
dependencyEdits,
|
|
470
672
|
entrypointEdits,
|
|
471
|
-
envEdits: [OTEL_ENV]
|
|
673
|
+
envEdits: [OTEL_ENV],
|
|
674
|
+
generatedFiles,
|
|
675
|
+
entryFile,
|
|
676
|
+
libOnly: false
|
|
472
677
|
};
|
|
473
678
|
}
|
|
679
|
+
function isAllowedWritePath(serviceDir, target) {
|
|
680
|
+
const rel = path2.relative(serviceDir, target);
|
|
681
|
+
if (rel.startsWith("..")) return false;
|
|
682
|
+
const base = path2.basename(target);
|
|
683
|
+
if (base === "package.json") return true;
|
|
684
|
+
if (base === ".env.neat") return true;
|
|
685
|
+
if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
async function writeAtomic(file, contents) {
|
|
689
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
690
|
+
await fs2.writeFile(tmp, contents, "utf8");
|
|
691
|
+
await fs2.rename(tmp, file);
|
|
692
|
+
}
|
|
474
693
|
async function apply(installPlan) {
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
694
|
+
const { serviceDir } = installPlan;
|
|
695
|
+
if (installPlan.libOnly) {
|
|
696
|
+
return {
|
|
697
|
+
serviceDir,
|
|
698
|
+
outcome: "lib-only",
|
|
699
|
+
reason: "no resolvable entry point",
|
|
700
|
+
writtenFiles: []
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
if (installPlan.dependencyEdits.length === 0 && installPlan.entrypointEdits.length === 0 && (installPlan.generatedFiles?.length ?? 0) === 0) {
|
|
704
|
+
return {
|
|
705
|
+
serviceDir,
|
|
706
|
+
outcome: "already-instrumented",
|
|
707
|
+
writtenFiles: []
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
const allTargets = /* @__PURE__ */ new Set();
|
|
711
|
+
for (const d of installPlan.dependencyEdits) allTargets.add(d.file);
|
|
712
|
+
for (const e of installPlan.entrypointEdits) allTargets.add(e.file);
|
|
713
|
+
for (const g of installPlan.generatedFiles ?? []) allTargets.add(g.file);
|
|
714
|
+
for (const target of allTargets) {
|
|
715
|
+
const isEntryEdit = installPlan.entrypointEdits.some((e) => e.file === target);
|
|
716
|
+
if (isEntryEdit) continue;
|
|
717
|
+
if (!isAllowedWritePath(serviceDir, target)) {
|
|
718
|
+
throw new Error(
|
|
719
|
+
`javascript installer: refusing to write outside the allowed path set (ADR-069 \xA77): ${target}`
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
479
723
|
const originals = /* @__PURE__ */ new Map();
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
724
|
+
const createdFiles = [];
|
|
725
|
+
for (const target of allTargets) {
|
|
726
|
+
if (await exists(target)) {
|
|
727
|
+
try {
|
|
728
|
+
originals.set(target, await fs2.readFile(target, "utf8"));
|
|
729
|
+
} catch {
|
|
730
|
+
}
|
|
484
731
|
}
|
|
485
732
|
}
|
|
733
|
+
const writtenFiles = [];
|
|
486
734
|
try {
|
|
487
|
-
|
|
488
|
-
|
|
735
|
+
const manifestTargets = installPlan.dependencyEdits.reduce((acc, e) => {
|
|
736
|
+
acc.add(e.file);
|
|
737
|
+
return acc;
|
|
738
|
+
}, /* @__PURE__ */ new Set());
|
|
739
|
+
for (const file of manifestTargets) {
|
|
740
|
+
const raw = originals.get(file);
|
|
741
|
+
if (raw === void 0) {
|
|
742
|
+
throw new Error(`javascript installer: cannot read ${file} during apply`);
|
|
743
|
+
}
|
|
489
744
|
const pkg = JSON.parse(raw);
|
|
490
745
|
pkg.dependencies = pkg.dependencies ?? {};
|
|
491
746
|
for (const dep of installPlan.dependencyEdits) {
|
|
492
747
|
if (dep.file !== file) continue;
|
|
493
748
|
if (dep.kind === "add") {
|
|
494
|
-
|
|
749
|
+
if (!(dep.name in (pkg.dependencies ?? {}))) {
|
|
750
|
+
pkg.dependencies[dep.name] = dep.version;
|
|
751
|
+
}
|
|
495
752
|
} else {
|
|
496
753
|
delete pkg.dependencies[dep.name];
|
|
497
754
|
}
|
|
498
755
|
}
|
|
499
|
-
for (const ep of installPlan.entrypointEdits) {
|
|
500
|
-
if (ep.file !== file) continue;
|
|
501
|
-
pkg.scripts = pkg.scripts ?? {};
|
|
502
|
-
if (pkg.scripts.start === ep.before) {
|
|
503
|
-
pkg.scripts.start = ep.after;
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
756
|
const newRaw = JSON.stringify(pkg, null, 2) + "\n";
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
757
|
+
await writeAtomic(file, newRaw);
|
|
758
|
+
writtenFiles.push(file);
|
|
759
|
+
}
|
|
760
|
+
for (const gen of installPlan.generatedFiles ?? []) {
|
|
761
|
+
if (gen.skipIfExists && await exists(gen.file)) {
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
await writeAtomic(gen.file, gen.contents);
|
|
765
|
+
if (!originals.has(gen.file)) createdFiles.push(gen.file);
|
|
766
|
+
writtenFiles.push(gen.file);
|
|
767
|
+
}
|
|
768
|
+
for (const ep of installPlan.entrypointEdits) {
|
|
769
|
+
const raw = originals.get(ep.file);
|
|
770
|
+
if (raw === void 0) {
|
|
771
|
+
throw new Error(`javascript installer: cannot read entry ${ep.file} during apply`);
|
|
772
|
+
}
|
|
773
|
+
const lines = raw.split(/\r?\n/);
|
|
774
|
+
const hasShebang = lines[0]?.startsWith("#!") ?? false;
|
|
775
|
+
const insertAt = hasShebang ? 1 : 0;
|
|
776
|
+
const firstReal = lines[insertAt] ?? "";
|
|
777
|
+
if (lineIsOtelInjection(firstReal)) continue;
|
|
778
|
+
lines.splice(insertAt, 0, ep.after);
|
|
779
|
+
const newRaw = lines.join("\n");
|
|
780
|
+
await writeAtomic(ep.file, newRaw);
|
|
781
|
+
writtenFiles.push(ep.file);
|
|
510
782
|
}
|
|
511
783
|
} catch (err) {
|
|
512
|
-
await rollback(installPlan, originals);
|
|
784
|
+
await rollback(installPlan, originals, createdFiles);
|
|
513
785
|
throw err;
|
|
514
786
|
}
|
|
787
|
+
return {
|
|
788
|
+
serviceDir,
|
|
789
|
+
outcome: "instrumented",
|
|
790
|
+
writtenFiles
|
|
791
|
+
};
|
|
515
792
|
}
|
|
516
|
-
async function rollback(installPlan, originals) {
|
|
793
|
+
async function rollback(installPlan, originals, createdFiles) {
|
|
517
794
|
const restored = [];
|
|
795
|
+
const removed = [];
|
|
518
796
|
for (const [file, raw] of originals.entries()) {
|
|
519
797
|
try {
|
|
520
798
|
await fs2.writeFile(file, raw, "utf8");
|
|
@@ -522,6 +800,13 @@ async function rollback(installPlan, originals) {
|
|
|
522
800
|
} catch {
|
|
523
801
|
}
|
|
524
802
|
}
|
|
803
|
+
for (const file of createdFiles) {
|
|
804
|
+
try {
|
|
805
|
+
await fs2.unlink(file);
|
|
806
|
+
removed.push(file);
|
|
807
|
+
} catch {
|
|
808
|
+
}
|
|
809
|
+
}
|
|
525
810
|
const lines = [
|
|
526
811
|
"# neat-rollback.patch",
|
|
527
812
|
"",
|
|
@@ -529,6 +814,7 @@ async function rollback(installPlan, originals) {
|
|
|
529
814
|
"# Files listed below were restored to their pre-apply contents.",
|
|
530
815
|
"",
|
|
531
816
|
...restored.map((f) => `restored: ${f}`),
|
|
817
|
+
...removed.map((f) => `removed: ${f}`),
|
|
532
818
|
""
|
|
533
819
|
];
|
|
534
820
|
const rollbackPath = path2.join(installPlan.serviceDir, "neat-rollback.patch");
|
|
@@ -553,7 +839,7 @@ var OTEL_ENV2 = {
|
|
|
553
839
|
key: "OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
554
840
|
value: "http://localhost:4318"
|
|
555
841
|
};
|
|
556
|
-
async function
|
|
842
|
+
async function exists2(p) {
|
|
557
843
|
try {
|
|
558
844
|
await fs3.stat(p);
|
|
559
845
|
return true;
|
|
@@ -564,7 +850,7 @@ async function exists(p) {
|
|
|
564
850
|
async function detect2(serviceDir) {
|
|
565
851
|
const markers = ["requirements.txt", "pyproject.toml", "setup.py"];
|
|
566
852
|
for (const m of markers) {
|
|
567
|
-
if (await
|
|
853
|
+
if (await exists2(path3.join(serviceDir, m))) return true;
|
|
568
854
|
}
|
|
569
855
|
return false;
|
|
570
856
|
}
|
|
@@ -575,7 +861,7 @@ function reqPackageName(line) {
|
|
|
575
861
|
}
|
|
576
862
|
async function planRequirementsTxtEdits(serviceDir) {
|
|
577
863
|
const file = path3.join(serviceDir, "requirements.txt");
|
|
578
|
-
if (!await
|
|
864
|
+
if (!await exists2(file)) return null;
|
|
579
865
|
const raw = await fs3.readFile(file, "utf8");
|
|
580
866
|
const presentNames = new Set(
|
|
581
867
|
raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
|
|
@@ -585,7 +871,7 @@ async function planRequirementsTxtEdits(serviceDir) {
|
|
|
585
871
|
}
|
|
586
872
|
async function planProcfileEdits(serviceDir) {
|
|
587
873
|
const procfile = path3.join(serviceDir, "Procfile");
|
|
588
|
-
if (!await
|
|
874
|
+
if (!await exists2(procfile)) return [];
|
|
589
875
|
const raw = await fs3.readFile(procfile, "utf8");
|
|
590
876
|
const edits = [];
|
|
591
877
|
for (const line of raw.split(/\r?\n/)) {
|
|
@@ -652,10 +938,13 @@ async function applyProcfile(procfile, edits, original) {
|
|
|
652
938
|
await fs3.rename(tmp, procfile);
|
|
653
939
|
}
|
|
654
940
|
async function apply2(installPlan) {
|
|
941
|
+
const { serviceDir } = installPlan;
|
|
655
942
|
const touched = /* @__PURE__ */ new Set();
|
|
656
943
|
for (const e of installPlan.dependencyEdits) touched.add(e.file);
|
|
657
944
|
for (const e of installPlan.entrypointEdits) touched.add(e.file);
|
|
658
|
-
if (touched.size === 0)
|
|
945
|
+
if (touched.size === 0) {
|
|
946
|
+
return { serviceDir, outcome: "already-instrumented", writtenFiles: [] };
|
|
947
|
+
}
|
|
659
948
|
const originals = /* @__PURE__ */ new Map();
|
|
660
949
|
for (const file of touched) {
|
|
661
950
|
try {
|
|
@@ -663,6 +952,7 @@ async function apply2(installPlan) {
|
|
|
663
952
|
} catch {
|
|
664
953
|
}
|
|
665
954
|
}
|
|
955
|
+
const writtenFiles = [];
|
|
666
956
|
try {
|
|
667
957
|
for (const file of touched) {
|
|
668
958
|
const raw = originals.get(file);
|
|
@@ -672,16 +962,23 @@ async function apply2(installPlan) {
|
|
|
672
962
|
const base = path3.basename(file);
|
|
673
963
|
if (base === "requirements.txt") {
|
|
674
964
|
const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
|
|
675
|
-
if (edits.length > 0)
|
|
965
|
+
if (edits.length > 0) {
|
|
966
|
+
await applyRequirementsTxt(file, edits, raw);
|
|
967
|
+
writtenFiles.push(file);
|
|
968
|
+
}
|
|
676
969
|
} else if (base === "Procfile") {
|
|
677
970
|
const edits = installPlan.entrypointEdits.filter((e) => e.file === file);
|
|
678
|
-
if (edits.length > 0)
|
|
971
|
+
if (edits.length > 0) {
|
|
972
|
+
await applyProcfile(file, edits, raw);
|
|
973
|
+
writtenFiles.push(file);
|
|
974
|
+
}
|
|
679
975
|
}
|
|
680
976
|
}
|
|
681
977
|
} catch (err) {
|
|
682
978
|
await rollback2(installPlan, originals);
|
|
683
979
|
throw err;
|
|
684
980
|
}
|
|
981
|
+
return { serviceDir, outcome: "instrumented", writtenFiles };
|
|
685
982
|
}
|
|
686
983
|
async function rollback2(installPlan, originals) {
|
|
687
984
|
const restored = [];
|
|
@@ -713,7 +1010,7 @@ var pythonInstaller = {
|
|
|
713
1010
|
|
|
714
1011
|
// src/installers/shared.ts
|
|
715
1012
|
function isEmptyPlan(plan3) {
|
|
716
|
-
return plan3.dependencyEdits.length === 0 && plan3.entrypointEdits.length === 0 && plan3.envEdits.length === 0;
|
|
1013
|
+
return plan3.dependencyEdits.length === 0 && plan3.entrypointEdits.length === 0 && plan3.envEdits.length === 0 && (plan3.generatedFiles?.length ?? 0) === 0;
|
|
717
1014
|
}
|
|
718
1015
|
|
|
719
1016
|
// src/installers/index.ts
|
|
@@ -755,8 +1052,18 @@ function renderPatch(sections) {
|
|
|
755
1052
|
const { installer, plan: plan3 } = section;
|
|
756
1053
|
lines.push(`## ${installer} (${plan3.language}) \u2014 ${plan3.serviceDir}`);
|
|
757
1054
|
lines.push("");
|
|
1055
|
+
if (plan3.libOnly) {
|
|
1056
|
+
lines.push("### skipped \u2014 no resolvable entry point (lib-only)");
|
|
1057
|
+
lines.push("");
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
if (plan3.entryFile) {
|
|
1061
|
+
lines.push(`entry: ${plan3.entryFile}`);
|
|
1062
|
+
lines.push("");
|
|
1063
|
+
}
|
|
758
1064
|
if (plan3.dependencyEdits.length > 0) {
|
|
759
1065
|
lines.push("### dependencies");
|
|
1066
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
760
1067
|
for (const dep of plan3.dependencyEdits) {
|
|
761
1068
|
const base = dep.file.split(/[\\/]/).pop() ?? dep.file;
|
|
762
1069
|
if (FORBIDDEN_LOCKFILES.has(base)) {
|
|
@@ -764,24 +1071,41 @@ function renderPatch(sections) {
|
|
|
764
1071
|
`installer "${installer}" produced a dependency edit against a lockfile (${dep.file}); lockfiles must never be touched (ADR-047).`
|
|
765
1072
|
);
|
|
766
1073
|
}
|
|
767
|
-
|
|
1074
|
+
const existing = byFile.get(dep.file) ?? [];
|
|
1075
|
+
existing.push(dep);
|
|
1076
|
+
byFile.set(dep.file, existing);
|
|
1077
|
+
}
|
|
1078
|
+
for (const [file, deps] of byFile) {
|
|
1079
|
+
lines.push(`--- ${file}`);
|
|
1080
|
+
for (const dep of deps) {
|
|
1081
|
+
lines.push(`+ "${dep.name}": "${dep.version}"`);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
lines.push("");
|
|
1085
|
+
}
|
|
1086
|
+
if (plan3.generatedFiles && plan3.generatedFiles.length > 0) {
|
|
1087
|
+
lines.push("### generated files");
|
|
1088
|
+
for (const gen of plan3.generatedFiles) {
|
|
1089
|
+
lines.push(`--- (new file) ${gen.file}`);
|
|
1090
|
+
for (const ln of gen.contents.split(/\r?\n/)) {
|
|
1091
|
+
lines.push(`+ ${ln}`);
|
|
1092
|
+
}
|
|
768
1093
|
}
|
|
769
1094
|
lines.push("");
|
|
770
1095
|
}
|
|
771
1096
|
if (plan3.entrypointEdits.length > 0) {
|
|
772
|
-
lines.push("###
|
|
1097
|
+
lines.push("### entry-point injection");
|
|
773
1098
|
for (const e of plan3.entrypointEdits) {
|
|
774
|
-
lines.push(
|
|
775
|
-
lines.push(
|
|
776
|
-
lines.push(`
|
|
1099
|
+
lines.push(`--- ${e.file}`);
|
|
1100
|
+
lines.push(`+ ${e.after}`);
|
|
1101
|
+
lines.push(` ${e.before}`);
|
|
777
1102
|
}
|
|
778
1103
|
lines.push("");
|
|
779
1104
|
}
|
|
780
1105
|
if (plan3.envEdits.length > 0) {
|
|
781
|
-
lines.push("### env");
|
|
1106
|
+
lines.push("### env (written to <package-dir>/.env.neat)");
|
|
782
1107
|
for (const env of plan3.envEdits) {
|
|
783
|
-
|
|
784
|
-
lines.push(`- ${env.key}=${env.value} \u2192 ${target}`);
|
|
1108
|
+
lines.push(`- ${env.key}=${env.value}`);
|
|
785
1109
|
}
|
|
786
1110
|
lines.push("");
|
|
787
1111
|
}
|
|
@@ -1529,7 +1853,7 @@ async function buildPatchSections(services) {
|
|
|
1529
1853
|
const installer = await pickInstaller(svc.dir);
|
|
1530
1854
|
if (!installer) continue;
|
|
1531
1855
|
const plan3 = await installer.plan(svc.dir);
|
|
1532
|
-
if (isEmptyPlan(plan3)) continue;
|
|
1856
|
+
if (isEmptyPlan(plan3) && !plan3.libOnly) continue;
|
|
1533
1857
|
sections.push({ installer: installer.name, plan: plan3 });
|
|
1534
1858
|
}
|
|
1535
1859
|
return sections;
|
|
@@ -1582,14 +1906,28 @@ async function runInit(opts) {
|
|
|
1582
1906
|
}
|
|
1583
1907
|
if (!opts.noInstall) {
|
|
1584
1908
|
if (opts.apply) {
|
|
1909
|
+
let instrumented = 0;
|
|
1910
|
+
let alreadyInstrumented = 0;
|
|
1911
|
+
let libOnly = 0;
|
|
1585
1912
|
for (const section of sections) {
|
|
1586
1913
|
const installer = INSTALLERS.find((i) => i.name === section.installer);
|
|
1587
1914
|
if (!installer) continue;
|
|
1588
|
-
await installer.apply(section.plan);
|
|
1915
|
+
const outcome = await installer.apply(section.plan);
|
|
1916
|
+
if (outcome.outcome === "instrumented") {
|
|
1917
|
+
instrumented++;
|
|
1918
|
+
for (const f of outcome.writtenFiles) written.push(f);
|
|
1919
|
+
} else if (outcome.outcome === "already-instrumented") {
|
|
1920
|
+
alreadyInstrumented++;
|
|
1921
|
+
} else if (outcome.outcome === "lib-only") {
|
|
1922
|
+
libOnly++;
|
|
1923
|
+
}
|
|
1589
1924
|
}
|
|
1590
1925
|
if (sections.length > 0) {
|
|
1591
1926
|
console.log("");
|
|
1592
|
-
console.log(
|
|
1927
|
+
console.log(
|
|
1928
|
+
`apply: instrumented ${instrumented}, already-instrumented ${alreadyInstrumented}, lib-only ${libOnly}`
|
|
1929
|
+
);
|
|
1930
|
+
console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
|
|
1593
1931
|
}
|
|
1594
1932
|
} else {
|
|
1595
1933
|
await fs4.writeFile(patchPath, patch, "utf8");
|