@codyswann/lisa 3.34.0 → 3.35.1

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 (69) hide show
  1. package/all/copy-overwrite/scripts/lisa-lint-staged-preflight.mjs +399 -0
  2. package/dist/cli/doctor-traceability-gate.d.ts +39 -0
  3. package/dist/cli/doctor-traceability-gate.d.ts.map +1 -0
  4. package/dist/cli/doctor-traceability-gate.js +164 -0
  5. package/dist/cli/doctor-traceability-gate.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-owned-hash-ledger.d.ts.map +1 -1
  10. package/dist/core/lisa-owned-hash-ledger.js +3 -0
  11. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  12. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  13. package/dist/core/upstream-evidence-manifest.js +11 -3
  14. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  15. package/expo/create-only/.github/workflows/nightly-e2e-health.yml +1 -1
  16. package/expo/create-only/.github/workflows/nightly-e2e-report.yml +1 -1
  17. package/package.json +2 -1
  18. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  20. package/plugins/lisa-agy/plugin.json +1 -1
  21. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  23. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  24. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  30. package/plugins/lisa-expo-agy/plugin.json +1 -1
  31. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  35. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  36. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  40. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  41. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  45. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  46. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  50. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  51. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  55. package/plugins/lisa-rails-agy/plugin.json +1 -1
  56. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  60. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  61. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  65. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  66. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  68. package/scripts/check-template-workflow-refs.mjs +363 -0
  69. package/typescript/copy-contents/.husky/pre-commit +64 -0
