@neat.is/core 0.3.5 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-KCEZSFU2.js → chunk-YHQYHFI3.js} +2 -2
- package/dist/{chunk-BUB3ASD5.js → chunk-ZU2RQRCN.js} +3 -3
- package/dist/chunk-ZU2RQRCN.js.map +1 -0
- package/dist/cli.cjs +341 -61
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +339 -60
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +83 -2
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +82 -2
- package/dist/neatd.js.map +1 -1
- package/dist/server.cjs +1 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-BUB3ASD5.js.map +0 -1
- /package/dist/{chunk-KCEZSFU2.js.map → chunk-YHQYHFI3.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -39,7 +39,7 @@ 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
45
|
} from "./chunk-G3PDTGOW.js";
|
|
@@ -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,75 @@ 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
|
-
if (
|
|
429
|
-
|
|
487
|
+
var INDEX_CANDIDATES = ["index.ts", "index.tsx", "index.js", "index.mjs", "index.cjs"];
|
|
488
|
+
async function resolveEntry(serviceDir, pkg) {
|
|
489
|
+
if (typeof pkg.main === "string" && pkg.main.length > 0) {
|
|
490
|
+
const candidate = path2.resolve(serviceDir, pkg.main);
|
|
491
|
+
if (await exists(candidate)) return candidate;
|
|
492
|
+
}
|
|
493
|
+
if (pkg.bin) {
|
|
494
|
+
let binEntry;
|
|
495
|
+
if (typeof pkg.bin === "string") {
|
|
496
|
+
binEntry = pkg.bin;
|
|
497
|
+
} else if (pkg.name && typeof pkg.bin[pkg.name] === "string") {
|
|
498
|
+
binEntry = pkg.bin[pkg.name];
|
|
499
|
+
} else {
|
|
500
|
+
const first = Object.values(pkg.bin)[0];
|
|
501
|
+
if (typeof first === "string") binEntry = first;
|
|
502
|
+
}
|
|
503
|
+
if (binEntry) {
|
|
504
|
+
const candidate = path2.resolve(serviceDir, binEntry);
|
|
505
|
+
if (await exists(candidate)) return candidate;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
for (const name of INDEX_CANDIDATES) {
|
|
509
|
+
const candidate = path2.join(serviceDir, name);
|
|
510
|
+
if (await exists(candidate)) return candidate;
|
|
430
511
|
}
|
|
431
|
-
return
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
function dispatchEntry(entryFile, pkg) {
|
|
515
|
+
const ext = path2.extname(entryFile).toLowerCase();
|
|
516
|
+
if (ext === ".ts" || ext === ".tsx") return "ts";
|
|
517
|
+
if (ext === ".mjs") return "esm";
|
|
518
|
+
if (ext === ".cjs") return "cjs";
|
|
519
|
+
return pkg.type === "module" ? "esm" : "cjs";
|
|
520
|
+
}
|
|
521
|
+
function otelInitFilename(flavor) {
|
|
522
|
+
if (flavor === "ts") return "otel-init.ts";
|
|
523
|
+
if (flavor === "esm") return "otel-init.mjs";
|
|
524
|
+
return "otel-init.cjs";
|
|
525
|
+
}
|
|
526
|
+
function otelInitContents(flavor) {
|
|
527
|
+
if (flavor === "ts") return OTEL_INIT_TS;
|
|
528
|
+
if (flavor === "esm") return OTEL_INIT_ESM;
|
|
529
|
+
return OTEL_INIT_CJS;
|
|
530
|
+
}
|
|
531
|
+
function injectionLine(flavor, entryFile, otelInitFile) {
|
|
532
|
+
let rel = path2.relative(path2.dirname(entryFile), otelInitFile);
|
|
533
|
+
if (!rel.startsWith(".")) rel = `./${rel}`;
|
|
534
|
+
rel = rel.split(path2.sep).join("/");
|
|
535
|
+
if (flavor === "cjs") return `require('${rel}')`;
|
|
536
|
+
if (flavor === "esm") return `import '${rel}'`;
|
|
537
|
+
const tsRel = rel.replace(/\.ts$/, "");
|
|
538
|
+
return `import '${tsRel}'`;
|
|
539
|
+
}
|
|
540
|
+
function lineIsOtelInjection(line) {
|
|
541
|
+
const trimmed = line.trim();
|
|
542
|
+
if (trimmed.length === 0) return false;
|
|
543
|
+
return /(?:require\(|import\s+)['"]\.\/otel-init[^'"]*['"]/.test(trimmed);
|
|
432
544
|
}
|
|
433
545
|
async function plan(serviceDir) {
|
|
434
546
|
const pkg = await readPackageJson(serviceDir);
|
|
@@ -438,9 +550,17 @@ async function plan(serviceDir) {
|
|
|
438
550
|
serviceDir,
|
|
439
551
|
dependencyEdits: [],
|
|
440
552
|
entrypointEdits: [],
|
|
441
|
-
envEdits: []
|
|
553
|
+
envEdits: [],
|
|
554
|
+
generatedFiles: []
|
|
442
555
|
};
|
|
443
556
|
if (!pkg) return empty;
|
|
557
|
+
const entryFile = await resolveEntry(serviceDir, pkg);
|
|
558
|
+
if (!entryFile) {
|
|
559
|
+
return { ...empty, libOnly: true };
|
|
560
|
+
}
|
|
561
|
+
const flavor = dispatchEntry(entryFile, pkg);
|
|
562
|
+
const otelInitFile = path2.join(path2.dirname(entryFile), otelInitFilename(flavor));
|
|
563
|
+
const envNeatFile = path2.join(serviceDir, ".env.neat");
|
|
444
564
|
const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
445
565
|
const dependencyEdits = [];
|
|
446
566
|
for (const sdk of SDK_PACKAGES) {
|
|
@@ -453,14 +573,37 @@ async function plan(serviceDir) {
|
|
|
453
573
|
});
|
|
454
574
|
}
|
|
455
575
|
const entrypointEdits = [];
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
576
|
+
try {
|
|
577
|
+
const raw = await fs2.readFile(entryFile, "utf8");
|
|
578
|
+
const lines = raw.split(/\r?\n/);
|
|
579
|
+
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
580
|
+
if (!lineIsOtelInjection(firstReal)) {
|
|
581
|
+
const inject = injectionLine(flavor, entryFile, otelInitFile);
|
|
582
|
+
entrypointEdits.push({
|
|
583
|
+
file: entryFile,
|
|
584
|
+
before: firstReal,
|
|
585
|
+
after: inject
|
|
586
|
+
});
|
|
461
587
|
}
|
|
588
|
+
} catch {
|
|
589
|
+
return { ...empty, libOnly: true };
|
|
590
|
+
}
|
|
591
|
+
const generatedFiles = [];
|
|
592
|
+
if (!await exists(otelInitFile)) {
|
|
593
|
+
generatedFiles.push({
|
|
594
|
+
file: otelInitFile,
|
|
595
|
+
contents: otelInitContents(flavor),
|
|
596
|
+
skipIfExists: true
|
|
597
|
+
});
|
|
462
598
|
}
|
|
463
|
-
if (
|
|
599
|
+
if (!await exists(envNeatFile)) {
|
|
600
|
+
generatedFiles.push({
|
|
601
|
+
file: envNeatFile,
|
|
602
|
+
contents: renderEnvNeat(pkg.name ?? path2.basename(serviceDir)),
|
|
603
|
+
skipIfExists: true
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
if (dependencyEdits.length === 0 && entrypointEdits.length === 0 && generatedFiles.length === 0) {
|
|
464
607
|
return empty;
|
|
465
608
|
}
|
|
466
609
|
return {
|
|
@@ -468,53 +611,129 @@ async function plan(serviceDir) {
|
|
|
468
611
|
serviceDir,
|
|
469
612
|
dependencyEdits,
|
|
470
613
|
entrypointEdits,
|
|
471
|
-
envEdits: [OTEL_ENV]
|
|
614
|
+
envEdits: [OTEL_ENV],
|
|
615
|
+
generatedFiles,
|
|
616
|
+
entryFile,
|
|
617
|
+
libOnly: false
|
|
472
618
|
};
|
|
473
619
|
}
|
|
620
|
+
function isAllowedWritePath(serviceDir, target) {
|
|
621
|
+
const rel = path2.relative(serviceDir, target);
|
|
622
|
+
if (rel.startsWith("..")) return false;
|
|
623
|
+
const base = path2.basename(target);
|
|
624
|
+
if (base === "package.json") return true;
|
|
625
|
+
if (base === ".env.neat") return true;
|
|
626
|
+
if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
async function writeAtomic(file, contents) {
|
|
630
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
631
|
+
await fs2.writeFile(tmp, contents, "utf8");
|
|
632
|
+
await fs2.rename(tmp, file);
|
|
633
|
+
}
|
|
474
634
|
async function apply(installPlan) {
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
635
|
+
const { serviceDir } = installPlan;
|
|
636
|
+
if (installPlan.libOnly) {
|
|
637
|
+
return {
|
|
638
|
+
serviceDir,
|
|
639
|
+
outcome: "lib-only",
|
|
640
|
+
reason: "no resolvable entry point",
|
|
641
|
+
writtenFiles: []
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
if (installPlan.dependencyEdits.length === 0 && installPlan.entrypointEdits.length === 0 && (installPlan.generatedFiles?.length ?? 0) === 0) {
|
|
645
|
+
return {
|
|
646
|
+
serviceDir,
|
|
647
|
+
outcome: "already-instrumented",
|
|
648
|
+
writtenFiles: []
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
const allTargets = /* @__PURE__ */ new Set();
|
|
652
|
+
for (const d of installPlan.dependencyEdits) allTargets.add(d.file);
|
|
653
|
+
for (const e of installPlan.entrypointEdits) allTargets.add(e.file);
|
|
654
|
+
for (const g of installPlan.generatedFiles ?? []) allTargets.add(g.file);
|
|
655
|
+
for (const target of allTargets) {
|
|
656
|
+
const isEntryEdit = installPlan.entrypointEdits.some((e) => e.file === target);
|
|
657
|
+
if (isEntryEdit) continue;
|
|
658
|
+
if (!isAllowedWritePath(serviceDir, target)) {
|
|
659
|
+
throw new Error(
|
|
660
|
+
`javascript installer: refusing to write outside the allowed path set (ADR-069 \xA77): ${target}`
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
479
664
|
const originals = /* @__PURE__ */ new Map();
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
665
|
+
const createdFiles = [];
|
|
666
|
+
for (const target of allTargets) {
|
|
667
|
+
if (await exists(target)) {
|
|
668
|
+
try {
|
|
669
|
+
originals.set(target, await fs2.readFile(target, "utf8"));
|
|
670
|
+
} catch {
|
|
671
|
+
}
|
|
484
672
|
}
|
|
485
673
|
}
|
|
674
|
+
const writtenFiles = [];
|
|
486
675
|
try {
|
|
487
|
-
|
|
488
|
-
|
|
676
|
+
const manifestTargets = installPlan.dependencyEdits.reduce((acc, e) => {
|
|
677
|
+
acc.add(e.file);
|
|
678
|
+
return acc;
|
|
679
|
+
}, /* @__PURE__ */ new Set());
|
|
680
|
+
for (const file of manifestTargets) {
|
|
681
|
+
const raw = originals.get(file);
|
|
682
|
+
if (raw === void 0) {
|
|
683
|
+
throw new Error(`javascript installer: cannot read ${file} during apply`);
|
|
684
|
+
}
|
|
489
685
|
const pkg = JSON.parse(raw);
|
|
490
686
|
pkg.dependencies = pkg.dependencies ?? {};
|
|
491
687
|
for (const dep of installPlan.dependencyEdits) {
|
|
492
688
|
if (dep.file !== file) continue;
|
|
493
689
|
if (dep.kind === "add") {
|
|
494
|
-
|
|
690
|
+
if (!(dep.name in (pkg.dependencies ?? {}))) {
|
|
691
|
+
pkg.dependencies[dep.name] = dep.version;
|
|
692
|
+
}
|
|
495
693
|
} else {
|
|
496
694
|
delete pkg.dependencies[dep.name];
|
|
497
695
|
}
|
|
498
696
|
}
|
|
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
697
|
const newRaw = JSON.stringify(pkg, null, 2) + "\n";
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
698
|
+
await writeAtomic(file, newRaw);
|
|
699
|
+
writtenFiles.push(file);
|
|
700
|
+
}
|
|
701
|
+
for (const gen of installPlan.generatedFiles ?? []) {
|
|
702
|
+
if (gen.skipIfExists && await exists(gen.file)) {
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
await writeAtomic(gen.file, gen.contents);
|
|
706
|
+
if (!originals.has(gen.file)) createdFiles.push(gen.file);
|
|
707
|
+
writtenFiles.push(gen.file);
|
|
708
|
+
}
|
|
709
|
+
for (const ep of installPlan.entrypointEdits) {
|
|
710
|
+
const raw = originals.get(ep.file);
|
|
711
|
+
if (raw === void 0) {
|
|
712
|
+
throw new Error(`javascript installer: cannot read entry ${ep.file} during apply`);
|
|
713
|
+
}
|
|
714
|
+
const lines = raw.split(/\r?\n/);
|
|
715
|
+
const hasShebang = lines[0]?.startsWith("#!") ?? false;
|
|
716
|
+
const insertAt = hasShebang ? 1 : 0;
|
|
717
|
+
const firstReal = lines[insertAt] ?? "";
|
|
718
|
+
if (lineIsOtelInjection(firstReal)) continue;
|
|
719
|
+
lines.splice(insertAt, 0, ep.after);
|
|
720
|
+
const newRaw = lines.join("\n");
|
|
721
|
+
await writeAtomic(ep.file, newRaw);
|
|
722
|
+
writtenFiles.push(ep.file);
|
|
510
723
|
}
|
|
511
724
|
} catch (err) {
|
|
512
|
-
await rollback(installPlan, originals);
|
|
725
|
+
await rollback(installPlan, originals, createdFiles);
|
|
513
726
|
throw err;
|
|
514
727
|
}
|
|
728
|
+
return {
|
|
729
|
+
serviceDir,
|
|
730
|
+
outcome: "instrumented",
|
|
731
|
+
writtenFiles
|
|
732
|
+
};
|
|
515
733
|
}
|
|
516
|
-
async function rollback(installPlan, originals) {
|
|
734
|
+
async function rollback(installPlan, originals, createdFiles) {
|
|
517
735
|
const restored = [];
|
|
736
|
+
const removed = [];
|
|
518
737
|
for (const [file, raw] of originals.entries()) {
|
|
519
738
|
try {
|
|
520
739
|
await fs2.writeFile(file, raw, "utf8");
|
|
@@ -522,6 +741,13 @@ async function rollback(installPlan, originals) {
|
|
|
522
741
|
} catch {
|
|
523
742
|
}
|
|
524
743
|
}
|
|
744
|
+
for (const file of createdFiles) {
|
|
745
|
+
try {
|
|
746
|
+
await fs2.unlink(file);
|
|
747
|
+
removed.push(file);
|
|
748
|
+
} catch {
|
|
749
|
+
}
|
|
750
|
+
}
|
|
525
751
|
const lines = [
|
|
526
752
|
"# neat-rollback.patch",
|
|
527
753
|
"",
|
|
@@ -529,6 +755,7 @@ async function rollback(installPlan, originals) {
|
|
|
529
755
|
"# Files listed below were restored to their pre-apply contents.",
|
|
530
756
|
"",
|
|
531
757
|
...restored.map((f) => `restored: ${f}`),
|
|
758
|
+
...removed.map((f) => `removed: ${f}`),
|
|
532
759
|
""
|
|
533
760
|
];
|
|
534
761
|
const rollbackPath = path2.join(installPlan.serviceDir, "neat-rollback.patch");
|
|
@@ -553,7 +780,7 @@ var OTEL_ENV2 = {
|
|
|
553
780
|
key: "OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
554
781
|
value: "http://localhost:4318"
|
|
555
782
|
};
|
|
556
|
-
async function
|
|
783
|
+
async function exists2(p) {
|
|
557
784
|
try {
|
|
558
785
|
await fs3.stat(p);
|
|
559
786
|
return true;
|
|
@@ -564,7 +791,7 @@ async function exists(p) {
|
|
|
564
791
|
async function detect2(serviceDir) {
|
|
565
792
|
const markers = ["requirements.txt", "pyproject.toml", "setup.py"];
|
|
566
793
|
for (const m of markers) {
|
|
567
|
-
if (await
|
|
794
|
+
if (await exists2(path3.join(serviceDir, m))) return true;
|
|
568
795
|
}
|
|
569
796
|
return false;
|
|
570
797
|
}
|
|
@@ -575,7 +802,7 @@ function reqPackageName(line) {
|
|
|
575
802
|
}
|
|
576
803
|
async function planRequirementsTxtEdits(serviceDir) {
|
|
577
804
|
const file = path3.join(serviceDir, "requirements.txt");
|
|
578
|
-
if (!await
|
|
805
|
+
if (!await exists2(file)) return null;
|
|
579
806
|
const raw = await fs3.readFile(file, "utf8");
|
|
580
807
|
const presentNames = new Set(
|
|
581
808
|
raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
|
|
@@ -585,7 +812,7 @@ async function planRequirementsTxtEdits(serviceDir) {
|
|
|
585
812
|
}
|
|
586
813
|
async function planProcfileEdits(serviceDir) {
|
|
587
814
|
const procfile = path3.join(serviceDir, "Procfile");
|
|
588
|
-
if (!await
|
|
815
|
+
if (!await exists2(procfile)) return [];
|
|
589
816
|
const raw = await fs3.readFile(procfile, "utf8");
|
|
590
817
|
const edits = [];
|
|
591
818
|
for (const line of raw.split(/\r?\n/)) {
|
|
@@ -652,10 +879,13 @@ async function applyProcfile(procfile, edits, original) {
|
|
|
652
879
|
await fs3.rename(tmp, procfile);
|
|
653
880
|
}
|
|
654
881
|
async function apply2(installPlan) {
|
|
882
|
+
const { serviceDir } = installPlan;
|
|
655
883
|
const touched = /* @__PURE__ */ new Set();
|
|
656
884
|
for (const e of installPlan.dependencyEdits) touched.add(e.file);
|
|
657
885
|
for (const e of installPlan.entrypointEdits) touched.add(e.file);
|
|
658
|
-
if (touched.size === 0)
|
|
886
|
+
if (touched.size === 0) {
|
|
887
|
+
return { serviceDir, outcome: "already-instrumented", writtenFiles: [] };
|
|
888
|
+
}
|
|
659
889
|
const originals = /* @__PURE__ */ new Map();
|
|
660
890
|
for (const file of touched) {
|
|
661
891
|
try {
|
|
@@ -663,6 +893,7 @@ async function apply2(installPlan) {
|
|
|
663
893
|
} catch {
|
|
664
894
|
}
|
|
665
895
|
}
|
|
896
|
+
const writtenFiles = [];
|
|
666
897
|
try {
|
|
667
898
|
for (const file of touched) {
|
|
668
899
|
const raw = originals.get(file);
|
|
@@ -672,16 +903,23 @@ async function apply2(installPlan) {
|
|
|
672
903
|
const base = path3.basename(file);
|
|
673
904
|
if (base === "requirements.txt") {
|
|
674
905
|
const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
|
|
675
|
-
if (edits.length > 0)
|
|
906
|
+
if (edits.length > 0) {
|
|
907
|
+
await applyRequirementsTxt(file, edits, raw);
|
|
908
|
+
writtenFiles.push(file);
|
|
909
|
+
}
|
|
676
910
|
} else if (base === "Procfile") {
|
|
677
911
|
const edits = installPlan.entrypointEdits.filter((e) => e.file === file);
|
|
678
|
-
if (edits.length > 0)
|
|
912
|
+
if (edits.length > 0) {
|
|
913
|
+
await applyProcfile(file, edits, raw);
|
|
914
|
+
writtenFiles.push(file);
|
|
915
|
+
}
|
|
679
916
|
}
|
|
680
917
|
}
|
|
681
918
|
} catch (err) {
|
|
682
919
|
await rollback2(installPlan, originals);
|
|
683
920
|
throw err;
|
|
684
921
|
}
|
|
922
|
+
return { serviceDir, outcome: "instrumented", writtenFiles };
|
|
685
923
|
}
|
|
686
924
|
async function rollback2(installPlan, originals) {
|
|
687
925
|
const restored = [];
|
|
@@ -713,7 +951,7 @@ var pythonInstaller = {
|
|
|
713
951
|
|
|
714
952
|
// src/installers/shared.ts
|
|
715
953
|
function isEmptyPlan(plan3) {
|
|
716
|
-
return plan3.dependencyEdits.length === 0 && plan3.entrypointEdits.length === 0 && plan3.envEdits.length === 0;
|
|
954
|
+
return plan3.dependencyEdits.length === 0 && plan3.entrypointEdits.length === 0 && plan3.envEdits.length === 0 && (plan3.generatedFiles?.length ?? 0) === 0;
|
|
717
955
|
}
|
|
718
956
|
|
|
719
957
|
// src/installers/index.ts
|
|
@@ -755,8 +993,18 @@ function renderPatch(sections) {
|
|
|
755
993
|
const { installer, plan: plan3 } = section;
|
|
756
994
|
lines.push(`## ${installer} (${plan3.language}) \u2014 ${plan3.serviceDir}`);
|
|
757
995
|
lines.push("");
|
|
996
|
+
if (plan3.libOnly) {
|
|
997
|
+
lines.push("### skipped \u2014 no resolvable entry point (lib-only)");
|
|
998
|
+
lines.push("");
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
if (plan3.entryFile) {
|
|
1002
|
+
lines.push(`entry: ${plan3.entryFile}`);
|
|
1003
|
+
lines.push("");
|
|
1004
|
+
}
|
|
758
1005
|
if (plan3.dependencyEdits.length > 0) {
|
|
759
1006
|
lines.push("### dependencies");
|
|
1007
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
760
1008
|
for (const dep of plan3.dependencyEdits) {
|
|
761
1009
|
const base = dep.file.split(/[\\/]/).pop() ?? dep.file;
|
|
762
1010
|
if (FORBIDDEN_LOCKFILES.has(base)) {
|
|
@@ -764,24 +1012,41 @@ function renderPatch(sections) {
|
|
|
764
1012
|
`installer "${installer}" produced a dependency edit against a lockfile (${dep.file}); lockfiles must never be touched (ADR-047).`
|
|
765
1013
|
);
|
|
766
1014
|
}
|
|
767
|
-
|
|
1015
|
+
const existing = byFile.get(dep.file) ?? [];
|
|
1016
|
+
existing.push(dep);
|
|
1017
|
+
byFile.set(dep.file, existing);
|
|
1018
|
+
}
|
|
1019
|
+
for (const [file, deps] of byFile) {
|
|
1020
|
+
lines.push(`--- ${file}`);
|
|
1021
|
+
for (const dep of deps) {
|
|
1022
|
+
lines.push(`+ "${dep.name}": "${dep.version}"`);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
lines.push("");
|
|
1026
|
+
}
|
|
1027
|
+
if (plan3.generatedFiles && plan3.generatedFiles.length > 0) {
|
|
1028
|
+
lines.push("### generated files");
|
|
1029
|
+
for (const gen of plan3.generatedFiles) {
|
|
1030
|
+
lines.push(`--- (new file) ${gen.file}`);
|
|
1031
|
+
for (const ln of gen.contents.split(/\r?\n/)) {
|
|
1032
|
+
lines.push(`+ ${ln}`);
|
|
1033
|
+
}
|
|
768
1034
|
}
|
|
769
1035
|
lines.push("");
|
|
770
1036
|
}
|
|
771
1037
|
if (plan3.entrypointEdits.length > 0) {
|
|
772
|
-
lines.push("###
|
|
1038
|
+
lines.push("### entry-point injection");
|
|
773
1039
|
for (const e of plan3.entrypointEdits) {
|
|
774
|
-
lines.push(
|
|
775
|
-
lines.push(
|
|
776
|
-
lines.push(`
|
|
1040
|
+
lines.push(`--- ${e.file}`);
|
|
1041
|
+
lines.push(`+ ${e.after}`);
|
|
1042
|
+
lines.push(` ${e.before}`);
|
|
777
1043
|
}
|
|
778
1044
|
lines.push("");
|
|
779
1045
|
}
|
|
780
1046
|
if (plan3.envEdits.length > 0) {
|
|
781
|
-
lines.push("### env");
|
|
1047
|
+
lines.push("### env (written to <package-dir>/.env.neat)");
|
|
782
1048
|
for (const env of plan3.envEdits) {
|
|
783
|
-
|
|
784
|
-
lines.push(`- ${env.key}=${env.value} \u2192 ${target}`);
|
|
1049
|
+
lines.push(`- ${env.key}=${env.value}`);
|
|
785
1050
|
}
|
|
786
1051
|
lines.push("");
|
|
787
1052
|
}
|
|
@@ -1529,7 +1794,7 @@ async function buildPatchSections(services) {
|
|
|
1529
1794
|
const installer = await pickInstaller(svc.dir);
|
|
1530
1795
|
if (!installer) continue;
|
|
1531
1796
|
const plan3 = await installer.plan(svc.dir);
|
|
1532
|
-
if (isEmptyPlan(plan3)) continue;
|
|
1797
|
+
if (isEmptyPlan(plan3) && !plan3.libOnly) continue;
|
|
1533
1798
|
sections.push({ installer: installer.name, plan: plan3 });
|
|
1534
1799
|
}
|
|
1535
1800
|
return sections;
|
|
@@ -1582,14 +1847,28 @@ async function runInit(opts) {
|
|
|
1582
1847
|
}
|
|
1583
1848
|
if (!opts.noInstall) {
|
|
1584
1849
|
if (opts.apply) {
|
|
1850
|
+
let instrumented = 0;
|
|
1851
|
+
let alreadyInstrumented = 0;
|
|
1852
|
+
let libOnly = 0;
|
|
1585
1853
|
for (const section of sections) {
|
|
1586
1854
|
const installer = INSTALLERS.find((i) => i.name === section.installer);
|
|
1587
1855
|
if (!installer) continue;
|
|
1588
|
-
await installer.apply(section.plan);
|
|
1856
|
+
const outcome = await installer.apply(section.plan);
|
|
1857
|
+
if (outcome.outcome === "instrumented") {
|
|
1858
|
+
instrumented++;
|
|
1859
|
+
for (const f of outcome.writtenFiles) written.push(f);
|
|
1860
|
+
} else if (outcome.outcome === "already-instrumented") {
|
|
1861
|
+
alreadyInstrumented++;
|
|
1862
|
+
} else if (outcome.outcome === "lib-only") {
|
|
1863
|
+
libOnly++;
|
|
1864
|
+
}
|
|
1589
1865
|
}
|
|
1590
1866
|
if (sections.length > 0) {
|
|
1591
1867
|
console.log("");
|
|
1592
|
-
console.log(
|
|
1868
|
+
console.log(
|
|
1869
|
+
`apply: instrumented ${instrumented}, already-instrumented ${alreadyInstrumented}, lib-only ${libOnly}`
|
|
1870
|
+
);
|
|
1871
|
+
console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
|
|
1593
1872
|
}
|
|
1594
1873
|
} else {
|
|
1595
1874
|
await fs4.writeFile(patchPath, patch, "utf8");
|