@codyswann/lisa 2.106.3 → 2.106.4
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/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/scripts/plugin-sync-explain.mjs +186 -5
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/src/base/scripts/plugin-sync-explain.mjs +186 -5
package/package.json
CHANGED
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"lodash": ">=4.18.1"
|
|
83
83
|
},
|
|
84
84
|
"name": "@codyswann/lisa",
|
|
85
|
-
"version": "2.106.
|
|
85
|
+
"version": "2.106.4",
|
|
86
86
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
87
87
|
"main": "dist/index.js",
|
|
88
88
|
"exports": {
|
|
@@ -7,7 +7,16 @@
|
|
|
7
7
|
* diagnostic explains likely causes before an operator mutates the tree.
|
|
8
8
|
*/
|
|
9
9
|
import { execFileSync } from "node:child_process";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
cpSync,
|
|
12
|
+
existsSync,
|
|
13
|
+
mkdtempSync,
|
|
14
|
+
readdirSync,
|
|
15
|
+
readFileSync,
|
|
16
|
+
rmSync,
|
|
17
|
+
statSync,
|
|
18
|
+
} from "node:fs";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
11
20
|
import path from "node:path";
|
|
12
21
|
import process from "node:process";
|
|
13
22
|
|
|
@@ -29,6 +38,7 @@ const GIT_BIN = "/usr/bin/git";
|
|
|
29
38
|
* readonly classification: string
|
|
30
39
|
* readonly path: string
|
|
31
40
|
* readonly counterpart?: string
|
|
41
|
+
* readonly likelyCause: string
|
|
32
42
|
* readonly nextAction: string
|
|
33
43
|
* }} PluginSyncFinding
|
|
34
44
|
*
|
|
@@ -50,8 +60,13 @@ export function explainPluginSync(root = process.cwd()) {
|
|
|
50
60
|
const repoRoot = path.resolve(root);
|
|
51
61
|
const statusBefore = gitStatus(repoRoot);
|
|
52
62
|
const statusEntries = parseGitStatus(statusBefore);
|
|
63
|
+
const expectedGeneratedFiles = buildExpectedGeneratedFiles(repoRoot);
|
|
53
64
|
const findings = [
|
|
54
|
-
...classifySourceGeneratedDrift(
|
|
65
|
+
...classifySourceGeneratedDrift(
|
|
66
|
+
repoRoot,
|
|
67
|
+
statusEntries,
|
|
68
|
+
expectedGeneratedFiles
|
|
69
|
+
),
|
|
55
70
|
...classifyMarketplaceDrift(repoRoot),
|
|
56
71
|
];
|
|
57
72
|
const statusAfter = gitStatus(repoRoot);
|
|
@@ -97,6 +112,7 @@ export function renderPluginSyncReport(report) {
|
|
|
97
112
|
lines.push(
|
|
98
113
|
`- ${finding.classification}: ${finding.path}`,
|
|
99
114
|
` Evidence: ${finding.counterpart ? `${finding.path} -> ${finding.counterpart}` : finding.path}`,
|
|
115
|
+
` Likely cause: ${finding.likelyCause}`,
|
|
100
116
|
` Next action: ${finding.nextAction}`
|
|
101
117
|
);
|
|
102
118
|
}
|
|
@@ -128,9 +144,10 @@ function highestClassification(findings) {
|
|
|
128
144
|
|
|
129
145
|
/**
|
|
130
146
|
* @param {readonly { readonly code: string, readonly file: string }[]} entries
|
|
147
|
+
* @param {ReadonlyMap<string, Buffer> | undefined} expectedGeneratedFiles
|
|
131
148
|
* @returns {PluginSyncFinding[]}
|
|
132
149
|
*/
|
|
133
|
-
function classifySourceGeneratedDrift(entries) {
|
|
150
|
+
function classifySourceGeneratedDrift(root, entries, expectedGeneratedFiles) {
|
|
134
151
|
const changed = new Set(entries.map(entry => entry.file));
|
|
135
152
|
const findings = [];
|
|
136
153
|
|
|
@@ -138,10 +155,24 @@ function classifySourceGeneratedDrift(entries) {
|
|
|
138
155
|
const sourceCounterpart = sourceToBuilt(entry.file);
|
|
139
156
|
if (sourceCounterpart) {
|
|
140
157
|
const generatedChanged = changed.has(sourceCounterpart);
|
|
158
|
+
if (expectedGeneratedFiles) {
|
|
159
|
+
const expected = expectedGeneratedFiles.get(sourceCounterpart);
|
|
160
|
+
const active = readOptionalFile(root, sourceCounterpart);
|
|
161
|
+
const generatedMatchesSource =
|
|
162
|
+
expected !== undefined &&
|
|
163
|
+
active !== undefined &&
|
|
164
|
+
expected.equals(active);
|
|
165
|
+
if (generatedMatchesSource) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
141
169
|
findings.push({
|
|
142
170
|
classification: generatedChanged ? "OUT_OF_SYNC" : "SOURCE_NOT_BUILT",
|
|
143
171
|
path: entry.file,
|
|
144
172
|
counterpart: sourceCounterpart,
|
|
173
|
+
likelyCause: generatedChanged
|
|
174
|
+
? "Both source and generated artifacts changed, but the generated artifact does not match a fresh plugin build."
|
|
175
|
+
: "A source plugin file changed, but its generated artifact was not rebuilt from plugins/src.",
|
|
145
176
|
nextAction: generatedChanged
|
|
146
177
|
? "Review both source and generated changes, keep source authoritative, then run `bun run build:plugins && bun run check:plugins`."
|
|
147
178
|
: "Run `bun run build:plugins`, then `bun run check:plugins`, and commit source plus regenerated artifacts.",
|
|
@@ -151,10 +182,23 @@ function classifySourceGeneratedDrift(entries) {
|
|
|
151
182
|
|
|
152
183
|
const builtCounterpart = builtToSource(entry.file);
|
|
153
184
|
if (builtCounterpart && !changed.has(builtCounterpart)) {
|
|
185
|
+
if (expectedGeneratedFiles) {
|
|
186
|
+
const expected = expectedGeneratedFiles.get(entry.file);
|
|
187
|
+
const active = readOptionalFile(root, entry.file);
|
|
188
|
+
if (
|
|
189
|
+
expected !== undefined &&
|
|
190
|
+
active !== undefined &&
|
|
191
|
+
expected.equals(active)
|
|
192
|
+
) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
154
196
|
findings.push({
|
|
155
197
|
classification: "GENERATED_ONLY",
|
|
156
198
|
path: entry.file,
|
|
157
199
|
counterpart: builtCounterpart,
|
|
200
|
+
likelyCause:
|
|
201
|
+
"A generated plugin artifact changed without the matching plugins/src source change.",
|
|
158
202
|
nextAction:
|
|
159
203
|
"Move the edit to the matching plugins/src path, rebuild with `bun run build:plugins`, then run `bun run check:plugins`.",
|
|
160
204
|
});
|
|
@@ -198,6 +242,8 @@ function classifyMarketplaceDrift(root) {
|
|
|
198
242
|
classification: "MARKETPLACE_REGISTRATION_DRIFT",
|
|
199
243
|
path: source.replace(/^\.\//, ""),
|
|
200
244
|
counterpart: MARKETPLACE,
|
|
245
|
+
likelyCause:
|
|
246
|
+
"A built plugin directory exists but is not advertised by the marketplace manifest.",
|
|
201
247
|
nextAction: `Add marketplace source "${source}" to \`${MARKETPLACE}\`, then run \`bun run check:plugins\`.`,
|
|
202
248
|
});
|
|
203
249
|
}
|
|
@@ -209,6 +255,8 @@ function classifyMarketplaceDrift(root) {
|
|
|
209
255
|
classification: "MARKETPLACE_REGISTRATION_DRIFT",
|
|
210
256
|
path: MARKETPLACE,
|
|
211
257
|
counterpart: source.replace(/^\.\//, ""),
|
|
258
|
+
likelyCause:
|
|
259
|
+
"The marketplace manifest points at a built plugin directory that is missing.",
|
|
212
260
|
nextAction:
|
|
213
261
|
"Either restore the built plugin directory or remove the stale marketplace source, then run `bun run check:plugins`.",
|
|
214
262
|
});
|
|
@@ -218,6 +266,113 @@ function classifyMarketplaceDrift(root) {
|
|
|
218
266
|
return findings;
|
|
219
267
|
}
|
|
220
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Build plugins in a disposable copy and return the generated bytes that source
|
|
271
|
+
* changes should produce. If a minimal fixture cannot run the build script,
|
|
272
|
+
* callers fall back to status-based classification without mutating the repo.
|
|
273
|
+
* @param {string} root
|
|
274
|
+
* @returns {ReadonlyMap<string, Buffer> | undefined}
|
|
275
|
+
*/
|
|
276
|
+
function buildExpectedGeneratedFiles(root) {
|
|
277
|
+
if (!existsSync(path.join(root, "scripts/build-plugins.sh"))) {
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const tempRoot = mkdtempSync(path.join(tmpdir(), "lisa-plugin-sync-"));
|
|
282
|
+
const scratchRoot = path.join(tempRoot, "repo");
|
|
283
|
+
try {
|
|
284
|
+
cpSync(root, scratchRoot, {
|
|
285
|
+
recursive: true,
|
|
286
|
+
filter: src => shouldCopyToScratch(root, src),
|
|
287
|
+
});
|
|
288
|
+
execFileSync("bash", ["scripts/build-plugins.sh"], {
|
|
289
|
+
cwd: scratchRoot,
|
|
290
|
+
encoding: "utf8",
|
|
291
|
+
env: gitEnv(),
|
|
292
|
+
stdio: "ignore",
|
|
293
|
+
});
|
|
294
|
+
return collectGeneratedFiles(scratchRoot);
|
|
295
|
+
} catch {
|
|
296
|
+
return undefined;
|
|
297
|
+
} finally {
|
|
298
|
+
rmSync(tempRoot, { force: true, recursive: true });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* @param {string} root
|
|
304
|
+
* @param {string} src
|
|
305
|
+
* @returns {boolean}
|
|
306
|
+
*/
|
|
307
|
+
function shouldCopyToScratch(root, src) {
|
|
308
|
+
const rel = path.relative(root, src).split(path.sep).join("/");
|
|
309
|
+
if (rel === "") {
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
return !(
|
|
313
|
+
rel === ".git" ||
|
|
314
|
+
rel.startsWith(".git/") ||
|
|
315
|
+
rel === "node_modules" ||
|
|
316
|
+
rel.startsWith("node_modules/") ||
|
|
317
|
+
rel === "dist" ||
|
|
318
|
+
rel.startsWith("dist/") ||
|
|
319
|
+
rel === "coverage" ||
|
|
320
|
+
rel.startsWith("coverage/")
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* @param {string} root
|
|
326
|
+
* @returns {Map<string, Buffer>}
|
|
327
|
+
*/
|
|
328
|
+
function collectGeneratedFiles(root) {
|
|
329
|
+
const files = new Map();
|
|
330
|
+
const pluginsRoot = path.join(root, PLUGINS_DIR);
|
|
331
|
+
if (!existsSync(pluginsRoot)) {
|
|
332
|
+
return files;
|
|
333
|
+
}
|
|
334
|
+
for (const name of readdirSync(pluginsRoot)) {
|
|
335
|
+
if (name === "src" || !name.startsWith("lisa")) {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
collectFiles(root, path.join(pluginsRoot, name), files);
|
|
339
|
+
}
|
|
340
|
+
return files;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* @param {string} root
|
|
345
|
+
* @param {string} current
|
|
346
|
+
* @param {Map<string, Buffer>} files
|
|
347
|
+
*/
|
|
348
|
+
function collectFiles(root, current, files) {
|
|
349
|
+
const stats = statSync(current);
|
|
350
|
+
if (stats.isDirectory()) {
|
|
351
|
+
for (const child of readdirSync(current)) {
|
|
352
|
+
collectFiles(root, path.join(current, child), files);
|
|
353
|
+
}
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (stats.isFile()) {
|
|
357
|
+
files.set(
|
|
358
|
+
path.relative(root, current).split(path.sep).join("/"),
|
|
359
|
+
readFileSync(current)
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* @param {string} root
|
|
366
|
+
* @param {string} rel
|
|
367
|
+
* @returns {Buffer | undefined}
|
|
368
|
+
*/
|
|
369
|
+
function readOptionalFile(root, rel) {
|
|
370
|
+
const abs = path.join(root, rel);
|
|
371
|
+
return existsSync(abs) && statSync(abs).isFile()
|
|
372
|
+
? readFileSync(abs)
|
|
373
|
+
: undefined;
|
|
374
|
+
}
|
|
375
|
+
|
|
221
376
|
/**
|
|
222
377
|
* @param {string} root
|
|
223
378
|
* @returns {string}
|
|
@@ -229,7 +384,7 @@ function gitStatus(root) {
|
|
|
229
384
|
{
|
|
230
385
|
cwd: root,
|
|
231
386
|
encoding: "utf8",
|
|
232
|
-
env: gitEnv(),
|
|
387
|
+
env: gitEnv(root),
|
|
233
388
|
}
|
|
234
389
|
);
|
|
235
390
|
}
|
|
@@ -308,15 +463,41 @@ function dedupeFindings(findings) {
|
|
|
308
463
|
|
|
309
464
|
/**
|
|
310
465
|
* Remove parent-hook Git environment so diagnostics inspect the requested repo.
|
|
466
|
+
* Bare common-dir worktrees in Codex need explicit GIT_DIR/GIT_WORK_TREE.
|
|
467
|
+
* @param {string} [root] Repository path the nested git command should inspect.
|
|
311
468
|
* @returns {NodeJS.ProcessEnv} Process environment for nested git commands.
|
|
312
469
|
*/
|
|
313
|
-
function gitEnv() {
|
|
470
|
+
function gitEnv(root) {
|
|
314
471
|
const env = { ...process.env };
|
|
315
472
|
delete env.GIT_DIR;
|
|
316
473
|
delete env.GIT_WORK_TREE;
|
|
474
|
+
if (root) {
|
|
475
|
+
const gitDir = linkedWorktreeGitDir(root);
|
|
476
|
+
if (gitDir) {
|
|
477
|
+
env.GIT_DIR = gitDir;
|
|
478
|
+
env.GIT_WORK_TREE = root;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
317
481
|
return env;
|
|
318
482
|
}
|
|
319
483
|
|
|
484
|
+
/**
|
|
485
|
+
* @param {string} root
|
|
486
|
+
* @returns {string | undefined}
|
|
487
|
+
*/
|
|
488
|
+
function linkedWorktreeGitDir(root) {
|
|
489
|
+
const dotGit = path.join(root, ".git");
|
|
490
|
+
if (!existsSync(dotGit) || statSync(dotGit).isDirectory()) {
|
|
491
|
+
return undefined;
|
|
492
|
+
}
|
|
493
|
+
const match = /^gitdir:\s*(.+)$/m.exec(readFileSync(dotGit, "utf8"));
|
|
494
|
+
if (!match) {
|
|
495
|
+
return undefined;
|
|
496
|
+
}
|
|
497
|
+
const gitDir = match[1].trim();
|
|
498
|
+
return path.isAbsolute(gitDir) ? gitDir : path.resolve(root, gitDir);
|
|
499
|
+
}
|
|
500
|
+
|
|
320
501
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
321
502
|
try {
|
|
322
503
|
const report = explainPluginSync(process.argv[2] ?? process.cwd());
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "2.106.
|
|
3
|
+
"version": "2.106.4",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "2.106.
|
|
3
|
+
"version": "2.106.4",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -7,7 +7,16 @@
|
|
|
7
7
|
* diagnostic explains likely causes before an operator mutates the tree.
|
|
8
8
|
*/
|
|
9
9
|
import { execFileSync } from "node:child_process";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
cpSync,
|
|
12
|
+
existsSync,
|
|
13
|
+
mkdtempSync,
|
|
14
|
+
readdirSync,
|
|
15
|
+
readFileSync,
|
|
16
|
+
rmSync,
|
|
17
|
+
statSync,
|
|
18
|
+
} from "node:fs";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
11
20
|
import path from "node:path";
|
|
12
21
|
import process from "node:process";
|
|
13
22
|
|
|
@@ -29,6 +38,7 @@ const GIT_BIN = "/usr/bin/git";
|
|
|
29
38
|
* readonly classification: string
|
|
30
39
|
* readonly path: string
|
|
31
40
|
* readonly counterpart?: string
|
|
41
|
+
* readonly likelyCause: string
|
|
32
42
|
* readonly nextAction: string
|
|
33
43
|
* }} PluginSyncFinding
|
|
34
44
|
*
|
|
@@ -50,8 +60,13 @@ export function explainPluginSync(root = process.cwd()) {
|
|
|
50
60
|
const repoRoot = path.resolve(root);
|
|
51
61
|
const statusBefore = gitStatus(repoRoot);
|
|
52
62
|
const statusEntries = parseGitStatus(statusBefore);
|
|
63
|
+
const expectedGeneratedFiles = buildExpectedGeneratedFiles(repoRoot);
|
|
53
64
|
const findings = [
|
|
54
|
-
...classifySourceGeneratedDrift(
|
|
65
|
+
...classifySourceGeneratedDrift(
|
|
66
|
+
repoRoot,
|
|
67
|
+
statusEntries,
|
|
68
|
+
expectedGeneratedFiles
|
|
69
|
+
),
|
|
55
70
|
...classifyMarketplaceDrift(repoRoot),
|
|
56
71
|
];
|
|
57
72
|
const statusAfter = gitStatus(repoRoot);
|
|
@@ -97,6 +112,7 @@ export function renderPluginSyncReport(report) {
|
|
|
97
112
|
lines.push(
|
|
98
113
|
`- ${finding.classification}: ${finding.path}`,
|
|
99
114
|
` Evidence: ${finding.counterpart ? `${finding.path} -> ${finding.counterpart}` : finding.path}`,
|
|
115
|
+
` Likely cause: ${finding.likelyCause}`,
|
|
100
116
|
` Next action: ${finding.nextAction}`
|
|
101
117
|
);
|
|
102
118
|
}
|
|
@@ -128,9 +144,10 @@ function highestClassification(findings) {
|
|
|
128
144
|
|
|
129
145
|
/**
|
|
130
146
|
* @param {readonly { readonly code: string, readonly file: string }[]} entries
|
|
147
|
+
* @param {ReadonlyMap<string, Buffer> | undefined} expectedGeneratedFiles
|
|
131
148
|
* @returns {PluginSyncFinding[]}
|
|
132
149
|
*/
|
|
133
|
-
function classifySourceGeneratedDrift(entries) {
|
|
150
|
+
function classifySourceGeneratedDrift(root, entries, expectedGeneratedFiles) {
|
|
134
151
|
const changed = new Set(entries.map(entry => entry.file));
|
|
135
152
|
const findings = [];
|
|
136
153
|
|
|
@@ -138,10 +155,24 @@ function classifySourceGeneratedDrift(entries) {
|
|
|
138
155
|
const sourceCounterpart = sourceToBuilt(entry.file);
|
|
139
156
|
if (sourceCounterpart) {
|
|
140
157
|
const generatedChanged = changed.has(sourceCounterpart);
|
|
158
|
+
if (expectedGeneratedFiles) {
|
|
159
|
+
const expected = expectedGeneratedFiles.get(sourceCounterpart);
|
|
160
|
+
const active = readOptionalFile(root, sourceCounterpart);
|
|
161
|
+
const generatedMatchesSource =
|
|
162
|
+
expected !== undefined &&
|
|
163
|
+
active !== undefined &&
|
|
164
|
+
expected.equals(active);
|
|
165
|
+
if (generatedMatchesSource) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
141
169
|
findings.push({
|
|
142
170
|
classification: generatedChanged ? "OUT_OF_SYNC" : "SOURCE_NOT_BUILT",
|
|
143
171
|
path: entry.file,
|
|
144
172
|
counterpart: sourceCounterpart,
|
|
173
|
+
likelyCause: generatedChanged
|
|
174
|
+
? "Both source and generated artifacts changed, but the generated artifact does not match a fresh plugin build."
|
|
175
|
+
: "A source plugin file changed, but its generated artifact was not rebuilt from plugins/src.",
|
|
145
176
|
nextAction: generatedChanged
|
|
146
177
|
? "Review both source and generated changes, keep source authoritative, then run `bun run build:plugins && bun run check:plugins`."
|
|
147
178
|
: "Run `bun run build:plugins`, then `bun run check:plugins`, and commit source plus regenerated artifacts.",
|
|
@@ -151,10 +182,23 @@ function classifySourceGeneratedDrift(entries) {
|
|
|
151
182
|
|
|
152
183
|
const builtCounterpart = builtToSource(entry.file);
|
|
153
184
|
if (builtCounterpart && !changed.has(builtCounterpart)) {
|
|
185
|
+
if (expectedGeneratedFiles) {
|
|
186
|
+
const expected = expectedGeneratedFiles.get(entry.file);
|
|
187
|
+
const active = readOptionalFile(root, entry.file);
|
|
188
|
+
if (
|
|
189
|
+
expected !== undefined &&
|
|
190
|
+
active !== undefined &&
|
|
191
|
+
expected.equals(active)
|
|
192
|
+
) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
154
196
|
findings.push({
|
|
155
197
|
classification: "GENERATED_ONLY",
|
|
156
198
|
path: entry.file,
|
|
157
199
|
counterpart: builtCounterpart,
|
|
200
|
+
likelyCause:
|
|
201
|
+
"A generated plugin artifact changed without the matching plugins/src source change.",
|
|
158
202
|
nextAction:
|
|
159
203
|
"Move the edit to the matching plugins/src path, rebuild with `bun run build:plugins`, then run `bun run check:plugins`.",
|
|
160
204
|
});
|
|
@@ -198,6 +242,8 @@ function classifyMarketplaceDrift(root) {
|
|
|
198
242
|
classification: "MARKETPLACE_REGISTRATION_DRIFT",
|
|
199
243
|
path: source.replace(/^\.\//, ""),
|
|
200
244
|
counterpart: MARKETPLACE,
|
|
245
|
+
likelyCause:
|
|
246
|
+
"A built plugin directory exists but is not advertised by the marketplace manifest.",
|
|
201
247
|
nextAction: `Add marketplace source "${source}" to \`${MARKETPLACE}\`, then run \`bun run check:plugins\`.`,
|
|
202
248
|
});
|
|
203
249
|
}
|
|
@@ -209,6 +255,8 @@ function classifyMarketplaceDrift(root) {
|
|
|
209
255
|
classification: "MARKETPLACE_REGISTRATION_DRIFT",
|
|
210
256
|
path: MARKETPLACE,
|
|
211
257
|
counterpart: source.replace(/^\.\//, ""),
|
|
258
|
+
likelyCause:
|
|
259
|
+
"The marketplace manifest points at a built plugin directory that is missing.",
|
|
212
260
|
nextAction:
|
|
213
261
|
"Either restore the built plugin directory or remove the stale marketplace source, then run `bun run check:plugins`.",
|
|
214
262
|
});
|
|
@@ -218,6 +266,113 @@ function classifyMarketplaceDrift(root) {
|
|
|
218
266
|
return findings;
|
|
219
267
|
}
|
|
220
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Build plugins in a disposable copy and return the generated bytes that source
|
|
271
|
+
* changes should produce. If a minimal fixture cannot run the build script,
|
|
272
|
+
* callers fall back to status-based classification without mutating the repo.
|
|
273
|
+
* @param {string} root
|
|
274
|
+
* @returns {ReadonlyMap<string, Buffer> | undefined}
|
|
275
|
+
*/
|
|
276
|
+
function buildExpectedGeneratedFiles(root) {
|
|
277
|
+
if (!existsSync(path.join(root, "scripts/build-plugins.sh"))) {
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const tempRoot = mkdtempSync(path.join(tmpdir(), "lisa-plugin-sync-"));
|
|
282
|
+
const scratchRoot = path.join(tempRoot, "repo");
|
|
283
|
+
try {
|
|
284
|
+
cpSync(root, scratchRoot, {
|
|
285
|
+
recursive: true,
|
|
286
|
+
filter: src => shouldCopyToScratch(root, src),
|
|
287
|
+
});
|
|
288
|
+
execFileSync("bash", ["scripts/build-plugins.sh"], {
|
|
289
|
+
cwd: scratchRoot,
|
|
290
|
+
encoding: "utf8",
|
|
291
|
+
env: gitEnv(),
|
|
292
|
+
stdio: "ignore",
|
|
293
|
+
});
|
|
294
|
+
return collectGeneratedFiles(scratchRoot);
|
|
295
|
+
} catch {
|
|
296
|
+
return undefined;
|
|
297
|
+
} finally {
|
|
298
|
+
rmSync(tempRoot, { force: true, recursive: true });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* @param {string} root
|
|
304
|
+
* @param {string} src
|
|
305
|
+
* @returns {boolean}
|
|
306
|
+
*/
|
|
307
|
+
function shouldCopyToScratch(root, src) {
|
|
308
|
+
const rel = path.relative(root, src).split(path.sep).join("/");
|
|
309
|
+
if (rel === "") {
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
return !(
|
|
313
|
+
rel === ".git" ||
|
|
314
|
+
rel.startsWith(".git/") ||
|
|
315
|
+
rel === "node_modules" ||
|
|
316
|
+
rel.startsWith("node_modules/") ||
|
|
317
|
+
rel === "dist" ||
|
|
318
|
+
rel.startsWith("dist/") ||
|
|
319
|
+
rel === "coverage" ||
|
|
320
|
+
rel.startsWith("coverage/")
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* @param {string} root
|
|
326
|
+
* @returns {Map<string, Buffer>}
|
|
327
|
+
*/
|
|
328
|
+
function collectGeneratedFiles(root) {
|
|
329
|
+
const files = new Map();
|
|
330
|
+
const pluginsRoot = path.join(root, PLUGINS_DIR);
|
|
331
|
+
if (!existsSync(pluginsRoot)) {
|
|
332
|
+
return files;
|
|
333
|
+
}
|
|
334
|
+
for (const name of readdirSync(pluginsRoot)) {
|
|
335
|
+
if (name === "src" || !name.startsWith("lisa")) {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
collectFiles(root, path.join(pluginsRoot, name), files);
|
|
339
|
+
}
|
|
340
|
+
return files;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* @param {string} root
|
|
345
|
+
* @param {string} current
|
|
346
|
+
* @param {Map<string, Buffer>} files
|
|
347
|
+
*/
|
|
348
|
+
function collectFiles(root, current, files) {
|
|
349
|
+
const stats = statSync(current);
|
|
350
|
+
if (stats.isDirectory()) {
|
|
351
|
+
for (const child of readdirSync(current)) {
|
|
352
|
+
collectFiles(root, path.join(current, child), files);
|
|
353
|
+
}
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (stats.isFile()) {
|
|
357
|
+
files.set(
|
|
358
|
+
path.relative(root, current).split(path.sep).join("/"),
|
|
359
|
+
readFileSync(current)
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* @param {string} root
|
|
366
|
+
* @param {string} rel
|
|
367
|
+
* @returns {Buffer | undefined}
|
|
368
|
+
*/
|
|
369
|
+
function readOptionalFile(root, rel) {
|
|
370
|
+
const abs = path.join(root, rel);
|
|
371
|
+
return existsSync(abs) && statSync(abs).isFile()
|
|
372
|
+
? readFileSync(abs)
|
|
373
|
+
: undefined;
|
|
374
|
+
}
|
|
375
|
+
|
|
221
376
|
/**
|
|
222
377
|
* @param {string} root
|
|
223
378
|
* @returns {string}
|
|
@@ -229,7 +384,7 @@ function gitStatus(root) {
|
|
|
229
384
|
{
|
|
230
385
|
cwd: root,
|
|
231
386
|
encoding: "utf8",
|
|
232
|
-
env: gitEnv(),
|
|
387
|
+
env: gitEnv(root),
|
|
233
388
|
}
|
|
234
389
|
);
|
|
235
390
|
}
|
|
@@ -308,15 +463,41 @@ function dedupeFindings(findings) {
|
|
|
308
463
|
|
|
309
464
|
/**
|
|
310
465
|
* Remove parent-hook Git environment so diagnostics inspect the requested repo.
|
|
466
|
+
* Bare common-dir worktrees in Codex need explicit GIT_DIR/GIT_WORK_TREE.
|
|
467
|
+
* @param {string} [root] Repository path the nested git command should inspect.
|
|
311
468
|
* @returns {NodeJS.ProcessEnv} Process environment for nested git commands.
|
|
312
469
|
*/
|
|
313
|
-
function gitEnv() {
|
|
470
|
+
function gitEnv(root) {
|
|
314
471
|
const env = { ...process.env };
|
|
315
472
|
delete env.GIT_DIR;
|
|
316
473
|
delete env.GIT_WORK_TREE;
|
|
474
|
+
if (root) {
|
|
475
|
+
const gitDir = linkedWorktreeGitDir(root);
|
|
476
|
+
if (gitDir) {
|
|
477
|
+
env.GIT_DIR = gitDir;
|
|
478
|
+
env.GIT_WORK_TREE = root;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
317
481
|
return env;
|
|
318
482
|
}
|
|
319
483
|
|
|
484
|
+
/**
|
|
485
|
+
* @param {string} root
|
|
486
|
+
* @returns {string | undefined}
|
|
487
|
+
*/
|
|
488
|
+
function linkedWorktreeGitDir(root) {
|
|
489
|
+
const dotGit = path.join(root, ".git");
|
|
490
|
+
if (!existsSync(dotGit) || statSync(dotGit).isDirectory()) {
|
|
491
|
+
return undefined;
|
|
492
|
+
}
|
|
493
|
+
const match = /^gitdir:\s*(.+)$/m.exec(readFileSync(dotGit, "utf8"));
|
|
494
|
+
if (!match) {
|
|
495
|
+
return undefined;
|
|
496
|
+
}
|
|
497
|
+
const gitDir = match[1].trim();
|
|
498
|
+
return path.isAbsolute(gitDir) ? gitDir : path.resolve(root, gitDir);
|
|
499
|
+
}
|
|
500
|
+
|
|
320
501
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
321
502
|
try {
|
|
322
503
|
const report = explainPluginSync(process.argv[2] ?? process.cwd());
|