@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/cli.cjs CHANGED
@@ -1663,7 +1663,7 @@ function buildErrorEventForReceiver(span) {
1663
1663
  service: span.service,
1664
1664
  traceId: span.traceId,
1665
1665
  spanId: span.spanId,
1666
- errorMessage: span.exception?.message ?? span.name ?? "unknown error",
1666
+ errorMessage: span.exception?.message ?? "unknown error",
1667
1667
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
1668
1668
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
1669
1669
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
@@ -1755,7 +1755,7 @@ async function handleSpan(ctx, span) {
1755
1755
  service: span.service,
1756
1756
  traceId: span.traceId,
1757
1757
  spanId: span.spanId,
1758
- errorMessage: span.exception?.message ?? span.name ?? "unknown error",
1758
+ errorMessage: span.exception?.message ?? "unknown error",
1759
1759
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
1760
1760
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
1761
1761
  ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
@@ -5911,15 +5911,69 @@ init_cjs_shims();
5911
5911
  init_cjs_shims();
5912
5912
  var import_node_fs23 = require("fs");
5913
5913
  var import_node_path38 = __toESM(require("path"), 1);
5914
+
5915
+ // src/installers/templates.ts
5916
+ init_cjs_shims();
5917
+ var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
5918
+ var OTEL_INIT_CJS = `${OTEL_INIT_HEADER}
5919
+ // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
5920
+ // the auto-instrumentation hook attaches. Configure via the env file.
5921
+ const path = require('node:path')
5922
+ try {
5923
+ require('dotenv').config({ path: path.join(__dirname, '.env.neat') })
5924
+ } catch (err) {
5925
+ // dotenv unavailable \u2014 fall through to process.env.
5926
+ }
5927
+ require('@opentelemetry/auto-instrumentations-node/register')
5928
+ `;
5929
+ var OTEL_INIT_ESM = `${OTEL_INIT_HEADER}
5930
+ // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
5931
+ // the auto-instrumentation hook attaches. Configure via the env file.
5932
+ import { fileURLToPath } from 'node:url'
5933
+ import path from 'node:path'
5934
+ import dotenv from 'dotenv'
5935
+
5936
+ const here = path.dirname(fileURLToPath(import.meta.url))
5937
+ dotenv.config({ path: path.join(here, '.env.neat') })
5938
+
5939
+ await import('@opentelemetry/auto-instrumentations-node/register')
5940
+ `;
5941
+ var OTEL_INIT_TS = `${OTEL_INIT_HEADER}
5942
+ // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
5943
+ // the auto-instrumentation hook attaches. Configure via the env file.
5944
+ import { fileURLToPath } from 'node:url'
5945
+ import path from 'node:path'
5946
+ import dotenv from 'dotenv'
5947
+
5948
+ const here = path.dirname(fileURLToPath(import.meta.url))
5949
+ dotenv.config({ path: path.join(here, '.env.neat') })
5950
+
5951
+ await import('@opentelemetry/auto-instrumentations-node/register')
5952
+ `;
5953
+ function renderEnvNeat(serviceName) {
5954
+ return [
5955
+ "# Generated by `neat init --apply` (ADR-069).",
5956
+ `OTEL_SERVICE_NAME=${serviceName}`,
5957
+ "OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318",
5958
+ ""
5959
+ ].join("\n");
5960
+ }
5961
+
5962
+ // src/installers/javascript.ts
5914
5963
  var SDK_PACKAGES = [
5915
5964
  { name: "@opentelemetry/api", version: "^1.9.0" },
5916
5965
  { name: "@opentelemetry/sdk-node", version: "^0.57.0" },
5917
- { name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" }
5966
+ { name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" },
5967
+ // ADR-069 §5 — dotenv is the fourth dep. The generated otel-init loads
5968
+ // .env.neat through it so OTEL_SERVICE_NAME and the endpoint are in scope
5969
+ // before the auto-instrumentation hook attaches.
5970
+ { name: "dotenv", version: "^16.4.5" }
5918
5971
  ];
5919
- var AUTO_INSTRUMENT_REQUIRE = "--require @opentelemetry/auto-instrumentations-node/register";
5920
5972
  var OTEL_ENV = {
5921
- // null targetNEAT does not write `.env` itself; the user sets the env
5922
- // var in their orchestration layer.
5973
+ // ADR-069 §4endpoint moves into the per-package .env.neat (written
5974
+ // by the apply phase). The envEdits surface stays for the dry-run
5975
+ // patch render: it documents the key/value the user can inspect in the
5976
+ // generated .env.neat.
5923
5977
  file: null,
5924
5978
  key: "OTEL_EXPORTER_OTLP_ENDPOINT",
5925
5979
  value: "http://localhost:4318"
@@ -5932,16 +5986,75 @@ async function readPackageJson(serviceDir) {
5932
5986
  return null;
5933
5987
  }
5934
5988
  }
5989
+ async function exists2(p) {
5990
+ try {
5991
+ await import_node_fs23.promises.stat(p);
5992
+ return true;
5993
+ } catch {
5994
+ return false;
5995
+ }
5996
+ }
5935
5997
  async function detect(serviceDir) {
5936
5998
  const pkg = await readPackageJson(serviceDir);
5937
5999
  return pkg !== null && typeof pkg.name === "string";
5938
6000
  }
5939
- function rewriteStartScript(start) {
5940
- if (start.includes(AUTO_INSTRUMENT_REQUIRE)) return start;
5941
- if (/^\s*node\b/.test(start)) {
5942
- return start.replace(/^\s*node\b\s*/, `node ${AUTO_INSTRUMENT_REQUIRE} `);
6001
+ var INDEX_CANDIDATES = ["index.ts", "index.tsx", "index.js", "index.mjs", "index.cjs"];
6002
+ async function resolveEntry(serviceDir, pkg) {
6003
+ if (typeof pkg.main === "string" && pkg.main.length > 0) {
6004
+ const candidate = import_node_path38.default.resolve(serviceDir, pkg.main);
6005
+ if (await exists2(candidate)) return candidate;
6006
+ }
6007
+ if (pkg.bin) {
6008
+ let binEntry;
6009
+ if (typeof pkg.bin === "string") {
6010
+ binEntry = pkg.bin;
6011
+ } else if (pkg.name && typeof pkg.bin[pkg.name] === "string") {
6012
+ binEntry = pkg.bin[pkg.name];
6013
+ } else {
6014
+ const first = Object.values(pkg.bin)[0];
6015
+ if (typeof first === "string") binEntry = first;
6016
+ }
6017
+ if (binEntry) {
6018
+ const candidate = import_node_path38.default.resolve(serviceDir, binEntry);
6019
+ if (await exists2(candidate)) return candidate;
6020
+ }
5943
6021
  }
5944
- return `node ${AUTO_INSTRUMENT_REQUIRE} -- ${start}`;
6022
+ for (const name of INDEX_CANDIDATES) {
6023
+ const candidate = import_node_path38.default.join(serviceDir, name);
6024
+ if (await exists2(candidate)) return candidate;
6025
+ }
6026
+ return null;
6027
+ }
6028
+ function dispatchEntry(entryFile, pkg) {
6029
+ const ext = import_node_path38.default.extname(entryFile).toLowerCase();
6030
+ if (ext === ".ts" || ext === ".tsx") return "ts";
6031
+ if (ext === ".mjs") return "esm";
6032
+ if (ext === ".cjs") return "cjs";
6033
+ return pkg.type === "module" ? "esm" : "cjs";
6034
+ }
6035
+ function otelInitFilename(flavor) {
6036
+ if (flavor === "ts") return "otel-init.ts";
6037
+ if (flavor === "esm") return "otel-init.mjs";
6038
+ return "otel-init.cjs";
6039
+ }
6040
+ function otelInitContents(flavor) {
6041
+ if (flavor === "ts") return OTEL_INIT_TS;
6042
+ if (flavor === "esm") return OTEL_INIT_ESM;
6043
+ return OTEL_INIT_CJS;
6044
+ }
6045
+ function injectionLine(flavor, entryFile, otelInitFile) {
6046
+ let rel = import_node_path38.default.relative(import_node_path38.default.dirname(entryFile), otelInitFile);
6047
+ if (!rel.startsWith(".")) rel = `./${rel}`;
6048
+ rel = rel.split(import_node_path38.default.sep).join("/");
6049
+ if (flavor === "cjs") return `require('${rel}')`;
6050
+ if (flavor === "esm") return `import '${rel}'`;
6051
+ const tsRel = rel.replace(/\.ts$/, "");
6052
+ return `import '${tsRel}'`;
6053
+ }
6054
+ function lineIsOtelInjection(line) {
6055
+ const trimmed = line.trim();
6056
+ if (trimmed.length === 0) return false;
6057
+ return /(?:require\(|import\s+)['"]\.\/otel-init[^'"]*['"]/.test(trimmed);
5945
6058
  }
5946
6059
  async function plan(serviceDir) {
5947
6060
  const pkg = await readPackageJson(serviceDir);
@@ -5951,9 +6064,17 @@ async function plan(serviceDir) {
5951
6064
  serviceDir,
5952
6065
  dependencyEdits: [],
5953
6066
  entrypointEdits: [],
5954
- envEdits: []
6067
+ envEdits: [],
6068
+ generatedFiles: []
5955
6069
  };
5956
6070
  if (!pkg) return empty;
6071
+ const entryFile = await resolveEntry(serviceDir, pkg);
6072
+ if (!entryFile) {
6073
+ return { ...empty, libOnly: true };
6074
+ }
6075
+ const flavor = dispatchEntry(entryFile, pkg);
6076
+ const otelInitFile = import_node_path38.default.join(import_node_path38.default.dirname(entryFile), otelInitFilename(flavor));
6077
+ const envNeatFile = import_node_path38.default.join(serviceDir, ".env.neat");
5957
6078
  const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
5958
6079
  const dependencyEdits = [];
5959
6080
  for (const sdk of SDK_PACKAGES) {
@@ -5966,14 +6087,37 @@ async function plan(serviceDir) {
5966
6087
  });
5967
6088
  }
5968
6089
  const entrypointEdits = [];
5969
- const startScript = pkg.scripts?.start;
5970
- if (typeof startScript === "string" && startScript.trim().length > 0) {
5971
- const rewritten = rewriteStartScript(startScript);
5972
- if (rewritten !== startScript) {
5973
- entrypointEdits.push({ file: manifestPath, before: startScript, after: rewritten });
6090
+ try {
6091
+ const raw = await import_node_fs23.promises.readFile(entryFile, "utf8");
6092
+ const lines = raw.split(/\r?\n/);
6093
+ const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
6094
+ if (!lineIsOtelInjection(firstReal)) {
6095
+ const inject = injectionLine(flavor, entryFile, otelInitFile);
6096
+ entrypointEdits.push({
6097
+ file: entryFile,
6098
+ before: firstReal,
6099
+ after: inject
6100
+ });
5974
6101
  }
6102
+ } catch {
6103
+ return { ...empty, libOnly: true };
6104
+ }
6105
+ const generatedFiles = [];
6106
+ if (!await exists2(otelInitFile)) {
6107
+ generatedFiles.push({
6108
+ file: otelInitFile,
6109
+ contents: otelInitContents(flavor),
6110
+ skipIfExists: true
6111
+ });
5975
6112
  }
5976
- if (dependencyEdits.length === 0 && entrypointEdits.length === 0) {
6113
+ if (!await exists2(envNeatFile)) {
6114
+ generatedFiles.push({
6115
+ file: envNeatFile,
6116
+ contents: renderEnvNeat(pkg.name ?? import_node_path38.default.basename(serviceDir)),
6117
+ skipIfExists: true
6118
+ });
6119
+ }
6120
+ if (dependencyEdits.length === 0 && entrypointEdits.length === 0 && generatedFiles.length === 0) {
5977
6121
  return empty;
5978
6122
  }
5979
6123
  return {
@@ -5981,53 +6125,129 @@ async function plan(serviceDir) {
5981
6125
  serviceDir,
5982
6126
  dependencyEdits,
5983
6127
  entrypointEdits,
5984
- envEdits: [OTEL_ENV]
6128
+ envEdits: [OTEL_ENV],
6129
+ generatedFiles,
6130
+ entryFile,
6131
+ libOnly: false
5985
6132
  };
5986
6133
  }
6134
+ function isAllowedWritePath(serviceDir, target) {
6135
+ const rel = import_node_path38.default.relative(serviceDir, target);
6136
+ if (rel.startsWith("..")) return false;
6137
+ const base = import_node_path38.default.basename(target);
6138
+ if (base === "package.json") return true;
6139
+ if (base === ".env.neat") return true;
6140
+ if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
6141
+ return false;
6142
+ }
6143
+ async function writeAtomic(file, contents) {
6144
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
6145
+ await import_node_fs23.promises.writeFile(tmp, contents, "utf8");
6146
+ await import_node_fs23.promises.rename(tmp, file);
6147
+ }
5987
6148
  async function apply(installPlan) {
5988
- const touched = /* @__PURE__ */ new Set();
5989
- for (const e of installPlan.dependencyEdits) touched.add(e.file);
5990
- for (const e of installPlan.entrypointEdits) touched.add(e.file);
5991
- if (touched.size === 0) return;
6149
+ const { serviceDir } = installPlan;
6150
+ if (installPlan.libOnly) {
6151
+ return {
6152
+ serviceDir,
6153
+ outcome: "lib-only",
6154
+ reason: "no resolvable entry point",
6155
+ writtenFiles: []
6156
+ };
6157
+ }
6158
+ if (installPlan.dependencyEdits.length === 0 && installPlan.entrypointEdits.length === 0 && (installPlan.generatedFiles?.length ?? 0) === 0) {
6159
+ return {
6160
+ serviceDir,
6161
+ outcome: "already-instrumented",
6162
+ writtenFiles: []
6163
+ };
6164
+ }
6165
+ const allTargets = /* @__PURE__ */ new Set();
6166
+ for (const d of installPlan.dependencyEdits) allTargets.add(d.file);
6167
+ for (const e of installPlan.entrypointEdits) allTargets.add(e.file);
6168
+ for (const g of installPlan.generatedFiles ?? []) allTargets.add(g.file);
6169
+ for (const target of allTargets) {
6170
+ const isEntryEdit = installPlan.entrypointEdits.some((e) => e.file === target);
6171
+ if (isEntryEdit) continue;
6172
+ if (!isAllowedWritePath(serviceDir, target)) {
6173
+ throw new Error(
6174
+ `javascript installer: refusing to write outside the allowed path set (ADR-069 \xA77): ${target}`
6175
+ );
6176
+ }
6177
+ }
5992
6178
  const originals = /* @__PURE__ */ new Map();
5993
- for (const file of touched) {
5994
- try {
5995
- originals.set(file, await import_node_fs23.promises.readFile(file, "utf8"));
5996
- } catch {
6179
+ const createdFiles = [];
6180
+ for (const target of allTargets) {
6181
+ if (await exists2(target)) {
6182
+ try {
6183
+ originals.set(target, await import_node_fs23.promises.readFile(target, "utf8"));
6184
+ } catch {
6185
+ }
5997
6186
  }
5998
6187
  }
6188
+ const writtenFiles = [];
5999
6189
  try {
6000
- for (const file of touched) {
6001
- const raw = originals.get(file) ?? "";
6190
+ const manifestTargets = installPlan.dependencyEdits.reduce((acc, e) => {
6191
+ acc.add(e.file);
6192
+ return acc;
6193
+ }, /* @__PURE__ */ new Set());
6194
+ for (const file of manifestTargets) {
6195
+ const raw = originals.get(file);
6196
+ if (raw === void 0) {
6197
+ throw new Error(`javascript installer: cannot read ${file} during apply`);
6198
+ }
6002
6199
  const pkg = JSON.parse(raw);
6003
6200
  pkg.dependencies = pkg.dependencies ?? {};
6004
6201
  for (const dep of installPlan.dependencyEdits) {
6005
6202
  if (dep.file !== file) continue;
6006
6203
  if (dep.kind === "add") {
6007
- pkg.dependencies[dep.name] = dep.version;
6204
+ if (!(dep.name in (pkg.dependencies ?? {}))) {
6205
+ pkg.dependencies[dep.name] = dep.version;
6206
+ }
6008
6207
  } else {
6009
6208
  delete pkg.dependencies[dep.name];
6010
6209
  }
6011
6210
  }
6012
- for (const ep of installPlan.entrypointEdits) {
6013
- if (ep.file !== file) continue;
6014
- pkg.scripts = pkg.scripts ?? {};
6015
- if (pkg.scripts.start === ep.before) {
6016
- pkg.scripts.start = ep.after;
6017
- }
6018
- }
6019
6211
  const newRaw = JSON.stringify(pkg, null, 2) + "\n";
6020
- const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
6021
- await import_node_fs23.promises.writeFile(tmp, newRaw, "utf8");
6022
- await import_node_fs23.promises.rename(tmp, file);
6212
+ await writeAtomic(file, newRaw);
6213
+ writtenFiles.push(file);
6214
+ }
6215
+ for (const gen of installPlan.generatedFiles ?? []) {
6216
+ if (gen.skipIfExists && await exists2(gen.file)) {
6217
+ continue;
6218
+ }
6219
+ await writeAtomic(gen.file, gen.contents);
6220
+ if (!originals.has(gen.file)) createdFiles.push(gen.file);
6221
+ writtenFiles.push(gen.file);
6222
+ }
6223
+ for (const ep of installPlan.entrypointEdits) {
6224
+ const raw = originals.get(ep.file);
6225
+ if (raw === void 0) {
6226
+ throw new Error(`javascript installer: cannot read entry ${ep.file} during apply`);
6227
+ }
6228
+ const lines = raw.split(/\r?\n/);
6229
+ const hasShebang = lines[0]?.startsWith("#!") ?? false;
6230
+ const insertAt = hasShebang ? 1 : 0;
6231
+ const firstReal = lines[insertAt] ?? "";
6232
+ if (lineIsOtelInjection(firstReal)) continue;
6233
+ lines.splice(insertAt, 0, ep.after);
6234
+ const newRaw = lines.join("\n");
6235
+ await writeAtomic(ep.file, newRaw);
6236
+ writtenFiles.push(ep.file);
6023
6237
  }
6024
6238
  } catch (err) {
6025
- await rollback(installPlan, originals);
6239
+ await rollback(installPlan, originals, createdFiles);
6026
6240
  throw err;
6027
6241
  }
6242
+ return {
6243
+ serviceDir,
6244
+ outcome: "instrumented",
6245
+ writtenFiles
6246
+ };
6028
6247
  }
6029
- async function rollback(installPlan, originals) {
6248
+ async function rollback(installPlan, originals, createdFiles) {
6030
6249
  const restored = [];
6250
+ const removed = [];
6031
6251
  for (const [file, raw] of originals.entries()) {
6032
6252
  try {
6033
6253
  await import_node_fs23.promises.writeFile(file, raw, "utf8");
@@ -6035,6 +6255,13 @@ async function rollback(installPlan, originals) {
6035
6255
  } catch {
6036
6256
  }
6037
6257
  }
6258
+ for (const file of createdFiles) {
6259
+ try {
6260
+ await import_node_fs23.promises.unlink(file);
6261
+ removed.push(file);
6262
+ } catch {
6263
+ }
6264
+ }
6038
6265
  const lines = [
6039
6266
  "# neat-rollback.patch",
6040
6267
  "",
@@ -6042,6 +6269,7 @@ async function rollback(installPlan, originals) {
6042
6269
  "# Files listed below were restored to their pre-apply contents.",
6043
6270
  "",
6044
6271
  ...restored.map((f) => `restored: ${f}`),
6272
+ ...removed.map((f) => `removed: ${f}`),
6045
6273
  ""
6046
6274
  ];
6047
6275
  const rollbackPath = import_node_path38.default.join(installPlan.serviceDir, "neat-rollback.patch");
@@ -6067,7 +6295,7 @@ var OTEL_ENV2 = {
6067
6295
  key: "OTEL_EXPORTER_OTLP_ENDPOINT",
6068
6296
  value: "http://localhost:4318"
6069
6297
  };
6070
- async function exists2(p) {
6298
+ async function exists3(p) {
6071
6299
  try {
6072
6300
  await import_node_fs24.promises.stat(p);
6073
6301
  return true;
@@ -6078,7 +6306,7 @@ async function exists2(p) {
6078
6306
  async function detect2(serviceDir) {
6079
6307
  const markers = ["requirements.txt", "pyproject.toml", "setup.py"];
6080
6308
  for (const m of markers) {
6081
- if (await exists2(import_node_path39.default.join(serviceDir, m))) return true;
6309
+ if (await exists3(import_node_path39.default.join(serviceDir, m))) return true;
6082
6310
  }
6083
6311
  return false;
6084
6312
  }
@@ -6089,7 +6317,7 @@ function reqPackageName(line) {
6089
6317
  }
6090
6318
  async function planRequirementsTxtEdits(serviceDir) {
6091
6319
  const file = import_node_path39.default.join(serviceDir, "requirements.txt");
6092
- if (!await exists2(file)) return null;
6320
+ if (!await exists3(file)) return null;
6093
6321
  const raw = await import_node_fs24.promises.readFile(file, "utf8");
6094
6322
  const presentNames = new Set(
6095
6323
  raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
@@ -6099,7 +6327,7 @@ async function planRequirementsTxtEdits(serviceDir) {
6099
6327
  }
6100
6328
  async function planProcfileEdits(serviceDir) {
6101
6329
  const procfile = import_node_path39.default.join(serviceDir, "Procfile");
6102
- if (!await exists2(procfile)) return [];
6330
+ if (!await exists3(procfile)) return [];
6103
6331
  const raw = await import_node_fs24.promises.readFile(procfile, "utf8");
6104
6332
  const edits = [];
6105
6333
  for (const line of raw.split(/\r?\n/)) {
@@ -6166,10 +6394,13 @@ async function applyProcfile(procfile, edits, original) {
6166
6394
  await import_node_fs24.promises.rename(tmp, procfile);
6167
6395
  }
6168
6396
  async function apply2(installPlan) {
6397
+ const { serviceDir } = installPlan;
6169
6398
  const touched = /* @__PURE__ */ new Set();
6170
6399
  for (const e of installPlan.dependencyEdits) touched.add(e.file);
6171
6400
  for (const e of installPlan.entrypointEdits) touched.add(e.file);
6172
- if (touched.size === 0) return;
6401
+ if (touched.size === 0) {
6402
+ return { serviceDir, outcome: "already-instrumented", writtenFiles: [] };
6403
+ }
6173
6404
  const originals = /* @__PURE__ */ new Map();
6174
6405
  for (const file of touched) {
6175
6406
  try {
@@ -6177,6 +6408,7 @@ async function apply2(installPlan) {
6177
6408
  } catch {
6178
6409
  }
6179
6410
  }
6411
+ const writtenFiles = [];
6180
6412
  try {
6181
6413
  for (const file of touched) {
6182
6414
  const raw = originals.get(file);
@@ -6186,16 +6418,23 @@ async function apply2(installPlan) {
6186
6418
  const base = import_node_path39.default.basename(file);
6187
6419
  if (base === "requirements.txt") {
6188
6420
  const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
6189
- if (edits.length > 0) await applyRequirementsTxt(file, edits, raw);
6421
+ if (edits.length > 0) {
6422
+ await applyRequirementsTxt(file, edits, raw);
6423
+ writtenFiles.push(file);
6424
+ }
6190
6425
  } else if (base === "Procfile") {
6191
6426
  const edits = installPlan.entrypointEdits.filter((e) => e.file === file);
6192
- if (edits.length > 0) await applyProcfile(file, edits, raw);
6427
+ if (edits.length > 0) {
6428
+ await applyProcfile(file, edits, raw);
6429
+ writtenFiles.push(file);
6430
+ }
6193
6431
  }
6194
6432
  }
6195
6433
  } catch (err) {
6196
6434
  await rollback2(installPlan, originals);
6197
6435
  throw err;
6198
6436
  }
6437
+ return { serviceDir, outcome: "instrumented", writtenFiles };
6199
6438
  }
6200
6439
  async function rollback2(installPlan, originals) {
6201
6440
  const restored = [];
@@ -6228,7 +6467,7 @@ var pythonInstaller = {
6228
6467
  // src/installers/shared.ts
6229
6468
  init_cjs_shims();
6230
6469
  function isEmptyPlan(plan3) {
6231
- return plan3.dependencyEdits.length === 0 && plan3.entrypointEdits.length === 0 && plan3.envEdits.length === 0;
6470
+ return plan3.dependencyEdits.length === 0 && plan3.entrypointEdits.length === 0 && plan3.envEdits.length === 0 && (plan3.generatedFiles?.length ?? 0) === 0;
6232
6471
  }
6233
6472
 
6234
6473
  // src/installers/index.ts
@@ -6270,8 +6509,18 @@ function renderPatch(sections) {
6270
6509
  const { installer, plan: plan3 } = section;
6271
6510
  lines.push(`## ${installer} (${plan3.language}) \u2014 ${plan3.serviceDir}`);
6272
6511
  lines.push("");
6512
+ if (plan3.libOnly) {
6513
+ lines.push("### skipped \u2014 no resolvable entry point (lib-only)");
6514
+ lines.push("");
6515
+ continue;
6516
+ }
6517
+ if (plan3.entryFile) {
6518
+ lines.push(`entry: ${plan3.entryFile}`);
6519
+ lines.push("");
6520
+ }
6273
6521
  if (plan3.dependencyEdits.length > 0) {
6274
6522
  lines.push("### dependencies");
6523
+ const byFile = /* @__PURE__ */ new Map();
6275
6524
  for (const dep of plan3.dependencyEdits) {
6276
6525
  const base = dep.file.split(/[\\/]/).pop() ?? dep.file;
6277
6526
  if (FORBIDDEN_LOCKFILES.has(base)) {
@@ -6279,24 +6528,41 @@ function renderPatch(sections) {
6279
6528
  `installer "${installer}" produced a dependency edit against a lockfile (${dep.file}); lockfiles must never be touched (ADR-047).`
6280
6529
  );
6281
6530
  }
6282
- lines.push(`- ${dep.kind} ${dep.name}@${dep.version} in ${dep.file}`);
6531
+ const existing = byFile.get(dep.file) ?? [];
6532
+ existing.push(dep);
6533
+ byFile.set(dep.file, existing);
6534
+ }
6535
+ for (const [file, deps] of byFile) {
6536
+ lines.push(`--- ${file}`);
6537
+ for (const dep of deps) {
6538
+ lines.push(`+ "${dep.name}": "${dep.version}"`);
6539
+ }
6540
+ }
6541
+ lines.push("");
6542
+ }
6543
+ if (plan3.generatedFiles && plan3.generatedFiles.length > 0) {
6544
+ lines.push("### generated files");
6545
+ for (const gen of plan3.generatedFiles) {
6546
+ lines.push(`--- (new file) ${gen.file}`);
6547
+ for (const ln of gen.contents.split(/\r?\n/)) {
6548
+ lines.push(`+ ${ln}`);
6549
+ }
6283
6550
  }
6284
6551
  lines.push("");
6285
6552
  }
6286
6553
  if (plan3.entrypointEdits.length > 0) {
6287
- lines.push("### entrypoint");
6554
+ lines.push("### entry-point injection");
6288
6555
  for (const e of plan3.entrypointEdits) {
6289
- lines.push(`- ${e.file}`);
6290
- lines.push(` - before: ${e.before}`);
6291
- lines.push(` - after: ${e.after}`);
6556
+ lines.push(`--- ${e.file}`);
6557
+ lines.push(`+ ${e.after}`);
6558
+ lines.push(` ${e.before}`);
6292
6559
  }
6293
6560
  lines.push("");
6294
6561
  }
6295
6562
  if (plan3.envEdits.length > 0) {
6296
- lines.push("### env");
6563
+ lines.push("### env (written to <package-dir>/.env.neat)");
6297
6564
  for (const env of plan3.envEdits) {
6298
- const target = env.file ?? "(set in your orchestration layer)";
6299
- lines.push(`- ${env.key}=${env.value} \u2192 ${target}`);
6565
+ lines.push(`- ${env.key}=${env.value}`);
6300
6566
  }
6301
6567
  lines.push("");
6302
6568
  }
@@ -7045,7 +7311,7 @@ async function buildPatchSections(services) {
7045
7311
  const installer = await pickInstaller(svc.dir);
7046
7312
  if (!installer) continue;
7047
7313
  const plan3 = await installer.plan(svc.dir);
7048
- if (isEmptyPlan(plan3)) continue;
7314
+ if (isEmptyPlan(plan3) && !plan3.libOnly) continue;
7049
7315
  sections.push({ installer: installer.name, plan: plan3 });
7050
7316
  }
7051
7317
  return sections;
@@ -7098,14 +7364,28 @@ async function runInit(opts) {
7098
7364
  }
7099
7365
  if (!opts.noInstall) {
7100
7366
  if (opts.apply) {
7367
+ let instrumented = 0;
7368
+ let alreadyInstrumented = 0;
7369
+ let libOnly = 0;
7101
7370
  for (const section of sections) {
7102
7371
  const installer = INSTALLERS.find((i) => i.name === section.installer);
7103
7372
  if (!installer) continue;
7104
- await installer.apply(section.plan);
7373
+ const outcome = await installer.apply(section.plan);
7374
+ if (outcome.outcome === "instrumented") {
7375
+ instrumented++;
7376
+ for (const f of outcome.writtenFiles) written.push(f);
7377
+ } else if (outcome.outcome === "already-instrumented") {
7378
+ alreadyInstrumented++;
7379
+ } else if (outcome.outcome === "lib-only") {
7380
+ libOnly++;
7381
+ }
7105
7382
  }
7106
7383
  if (sections.length > 0) {
7107
7384
  console.log("");
7108
- console.log("patch applied. Run `npm install` (or your language equivalent) to refresh lockfiles.");
7385
+ console.log(
7386
+ `apply: instrumented ${instrumented}, already-instrumented ${alreadyInstrumented}, lib-only ${libOnly}`
7387
+ );
7388
+ console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
7109
7389
  }
7110
7390
  } else {
7111
7391
  await import_node_fs25.promises.writeFile(patchPath, patch, "utf8");