@codyswann/lisa 4.17.6 → 4.17.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.
Files changed (81) hide show
  1. package/all/copy-contents/gitignore +6 -0
  2. package/dist/cli/doctor-reconciliation.d.ts +15 -0
  3. package/dist/cli/doctor-reconciliation.d.ts.map +1 -0
  4. package/dist/cli/doctor-reconciliation.js +123 -0
  5. package/dist/cli/doctor-reconciliation.js.map +1 -0
  6. package/dist/cli/doctor.d.ts.map +1 -1
  7. package/dist/cli/doctor.js +7 -0
  8. package/dist/cli/doctor.js.map +1 -1
  9. package/dist/core/lisa.d.ts.map +1 -1
  10. package/dist/core/lisa.js +15 -1
  11. package/dist/core/lisa.js.map +1 -1
  12. package/dist/core/reconciliation-report.d.ts +100 -0
  13. package/dist/core/reconciliation-report.d.ts.map +1 -0
  14. package/dist/core/reconciliation-report.js +178 -0
  15. package/dist/core/reconciliation-report.js.map +1 -0
  16. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  17. package/dist/core/upstream-evidence-manifest.js +7 -1
  18. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  19. package/dist/utils/postinstall-trampoline-report-source.d.ts +50 -0
  20. package/dist/utils/postinstall-trampoline-report-source.d.ts.map +1 -0
  21. package/dist/utils/postinstall-trampoline-report-source.js +113 -0
  22. package/dist/utils/postinstall-trampoline-report-source.js.map +1 -0
  23. package/dist/utils/postinstall-trampoline-source.d.ts +65 -0
  24. package/dist/utils/postinstall-trampoline-source.d.ts.map +1 -0
  25. package/dist/utils/postinstall-trampoline-source.js +312 -0
  26. package/dist/utils/postinstall-trampoline-source.js.map +1 -0
  27. package/dist/utils/postinstall-trampoline.d.ts +4 -2
  28. package/dist/utils/postinstall-trampoline.d.ts.map +1 -1
  29. package/dist/utils/postinstall-trampoline.js +5 -197
  30. package/dist/utils/postinstall-trampoline.js.map +1 -1
  31. package/package.json +2 -2
  32. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  34. package/plugins/lisa-agy/plugin.json +1 -1
  35. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  37. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  38. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  44. package/plugins/lisa-expo-agy/plugin.json +1 -1
  45. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  49. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  50. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  54. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  55. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  59. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  60. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  64. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  65. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  69. package/plugins/lisa-rails-agy/plugin.json +1 -1
  70. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  74. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  75. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  79. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  80. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @file postinstall-trampoline-report-source.ts