@@ -0,0 +1,363 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Deterministic gate that every `uses:` in a shipped caller template resolves
4
+ * at the ref it names (issue #2702).
5
+ *
6
+ * Two expo templates shipped pinned at `@v2.345.1`, a Lisa version that
7
+ * predates the reusable they call. The referenced file does not exist at that
8
+ * tag, so the workflow cannot load: GitHub reports "this run likely failed
9
+ * because of a workflow file issue" and runs ZERO jobs.
10
+ *
11
+ * Nothing caught it, and the reason is the point of this gate:
12
+ *
13
+ * - a workflow that fails to LOAD produces no test output, so no suite goes
14
+ * red and no report shows a failure to read;
15
+ * - `nightly-e2e-report.yml` therefore stayed byte-identical to the broken
16
+ * template in every consumer that received it, and a filename/hash audit
17
+ * scored that as the HEALTHY row — identical-and-never-worked outranks
18
+ * diverged-and-working on every measure except whether it works;
19
+ * - it had never once succeeded: three scheduled runs across two repos,
20
+ * three failures, zero successes, from the day it shipped.
21
+ *
22
+ * The sibling defect was visible and so it was repaired four different ways.
23
+ * `nightly-e2e-health.yml` backs a required gate, so consumers noticed
24
+ * instantly: one repointed to `@v3.27.0`, one to `@v3.14.8`, and two abandoned
25
+ * the reusable and reimplemented it locally. None of those repairs flowed back
26
+ * upstream, so every new consumer inherited the same broken pin.
27
+ *
28
+ * This script reads what the templates actually reference and confirms the
29
+ * target exists. Only that.
30
+ *
31
+ * Determinism guarantees (so the unit test is reproducible and CI is stable):
32
+ * - zero third-party dependencies (Node built-ins only),
33
+ * - no network access,
34
+ * - no `Date` / `Math.random`,
35
+ * - the file list comes from `git ls-files`, so the gate sees exactly what a
36
+ * release would carry.
37
+ *
38
+ * A `@main` reference is checked against the working tree, which needs no
39
+ * history and so is correct in a depth-1 CI clone. A reference pinned to a tag
40
+ * or SHA needs that object locally; when it is missing the gate reports
41
+ * UNVERIFIABLE and exits 2. It never treats "I could not look" as "it is fine"
42
+ * — a gate that passes because it did nothing is the exact failure this file
43
+ * exists to prevent, and it would be perverse to reproduce it here.
44
+ *
45
+ * Discovering zero templates is likewise exit 2, not a clean pass. Finding
46
+ * nothing to check is a broken invocation, not conformance.
47
+ *
48
+ * CLI:
49
+ * node scripts/check-template-workflow-refs.mjs [--root <dir>] [--json]
50
+ *
51
+ * Exit codes (mirroring the sibling parity scripts):
52
+ * 0 — every `uses:` in every caller template resolves at its ref.
53
+ * 1 — ≥1 reference names a target that does not exist at that ref.
54
+ * 2 — operational/usage error: unknown flag, a flag missing its value,
55
+ * `--root` absent or not a git repository, git unavailable, zero
56
+ * templates discovered, or a pinned ref that is not present locally.
57
+ *
58
+ * @module scripts/check-template-workflow-refs
59
+ */
60
+ import { execFileSync } from "node:child_process";
61
+ import fs from "node:fs";
62
+ import path from "node:path";
63
+ import process from "node:process";
64
+ import { fileURLToPath } from "node:url";
65
+
66
+ import { invokedAsScript } from "./lib/invoked-as-script.mjs";
67
+
68
+ const REPO_ROOT = path.resolve(
69
+ path.dirname(fileURLToPath(import.meta.url)),
70
+ ".."
71
+ );
72
+
73
+ /**
74
+ * A caller template: `<lane>/<mode>/.github/workflows/<name>.yml`, e.g.
75
+ * `expo/create-only/.github/workflows/maestro-e2e.yml`. The two leading
76
+ * segments are what distinguishes a shipped template from Lisa's OWN
77
+ * `.github/workflows/`, which is not delivered to anyone.
78
+ */
79
+ const TEMPLATE_RE = /^[^/]+\/[^/]+\/\.github\/workflows\/[^/]+\.ya?ml$/;
80
+
81
+ /** `uses: CodySwannGT/lisa/<path>@<ref>` — the reference this gate verifies. */
82
+ const USES_RE = /uses:\s*CodySwannGT\/lisa\/([^\s@]+)@([^\s"'#]+)/g;
83
+
84
+ /** The moving ref, resolvable against the working tree with no history. */
85
+ const MOVING_REF = "main";
86
+
87
+ /** Max bytes of `git ls-files` output (7k+ tracked paths is ~0.3 MB today). */
88
+ const MAX_GIT_OUTPUT_BYTES = 64 * 1024 * 1024;
89
+
90
+ /**
91
+ * Usage error — thrown for an invalid invocation or an unverifiable state so
92
+ * `main` can distinguish it (exit 2) from a finding (exit 1).
93
+ */
94
+ export class UsageError extends Error {}
95
+
96
+ /**
97
+ * Extract every Lisa workflow reference in `content`.
98
+ *
99
+ * @param {string} content - full text of a template.
100
+ * @returns {{ target: string, ref: string, line: number }[]} one entry per
101
+ * reference, with 1-based line numbers, in file order.
102
+ */
103
+ export function findWorkflowRefs(content) {
104
+ const lines = String(content).split(/\r?\n/);
105
+ const refs = [];
106
+ for (let index = 0; index < lines.length; index++) {
107
+ // A fresh lastIndex per line: the regex is global and shared.
108
+ USES_RE.lastIndex = 0;
109
+ let match;
110
+ while ((match = USES_RE.exec(lines[index])) !== null) {
111
+ refs.push({ line: index + 1, ref: match[2], target: match[1] });
112
+ }
113
+ }
114
+ return refs;
115
+ }
116
+
117
+ /**
118
+ * List every tracked file in `root`, relative to it. Throws `UsageError` when
119
+ * git is unavailable or `root` is not a repository.
120
+ *
121
+ * @param {string} root - the repository root.
122
+ * @returns {string[]} tracked paths, relative to `root`.
123
+ */
124
+ function listTrackedFiles(root) {
125
+ let stdout;
126
+ try {
127
+ stdout = execFileSync("git", ["-C", root, "ls-files", "-z"], {
128
+ encoding: "utf8",
129
+ maxBuffer: MAX_GIT_OUTPUT_BYTES,
130
+ stdio: ["ignore", "pipe", "ignore"],
131
+ });
132
+ } catch (error) {
133
+ throw new UsageError(
134
+ `could not list tracked files in ${root}: ${error.message}`
135
+ );
136
+ }
137
+ return stdout.split("\0").filter(entry => entry !== "");
138
+ }
139
+
140
+ /**
141
+ * Whether `ref` names an object present in this clone.
142
+ *
143
+ * @param {string} root - the repository root.
144
+ * @param {string} ref - a tag, branch, or SHA.
145
+ * @returns {boolean} whether the ref resolves locally.
146
+ */
147
+ function refExists(root, ref) {
148
+ try {
149
+ execFileSync(
150
+ "git",
151
+ ["-C", root, "rev-parse", "--verify", `${ref}^{commit}`],
152
+ {
153
+ stdio: ["ignore", "ignore", "ignore"],
154
+ }
155
+ );
156
+ return true;
157
+ } catch {
158
+ return false;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Whether `target` exists in the tree named by `ref`.
164
+ *
165
+ * @param {string} root - the repository root.
166
+ * @param {string} ref - a tag, branch, or SHA known to resolve.
167
+ * @param {string} target - a repo-relative path.
168
+ * @returns {boolean} whether the path exists at that ref.
169
+ */
170
+ function pathExistsAtRef(root, ref, target) {
171
+ try {
172
+ execFileSync("git", ["-C", root, "cat-file", "-e", `${ref}:${target}`], {
173
+ stdio: ["ignore", "ignore", "ignore"],
174
+ });
175
+ return true;
176
+ } catch {
177
+ return false;
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Resolve one reference to a verdict.
183
+ *
184
+ * `@main` is checked against the working tree so the gate is correct in a
185
+ * depth-1 clone, where `main` frequently is not a local ref at all. A pinned
186
+ * ref that is absent locally yields `unverifiable`, never `ok`.
187
+ *
188
+ * @param {string} root - the repository root.
189
+ * @param {{ target: string, ref: string }} reference - the parsed reference.
190
+ * @returns {"ok" | "missing" | "unverifiable"} the verdict.
191
+ */
192
+ export function classifyRef(root, reference) {
193
+ const { ref, target } = reference;
194
+ if (ref === MOVING_REF) {
195
+ return fs.existsSync(path.join(root, target)) ? "ok" : "missing";
196
+ }
197
+ if (!refExists(root, ref)) {
198
+ return "unverifiable";
199
+ }
200
+ return pathExistsAtRef(root, ref, target) ? "ok" : "missing";
201
+ }
202
+
203
+ /**
204
+ * Assemble the machine-readable report.
205
+ *
206
+ * @param {ReadonlyArray<Record<string, unknown>>} results - non-ok references.
207
+ * @param {{ root: string, templates: number, checked: number }} opts - resolved
208
+ * options plus scan size.
209
+ * @returns {Record<string, unknown>} the report object.
210
+ */
211
+ export function buildReport(results, opts) {
212
+ const missing = results.filter(result => result.verdict === "missing");
213
+ const unverifiable = results.filter(
214
+ result => result.verdict === "unverifiable"
215
+ );
216
+ return {
217
+ results,
218
+ root: opts.root,
219
+ schemaVersion: 1,
220
+ summary: {
221
+ checked: opts.checked,
222
+ missing: missing.length,
223
+ ok: opts.checked - results.length,
224
+ templates: opts.templates,
225
+ unverifiable: unverifiable.length,
226
+ },
227
+ };
228
+ }
229
+
230
+ /**
231
+ * Render the human-readable report.
232
+ *
233
+ * @param {{ results: ReadonlyArray<Record<string, unknown>>, summary: Record<string, number> }} report
234
+ * the report object.
235
+ * @returns {string} the rendered report.
236
+ */
237
+ function humanReport(report) {
238
+ const { summary } = report;
239
+ if (summary.missing === 0 && summary.unverifiable === 0) {
240
+ return `✓ ${summary.checked} workflow reference(s) across ${summary.templates} caller template(s) all resolve`;
241
+ }
242
+ const lines = report.results.map(result =>
243
+ result.verdict === "missing"
244
+ ? `✗ ${result.file}:${result.line}\n ${result.target}@${result.ref} — target does not exist at that ref`
245
+ : `? ${result.file}:${result.line}\n ${result.target}@${result.ref} — ref not present in this clone, cannot verify`
246
+ );
247
+ const tail = [];
248
+ if (summary.missing > 0) {
249
+ tail.push(
250
+ `${summary.missing} reference(s) name a target that does not exist at the pinned ref.`,
251
+ "A consumer receiving this template gets a workflow that cannot load:",
252
+ "GitHub runs zero jobs and reports a workflow file issue, which produces",
253
+ "no test output and so reads as silence rather than as failure."
254
+ );
255
+ }
256
+ if (summary.unverifiable > 0) {
257
+ tail.push(
258
+ `${summary.unverifiable} reference(s) could not be verified because the ref`,
259
+ "is absent from this clone. Fetch tags and history, then re-run. This is",
260
+ "reported rather than passed: a gate that cannot look must not claim it saw."
261
+ );
262
+ }
263
+ return [...lines, "", ...tail].join("\n");
264
+ }
265
+
266
+ /**
267
+ * Parse argv into resolved options. Throws `UsageError` on a bad invocation.
268
+ *
269
+ * @param {readonly string[]} argv - arguments (without node/script prefix).
270
+ * @returns {{ root: string, json: boolean }} options.
271
+ */
272
+ export function parseArgs(argv) {
273
+ let root = null;
274
+ let json = false;
275
+ for (let i = 0; i < argv.length; i++) {
276
+ const arg = argv[i];
277
+ if (arg === "--json") {
278
+ json = true;
279
+ } else if (arg === "--root") {
280
+ const next = argv[i + 1];
281
+ if (next === undefined || next.startsWith("--")) {
282
+ throw new UsageError("--root requires a value");
283
+ }
284
+ root = next;
285
+ i += 1;
286
+ } else {
287
+ throw new UsageError(`unknown argument: ${arg}`);
288
+ }
289
+ }
290
+ return { json, root: path.resolve(root ?? REPO_ROOT) };
291
+ }
292
+
293
+ /**
294
+ * Run the gate. Returns the process exit code (does not call `exit`).
295
+ *
296
+ * @param {readonly string[]} argv - arguments (without node/script prefix).
297
+ * @param {{ stdout?: { write(s: string): void }, stderr?: { write(s: string): void } }} [io]
298
+ * injectable streams (defaults to process streams).
299
+ * @returns {number} exit code (0 clean, 1 finding, 2 usage/unverifiable).
300
+ */
301
+ export function main(argv, io = {}) {
302
+ const out = io.stdout ?? process.stdout;
303
+ const err = io.stderr ?? process.stderr;
304
+ let opts;
305
+ let templates;
306
+ try {
307
+ opts = parseArgs(argv);
308
+ if (!fs.existsSync(opts.root) || !fs.statSync(opts.root).isDirectory()) {
309
+ throw new UsageError(`--root is not a directory: ${opts.root}`);
310
+ }
311
+ templates = listTrackedFiles(opts.root).filter(file =>
312
+ TEMPLATE_RE.test(file)
313
+ );
314
+ if (templates.length === 0) {
315
+ // Finding nothing to check is a broken invocation, not conformance.
316
+ throw new UsageError(
317
+ `no caller templates found under ${opts.root} — expected paths like ` +
318
+ "expo/create-only/.github/workflows/*.yml. Refusing to report a " +
319
+ "clean run for a scan that examined nothing."
320
+ );
321
+ }
322
+ } catch (error) {
323
+ err.write(`error: ${error.message}\n`);
324
+ return 2;
325
+ }
326
+
327
+ let checked = 0;
328
+ const results = [];
329
+ try {
330
+ for (const file of templates) {
331
+ const content = fs.readFileSync(path.join(opts.root, file), "utf8");
332
+ for (const reference of findWorkflowRefs(content)) {
333
+ checked += 1;
334
+ const verdict = classifyRef(opts.root, reference);
335
+ if (verdict !== "ok") {
336
+ results.push({ ...reference, file, verdict });
337
+ }
338
+ }
339
+ }
340
+ } catch (error) {
341
+ err.write(`error: failed to scan caller templates: ${error.message}\n`);
342
+ return 2;
343
+ }
344
+
345
+ const report = buildReport(results, {
346
+ checked,
347
+ root: opts.root,
348
+ templates: templates.length,
349
+ });
350
+ out.write(
351
+ `${opts.json ? JSON.stringify(report, null, 2) : humanReport(report)}\n`
352
+ );
353
+ if (report.summary.unverifiable > 0) {
354
+ return 2;
355
+ }
356
+ return report.summary.missing === 0 ? 0 : 1;
357
+ }
358
+
359
+ if (invokedAsScript(import.meta.url)) {
360
+ // exitCode (not process.exit): when stdout is a pipe, writes are async and
361
+ // process.exit() truncates the report mid-flush.
362
+ process.exitCode = main(process.argv.slice(2));
363
+ }
@@ -43,6 +43,70 @@ else
43
43
  fi
44
44
  fi
45
45
 
46
+ # ---------------------------------------------------------------------------
47
+ # lint-staged preflight.
48
+ #
49
+ # lint-staged reports exit 0 when a task's executable cannot be SPAWNED.
50
+ # Measured on lint-staged 16.4.0 and 17.3.0: a file that is present and
51
+ # `chmod +x` but is not a runnable executable prints "[FAILED] spawn ENOEXEC"
52
+ # and the run still exits 0. An absent executable (ENOENT) and an ordinary
53
+ # non-zero exit both fail closed — only the spawn error does not.
54
+ #
55
+ # That state is not exotic. A tool whose real binary is materialized during
56
+ # `postinstall` leaves a shebang-less placeholder behind on any install that
57
+ # skips scripts, and lint-staged spawns without a shell, so the kernel refuses
58
+ # the file before the placeholder's own `exit 1` can run. The staged-file scan
59
+ # then runs, reports nothing, blocks nothing, and the commit proceeds.
60
+ #
61
+ # No exit-code check can catch this — not the one below, not any other. A
62
+ # process reporting 0 for work it did not do defeats every test of its status.
63
+ # So the tools are proved runnable BEFORE any of them is handed the work, by
64
+ # something that spawns them itself and reports what it spawned.
65
+ #
66
+ # OUTSIDE the gate-registry handover, deliberately. lint-staged runs at this
67
+ # moment by BOTH routes: the built-in step further down, and a `code-style`
68
+ # gate whose declared command is the project's lint-staged script. A preflight
69
+ # placed inside the built-in branch would stand down for exactly the projects
70
+ # whose registry then runs lint-staged anyway. The tools are the same either
71
+ # way, so the proof belongs before both.
72
+ #
73
+ # Skipped only when the project has no lint-staged config, in which case no
74
+ # route runs lint-staged and there is nothing to prove. A preflight that cannot
75
+ # RUN is not a skip and not a pass — it blocks.
76
+ # ---------------------------------------------------------------------------
77
+ if [ -f ".lintstagedrc.json" ]; then
78
+ LINT_STAGED_PREFLIGHT="node_modules/@codyswann/lisa/all/copy-overwrite/scripts/lisa-lint-staged-preflight.mjs"
79
+ if [ ! -f "$LINT_STAGED_PREFLIGHT" ]; then
80
+ LINT_STAGED_PREFLIGHT="scripts/lisa-lint-staged-preflight.mjs"
81
+ fi
82
+ if [ ! -f "$LINT_STAGED_PREFLIGHT" ]; then
83
+ LINT_STAGED_PREFLIGHT="all/copy-overwrite/scripts/lisa-lint-staged-preflight.mjs"
84
+ fi
85
+ if command -v node >/dev/null 2>&1; then
86
+ LINT_STAGED_PREFLIGHT_RUNTIME="node"
87
+ elif command -v bun >/dev/null 2>&1; then
88
+ LINT_STAGED_PREFLIGHT_RUNTIME="bun"
89
+ else
90
+ LINT_STAGED_PREFLIGHT_RUNTIME=""
91
+ fi
92
+ if [ ! -f "$LINT_STAGED_PREFLIGHT" ] || [ -z "$LINT_STAGED_PREFLIGHT_RUNTIME" ]; then
93
+ echo ""
94
+ echo "❌ Commit blocked: the lint-staged preflight could not run."
95
+ echo " Expected $LINT_STAGED_PREFLIGHT, and node or bun on PATH."
96
+ echo " Without it a tool that is installed but not executable makes"
97
+ echo " lint-staged exit 0 having scanned nothing, so skipping the"
98
+ echo " preflight cannot be treated as a pass."
99
+ echo " Reinstall dependencies, then re-run \`lisa apply\`."
100
+ echo ""
101
+ exit 1
102
+ fi
103
+ "$LINT_STAGED_PREFLIGHT_RUNTIME" "$LINT_STAGED_PREFLIGHT" --config .lintstagedrc.json
104
+ LINT_STAGED_PREFLIGHT_STATUS=$?
105
+ if [ $LINT_STAGED_PREFLIGHT_STATUS -ne 0 ]; then
106
+ exit $LINT_STAGED_PREFLIGHT_STATUS
107
+ fi
108
+ fi
109
+
46
110
  # ---------------------------------------------------------------------------
47
111
  # Gate-driven checks.
48
112
  #