@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
package/package.json
CHANGED
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
"brace-expansion": ">=5.0.9"
|
|
116
116
|
},
|
|
117
117
|
"name": "@codyswann/lisa",
|
|
118
|
-
"version": "2.
|
|
118
|
+
"version": "2.328.0",
|
|
119
119
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
120
120
|
"main": "dist/index.js",
|
|
121
121
|
"exports": {
|
|
@@ -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…"
|
|
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.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Bring a developer's machine in line with the toolchain the project declares.
|
|
4
|
+
*
|
|
5
|
+
* The manifest, the planner, the pins and the installers are the remote flow's,
|
|
6
|
+
* unchanged and imported — this is the surface that was missing, not a second
|
|
7
|
+
* implementation. `remoteEnv.tools` was always surface-aware and the executor
|
|
8
|
+
* always understood `local`; the only way to reach either was to run a file
|
|
9
|
+
* called `setup-remote-env.mjs` out of a directory called `lisa-remote-env`,
|
|
10
|
+
* which nobody looking for local setup ever found.
|
|
11
|
+
*
|
|
12
|
+
* Two things differ from the remote flow, and both are because a person is here:
|
|
13
|
+
*
|
|
14
|
+
* consent — a container provisions itself silently because it is disposable
|
|
15
|
+
* and nobody is watching. A laptop belongs to someone, so nothing
|
|
16
|
+
* is installed until they ask with --install-tools.
|
|
17
|
+
*
|
|
18
|
+
* tolerance — a container that cannot be fully provisioned should fail before
|
|
19
|
+
* it half-runs, because there is no one to read the report. A
|
|
20
|
+
* developer can act on a list, so an unprovisionable tool is
|
|
21
|
+
* reported, everything else is still installed, and the exit code
|
|
22
|
+
* carries the failure.
|
|
23
|
+
* @module local-env
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { existsSync } from "node:fs";
|
|
27
|
+
import { dirname, resolve } from "node:path";
|
|
28
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
29
|
+
|
|
30
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a script belonging to a sibling skill.
|
|
34
|
+
*
|
|
35
|
+
* Skills are always siblings under one `skills/` directory, in every install
|
|
36
|
+
* layout, so this holds for a plugin install, a checkout, and node_modules
|
|
37
|
+
* alike. Failing loudly here beats reimplementing what the sibling owns.
|
|
38
|
+
* @param {string} skill Sibling skill slug.
|
|
39
|
+
* @param {string} script Script filename.
|
|
40
|
+
* @returns {string} Absolute path.
|
|
41
|
+
*/
|
|
42
|
+
function siblingScript(skill, script) {
|
|
43
|
+
const path = resolve(HERE, "..", "..", skill, "scripts", script);
|
|
44
|
+
if (!existsSync(path)) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`cannot find ${skill}/scripts/${script} beside this skill.\n` +
|
|
47
|
+
`Local setup composes with that skill rather than reimplementing it: ` +
|
|
48
|
+
`the pins, checksums and installers must be the same ones the remote ` +
|
|
49
|
+
`surface uses, or the two drift and only one gets fixed.`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return path;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Load the pieces the remote flow owns.
|
|
57
|
+
* @returns {Promise<object>} The imported helpers.
|
|
58
|
+
*/
|
|
59
|
+
async function loadRemoteEnv() {
|
|
60
|
+
const setup = await import(
|
|
61
|
+
pathToFileURL(
|
|
62
|
+
siblingScript("lisa-setup-remote-env", "setup-remote-env.mjs")
|
|
63
|
+
).href
|
|
64
|
+
);
|
|
65
|
+
const toolchain = await import(
|
|
66
|
+
pathToFileURL(siblingScript("lisa-setup-remote-env", "toolchain.mjs")).href
|
|
67
|
+
);
|
|
68
|
+
return { ...toolchain, ...setup };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Describe one blocked decision in terms a person can act on.
|
|
73
|
+
*
|
|
74
|
+
* A blocked step on a laptop is usually one of two things, and they are fixed
|
|
75
|
+
* differently: a tool the project requires and the machine lacks (install it),
|
|
76
|
+
* or a tool with no artifact pinned for this platform (add the pin, or install
|
|
77
|
+
* it however this platform normally would). Printing the planner's reason alone
|
|
78
|
+
* left the reader to work out which.
|
|
79
|
+
* @param {object} step A plan step.
|
|
80
|
+
* @param {string} platform The platform key that was resolved against.
|
|
81
|
+
* @returns {string} A rendered block.
|
|
82
|
+
*/
|
|
83
|
+
function explainBlocked(step, platform) {
|
|
84
|
+
if (step.action === "invalid" && step.reason.includes("no pin for")) {
|
|
85
|
+
return (
|
|
86
|
+
` ${step.name} — declared, but no artifact is pinned for ${platform}.\n` +
|
|
87
|
+
` Lisa will not guess a download URL, because a guessed artifact is ` +
|
|
88
|
+
`one the checksum cannot vouch for.\n` +
|
|
89
|
+
` Add a "${platform}" block under this tool's "platforms" map, or ` +
|
|
90
|
+
`install it yourself.`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return ` ${step.name} — ${step.reason.split("\n")[0]}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Report and optionally apply the local toolchain.
|
|
98
|
+
* @param {object} argv Parsed arguments.
|
|
99
|
+
* @returns {Promise<number>} Process exit code.
|
|
100
|
+
*/
|
|
101
|
+
export async function run(argv) {
|
|
102
|
+
const env = await loadRemoteEnv();
|
|
103
|
+
// Both seams exist for tests, and both default to this machine. A platform
|
|
104
|
+
// that can only be exercised by running on it is one CI will never cover,
|
|
105
|
+
// which is how a resolution bug ships to the platform nobody tested.
|
|
106
|
+
const platform = argv.platform ?? env.currentPlatform();
|
|
107
|
+
const { tools } = env.readRemoteEnvConfig(argv.cwd);
|
|
108
|
+
|
|
109
|
+
const declared = (tools.require ?? []).length + (tools.install ?? []).length;
|
|
110
|
+
if (declared === 0) {
|
|
111
|
+
console.log(
|
|
112
|
+
"No tools are declared in remoteEnv.tools, so there is nothing to check.\n" +
|
|
113
|
+
"Run /lisa:detect-tooling — it reads npm scripts, MCP servers, " +
|
|
114
|
+
"credential\nnotes and quality config, and proposes pinned entries for " +
|
|
115
|
+
"what it finds."
|
|
116
|
+
);
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const plan = env.planToolchain(tools, env.probe, "local", platform);
|
|
121
|
+
const blocked = plan.filter(
|
|
122
|
+
step => step.action === "missing" || step.action === "invalid"
|
|
123
|
+
);
|
|
124
|
+
const installable = plan.filter(step => step.action === "install");
|
|
125
|
+
|
|
126
|
+
if (argv.json) {
|
|
127
|
+
console.log(JSON.stringify({ platform, plan }, null, 2));
|
|
128
|
+
return blocked.length ? 1 : 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log(`Local toolchain for ${platform}\n`);
|
|
132
|
+
for (const step of plan) {
|
|
133
|
+
console.log(` ${step.action.padEnd(8)} ${step.reason.split("\n")[0]}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (installable.length && !argv.installTools) {
|
|
137
|
+
console.log(
|
|
138
|
+
`\n${installable.length} declared tool(s) can be installed here.\n` +
|
|
139
|
+
`Nothing has been installed: this machine is yours, not a container.\n` +
|
|
140
|
+
`Re-run with --install-tools to provision them into ~/.local/bin.`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (installable.length && argv.installTools) {
|
|
145
|
+
const binDir = env.ensureBinDir();
|
|
146
|
+
console.log(`\nInstalling into ${binDir}`);
|
|
147
|
+
for (const step of installable) {
|
|
148
|
+
console.log(` ${step.name}`);
|
|
149
|
+
if (!argv.dryRun) env.installTool(step.tool, binDir);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (blocked.length) {
|
|
154
|
+
console.log(`\n${blocked.length} tool(s) this command cannot resolve:\n`);
|
|
155
|
+
for (const step of blocked) console.log(explainBlocked(step, platform));
|
|
156
|
+
return 1;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
163
|
+
const argv = {
|
|
164
|
+
installTools: process.argv.includes("--install-tools"),
|
|
165
|
+
dryRun: process.argv.includes("--dry-run"),
|
|
166
|
+
json: process.argv.includes("--json"),
|
|
167
|
+
};
|
|
168
|
+
run(argv)
|
|
169
|
+
.then(code => process.exit(code))
|
|
170
|
+
.catch(err => {
|
|
171
|
+
console.error(err.message);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
@@ -95,6 +95,19 @@ This repository has paid for that twice: `gh` was declared nowhere and a cloud s
|
|
|
95
95
|
|
|
96
96
|
The detector proposes and a human decides. It writes nothing, so provisioning still only ever happens from a reviewed, pinned, checksummed entry.
|
|
97
97
|
|
|
98
|
+
## The same manifest provisions a laptop
|
|
99
|
+
|
|
100
|
+
`remoteEnv.tools` is not a remote-only manifest, and `/lisa:setup:local-env` is how a developer
|
|
101
|
+
applies it to their own machine — same pins, same checksums, same installers, differing only in
|
|
102
|
+
that nothing installs without `--install-tools` and that a newer tool already on `PATH` is left
|
|
103
|
+
alone rather than downgraded.
|
|
104
|
+
|
|
105
|
+
A downloaded tool declares a `platforms` map keyed `<platform>-<arch>`, each block carrying its
|
|
106
|
+
own `install` method, `url`, and `sha256`, because the artifact and the archive kind both vary
|
|
107
|
+
by platform. Before that existed, the only way to keep a Linux binary off a laptop was
|
|
108
|
+
`surfaces: ["remote"]`, which achieved it by making the tool uninstallable there — so `bws` and
|
|
109
|
+
`gh` were required on developer machines and provisionable only in containers.
|
|
110
|
+
|
|
98
111
|
## Toolchain manifest — two entry kinds
|
|
99
112
|
|
|
100
113
|
```json
|
|
@@ -108,20 +121,23 @@ The detector proposes and a human decides. It writes nothing, so provisioning st
|
|
|
108
121
|
{ "name": "unzip" }
|
|
109
122
|
],
|
|
110
123
|
"install": [
|
|
111
|
-
{
|
|
112
|
-
"name": "bws",
|
|
113
|
-
"version": "2.1.0",
|
|
114
|
-
"install": "release-zip",
|
|
115
|
-
"url": "https://<vendor>/releases/download/bws-v2.1.0/bws-<platform>-2.1.0.zip",
|
|
116
|
-
"sha256": "<sha256 published with that exact release>"
|
|
117
|
-
},
|
|
118
124
|
{
|
|
119
125
|
"name": "gh",
|
|
120
126
|
"version": "2.83.0",
|
|
121
|
-
"
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
127
|
+
"platforms": {
|
|
128
|
+
"linux-x64": {
|
|
129
|
+
"install": "release-tar",
|
|
130
|
+
"url": "https://<vendor>/releases/download/v2.83.0/gh_2.83.0_linux_amd64.tar.gz",
|
|
131
|
+
"sha256": "<sha256 published with that exact release>",
|
|
132
|
+
"binary": "gh_2.83.0_linux_amd64/bin/gh"
|
|
133
|
+
},
|
|
134
|
+
"darwin-arm64": {
|
|
135
|
+
"install": "release-zip",
|
|
136
|
+
"url": "https://<vendor>/releases/download/v2.83.0/gh_2.83.0_macOS_arm64.zip",
|
|
137
|
+
"sha256": "<sha256 published with that exact release>",
|
|
138
|
+
"binary": "gh_2.83.0_macOS_arm64/bin/gh"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
125
141
|
},
|
|
126
142
|
{
|
|
127
143
|
"name": "codex",
|