3
+ * @description The two reporting fragments inlined into the detached
4
+ * reconciliation child.
5
+ *
6
+ * These exist because the child is detached and `stdio: "ignore"`: anything it
7
+ * says to a stream is said to nobody, so the only evidence that survives the
8
+ * package manager exiting is a file. Before them, a trampoline that never
9
+ * spawned, one that died on arrival, and one that ran and correctly did nothing
10
+ * were indistinguishable — which is what made CodySwannGT/lisa#2750 invisible
11
+ * for as long as it existed.
12
+ * @module utils
13
+ */
14
+ /**
15
+ * The report writer inlined into the trampoline child.
16
+ *
17
+ * This is the deliverable half of CodySwannGT/lisa#2750. The child is detached
18
+ * and `stdio: "ignore"`, so anything it says to a stream is said to nobody; the
19
+ * only place it can leave evidence is the filesystem, and the only evidence that
20
+ * survives the parent exiting is a file. Every write is atomic (temp + rename)
21
+ * so `lisa doctor` never reads a half-written document, and every write is
22
+ * wrapped so a read-only or full disk degrades to the old silence rather than
23
+ * killing a reconciliation that would otherwise have worked.
24
+ *
25
+ * `scheduled_at` and `lisa_version` are inherited from the report the parent
26
+ * apply wrote, so the age doctor reports is the age of the WHOLE reconciliation
27
+ * rather than of its last heartbeat.
28
+ * @param literals - Inlined JSON-safe literals
29
+ * @param literals.reportPath - Absolute path of the report file
30
+ * @param literals.reportSchemaVersion - Schema version stamped into each write
31
+ * @param literals.lisaVersion - Fallback Lisa version when no prior report exists
32
+ * @returns JS source fragment
33
+ */
34
+ export declare function buildTrampolineReporter(literals: {
35
+ readonly reportPath: string;
36
+ readonly reportSchemaVersion: string;
37
+ readonly lisaVersion: string;
38
+ }): string;
39
+ /**
40
+ * The regen-outcome reporter inlined into the trampoline child.
41
+ *
42
+ * Split from the main IIFE only so each stays under the repo's function-length
43
+ * cap; it is the terminal half of one decision. Three outcomes, deliberately
44
+ * distinct: a regen that failed, a package.json change with no lockfile to
45
+ * repair, and a regen that landed. Collapsing any pair of them would restore
46
+ * exactly the ambiguity CodySwannGT/lisa#2750 was filed about.
47
+ * @returns JS source fragment
48
+ */
49
+ export declare function buildTrampolineRegenReporter(): string;
50
+ //# sourceMappingURL=postinstall-trampoline-report-source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postinstall-trampoline-report-source.d.ts","sourceRoot":"","sources":["../../src/utils/postinstall-trampoline-report-source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE;IAChD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B,GAAG,MAAM,CAqCT;AAED;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,IAAI,MAAM,CA8BrD"}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * @file postinstall-trampoline-report-source.ts
3
+ * @description The two reporting fragments inlined into the detached
4
+ * reconciliation child.
5
+ *
6
+ * These exist because the child is detached and `stdio: "ignore"`: anything it
7
+ * says to a stream is said to nobody, so the only evidence that survives the
8
+ * package manager exiting is a file. Before them, a trampoline that never
9
+ * spawned, one that died on arrival, and one that ran and correctly did nothing
10
+ * were indistinguishable — which is what made CodySwannGT/lisa#2750 invisible
11
+ * for as long as it existed.
12
+ * @module utils
13
+ */
14
+ /**
15
+ * The report writer inlined into the trampoline child.
16
+ *
17
+ * This is the deliverable half of CodySwannGT/lisa#2750. The child is detached
18
+ * and `stdio: "ignore"`, so anything it says to a stream is said to nobody; the
19
+ * only place it can leave evidence is the filesystem, and the only evidence that
20
+ * survives the parent exiting is a file. Every write is atomic (temp + rename)
21
+ * so `lisa doctor` never reads a half-written document, and every write is
22
+ * wrapped so a read-only or full disk degrades to the old silence rather than
23
+ * killing a reconciliation that would otherwise have worked.
24
+ *
25
+ * `scheduled_at` and `lisa_version` are inherited from the report the parent
26
+ * apply wrote, so the age doctor reports is the age of the WHOLE reconciliation
27
+ * rather than of its last heartbeat.
28
+ * @param literals - Inlined JSON-safe literals
29
+ * @param literals.reportPath - Absolute path of the report file
30
+ * @param literals.reportSchemaVersion - Schema version stamped into each write
31
+ * @param literals.lisaVersion - Fallback Lisa version when no prior report exists
32
+ * @returns JS source fragment
33
+ */
34
+ export function buildTrampolineReporter(literals) {
35
+ return `
36
+ const REPORT_PATH = ${literals.reportPath};
37
+
38
+ function readReport() {
39
+ try { return JSON.parse(readFileSync(REPORT_PATH, "utf8")); }
40
+ catch { return null; }
41
+ }
42
+
43
+ function report(outcome, detail, packageManagers) {
44
+ try {
45
+ const prior = readReport();
46
+ const now = new Date().toISOString();
47
+ const doc = {
48
+ schema_version: ${literals.reportSchemaVersion},
49
+ lisa_version:
50
+ prior && typeof prior.lisa_version === "string"
51
+ ? prior.lisa_version
52
+ : ${literals.lisaVersion},
53
+ outcome: outcome,
54
+ scheduled_at:
55
+ prior && typeof prior.scheduled_at === "string"
56
+ ? prior.scheduled_at
57
+ : now,
58
+ updated_at: now,
59
+ package_managers: packageManagers || [],
60
+ detail: detail,
61
+ };
62
+ mkdirSync(path.dirname(REPORT_PATH), { recursive: true });
63
+ const tempPath = REPORT_PATH + ".tmp";
64
+ writeFileSync(tempPath, JSON.stringify(doc, null, 2) + "\\n");
65
+ renameSync(tempPath, REPORT_PATH);
66
+ } catch {
67
+ // A report that cannot be written must never break an install.
68
+ }
69
+ }
70
+ `;
71
+ }
72
+ /**
73
+ * The regen-outcome reporter inlined into the trampoline child.
74
+ *
75
+ * Split from the main IIFE only so each stays under the repo's function-length
76
+ * cap; it is the terminal half of one decision. Three outcomes, deliberately
77
+ * distinct: a regen that failed, a package.json change with no lockfile to
78
+ * repair, and a regen that landed. Collapsing any pair of them would restore
79
+ * exactly the ambiguity CodySwannGT/lisa#2750 was filed about.
80
+ * @returns JS source fragment
81
+ */
82
+ export function buildTrampolineRegenReporter() {
83
+ return `
84
+ function reportRegen(regen, reapplyNote) {
85
+ if (regen.failed.length > 0) {
86
+ report(
87
+ "failed",
88
+ "Lockfile regeneration failed for: " + regen.failed.join(", ") +
89
+ ". Lockfiles are still out of sync with package.json, so a " +
90
+ "frozen-lockfile install will fail." + reapplyNote,
91
+ regen.attempted
92
+ );
93
+ return;
94
+ }
95
+ if (regen.attempted.length === 0) {
96
+ report(
97
+ "not-needed",
98
+ "package.json changed but no lockfile was found to regenerate." +
99
+ reapplyNote,
100
+ []
101
+ );
102
+ return;
103
+ }
104
+ report(
105
+ "regenerated",
106
+ "Regenerated lockfiles for: " + regen.attempted.join(", ") + "." +
107
+ reapplyNote,
108
+ regen.attempted
109
+ );
110
+ }
111
+ `;
112
+ }
113
+ //# sourceMappingURL=postinstall-trampoline-report-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postinstall-trampoline-report-source.js","sourceRoot":"","sources":["../../src/utils/postinstall-trampoline-report-source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,uBAAuB,CAAC,QAIvC;IACC,OAAO;0BACiB,QAAQ,CAAC,UAAU;;;;;;;;;;;;4BAYjB,QAAQ,CAAC,mBAAmB;;;;kBAItC,QAAQ,CAAC,WAAW;;;;;;;;;;;;;;;;;;GAkBnC,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,4BAA4B;IAC1C,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BN,CAAC;AACJ,CAAC"}
@@ -0,0 +1,65 @@
1
+ import type { LockfileRegenPlan, PackageManager } from "./package-manager-detect.js";
2
+ /**
3
+ * What the detached child needs in order to leave a durable record of itself.
4
+ *
5
+ * Passed in by the caller rather than imported, so this module stays free of a
6
+ * dependency on `core/`: the report's path and schema still have exactly one
7
+ * definition (`core/reconciliation-report`), and it travels here as data.
8
+ */
9
+ export interface TrampolineReporting {
10
+ /** Absolute path of the report file the child writes. */
11
+ readonly reportPath: string;
12
+ /** Schema version stamped into every write. */
13
+ readonly reportSchemaVersion: number;
14
+ /** Lisa version, used only when no scheduled report exists to inherit it from. */
15
+ readonly lisaVersion: string;
16
+ /**
17
+ * sha256 of package.json as it stood BEFORE the apply that scheduled this
18
+ * child — which is the content the package manager resolved its lockfile
19
+ * from.
20
+ *
21
+ * This is the whole of CodySwannGT/lisa#2750. The child used to regenerate
22
+ * lockfiles only when ITS OWN re-apply changed package.json, which on a plain
23
+ * `bun install` it never does: the first apply (inside postinstall) already
24
+ * made the change, and the re-apply is idempotent. So the drift the first
25
+ * apply created — the drift the trampoline exists to repair — was the one
26
+ * case that never triggered a regen. Comparing against this baseline asks the
27
+ * question that actually matters: does package.json still match what the
28
+ * lockfile was built from?
29
+ */
30
+ readonly baselinePackageJsonHash: string | null;
31
+ }
32
+ /**
33
+ * Shape of the parameters embedded into the trampoline's inline JS source.
34
+ * Grouping keeps the callsite readable and the injected literals explicit.
35
+ */
36
+ interface TrampolineSourceParams {
37
+ readonly parentPid: number;
38
+ readonly pollIntervalMs: number;
39
+ readonly maxWaitMs: number;
40
+ readonly settleDelayMs: number;
41
+ readonly lisaEntry: string;
42
+ readonly projectDir: string;
43
+ readonly nodeBin: string;
44
+ readonly trampolineEnvVar: string;
45
+ readonly lockfileRegenPlans: Readonly<Record<PackageManager, LockfileRegenPlan>>;
46
+ readonly reporting: TrampolineReporting;
47
+ }
48
+ /**
49
+ * Build the inline JS source that runs inside the detached trampoline child.
50
+ * The source is passed to `node -e` so it must be self-contained (no imports that
51
+ * require resolution via package.json, which is exactly the file we're racing).
52
+ *
53
+ * The trampoline now runs in two phases inside the child:
54
+ * 1. Wait for the parent PM to exit, then re-invoke Lisa to reconcile package.json.
55
+ * 2. If Lisa's re-invocation mutated package.json, regenerate whichever lockfiles
56
+ * are present in the project so `bun install --frozen-lockfile` / `npm ci` in
57
+ * downstream CI jobs do not fail with "lockfile had changes, but lockfile is
58
+ * frozen." Lockfile regen runs with `--ignore-scripts` so the parent PM's
59
+ * lifecycle hooks are not re-invoked (which would retrigger this trampoline).
60
+ * @param params - Embedded constants to inline into the trampoline source
61
+ * @returns JS source suitable for `node -e`
62
+ */
63
+ export declare function buildTrampolineSource(params: TrampolineSourceParams): string;
64
+ export {};
65
+ //# sourceMappingURL=postinstall-trampoline-source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postinstall-trampoline-source.d.ts","sourceRoot":"","sources":["../../src/utils/postinstall-trampoline-source.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EACV,iBAAiB,EACjB,cAAc,EACf,MAAM,6BAA6B,CAAC;AAMrC;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,yDAAyD;IACzD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,+CAA+C;IAC/C,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,kFAAkF;IAClF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,uBAAuB,EAAE,MAAM,GAAG,IAAI,CAAC;CACjD;AAED;;;GAGG;AACH,UAAU,sBAAsB;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,kBAAkB,EAAE,QAAQ,CACnC,MAAM,CAAC,cAAc,EAAE,iBAAiB,CAAC,CAC1C,CAAC;IACF,QAAQ,CAAC,SAAS,EAAE,mBAAmB,CAAC;CACzC;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CA4B5E"}
@@ -0,0 +1,312 @@
1
+ /**
2
+ * @file postinstall-trampoline-source.ts
3
+ * @description The JS source that runs inside the detached reconciliation child.
4
+ *
5
+ * Split out of `postinstall-trampoline` because it is a different kind of code:
6
+ * everything here is a string template compiled into a `node -e` payload, which
7
+ * cannot import anything (it runs while package.json — the file it is repairing
8
+ * — is in flux), so every constant it needs is inlined as a JSON literal at
9
+ * build time. Keeping it beside the scheduling logic made one module that was
10
+ * half TypeScript and half generated JavaScript.
11
+ *
12
+ * The child's job, in order: announce that it is alive, wait for the package
13
+ * manager to exit, re-apply Lisa, and regenerate lockfiles when package.json no
14
+ * longer matches the baseline the package manager resolved its lockfile from —
15
+ * then record what it did (CodySwannGT/lisa#2750).
16
+ * @module utils
17
+ */
18
+ import { PM_PATH_ENV_NAMES, PM_PATH_ENV_PREFIXES } from "./pm-env.js";
19
+ import { buildTrampolineRegenReporter, buildTrampolineReporter, } from "./postinstall-trampoline-report-source.js";
20
+ /**
21
+ * Build the inline JS source that runs inside the detached trampoline child.
22
+ * The source is passed to `node -e` so it must be self-contained (no imports that
23
+ * require resolution via package.json, which is exactly the file we're racing).
24
+ *
25
+ * The trampoline now runs in two phases inside the child:
26
+ * 1. Wait for the parent PM to exit, then re-invoke Lisa to reconcile package.json.
27
+ * 2. If Lisa's re-invocation mutated package.json, regenerate whichever lockfiles
28
+ * are present in the project so `bun install --frozen-lockfile` / `npm ci` in
29
+ * downstream CI jobs do not fail with "lockfile had changes, but lockfile is
30
+ * frozen." Lockfile regen runs with `--ignore-scripts` so the parent PM's
31
+ * lifecycle hooks are not re-invoked (which would retrigger this trampoline).
32
+ * @param params - Embedded constants to inline into the trampoline source
33
+ * @returns JS source suitable for `node -e`
34
+ */
35
+ export function buildTrampolineSource(params) {
36
+ // JSON.stringify gives us safe inline literals for all primitive types.
37
+ const literals = {
38
+ parentPid: JSON.stringify(params.parentPid),
39
+ pollIntervalMs: JSON.stringify(params.pollIntervalMs),
40
+ maxWaitMs: JSON.stringify(params.maxWaitMs),
41
+ settleDelayMs: JSON.stringify(params.settleDelayMs),
42
+ lisaEntry: JSON.stringify(params.lisaEntry),
43
+ projectDir: JSON.stringify(params.projectDir),
44
+ nodeBin: JSON.stringify(params.nodeBin),
45
+ trampolineEnvVar: JSON.stringify(params.trampolineEnvVar),
46
+ lockfilePlans: JSON.stringify(params.lockfileRegenPlans),
47
+ pmEnvPrefixes: JSON.stringify(PM_PATH_ENV_PREFIXES),
48
+ pmEnvNames: JSON.stringify(PM_PATH_ENV_NAMES),
49
+ reportPath: JSON.stringify(params.reporting.reportPath),
50
+ reportSchemaVersion: JSON.stringify(params.reporting.reportSchemaVersion),
51
+ lisaVersion: JSON.stringify(params.reporting.lisaVersion),
52
+ baselineHash: JSON.stringify(params.reporting.baselinePackageJsonHash),
53
+ };
54
+ return [
55
+ buildTrampolinePrelude(literals),
56
+ buildTrampolineReporter(literals),
57
+ buildTrampolinePmHelpers(literals),
58
+ buildTrampolineHelpers(literals),
59
+ buildTrampolineRegenReporter(),
60
+ buildTrampolineMain(literals),
61
+ ].join("\n");
62
+ }
63
+ /**
64
+ * Inline `require` prelude + the lockfile plan table. Kept separate so each
65
+ * chunk of the trampoline source stays under the 75-line max-lines-per-function
66
+ * cap enforced by eslint.
67
+ * @param literals - Inlined JSON-safe literals
68
+ * @param literals.lockfilePlans - JSON-serialized lockfile plan table
69
+ * @param literals.pmEnvPrefixes - JSON-serialized package-manager env var prefixes to strip
70
+ * @param literals.pmEnvNames - JSON-serialized package-manager env var names to strip
71
+ * @returns JS source fragment
72
+ */
73
+ function buildTrampolinePrelude(literals) {
74
+ return `
75
+ const { spawn } = require("node:child_process");
76
+ const { createHash } = require("node:crypto");
77
+ const {
78
+ existsSync,
79
+ mkdirSync,
80
+ readFileSync,
81
+ renameSync,
82
+ writeFileSync,
83
+ } = require("node:fs");
84
+ const path = require("node:path");
85
+
86
+ const LOCKFILE_PLANS = ${literals.lockfilePlans};
87
+ const PM_ENV_PREFIXES = ${literals.pmEnvPrefixes};
88
+ const PM_ENV_NAMES = ${literals.pmEnvNames};
89
+ // Strip PM path/lifecycle vars from spawned bun/Lisa env. Mirrors pm-env.ts.
90
+ function sanitizeEnv(env) {
91
+ const bad = (k) => PM_ENV_NAMES.indexOf(k) !== -1 || PM_ENV_PREFIXES.some((p) => k.startsWith(p));
92
+ return Object.fromEntries(Object.entries(env).filter(([k]) => !bad(k)));
93
+ }
94
+ `;
95
+ }
96
+ /**
97
+ * Package-manager helpers inlined into the trampoline child: the `engines`
98
+ * opt-out reader, the lockfile-presence detector, and the regen loop.
99
+ *
100
+ * The regen loop is still best-effort at the process level — a missing package
101
+ * manager binary must never cascade into a failed install — but it now returns
102
+ * which managers it tried and which failed, so the outcome can be recorded
103
+ * instead of swallowed (CodySwannGT/lisa#2750).
104
+ * @param literals - Inlined JSON-safe literals
105
+ * @param literals.projectDir - Project directory whose lockfiles are regenerated
106
+ * @returns JS source fragment
107
+ */
108
+ function buildTrampolinePmHelpers(literals) {
109
+ return `
110
+ function enginesForbiddenManagers(dir) {
111
+ try {
112
+ const pkg = JSON.parse(readFileSync(path.join(dir, "package.json"), "utf8"));
113
+ const engines = (pkg && pkg.engines) || {};
114
+ return ["bun", "npm", "yarn", "pnpm"].filter(
115
+ (pm) => typeof engines[pm] === "string" && /please-use|do-not-use/i.test(engines[pm])
116
+ );
117
+ } catch {
118
+ return [];
119
+ }
120
+ }
121
+
122
+ function detectPackageManagers(dir) {
123
+ const forbidden = enginesForbiddenManagers(dir);
124
+ return Object.values(LOCKFILE_PLANS)
125
+ .filter((plan) => [plan.lockfile].concat(plan.lockfileAlternatives || []).some((f) => existsSync(path.join(dir, f))))
126
+ .map((plan) => plan.pm)
127
+ .filter((pm) => forbidden.indexOf(pm) === -1);
128
+ }
129
+
130
+ async function regenerateLockfiles() {
131
+ const attempted = [];
132
+ const failed = [];
133
+ for (const pm of detectPackageManagers(${literals.projectDir})) {
134
+ const plan = LOCKFILE_PLANS[pm];
135
+ if (!plan) continue;
136
+ attempted.push(pm);
137
+ // Still best-effort at the process level: a missing PM binary must not
138
+ // cascade into an install failure. What changed is that the failure is
139
+ // now RECORDED instead of swallowed outright, which is what makes a
140
+ // regen that did not work distinguishable from one that had nothing to
141
+ // do (CodySwannGT/lisa#2750).
142
+ const ok = await spawnChild(plan.command, plan.args);
143
+ if (!ok) {
144
+ failed.push(pm + " (" + plan.command + " " + plan.args.join(" ") + ")");
145
+ }
146
+ }
147
+ return { attempted: attempted, failed: failed };
148
+ }
149
+ `;
150
+ }
151
+ /**
152
+ * Process helpers inlined into the trampoline child: parent-liveness probe,
153
+ * file hasher, the wait loop, the sanitised child spawner, and the Lisa
154
+ * re-invoker. Each mirrors an exported TS helper in `postinstall-trampoline` or
155
+ * `package-manager-detect`, so the logic stays test-covered via those.
156
+ * @param literals - Inlined JSON-safe literals
157
+ * @param literals.parentPid - Parent package-manager PID for liveness probe
158
+ * @param literals.pollIntervalMs - Poll interval for parent-liveness checks
159
+ * @param literals.maxWaitMs - Max wait deadline before bailing out
160
+ * @param literals.nodeBin - Node binary path to re-invoke Lisa with
161
+ * @param literals.lisaEntry - Absolute path to Lisa's dist/index.js
162
+ * @param literals.projectDir - Project directory Lisa will reconcile
163
+ * @param literals.trampolineEnvVar - Env var name used to mark child as trampoline
164
+ * @returns JS source fragment
165
+ */
166
+ function buildTrampolineHelpers(literals) {
167
+ return `
168
+ function isAlive(pid) {
169
+ if (!pid || pid <= 1) return false;
170
+ try { process.kill(pid, 0); return true; } catch { return false; }
171
+ }
172
+
173
+ function hashFile(p) {
174
+ try { return createHash("sha256").update(readFileSync(p)).digest("hex"); }
175
+ catch { return null; }
176
+ }
177
+
178
+ async function waitForParent() {
179
+ const deadline = Date.now() + ${literals.maxWaitMs};
180
+ while (Date.now() < deadline) {
181
+ if (!isAlive(${literals.parentPid})) return true;
182
+ await new Promise((r) => setTimeout(r, ${literals.pollIntervalMs}));
183
+ }
184
+ return false;
185
+ }
186
+
187
+ function spawnChild(command, args) {
188
+ return new Promise((resolve) => {
189
+ try {
190
+ const child = spawn(command, args, {
191
+ cwd: ${literals.projectDir},
192
+ stdio: "ignore",
193
+ env: Object.assign(sanitizeEnv(process.env), { [${literals.trampolineEnvVar}]: "1" }),
194
+ });
195
+ child.on("exit", (code) => resolve(code === 0));
196
+ child.on("error", () => resolve(false));
197
+ } catch {
198
+ resolve(false);
199
+ }
200
+ });
201
+ }
202
+
203
+ function runLisa() {
204
+ return spawnChild(${literals.nodeBin}, [${literals.lisaEntry}, "--yes", "--skip-git-check", ${literals.projectDir}]);
205
+ }
206
+
207
+ `;
208
+ }
209
+ /**
210
+ * Top-level async IIFE that orchestrates the trampoline child:
211
+ * 1) announce that the child is alive (so a spawn that produced nothing is
212
+ * distinguishable from one that ran),
213
+ * 2) wait for the parent PM to exit,
214
+ * 3) re-run Lisa,
215
+ * 4) regenerate lockfiles when package.json no longer matches the baseline the
216
+ * package manager resolved its lockfile from,
217
+ * 5) record the outcome either way.
218
+ *
219
+ * Step 4 is the fix for CodySwannGT/lisa#2750. It used to compare package.json
220
+ * before and after the child's OWN re-apply, and regenerate only when that
221
+ * re-apply changed something. On the path that matters — a plain `bun install`
222
+ * where the first apply (inside postinstall) already rewrote package.json and
223
+ * the re-apply is therefore idempotent — that comparison is always equal, so
224
+ * the regen never ran and the lockfile stayed exactly as stale as the install
225
+ * left it. Measured on a bun consumer: `bun.lock`'s mtime never moved, while
226
+ * the regen plan run by hand fixed the drift in 1.66 s.
227
+ *
228
+ * The baseline comparison asks the right question instead: package.json is
229
+ * compared against its content BEFORE the first apply, which is what the
230
+ * lockfile was built from. It also no longer requires the re-apply to have
231
+ * SUCCEEDED — a re-apply that fails leaves the first apply's drift in place,
232
+ * and refusing to regen there is the one case where the lockfile is guaranteed
233
+ * wrong.
234
+ *
235
+ * Timing out MUST NOT re-run Lisa — that would reintroduce the package.json
236
+ * race the trampoline is designed to avoid (parent PM still writing).
237
+ * @param literals - Inlined JSON-safe literals
238
+ * @param literals.settleDelayMs - Settle delay before re-invoking Lisa
239
+ * @param literals.projectDir - Project directory Lisa will reconcile
240
+ * @param literals.baselineHash - sha256 of package.json before the apply that scheduled this child
241
+ * @returns JS source fragment
242
+ */
243
+ function buildTrampolineMain(literals) {
244
+ return `
245
+ (async () => {
246
+ try {
247
+ report(
248
+ "started",
249
+ "Waiting for the package manager to exit before re-applying Lisa.",
250
+ []
251
+ );
252
+ const parentExited = await waitForParent();
253
+ if (!parentExited) {
254
+ report(
255
+ "parent-wait-timed-out",
256
+ "The package manager was still running after the wait deadline, so " +
257
+ "Lisa did not re-apply (re-applying under a live package manager " +
258
+ "is the race this exists to avoid). Lockfiles may be out of sync " +
259
+ "with package.json.",
260
+ []
261
+ );
262
+ process.exit(0);
263
+ }
264
+ await new Promise((r) => setTimeout(r, ${literals.settleDelayMs}));
265
+
266
+ const pkgPath = path.join(${literals.projectDir}, "package.json");
267
+ const baselineHash = ${literals.baselineHash};
268
+
269
+ const lisaOk = await runLisa();
270
+ const reapplyNote = lisaOk
271
+ ? ""
272
+ : " The re-apply itself exited non-zero; run it by hand to see why.";
273
+
274
+ const postHash = hashFile(pkgPath);
275
+ if (postHash === null) {
276
+ report(
277
+ "failed",
278
+ "package.json could not be read after the re-apply, so lockfile " +
279
+ "drift could not be assessed." + reapplyNote,
280
+ []
281
+ );
282
+ process.exit(0);
283
+ }
284
+
285
+ // Compare against what the package manager built the lockfile FROM,
286
+ // not against what this child's own idempotent re-apply changed.
287
+ if (baselineHash !== null && postHash === baselineHash) {
288
+ report(
289
+ "not-needed",
290
+ "package.json matches what the package manager resolved the " +
291
+ "lockfile from, so no lockfile can be stale." + reapplyNote,
292
+ []
293
+ );
294
+ process.exit(0);
295
+ }
296
+
297
+ reportRegen(await regenerateLockfiles(), reapplyNote);
298
+
299
+ process.exit(0);
300
+ } catch (error) {
301
+ report(
302
+ "failed",
303
+ "The reconciliation aborted before completing: " +
304
+ (error && error.message ? error.message : String(error)),
305
+ []
306
+ );
307
+ process.exit(0);
308
+ }
309
+ })();
310
+ `;
311
+ }
312
+ //# sourceMappingURL=postinstall-trampoline-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postinstall-trampoline-source.js","sourceRoot":"","sources":["../../src/utils/postinstall-trampoline-source.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,8 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { type LockfileRegenPlan, type PackageManager, LOCKFILE_REGEN_PLANS, detectPackageManagers, enginesForbiddenManagers, getLockfileRegenPlan } from "./package-manager-detect.js";
3
+ import type { TrampolineReporting } from "./postinstall-trampoline-source.js";
3
4
  export { LOCKFILE_REGEN_PLANS, detectPackageManagers, enginesForbiddenManagers, getLockfileRegenPlan, };
