@codyswann/lisa 4.66.5 → 4.68.0
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/all/copy-overwrite/scripts/lisa-self-update.mjs +1047 -0
- package/all/create-only/.github/workflows/lisa-update.yml +99 -0
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +16 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +18 -4
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +5 -5
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/enforcement-vintage-npm.mjs +320 -0
- package/plugins/lisa/hooks/enforcement-vintage.mjs +22 -3
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/enforcement-vintage-npm.mjs +320 -0
- package/plugins/lisa-copilot/hooks/enforcement-vintage.mjs +22 -3
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/enforcement-vintage-npm.mjs +320 -0
- package/plugins/lisa-cursor/hooks/enforcement-vintage.mjs +22 -3
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-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-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-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-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-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-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-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-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-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-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-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/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/hooks/enforcement-vintage-npm.mjs +320 -0
- package/plugins/src/base/hooks/enforcement-vintage.mjs +22 -3
- package/scripts/lisa-self-update.mjs +14 -0
- package/scripts/two-channel-couplings.json +1 -1
- package/typescript/copy-overwrite/.prettierignore +1 -0
- package/typescript/copy-overwrite/audit.ignore.config.json +6 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two rows `enforcement-vintage` could not show before: the Lisa version the
|
|
3
|
+
* PROJECT installed, and the newest Lisa PUBLISHED (CodySwannGT/lisa#4325).
|
|
4
|
+
*
|
|
5
|
+
* Why these two. The session-copy rows answer "is this session running the
|
|
6
|
+
* newest Lisa on this disk?" — and a whole host can be current by that measure
|
|
7
|
+
* while sitting many releases behind npm, because nothing on the disk is newer
|
|
8
|
+
* than the version the project last installed. A host only moves when something
|
|
9
|
+
* opens an update PR for it. The agent standing in that host is the party most
|
|
10
|
+
* able to notice and least likely to be told, so the comparison goes where the
|
|
11
|
+
* agent reads: the injected context.
|
|
12
|
+
*
|
|
13
|
+
* NO NETWORK ON THE STARTUP PATH. A session start must never wait on npm. The
|
|
14
|
+
* row is read from the cache the `lisa` CLI's own update check already writes
|
|
15
|
+
* (`node_modules/.cache/@codyswann/lisa/update-check.json`, same shape, same
|
|
16
|
+
* 6-hour freshness), so the two surfaces cannot disagree about "latest". When
|
|
17
|
+
* that cache is missing or stale, a DETACHED child refreshes it for the next
|
|
18
|
+
* session and this session reports what it has — a stale reading named as
|
|
19
|
+
* stale, never a guess.
|
|
20
|
+
*
|
|
21
|
+
* FAIL SOFT, ALWAYS. Every function here returns a neutral value on any error.
|
|
22
|
+
* @module plugins/src/base/hooks/enforcement-vintage-npm
|
|
23
|
+
*/
|
|
24
|
+
import { spawn } from "child_process";
|
|
25
|
+
import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "fs";
|
|
26
|
+
import path from "path";
|
|
27
|
+
import { fileURLToPath } from "url";
|
|
28
|
+
|
|
29
|
+
/** The package whose versions are compared. */
|
|
30
|
+
export const LISA_PACKAGE = "@codyswann/lisa";
|
|
31
|
+
|
|
32
|
+
/** Same freshness window as the CLI update check (`src/cli/update-check.ts`). */
|
|
33
|
+
export const NPM_LATEST_TTL_MS = 6 * 60 * 60 * 1000;
|
|
34
|
+
|
|
35
|
+
/** Budget for the detached refresh; it runs after the session has started. */
|
|
36
|
+
const REFRESH_TIMEOUT_MS = 5000;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Registry endpoint for the latest dist-tag.
|
|
40
|
+
*
|
|
41
|
+
* The cached mutable pointer lags a publish by minutes (CodySwannGT/lisa#3685).
|
|
42
|
+
* That is fine here for the same reason it is fine in the CLI nag: nothing
|
|
43
|
+
* downstream of this value chooses, publishes or gates — it is advice.
|
|
44
|
+
*/
|
|
45
|
+
const NPM_LATEST_URL = "https://registry.npmjs.org/@codyswann/lisa/latest";
|
|
46
|
+
|
|
47
|
+
/** A plain `x.y.z` release, optionally with a prerelease or build suffix. */
|
|
48
|
+
const VERSION_SHAPE = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.+-]+)?$/u;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Compare two prerelease identifier lists by semver precedence.
|
|
52
|
+
* @param {string[]} a Identifiers of the first prerelease.
|
|
53
|
+
* @param {string[]} b Identifiers of the second prerelease.
|
|
54
|
+
* @returns {number} Negative when `a` precedes `b`, positive when it follows.
|
|
55
|
+
*/
|
|
56
|
+
function comparePrerelease(a, b) {
|
|
57
|
+
for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
|
|
58
|
+
if (a[index] === undefined) return -1;
|
|
59
|
+
if (b[index] === undefined) return 1;
|
|
60
|
+
if (a[index] === b[index]) continue;
|
|
61
|
+
const numeric = /^\d+$/u;
|
|
62
|
+
if (numeric.test(a[index]) && numeric.test(b[index])) {
|
|
63
|
+
return Number(a[index]) - Number(b[index]);
|
|
64
|
+
}
|
|
65
|
+
if (numeric.test(a[index])) return -1;
|
|
66
|
+
if (numeric.test(b[index])) return 1;
|
|
67
|
+
return a[index] < b[index] ? -1 : 1;
|
|
68
|
+
}
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Whether version `a` has lower semver precedence than `b`.
|
|
74
|
+
*
|
|
75
|
+
* Prerelease-aware on purpose: a project pinned to `4.66.5-rc.1` is BEHIND a
|
|
76
|
+
* published `4.66.5`, and comparing release fields alone would call it current.
|
|
77
|
+
* Build metadata is ignored, as semver requires.
|
|
78
|
+
* @param {string} a Candidate older version.
|
|
79
|
+
* @param {string} b Candidate newer version.
|
|
80
|
+
* @returns {boolean} True when `a` precedes `b`.
|
|
81
|
+
*/
|
|
82
|
+
export function precedes(a, b) {
|
|
83
|
+
const split = version => {
|
|
84
|
+
const [core, pre] = String(version).split("+")[0].split(/-(.*)/su);
|
|
85
|
+
const fields = core.split(".").map(field => Number(field) || 0);
|
|
86
|
+
return { fields, pre: pre ? pre.split(".") : [] };
|
|
87
|
+
};
|
|
88
|
+
const left = split(a);
|
|
89
|
+
const right = split(b);
|
|
90
|
+
for (let index = 0; index < 3; index += 1) {
|
|
91
|
+
const l = left.fields[index] ?? 0;
|
|
92
|
+
const r = right.fields[index] ?? 0;
|
|
93
|
+
if (l !== r) return l < r;
|
|
94
|
+
}
|
|
95
|
+
if (left.pre.length === 0) return false;
|
|
96
|
+
if (right.pre.length === 0) return true;
|
|
97
|
+
return comparePrerelease(left.pre, right.pre) < 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Parse a JSON file, or return null.
|
|
102
|
+
* @param {string} file Absolute path.
|
|
103
|
+
* @returns {any} Parsed JSON, or null when unreadable or malformed.
|
|
104
|
+
*/
|
|
105
|
+
function readJson(file) {
|
|
106
|
+
try {
|
|
107
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The update-check cache path for a project — the CLI's own path.
|
|
115
|
+
* @param {string} projectDir Project root.
|
|
116
|
+
* @returns {string} Absolute cache path.
|
|
117
|
+
*/
|
|
118
|
+
export function npmLatestCachePath(projectDir) {
|
|
119
|
+
return path.join(
|
|
120
|
+
projectDir,
|
|
121
|
+
"node_modules",
|
|
122
|
+
".cache",
|
|
123
|
+
"@codyswann",
|
|
124
|
+
"lisa",
|
|
125
|
+
"update-check.json"
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The Lisa version this project installed, or null when it has none to show.
|
|
131
|
+
*
|
|
132
|
+
* Null inside the Lisa monorepo itself: there `node_modules/@codyswann/lisa` is
|
|
133
|
+
* a fixture pinned majors behind the repository, and reporting it as the
|
|
134
|
+
* project's pin would tell every Lisa session its project is stale.
|
|
135
|
+
* @param {string} projectDir Project root.
|
|
136
|
+
* @returns {{version: string, source: string} | null} The installed copy.
|
|
137
|
+
*/
|
|
138
|
+
export function projectPin(projectDir) {
|
|
139
|
+
const own = readJson(path.join(projectDir, "package.json"));
|
|
140
|
+
if (own?.name === LISA_PACKAGE) return null;
|
|
141
|
+
const source = path.join(
|
|
142
|
+
projectDir,
|
|
143
|
+
"node_modules",
|
|
144
|
+
"@codyswann",
|
|
145
|
+
"lisa",
|
|
146
|
+
"package.json"
|
|
147
|
+
);
|
|
148
|
+
const version = readJson(source)?.version;
|
|
149
|
+
return typeof version === "string" && VERSION_SHAPE.test(version)
|
|
150
|
+
? { version, source }
|
|
151
|
+
: null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Read the cached npm latest.
|
|
156
|
+
* @param {string} cachePath Absolute cache path.
|
|
157
|
+
* @param {number} nowMs Current time in ms.
|
|
158
|
+
* @param {number} [ttlMs] Freshness window.
|
|
159
|
+
* @returns {{version: string, fetchedAt: string, fresh: boolean} | null} Cached value.
|
|
160
|
+
*/
|
|
161
|
+
export function cachedNpmLatest(cachePath, nowMs, ttlMs = NPM_LATEST_TTL_MS) {
|
|
162
|
+
const cache = readJson(cachePath);
|
|
163
|
+
const version = cache?.latest;
|
|
164
|
+
const fetchedAt = cache?.fetchedAt;
|
|
165
|
+
if (typeof version !== "string" || !VERSION_SHAPE.test(version)) return null;
|
|
166
|
+
if (typeof fetchedAt !== "string") return null;
|
|
167
|
+
const fetchedMs = Date.parse(fetchedAt);
|
|
168
|
+
if (!Number.isFinite(fetchedMs)) return null;
|
|
169
|
+
return { version, fetchedAt, fresh: nowMs - fetchedMs <= ttlMs };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Whether a refresh should be started for this session.
|
|
174
|
+
* @param {{version: string, fresh: boolean} | null} cached Cached value.
|
|
175
|
+
* @param {NodeJS.ProcessEnv} env Environment.
|
|
176
|
+
* @returns {boolean} True when the cache is missing or stale and checks are on.
|
|
177
|
+
*/
|
|
178
|
+
export function needsRefresh(cached, env) {
|
|
179
|
+
if (env.LISA_SKIP_UPDATE_CHECK === "1") return false;
|
|
180
|
+
return cached === null || !cached.fresh;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Start a detached refresh of the cache and return immediately.
|
|
185
|
+
* @param {string} script Absolute path of this module.
|
|
186
|
+
* @param {string} cachePath Absolute cache path.
|
|
187
|
+
* @param {typeof spawn} [spawnImpl] Injected for tests.
|
|
188
|
+
* @returns {boolean} Whether a child was started.
|
|
189
|
+
*/
|
|
190
|
+
export function startDetachedRefresh(script, cachePath, spawnImpl = spawn) {
|
|
191
|
+
try {
|
|
192
|
+
const child = spawnImpl(
|
|
193
|
+
process.execPath,
|
|
194
|
+
[script, "--refresh-npm-latest", cachePath],
|
|
195
|
+
{ detached: true, stdio: "ignore" }
|
|
196
|
+
);
|
|
197
|
+
child.on?.("error", () => {});
|
|
198
|
+
child.unref?.();
|
|
199
|
+
return true;
|
|
200
|
+
} catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Fetch npm latest and write it to the cache in the CLI's shape.
|
|
207
|
+
*
|
|
208
|
+
* probe-direction: neutral — a failed fetch writes nothing, so the next
|
|
209
|
+
* session reports "npm latest: unknown" (or the older cached value, labelled
|
|
210
|
+
* stale); no gate reads this value, it only shapes advisory context.
|
|
211
|
+
* @param {string} cachePath Absolute cache path.
|
|
212
|
+
* @param {{fetchImpl?: typeof fetch, now?: () => Date}} [deps] Injected for tests.
|
|
213
|
+
* @returns {Promise<string | null>} The version written, or null.
|
|
214
|
+
*/
|
|
215
|
+
export async function refreshNpmLatest(cachePath, deps = {}) {
|
|
216
|
+
const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
|
|
217
|
+
const now = deps.now ?? (() => new Date());
|
|
218
|
+
try {
|
|
219
|
+
const response = await fetchImpl(NPM_LATEST_URL, {
|
|
220
|
+
signal: AbortSignal.timeout(REFRESH_TIMEOUT_MS),
|
|
221
|
+
});
|
|
222
|
+
if (!response.ok) return null;
|
|
223
|
+
const body = await response.json();
|
|
224
|
+
const version = body?.version;
|
|
225
|
+
if (typeof version !== "string" || !VERSION_SHAPE.test(version)) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
229
|
+
writeFileSync(
|
|
230
|
+
cachePath,
|
|
231
|
+
`${JSON.stringify({ latest: version, fetchedAt: now().toISOString() }, null, 2)}\n`,
|
|
232
|
+
"utf8"
|
|
233
|
+
);
|
|
234
|
+
return version;
|
|
235
|
+
} catch {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Render the project and npm rows, plus the guidance when the project is behind.
|
|
242
|
+
* @param {{pin: {version: string, source: string} | null, latest: {version: string, fetchedAt: string, fresh: boolean} | null, refreshing: boolean, projectBehind: boolean}} state Resolved rows.
|
|
243
|
+
* @returns {{rows: string[], guidance: string[]}} Lines to splice into the block.
|
|
244
|
+
*/
|
|
245
|
+
export function renderNpmRows(state) {
|
|
246
|
+
const rows = [];
|
|
247
|
+
if (state.pin) {
|
|
248
|
+
rows.push(`project pin: lisa ${state.pin.version} at ${state.pin.source}`);
|
|
249
|
+
}
|
|
250
|
+
if (state.latest) {
|
|
251
|
+
rows.push(
|
|
252
|
+
`npm latest: lisa ${state.latest.version} (checked ${state.latest.fetchedAt}${state.latest.fresh ? "" : "; stale"}${state.refreshing ? ", refreshing for the next session" : ""})`
|
|
253
|
+
);
|
|
254
|
+
} else if (state.pin) {
|
|
255
|
+
rows.push(
|
|
256
|
+
`npm latest: unknown${state.refreshing ? " (checking in the background for the next session)" : ""}`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
const guidance = state.projectBehind
|
|
260
|
+
? [
|
|
261
|
+
`PROJECT BEHIND — this project installs lisa ${state.pin?.version} while lisa ${state.latest?.version} is published.`,
|
|
262
|
+
"- Do NOT upgrade Lisa inside the current task: a version bump plus its template apply belongs in its own pull request, never mixed into feature work.",
|
|
263
|
+
"- Check for an open pull request on a `lisa/update-*` branch. If none is open, the project's Lisa Update workflow (`.github/workflows/lisa-update.yml`) opens one on its schedule; say so in your report, and if that workflow is missing, recommend running `lisa apply` once so the project receives it.",
|
|
264
|
+
]
|
|
265
|
+
: [];
|
|
266
|
+
return { rows, guidance };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Resolve everything the npm rows need, starting a refresh when warranted.
|
|
271
|
+
* @param {{projectDir: string, script?: string, env: NodeJS.ProcessEnv, nowMs: number, spawnImpl?: typeof spawn}} input Inputs.
|
|
272
|
+
* @returns {{pin: {version: string, source: string} | null, latest: {version: string, fetchedAt: string, fresh: boolean} | null, refreshing: boolean, projectBehind: boolean}} Resolved rows.
|
|
273
|
+
*/
|
|
274
|
+
export function resolveNpmState(input) {
|
|
275
|
+
try {
|
|
276
|
+
const pin = projectPin(input.projectDir);
|
|
277
|
+
if (pin === null) {
|
|
278
|
+
return { pin, latest: null, refreshing: false, projectBehind: false };
|
|
279
|
+
}
|
|
280
|
+
const cachePath = npmLatestCachePath(input.projectDir);
|
|
281
|
+
const latest = cachedNpmLatest(cachePath, input.nowMs);
|
|
282
|
+
const refreshing =
|
|
283
|
+
needsRefresh(latest, input.env) &&
|
|
284
|
+
startDetachedRefresh(
|
|
285
|
+
input.script ?? fileURLToPath(import.meta.url),
|
|
286
|
+
cachePath,
|
|
287
|
+
input.spawnImpl
|
|
288
|
+
);
|
|
289
|
+
const projectBehind = Boolean(
|
|
290
|
+
latest && precedes(pin.version, latest.version)
|
|
291
|
+
);
|
|
292
|
+
return { pin, latest, refreshing, projectBehind };
|
|
293
|
+
} catch {
|
|
294
|
+
return { pin: null, latest: null, refreshing: false, projectBehind: false };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Whether this module is the process entry point (the detached refresh child).
|
|
300
|
+
* Realpaths both sides for the reason `enforcement-vintage.mjs` gives.
|
|
301
|
+
* @param {string} moduleUrl This module's `import.meta.url`.
|
|
302
|
+
* @param {string | undefined} [argv1] Entry path.
|
|
303
|
+
* @returns {boolean} True when run directly.
|
|
304
|
+
*/
|
|
305
|
+
function invokedDirectly(moduleUrl, argv1 = process.argv[1]) {
|
|
306
|
+
if (!argv1) return false;
|
|
307
|
+
try {
|
|
308
|
+
return realpathSync(argv1) === realpathSync(fileURLToPath(moduleUrl));
|
|
309
|
+
} catch {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (
|
|
315
|
+
invokedDirectly(import.meta.url) &&
|
|
316
|
+
process.argv[2] === "--refresh-npm-latest" &&
|
|
317
|
+
process.argv[3]
|
|
318
|
+
) {
|
|
319
|
+
refreshNpmLatest(process.argv[3]).catch(() => {});
|
|
320
|
+
}
|
|
@@ -52,6 +52,7 @@ import { readFileSync, existsSync } from "fs";
|
|
|
52
52
|
import path from "path";
|
|
53
53
|
import { fileURLToPath } from "url";
|
|
54
54
|
import { realpathSync } from "fs";
|
|
55
|
+
import { renderNpmRows, resolveNpmState } from "./enforcement-vintage-npm.mjs";
|
|
55
56
|
|
|
56
57
|
/** Marker the block is wrapped in, so an agent can cite it by name. */
|
|
57
58
|
const BLOCK_TAG = "lisa-enforcement-vintage";
|
|
@@ -336,6 +337,15 @@ export function renderBlock(state) {
|
|
|
336
337
|
`newest on this disk: lisa ${state.newest.version} at ${state.newest.source}`
|
|
337
338
|
);
|
|
338
339
|
}
|
|
340
|
+
const npm = renderNpmRows(
|
|
341
|
+
state.npm ?? {
|
|
342
|
+
pin: null,
|
|
343
|
+
latest: null,
|
|
344
|
+
refreshing: false,
|
|
345
|
+
projectBehind: false,
|
|
346
|
+
}
|
|
347
|
+
);
|
|
348
|
+
npm.rows.forEach(row => lines.push(row));
|
|
339
349
|
lines.push("");
|
|
340
350
|
if (state.behind || !state.running.version || state.treeBehind) {
|
|
341
351
|
lines.push(
|
|
@@ -363,6 +373,10 @@ export function renderBlock(state) {
|
|
|
363
373
|
"when you report on guard or contract behaviour; it is only current until the next release."
|
|
364
374
|
);
|
|
365
375
|
}
|
|
376
|
+
if (npm.guidance.length > 0) {
|
|
377
|
+
lines.push("");
|
|
378
|
+
npm.guidance.forEach(line => lines.push(line));
|
|
379
|
+
}
|
|
366
380
|
lines.push(`</${BLOCK_TAG}>`);
|
|
367
381
|
return lines.join("\n");
|
|
368
382
|
}
|
|
@@ -405,9 +419,14 @@ export function run(argv, stdin, env = process.env) {
|
|
|
405
419
|
return JSON.stringify({
|
|
406
420
|
hookSpecificOutput: {
|
|
407
421
|
hookEventName: eventName(stdin),
|
|
408
|
-
additionalContext: renderBlock(
|
|
409
|
-
resolveState({ hooksDir, projectDir, configDir })
|
|
410
|
-
|
|
422
|
+
additionalContext: renderBlock({
|
|
423
|
+
...resolveState({ hooksDir, projectDir, configDir }),
|
|
424
|
+
npm: resolveNpmState({
|
|
425
|
+
projectDir,
|
|
426
|
+
env,
|
|
427
|
+
nowMs: Date.now(),
|
|
428
|
+
}),
|
|
429
|
+
}),
|
|
411
430
|
},
|
|
412
431
|
});
|
|
413
432
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Lisa's own entry point for the shipped updater (CodySwannGT/lisa#4331).
|
|
4
|
+
*
|
|
5
|
+
* `.github/workflows/lisa-update.yml` runs `node scripts/lisa-self-update.mjs`,
|
|
6
|
+
* and that path is the CALLER's tree: a host gets it from
|
|
7
|
+
* `all/copy-overwrite/scripts/` on apply, and in this repository it is this
|
|
8
|
+
* file. A re-export rather than a second copy, so there is one implementation;
|
|
9
|
+
* here it runs in self mode because this manifest's name is `@codyswann/lisa`.
|
|
10
|
+
* @module scripts/lisa-self-update
|
|
11
|
+
*/
|
|
12
|
+
import { runCli } from "../all/copy-overwrite/scripts/lisa-self-update.mjs";
|
|
13
|
+
|
|
14
|
+
runCli();
|
|
@@ -137,6 +137,12 @@
|
|
|
137
137
|
"id": "GHSA-j759-j44w-7fr8",
|
|
138
138
|
"package": "@xmldom/xmldom",
|
|
139
139
|
"reason": "XML node injection via unvalidated comment serialization. Transitive via expo > @expo/config-plugins > @expo/plist; only serializes developer-authored plist files at build/prebuild time, no runtime code path serializes attacker-controlled XML."
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"id": "GHSA-vfj7-8cjw-p6xm",
|
|
143
|
+
"cve": "CVE-2026-93687",
|
|
144
|
+
"package": "braces",
|
|
145
|
+
"reason": "Stack-exhaustion DoS (uncaught RangeError) via deeply nested brace patterns; no patched release exists. Keep this exclusion only if no runtime code path passes attacker-supplied glob or brace patterns to braces/micromatch (dev tooling expanding repo-owned globs is unaffected)."
|
|
140
146
|
}
|
|
141
147
|
]
|
|
142
148
|
}
|