@codyswann/lisa 2.327.0 → 2.328.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/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +30 -6
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- 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/.codex-plugin/skills/lisa-detect-tooling/SKILL.md +21 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +25 -13
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/validate-config.mjs +72 -10
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-local-env/SKILL.md +95 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-local-env/agents/openai.yaml +4 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-local-env/scripts/local-env.mjs +174 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/SKILL.md +27 -11
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +70 -30
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/toolchain.mjs +132 -6
- package/plugins/lisa/commands/setup/local-env.md +7 -0
- package/plugins/lisa/skills/lisa-detect-tooling/SKILL.md +21 -1
- package/plugins/lisa/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +25 -13
- package/plugins/lisa/skills/lisa-secrets-access/scripts/validate-config.mjs +72 -10
- package/plugins/lisa/skills/lisa-setup-local-env/SKILL.md +95 -0
- package/plugins/lisa/skills/lisa-setup-local-env/agents/openai.yaml +4 -0
- package/plugins/lisa/skills/lisa-setup-local-env/scripts/local-env.mjs +174 -0
- package/plugins/lisa/skills/lisa-setup-remote-env/SKILL.md +27 -11
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +70 -30
- package/plugins/lisa/skills/lisa-setup-remote-env/scripts/toolchain.mjs +132 -6
- package/plugins/lisa-agy/commands/lisa/setup/local-env.md +7 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-detect-tooling/SKILL.md +21 -1
- package/plugins/lisa-agy/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +25 -13
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/validate-config.mjs +72 -10
- package/plugins/lisa-agy/skills/lisa-setup-local-env/SKILL.md +95 -0
- package/plugins/lisa-agy/skills/lisa-setup-local-env/scripts/local-env.mjs +174 -0
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/SKILL.md +27 -11
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +70 -30
- package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/toolchain.mjs +132 -6
- 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/commands/lisa/setup/local-env.md +7 -0
- package/plugins/lisa-copilot/skills/lisa-detect-tooling/SKILL.md +21 -1
- package/plugins/lisa-copilot/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +25 -13
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/validate-config.mjs +72 -10
- package/plugins/lisa-copilot/skills/lisa-setup-local-env/SKILL.md +95 -0
- package/plugins/lisa-copilot/skills/lisa-setup-local-env/scripts/local-env.mjs +174 -0
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/SKILL.md +27 -11
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +70 -30
- package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/toolchain.mjs +132 -6
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/commands/lisa/setup/local-env.md +7 -0
- package/plugins/lisa-cursor/skills/lisa-detect-tooling/SKILL.md +21 -1
- package/plugins/lisa-cursor/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +25 -13
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/validate-config.mjs +72 -10
- package/plugins/lisa-cursor/skills/lisa-setup-local-env/SKILL.md +95 -0
- package/plugins/lisa-cursor/skills/lisa-setup-local-env/scripts/local-env.mjs +174 -0
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/SKILL.md +27 -11
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +70 -30
- package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/toolchain.mjs +132 -6
- 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/commands/setup/local-env.md +7 -0
- package/plugins/src/base/skills/lisa-detect-tooling/SKILL.md +21 -1
- package/plugins/src/base/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +25 -13
- package/plugins/src/base/skills/lisa-secrets-access/scripts/validate-config.mjs +72 -10
- package/plugins/src/base/skills/lisa-setup-local-env/SKILL.md +95 -0
- package/plugins/src/base/skills/lisa-setup-local-env/scripts/local-env.mjs +174 -0
- package/plugins/src/base/skills/lisa-setup-remote-env/SKILL.md +27 -11
- package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +70 -30
- package/plugins/src/base/skills/lisa-setup-remote-env/scripts/toolchain.mjs +132 -6
|
@@ -100,7 +100,7 @@ function planRequired(tool, found) {
|
|
|
100
100
|
* @param {{version: string|null, present: boolean}} found Probe result.
|
|
101
101
|
* @returns {{name: string, action: string, reason: string}} The decision.
|
|
102
102
|
*/
|
|
103
|
-
function planInstallable(tool, found) {
|
|
103
|
+
function planInstallable(tool, found, pinIsFloor = false) {
|
|
104
104
|
if (!tool.version) {
|
|
105
105
|
return {
|
|
106
106
|
name: tool.name,
|
|
@@ -115,6 +115,25 @@ function planInstallable(tool, found) {
|
|
|
115
115
|
reason: `${tool.name} ${tool.version} already installed`,
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
|
+
// On a laptop the pin is a floor, not an equality. A container is disposable
|
|
119
|
+
// and reproducible by construction, so an exact match is right there. A
|
|
120
|
+
// developer's machine is shared with every other project they work on, and
|
|
121
|
+
// installing a pinned binary into ~/.local/bin ahead of a NEWER one already on
|
|
122
|
+
// PATH is a downgrade this project imposed on all of them — for gh, pinned at
|
|
123
|
+
// 2.83.0 against a workstation running 2.96.0, that is the likely case rather
|
|
124
|
+
// than the exotic one.
|
|
125
|
+
if (
|
|
126
|
+
pinIsFloor &&
|
|
127
|
+
found.present &&
|
|
128
|
+
found.version &&
|
|
129
|
+
compareVersions(found.version, tool.version) > 0
|
|
130
|
+
) {
|
|
131
|
+
return {
|
|
132
|
+
name: tool.name,
|
|
133
|
+
action: "newer",
|
|
134
|
+
reason: `${tool.name} ${found.version} is newer than the pinned ${tool.version} — leaving it alone`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
118
137
|
return {
|
|
119
138
|
name: tool.name,
|
|
120
139
|
action: "install",
|
|
@@ -127,6 +146,72 @@ function planInstallable(tool, found) {
|
|
|
127
146
|
/** Surfaces a manifest entry may name. */
|
|
128
147
|
const KNOWN_SURFACES = new Set(["local", "remote"]);
|
|
129
148
|
|
|
149
|
+
/**
|
|
150
|
+
* The platform key a manifest entry is resolved against.
|
|
151
|
+
*
|
|
152
|
+
* `<platform>-<arch>` rather than either alone, because both halves change the
|
|
153
|
+
* artifact: an Apple Silicon laptop and an Intel one run different builds of the
|
|
154
|
+
* same release, and so do an arm64 container and an amd64 one.
|
|
155
|
+
* @param {{platform: string, arch: string}} [runtime] Injectable, for tests.
|
|
156
|
+
* @returns {string} A key such as "darwin-arm64" or "linux-x64".
|
|
157
|
+
*/
|
|
158
|
+
export function currentPlatform(runtime = process) {
|
|
159
|
+
return `${runtime.platform}-${runtime.arch}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Collapse a manifest entry to the artifact for one platform.
|
|
164
|
+
*
|
|
165
|
+
* A download URL is platform-specific and a checksum doubly so, which the
|
|
166
|
+
* single-URL shape could not express: the only way to stop a laptop being handed
|
|
167
|
+
* a Linux binary was `surfaces: ["remote"]`, which bought that safety by making
|
|
168
|
+
* the tool uninstallable on the laptop entirely. So `bws` and `gh` — the two
|
|
169
|
+
* tools Lisa's own guardrails shell out to — were declared, required, and
|
|
170
|
+
* unprovisionable on the machine most likely to be missing them.
|
|
171
|
+
*
|
|
172
|
+
* A `platforms` map fixes the cause instead of the symptom. `install` lives
|
|
173
|
+
* inside each block rather than beside it, because the method varies too: gh
|
|
174
|
+
* publishes a .tar.gz for Linux and a .zip for macOS, so a single install method
|
|
175
|
+
* would have forced one platform onto an archive kind its vendor does not ship.
|
|
176
|
+
*
|
|
177
|
+
* A flat entry is still valid and means "identical everywhere" — true of every
|
|
178
|
+
* `npm-global` install, which is genuinely platform-independent.
|
|
179
|
+
* @param {object} tool Manifest entry.
|
|
180
|
+
* @param {string} [platform] Platform key to resolve for.
|
|
181
|
+
* @returns {object} The entry with its platform block merged in.
|
|
182
|
+
*/
|
|
183
|
+
export function resolvePlatform(tool, platform = currentPlatform()) {
|
|
184
|
+
const { platforms } = tool;
|
|
185
|
+
if (platforms === undefined) return tool;
|
|
186
|
+
if (
|
|
187
|
+
typeof platforms !== "object" ||
|
|
188
|
+
platforms === null ||
|
|
189
|
+
Array.isArray(platforms)
|
|
190
|
+
) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`${tool.name}: platforms must be an object keyed by <platform>-<arch>, ` +
|
|
193
|
+
`got ${Array.isArray(platforms) ? "an array" : typeof platforms}.\n` +
|
|
194
|
+
`Omit it when one artifact serves every platform.`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
const block = platforms[platform];
|
|
198
|
+
if (!block) {
|
|
199
|
+
const known = Object.keys(platforms).sort().join(", ");
|
|
200
|
+
throw new Error(
|
|
201
|
+
`${tool.name}: no pin for ${platform}.\n` +
|
|
202
|
+
`Declared platforms: ${known || "(none)"}.\n` +
|
|
203
|
+
`Add a block for ${platform} with its own url and sha256, or drop the ` +
|
|
204
|
+
`tool from this surface. Guessing an artifact would defeat the ` +
|
|
205
|
+
`checksum.`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
// The platform block wins over the shared fields, and `platforms` itself is
|
|
209
|
+
// dropped so a resolved entry is indistinguishable from a flat one — that is
|
|
210
|
+
// what lets assertPinned and the installers stay unaware of any of this.
|
|
211
|
+
const { platforms: _discarded, ...shared } = tool;
|
|
212
|
+
return { ...shared, ...block };
|
|
213
|
+
}
|
|
214
|
+
|
|
130
215
|
/**
|
|
131
216
|
* Whether a manifest entry applies to the surface being provisioned.
|
|
132
217
|
*
|
|
@@ -171,18 +256,48 @@ export function appliesToSurface(tool, surface) {
|
|
|
171
256
|
|
|
172
257
|
/**
|
|
173
258
|
* Produce the complete plan for a toolchain manifest.
|
|
259
|
+
*
|
|
260
|
+
* Install entries are resolved to the running platform here rather than at
|
|
261
|
+
* install time, so a tool with no artifact for this machine is reported
|
|
262
|
+
* alongside every other problem instead of aborting the run at the first one.
|
|
263
|
+
* An operator fixing a manifest wants the whole list.
|
|
264
|
+
*
|
|
265
|
+
* `require` entries are deliberately not resolved: they carry a name and a
|
|
266
|
+
* minimum version, nothing platform-specific, and inventing a per-platform shape
|
|
267
|
+
* for them would be ceremony with no artifact behind it.
|
|
174
268
|
* @param {{require?: object[], install?: object[]}} tools Manifest.
|
|
175
269
|
* @param {(name: string) => {version: string|null, present: boolean}} probe Version probe.
|
|
176
|
-
* @
|
|
270
|
+
* @param {string} [surface] Surface being provisioned.
|
|
271
|
+
* @param {string} [platform] Platform key to resolve install entries against.
|
|
272
|
+
* @returns {Array<{name: string, action: string, reason: string, tool?: object}>} Ordered decisions.
|
|
177
273
|
*/
|
|
178
|
-
export function planToolchain(
|
|
274
|
+
export function planToolchain(
|
|
275
|
+
tools,
|
|
276
|
+
probe,
|
|
277
|
+
surface = "remote",
|
|
278
|
+
platform = currentPlatform()
|
|
279
|
+
) {
|
|
179
280
|
const plan = [];
|
|
180
281
|
for (const tool of tools.require ?? [])
|
|
181
282
|
if (appliesToSurface(tool, surface))
|
|
182
283
|
plan.push(planRequired(tool, probe(tool.name)));
|
|
183
|
-
for (const tool of tools.install ?? [])
|
|
184
|
-
if (appliesToSurface(tool, surface))
|
|
185
|
-
|
|
284
|
+
for (const tool of tools.install ?? []) {
|
|
285
|
+
if (!appliesToSurface(tool, surface)) continue;
|
|
286
|
+
let resolved;
|
|
287
|
+
try {
|
|
288
|
+
resolved = resolvePlatform(tool, platform);
|
|
289
|
+
} catch (err) {
|
|
290
|
+
plan.push({ name: tool.name, action: "invalid", reason: err.message });
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
// The resolved entry travels with the decision so the installer never has to
|
|
294
|
+
// resolve a second time — two resolutions are two chances to disagree, and
|
|
295
|
+
// the one that installs would be the one nothing tested.
|
|
296
|
+
plan.push({
|
|
297
|
+
...planInstallable(resolved, probe(tool.name), surface === "local"),
|
|
298
|
+
tool: resolved,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
186
301
|
return plan;
|
|
187
302
|
}
|
|
188
303
|
|
|
@@ -195,6 +310,17 @@ export function planToolchain(tools, probe, surface = "remote") {
|
|
|
195
310
|
* @param {object} tool Manifest entry.
|
|
196
311
|
*/
|
|
197
312
|
export function assertPinned(tool) {
|
|
313
|
+
// An unresolved entry reaching here means a caller skipped resolvePlatform and
|
|
314
|
+
// is about to read a url and sha256 that belong to no platform in particular.
|
|
315
|
+
// Refusing is the point: the failure this whole change exists to prevent is
|
|
316
|
+
// exactly "downloaded the wrong platform's artifact", and a silent pass here
|
|
317
|
+
// would reintroduce it one call site at a time.
|
|
318
|
+
if (tool.platforms !== undefined) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
`${tool.name}: platform-specific entry was not resolved before install.\n` +
|
|
321
|
+
`Call resolvePlatform() first — the shared fields alone do not name an artifact.`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
198
324
|
// Both archive kinds carry the same obligation, and differ only in how they
|
|
199
325
|
// are unpacked. gh, for one, publishes no zip for Linux at all — only .deb,
|
|
200
326
|
// .rpm and .tar.gz — so a zip-only installer could not pin the CLI that
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Bring this machine in line with the toolchain the project declares. Reports every tool in remoteEnv.tools that is missing, outdated, or unpinned for this platform, and installs the missing ones into ~/.local/bin from the same pinned, checksummed entries the remote surfaces use — but only when asked with --install-tools. A newer tool already on PATH is left alone."
|
|
3
|
+
allowed-tools: ["Skill"]
|
|
4
|
+
argument-hint: "[--install-tools] [--json]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Use the /lisa-setup-local-env skill to report and optionally install the project's declared toolchain on this machine. $ARGUMENTS
|
|
@@ -46,7 +46,27 @@ What genuinely differs is **consent**, not the list:
|
|
|
46
46
|
|
|
47
47
|
Omitting `surfaces` means every surface, because that is true of most tools and the cost of forgetting should be a redundant check rather than a silent absence.
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
Platform differences are **not** expressed with `surfaces`. A downloaded tool declares a
|
|
50
|
+
`platforms` map keyed `<platform>-<arch>`, each block carrying its own `install` method, `url`,
|
|
51
|
+
and `sha256`:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
"platforms": {
|
|
55
|
+
"linux-x64": { "install": "release-tar", "url": "...", "sha256": "..." },
|
|
56
|
+
"darwin-arm64": { "install": "release-zip", "url": "...", "sha256": "..." }
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The method lives inside the block because it varies — gh ships a `.tar.gz` for Linux and a
|
|
61
|
+
`.zip` for macOS. A flat entry means one artifact serves everything, which is true of
|
|
62
|
+
`npm-global` and nothing else.
|
|
63
|
+
|
|
64
|
+
This replaces an earlier convention worth naming, because its residue may still be in a
|
|
65
|
+
manifest you read: a Linux archive used to be marked `surfaces: ["remote"]` with a bare
|
|
66
|
+
`require` entry for `local`, so a laptop asserted the tool without being offered a binary it
|
|
67
|
+
could not run. That kept the wrong binary off the laptop by making the tool uninstallable
|
|
68
|
+
there — which is how `bws` and `gh`, the two CLIs Lisa's own guardrails shell out to, came to
|
|
69
|
+
be required on developer machines and provisionable only in containers.
|
|
50
70
|
|
|
51
71
|
## Usage
|
|
52
72
|
|
|
@@ -361,9 +361,11 @@ export function detectTooling(cwd = process.cwd()) {
|
|
|
361
361
|
/**
|
|
362
362
|
* The manifest entries an operator can paste, with the parts only they supply.
|
|
363
363
|
*
|
|
364
|
-
*
|
|
365
|
-
* `
|
|
366
|
-
*
|
|
364
|
+
* A release archive is proposed as a `platforms` map rather than a single URL
|
|
365
|
+
* plus `surfaces: ["remote"]`. The old shape could express only one artifact, so
|
|
366
|
+
* keeping a Linux binary off a laptop meant excluding the tool from laptops
|
|
367
|
+
* altogether — which is how `bws` and `gh` came to be required on a developer
|
|
368
|
+
* machine and installable only in a container.
|
|
367
369
|
* @param {{name: string}} proposal One detected tool.
|
|
368
370
|
* @returns {{install: object[], require: object[]}} Manifest skeletons.
|
|
369
371
|
*/
|
|
@@ -394,23 +396,33 @@ export function proposedEntries(proposal) {
|
|
|
394
396
|
require: [],
|
|
395
397
|
};
|
|
396
398
|
}
|
|
397
|
-
// A release archive is built for one platform, so
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
399
|
+
// A release archive is built for one platform, so every platform the project
|
|
400
|
+
// is developed or built on needs its own block — with its own install method,
|
|
401
|
+
// because a vendor may ship a tarball for one and a zip for another, as gh
|
|
402
|
+
// does. Both containers and laptops are proposed: a tool the manifest can
|
|
403
|
+
// only install remotely is one every developer installs by hand or does
|
|
404
|
+
// without.
|
|
405
|
+
//
|
|
406
|
+
// The placeholders stay unresolved on purpose. A guessed URL is an artifact
|
|
407
|
+
// the checksum cannot vouch for, which is the one property that makes a
|
|
408
|
+
// pinned entry worth reviewing.
|
|
409
|
+
const archive = platform => ({
|
|
410
|
+
install: "<release-tar or release-zip, whichever this platform ships>",
|
|
411
|
+
url: `<release url for this exact version, for ${platform}>`,
|
|
412
|
+
sha256: `<sha256 published with the ${platform} release>`,
|
|
413
|
+
});
|
|
402
414
|
return {
|
|
403
415
|
install: [
|
|
404
416
|
{
|
|
405
417
|
name: proposal.name,
|
|
406
418
|
version: "<pin>",
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
419
|
+
platforms: {
|
|
420
|
+
"linux-x64": archive("linux-x64"),
|
|
421
|
+
"darwin-arm64": archive("darwin-arm64"),
|
|
422
|
+
},
|
|
411
423
|
},
|
|
412
424
|
],
|
|
413
|
-
require: [
|
|
425
|
+
require: [],
|
|
414
426
|
};
|
|
415
427
|
}
|
|
416
428
|
|
|
@@ -73,8 +73,16 @@ function bindingsFor(surface) {
|
|
|
73
73
|
return SURFACE_BINDINGS[surface] ?? ["repository"];
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
/**
|
|
77
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Install methods the toolchain runner supports.
|
|
78
|
+
*
|
|
79
|
+
* `release-tar` was missing here while the runner had supported it for as long
|
|
80
|
+
* as gh has been pinned — so this validator would have rejected the project's
|
|
81
|
+
* own manifest, and the only reason nobody hit it is that nothing ran the two
|
|
82
|
+
* against each other. Kept in step with `assertPinned` in `toolchain.mjs`,
|
|
83
|
+
* which is the list that actually decides.
|
|
84
|
+
*/
|
|
85
|
+
const INSTALL_METHODS = new Set(["release-zip", "release-tar", "npm-global"]);
|
|
78
86
|
|
|
79
87
|
/**
|
|
80
88
|
* Validate the `secrets` block.
|
|
@@ -143,6 +151,44 @@ export function validateSecrets(secrets) {
|
|
|
143
151
|
return problems;
|
|
144
152
|
}
|
|
145
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Validate the artifact half of one install entry.
|
|
156
|
+
*
|
|
157
|
+
* Split out because the same obligations apply whether the fields sit directly
|
|
158
|
+
* on the entry or inside a per-platform block: a method must be one Lisa can
|
|
159
|
+
* execute, an archive must carry a checksum, and an npm install must name a
|
|
160
|
+
* package. Duplicating those three rules per shape is how one shape ends up
|
|
161
|
+
* quietly weaker than the other.
|
|
162
|
+
* @param {string} label How to name this entry in a message.
|
|
163
|
+
* @param {object} entry The entry or platform block.
|
|
164
|
+
* @param {string[]} problems Accumulator.
|
|
165
|
+
*/
|
|
166
|
+
function validateInstallArtifact(label, entry, problems) {
|
|
167
|
+
if (!INSTALL_METHODS.has(entry.install)) {
|
|
168
|
+
problems.push(
|
|
169
|
+
`remoteEnv install ${label} has method ${JSON.stringify(entry.install)}. ` +
|
|
170
|
+
`Supported: ${[...INSTALL_METHODS].join(", ")}.`
|
|
171
|
+
);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// Both archive kinds, not just zip. A tarball pinned without a checksum
|
|
175
|
+
// trusts whatever the URL serves today exactly as much as a zip does, and
|
|
176
|
+
// checking only one of them meant the tool Lisa's guardrails shell out to —
|
|
177
|
+
// gh, which ships a tarball on Linux — was the one going unverified.
|
|
178
|
+
if (
|
|
179
|
+
(entry.install === "release-zip" || entry.install === "release-tar") &&
|
|
180
|
+
!(entry.url && entry.sha256)
|
|
181
|
+
) {
|
|
182
|
+
problems.push(
|
|
183
|
+
`remoteEnv install ${label} needs both url and sha256. A pinned ` +
|
|
184
|
+
`version with no checksum still trusts whatever the URL serves today.`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
if (entry.install === "npm-global" && !entry.package) {
|
|
188
|
+
problems.push(`remoteEnv install ${label} needs a package`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
146
192
|
/**
|
|
147
193
|
* Validate the `remoteEnv` block.
|
|
148
194
|
* @param {object|undefined} remoteEnv The block, if present.
|
|
@@ -163,21 +209,37 @@ export function validateRemoteEnv(remoteEnv) {
|
|
|
163
209
|
if (!tool.version) {
|
|
164
210
|
problems.push(`remoteEnv install "${tool.name}" has no pinned version`);
|
|
165
211
|
}
|
|
166
|
-
|
|
212
|
+
// A flat entry means one artifact serves every platform — true of npm-global
|
|
213
|
+
// and of nothing else. Anything downloaded is platform-specific, so those
|
|
214
|
+
// declare a block per platform and each block is validated in full: a
|
|
215
|
+
// half-filled map is the failure this shape exists to prevent, and it is
|
|
216
|
+
// invisible until someone runs setup on the platform that was left out.
|
|
217
|
+
if (tool.platforms === undefined) {
|
|
218
|
+
validateInstallArtifact(`"${tool.name}"`, tool, problems);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (
|
|
222
|
+
typeof tool.platforms !== "object" ||
|
|
223
|
+
tool.platforms === null ||
|
|
224
|
+
Array.isArray(tool.platforms)
|
|
225
|
+
) {
|
|
167
226
|
problems.push(
|
|
168
|
-
`remoteEnv install "${tool.name}" has
|
|
169
|
-
`
|
|
227
|
+
`remoteEnv install "${tool.name}" has a platforms field that is not an ` +
|
|
228
|
+
`object keyed by <platform>-<arch>. Omit it when one artifact serves ` +
|
|
229
|
+
`every platform.`
|
|
170
230
|
);
|
|
171
231
|
continue;
|
|
172
232
|
}
|
|
173
|
-
|
|
233
|
+
const platforms = Object.entries(tool.platforms);
|
|
234
|
+
if (platforms.length === 0) {
|
|
174
235
|
problems.push(
|
|
175
|
-
`remoteEnv install "${tool.name}"
|
|
176
|
-
`
|
|
236
|
+
`remoteEnv install "${tool.name}" declares platforms but lists none, so ` +
|
|
237
|
+
`it can never be installed anywhere.`
|
|
177
238
|
);
|
|
239
|
+
continue;
|
|
178
240
|
}
|
|
179
|
-
|
|
180
|
-
|
|
241
|
+
for (const [platform, block] of platforms) {
|
|
242
|
+
validateInstallArtifact(`"${tool.name}" (${platform})`, block, problems);
|
|
181
243
|
}
|
|
182
244
|
}
|
|
183
245
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lisa-setup-local-env
|
|
3
|
+
description: "Bring a developer's machine in line with the toolchain the project declares. Reports every tool in remoteEnv.tools that is missing, outdated, or unpinned for this platform, and installs the missing ones into ~/.local/bin from the same pinned, checksummed entries the remote surfaces use — but only when asked. Same manifest, same pins, same installers as lisa-setup-remote-env; what differs is consent and that the pin is a floor rather than an equality. Run it on a fresh checkout, after a manifest change, or when a tool fails at the moment of use."
|
|
4
|
+
allowed-tools: ["Bash", "Read", "Skill"]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Setup Local Environment: $ARGUMENTS
|
|
8
|
+
|
|
9
|
+
Make this machine match what the project says it needs.
|
|
10
|
+
|
|
11
|
+
## Why this exists separately
|
|
12
|
+
|
|
13
|
+
`remoteEnv.tools` was always the manifest for **every** surface, and the executor always
|
|
14
|
+
understood `local`. What did not exist was a way to ask for it. The only path ran a file named
|
|
15
|
+
`setup-remote-env.mjs`, out of a directory named `lisa-remote-env`, via a skill named
|
|
16
|
+
`lisa-setup-remote-env` — and it only existed at all once someone had provisioned a *remote*
|
|
17
|
+
environment, because that is what writes it into the repository.
|
|
18
|
+
|
|
19
|
+
So the machine most likely to be missing a tool was the one with no way to ask about it.
|
|
20
|
+
|
|
21
|
+
This skill is that surface. It adds no manifest, no pins, and no installer: it imports the
|
|
22
|
+
ones the remote flow owns. A second installer would be a second thing to keep honest, and the
|
|
23
|
+
one people run least is the one that rots.
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
node scripts/local-env.mjs # report what diverges
|
|
29
|
+
node scripts/local-env.mjs --install-tools # install what is missing
|
|
30
|
+
node scripts/local-env.mjs --json # machine-readable plan
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Exit status is non-zero when a tool cannot be resolved, so this is usable as a gate.
|
|
34
|
+
|
|
35
|
+
## What differs from a container
|
|
36
|
+
|
|
37
|
+
Two things, and both are because a person is here.
|
|
38
|
+
|
|
39
|
+
**Consent.** A container is disposable and provisions itself silently — that is the whole
|
|
40
|
+
point of it. A laptop belongs to someone, and putting pinned binaries into their
|
|
41
|
+
`~/.local/bin` uninvited is not ours to decide. Nothing installs until `--install-tools`.
|
|
42
|
+
Reporting happens either way, because knowing the machine diverges from what the project
|
|
43
|
+
declares is most of the value.
|
|
44
|
+
|
|
45
|
+
**The pin is a floor, not an equality.** A container should hold exactly the pinned version;
|
|
46
|
+
it exists to be reproducible. A developer's machine is shared with every other project they
|
|
47
|
+
work on, so installing a pinned binary ahead of a **newer** one already on `PATH` is a
|
|
48
|
+
downgrade this project imposed on all of them. A newer tool is reported as `newer` and left
|
|
49
|
+
alone. Older, or absent, is an install.
|
|
50
|
+
|
|
51
|
+
## Platform pins
|
|
52
|
+
|
|
53
|
+
An install entry either serves every platform — true of `npm-global` and nothing else — or
|
|
54
|
+
declares a `platforms` map keyed `<platform>-<arch>`, each block carrying its own `install`
|
|
55
|
+
method, `url`, and `sha256`:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"name": "gh",
|
|
60
|
+
"version": "2.83.0",
|
|
61
|
+
"platforms": {
|
|
62
|
+
"linux-x64": { "install": "release-tar", "url": "...linux_amd64.tar.gz", "sha256": "...", "binary": "gh_2.83.0_linux_amd64/bin/gh" },
|
|
63
|
+
"darwin-arm64": { "install": "release-zip", "url": "...macOS_arm64.zip", "sha256": "...", "binary": "gh_2.83.0_macOS_arm64/bin/gh" }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The method lives **inside** each block because it varies: gh publishes a `.tar.gz` for Linux
|
|
69
|
+
and a `.zip` for macOS, so a single method would have forced one platform onto an archive kind
|
|
70
|
+
its vendor does not ship.
|
|
71
|
+
|
|
72
|
+
When a tool has no block for the running platform, this skill says so and stops there. It will
|
|
73
|
+
not guess a download URL — a guessed artifact is one the checksum cannot vouch for, and the
|
|
74
|
+
checksum is the only thing standing between a pinned entry and whatever a URL serves today.
|
|
75
|
+
Add the block, or install that one tool however the platform normally would.
|
|
76
|
+
|
|
77
|
+
## Detect before you install
|
|
78
|
+
|
|
79
|
+
Run `/lisa:detect-tooling` when the report looks thin. The manifest is the only thing that puts
|
|
80
|
+
a binary on `PATH`, and nothing populates it automatically — so a project ships npm scripts
|
|
81
|
+
invoking `maestro`, wires an MCP server whose CLI it also shells out to, and configures
|
|
82
|
+
Playwright thresholds, while `remoteEnv.tools` stays empty and each of those fails at the
|
|
83
|
+
moment of use instead of at setup.
|
|
84
|
+
|
|
85
|
+
The detector proposes and a human decides. It writes nothing, so a tool still only ever reaches
|
|
86
|
+
a machine from a reviewed, pinned, checksummed entry.
|
|
87
|
+
|
|
88
|
+
## What it will not do
|
|
89
|
+
|
|
90
|
+
- **No unpinned installs.** Every install goes through the same `assertPinned` gate the remote
|
|
91
|
+
flow uses, and the same in-process checksum verification, which refuses before unpacking.
|
|
92
|
+
- **No package-manager fallback.** Shelling out to Homebrew would give no version pin and no
|
|
93
|
+
checksum, which is the property this design exists to enforce.
|
|
94
|
+
- **No writes to the manifest.** Proposing entries belongs to `/lisa:detect-tooling`; deciding
|
|
95
|
+
belongs to a human reviewing a diff.
|