4
- export type { LockfileRegenPlan, PackageManager };
5
+ export type { LockfileRegenPlan, PackageManager, TrampolineReporting };
5
6
  /**
6
7
  * Determine whether this Lisa invocation is running as a package-manager lifecycle
7
8
  * script (postinstall, prepare, etc.). Works across npm, bun, yarn, and pnpm since
@@ -103,8 +104,9 @@ export declare function regenerateLockfilesInProcess(projectDir: string, spawnFn
103
104
  * @param projectDir - Absolute path to the project directory Lisa will reconcile
104
105
  * @param lisaDistDir - Absolute path to Lisa's dist directory (where index.js lives)
105
106
  * @param parentPid - PID of the package-manager process to wait on (usually process.ppid)
107
+ * @param reporting - Report path/schema plus the pre-apply package.json baseline the child compares against
106
108
  * @param spawnFn - Optional spawn implementation; defaults to node:child_process spawn. Tests pass a vi.fn() spy here as a dependency-injection seam, avoiding the unreliable vi.doMock-on-builtins pattern that breaks under v8 coverage in CI runners.
107
109
  * @returns Promise that resolves immediately after spawning the detached child
108
110
  */
109
- export declare function scheduleReconciliationChild(projectDir: string, lisaDistDir: string, parentPid: number, spawnFn?: typeof spawn): Promise<void>;
111
+ export declare function scheduleReconciliationChild(projectDir: string, lisaDistDir: string, parentPid: number, reporting: TrampolineReporting, spawnFn?: typeof spawn): Promise<void>;
110
112
  //# sourceMappingURL=postinstall-trampoline.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"postinstall-trampoline.d.ts","sourceRoot":"","sources":["../../src/utils/postinstall-trampoline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAU3C,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,oBAAoB,EACpB,qBAAqB,EACrB,wBAAwB,EACxB,oBAAoB,EACrB,MAAM,6BAA6B,CAAC;AAoCrC,OAAO,EACL,oBAAoB,EACpB,qBAAqB,EACrB,wBAAwB,EACxB,oBAAoB,GACrB,CAAC;AACF,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,CAAC;AAkBlD;;;;;GAKG;AACH,wBAAgB,0BAA0B,IAAI,OAAO,CAEpD;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,OAAO,CAE/C;AA0BD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,IAAI,OAAO,CASvC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,uCAAuC,CACrD,MAAM,EAAE,OAAO,GACd,OAAO,CAaT;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAIxD;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOxD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,4BAA4B,CAChD,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,OAAO,KAAa,GAC5B,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,2BAA2B,CAC/C,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EAMjB,OAAO,GAAE,OAAO,KAAa,GAC5B,OAAO,CAAC,IAAI,CAAC,CAmCf"}
1
+ {"version":3,"file":"postinstall-trampoline.d.ts","sourceRoot":"","sources":["../../src/utils/postinstall-trampoline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAM3C,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,oBAAoB,EACpB,qBAAqB,EACrB,wBAAwB,EACxB,oBAAoB,EACrB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;AAqC9E,OAAO,EACL,oBAAoB,EACpB,qBAAqB,EACrB,wBAAwB,EACxB,oBAAoB,GACrB,CAAC;AACF,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,mBAAmB,EAAE,CAAC;AAkBvE;;;;;GAKG;AACH,wBAAgB,0BAA0B,IAAI,OAAO,CAEpD;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,OAAO,CAE/C;AA0BD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,IAAI,OAAO,CASvC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,uCAAuC,CACrD,MAAM,EAAE,OAAO,GACd,OAAO,CAaT;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAIxD;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOxD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,4BAA4B,CAChD,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,OAAO,KAAa,GAC5B,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,2BAA2B,CAC/C,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,mBAAmB,EAM9B,OAAO,GAAE,OAAO,KAAa,GAC5B,OAAO,CAAC,IAAI,CAAC,CAoCf"}