@codyswann/lisa 2.328.4 → 2.329.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 +49 -2
- 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 +32 -2
- package/plugins/lisa/.codex-plugin/skills/lisa-detect-tooling/scripts/commands.mjs +479 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +170 -4
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/SKILL.md +121 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/agents/openai.yaml +4 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/scripts/catalogue.mjs +262 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/scripts/cli.mjs +315 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/scripts/workstation.mjs +403 -0
- package/plugins/lisa/commands/setup/workstation.md +7 -0
- package/plugins/lisa/skills/lisa-detect-tooling/SKILL.md +33 -3
- package/plugins/lisa/skills/lisa-detect-tooling/scripts/commands.mjs +479 -0
- package/plugins/lisa/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +170 -4
- package/plugins/lisa/skills/lisa-setup-workstation/SKILL.md +121 -0
- package/plugins/lisa/skills/lisa-setup-workstation/agents/openai.yaml +4 -0
- package/plugins/lisa/skills/lisa-setup-workstation/scripts/catalogue.mjs +262 -0
- package/plugins/lisa/skills/lisa-setup-workstation/scripts/cli.mjs +315 -0
- package/plugins/lisa/skills/lisa-setup-workstation/scripts/workstation.mjs +403 -0
- package/plugins/lisa-agy/commands/lisa/setup/workstation.md +7 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-detect-tooling/SKILL.md +33 -3
- package/plugins/lisa-agy/skills/lisa-detect-tooling/scripts/commands.mjs +479 -0
- package/plugins/lisa-agy/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +170 -4
- package/plugins/lisa-agy/skills/lisa-setup-workstation/SKILL.md +121 -0
- package/plugins/lisa-agy/skills/lisa-setup-workstation/scripts/catalogue.mjs +262 -0
- package/plugins/lisa-agy/skills/lisa-setup-workstation/scripts/cli.mjs +315 -0
- package/plugins/lisa-agy/skills/lisa-setup-workstation/scripts/workstation.mjs +403 -0
- 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/workstation.md +7 -0
- package/plugins/lisa-copilot/skills/lisa-detect-tooling/SKILL.md +33 -3
- package/plugins/lisa-copilot/skills/lisa-detect-tooling/scripts/commands.mjs +479 -0
- package/plugins/lisa-copilot/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +170 -4
- package/plugins/lisa-copilot/skills/lisa-setup-workstation/SKILL.md +121 -0
- package/plugins/lisa-copilot/skills/lisa-setup-workstation/scripts/catalogue.mjs +262 -0
- package/plugins/lisa-copilot/skills/lisa-setup-workstation/scripts/cli.mjs +315 -0
- package/plugins/lisa-copilot/skills/lisa-setup-workstation/scripts/workstation.mjs +403 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/commands/lisa/setup/workstation.md +7 -0
- package/plugins/lisa-cursor/skills/lisa-detect-tooling/SKILL.md +33 -3
- package/plugins/lisa-cursor/skills/lisa-detect-tooling/scripts/commands.mjs +479 -0
- package/plugins/lisa-cursor/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +170 -4
- package/plugins/lisa-cursor/skills/lisa-setup-workstation/SKILL.md +121 -0
- package/plugins/lisa-cursor/skills/lisa-setup-workstation/scripts/catalogue.mjs +262 -0
- package/plugins/lisa-cursor/skills/lisa-setup-workstation/scripts/cli.mjs +315 -0
- package/plugins/lisa-cursor/skills/lisa-setup-workstation/scripts/workstation.mjs +403 -0
- 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/workstation.md +7 -0
- package/plugins/src/base/skills/lisa-detect-tooling/SKILL.md +33 -3
- package/plugins/src/base/skills/lisa-detect-tooling/scripts/commands.mjs +479 -0
- package/plugins/src/base/skills/lisa-detect-tooling/scripts/detect-tooling.mjs +170 -4
- package/plugins/src/base/skills/lisa-setup-workstation/SKILL.md +121 -0
- package/plugins/src/base/skills/lisa-setup-workstation/scripts/catalogue.mjs +262 -0
- package/plugins/src/base/skills/lisa-setup-workstation/scripts/cli.mjs +315 -0
- package/plugins/src/base/skills/lisa-setup-workstation/scripts/workstation.mjs +403 -0
|
@@ -22,9 +22,11 @@
|
|
|
22
22
|
* @module detect-tooling
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
25
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
26
26
|
import { join } from "node:path";
|
|
27
27
|
|
|
28
|
+
import { commandsIn } from "./commands.mjs";
|
|
29
|
+
|
|
28
30
|
/** The tool named by the Expo template's scripts and its flows directory. */
|
|
29
31
|
const MAESTRO = "maestro";
|
|
30
32
|
|
|
@@ -109,6 +111,128 @@ export function toolsFromScripts(pkg) {
|
|
|
109
111
|
return found;
|
|
110
112
|
}
|
|
111
113
|
|
|
114
|
+
/** Where git hooks live, strongest signal first. */
|
|
115
|
+
const HOOK_DIRECTORIES = [".husky", join(".git", "hooks")];
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Coding agents, which a PROJECT manifest must never try to provision.
|
|
119
|
+
*
|
|
120
|
+
* A hook that shells out to `claude` is real evidence of a real dependency, but
|
|
121
|
+
* `remoteEnv.tools` is the wrong place to answer it: agents belong to the
|
|
122
|
+
* machine, not the checkout, and each is installed by its vendor's own method
|
|
123
|
+
* into a version directory it manages itself. Pinning one as a release archive
|
|
124
|
+
* would fight the self-updater that owns that directory.
|
|
125
|
+
*
|
|
126
|
+
* That layer is `lisa-setup-workstation`. Named here rather than imported from
|
|
127
|
+
* it, because a detector that stops working when a sibling skill is absent is
|
|
128
|
+
* worse than one carrying six strings.
|
|
129
|
+
*
|
|
130
|
+
* Deliberately NOT extended to `gh`, `bws`, `aws` or `sonar`: those genuinely
|
|
131
|
+
* belong in a project manifest, and `gh` going undeclared is the incident this
|
|
132
|
+
* skill was written after.
|
|
133
|
+
*/
|
|
134
|
+
const WORKSTATION_PROVISIONED = new Set([
|
|
135
|
+
"claude",
|
|
136
|
+
"codex",
|
|
137
|
+
"cursor-agent",
|
|
138
|
+
"opencode",
|
|
139
|
+
"agy",
|
|
140
|
+
"copilot",
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Tools a git hook runs — the strongest signal a project can give.
|
|
145
|
+
*
|
|
146
|
+
* Stronger than an npm script, because a hook runs on EVERY commit and push
|
|
147
|
+
* whether anyone asked or not. A container missing one of these cannot commit at
|
|
148
|
+
* all; the failure arrives mid-task, unattended, at the moment of use.
|
|
149
|
+
*
|
|
150
|
+
* The worst shape is the guarded one. This repository's pre-commit hook wraps
|
|
151
|
+
* its secret scan in `command -v gitleaks`, so on a machine without gitleaks the
|
|
152
|
+
* scan is SKIPPED SILENTLY — the hook passes, the commit succeeds, and nothing
|
|
153
|
+
* was scanned. A guard makes the absence invisible exactly where it matters
|
|
154
|
+
* most, which is why a probed name counts as evidence rather than being read as
|
|
155
|
+
* "optional".
|
|
156
|
+
* @param {string} cwd Project root.
|
|
157
|
+
* @returns {Map<string, string>} Tool name to the hook that proves it.
|
|
158
|
+
*/
|
|
159
|
+
export function toolsFromGitHooks(cwd) {
|
|
160
|
+
const found = new Map();
|
|
161
|
+
for (const directory of HOOK_DIRECTORIES) {
|
|
162
|
+
const full = join(cwd, directory);
|
|
163
|
+
let names = [];
|
|
164
|
+
try {
|
|
165
|
+
names = readdirSync(full);
|
|
166
|
+
} catch {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
for (const name of names.sort()) {
|
|
170
|
+
// `.sample` files ship with git and run nothing.
|
|
171
|
+
if (name.endsWith(".sample")) continue;
|
|
172
|
+
let body;
|
|
173
|
+
try {
|
|
174
|
+
body = readFileSync(join(full, name), "utf8");
|
|
175
|
+
} catch {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
for (const tool of commandsIn(body)) {
|
|
179
|
+
if (!found.has(tool)) found.set(tool, `git hook ${directory}/${name}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return found;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Tools an npm script invokes, discovered rather than matched against a list.
|
|
188
|
+
*
|
|
189
|
+
* `toolsFromScripts` answers "which of the tools I already know about appear
|
|
190
|
+
* here". This answers the open question — what does this project actually RUN —
|
|
191
|
+
* which is the one that finds a tool nobody thought to add. An Expo project
|
|
192
|
+
* invoking `eas` from eight scripts produced nothing under the old shape,
|
|
193
|
+
* because `eas` was not in `KNOWN_TOOLS`.
|
|
194
|
+
* @param {object|null} pkg Parsed package.json.
|
|
195
|
+
* @returns {Map<string, string>} Tool name to the script that proves it.
|
|
196
|
+
*/
|
|
197
|
+
export function toolsDiscoveredInScripts(pkg) {
|
|
198
|
+
const found = new Map();
|
|
199
|
+
for (const [name, body] of Object.entries(pkg?.scripts ?? {})) {
|
|
200
|
+
if (typeof body !== "string") continue;
|
|
201
|
+
for (const tool of commandsIn(body)) {
|
|
202
|
+
if (found.has(tool)) continue;
|
|
203
|
+
found.set(tool, `npm script "${name}": ${body.trim().slice(0, 60)}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return found;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Whether the package manager already puts this binary on the local path.
|
|
211
|
+
*
|
|
212
|
+
* A dependency's binary lands in `node_modules/.bin`, where npm scripts resolve
|
|
213
|
+
* it without it ever being on PATH. Proposing a manifest entry for one is noise,
|
|
214
|
+
* and noise is how a detector teaches people to skim it.
|
|
215
|
+
*
|
|
216
|
+
* Only meaningful once dependencies are installed, which is why
|
|
217
|
+
* `dependenciesInstalled` exists rather than this quietly returning false on a
|
|
218
|
+
* fresh clone and over-proposing every dev tool in the project.
|
|
219
|
+
* @param {string} cwd Project root.
|
|
220
|
+
* @param {string} tool Tool name.
|
|
221
|
+
* @returns {boolean} Whether npm already provides it.
|
|
222
|
+
*/
|
|
223
|
+
export function providedByNodeModules(cwd, tool) {
|
|
224
|
+
return existsSync(join(cwd, "node_modules", ".bin", tool));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Whether `node_modules` is populated, so its absence can be reported.
|
|
229
|
+
* @param {string} cwd Project root.
|
|
230
|
+
* @returns {boolean} Whether dependencies are installed.
|
|
231
|
+
*/
|
|
232
|
+
export function dependenciesInstalled(cwd) {
|
|
233
|
+
return existsSync(join(cwd, "node_modules", ".bin"));
|
|
234
|
+
}
|
|
235
|
+
|
|
112
236
|
/**
|
|
113
237
|
* MCP servers that are access substrates rather than evidence for a CLI.
|
|
114
238
|
*
|
|
@@ -330,8 +454,14 @@ export function detectTooling(cwd = process.cwd()) {
|
|
|
330
454
|
const notes = readJson(join(cwd, ".lisa", "secret-notes.json"));
|
|
331
455
|
|
|
332
456
|
const declared = declaredTools(config);
|
|
457
|
+
// Curated signals carry a vetted `why`. Discovered ones carry only the fact
|
|
458
|
+
// that the project runs the thing — weaker prose, identical standing as
|
|
459
|
+
// evidence, and the only kind that can find a tool nobody listed.
|
|
460
|
+
const hooks = toolsFromGitHooks(cwd);
|
|
333
461
|
const signals = [
|
|
462
|
+
hooks,
|
|
334
463
|
toolsFromScripts(pkg),
|
|
464
|
+
toolsDiscoveredInScripts(pkg),
|
|
335
465
|
toolsFromMcp(mcp),
|
|
336
466
|
toolsFromSecretNotes(notes),
|
|
337
467
|
toolsFromQuality(config, pkg, cwd),
|
|
@@ -343,8 +473,11 @@ export function detectTooling(cwd = process.cwd()) {
|
|
|
343
473
|
if (declared.has(tool)) continue;
|
|
344
474
|
// Already provided by the package manager, so there is nothing to pin.
|
|
345
475
|
if (satisfiedByNpm(pkg, tool)) continue;
|
|
476
|
+
if (providedByNodeModules(cwd, tool)) continue;
|
|
477
|
+
// Real dependency, wrong manifest — the workstation layer owns it.
|
|
478
|
+
if (WORKSTATION_PROVISIONED.has(tool)) continue;
|
|
346
479
|
const entry = merged.get(tool) ?? { evidence: [] };
|
|
347
|
-
entry.evidence.push(evidence);
|
|
480
|
+
if (!entry.evidence.includes(evidence)) entry.evidence.push(evidence);
|
|
348
481
|
merged.set(tool, entry);
|
|
349
482
|
}
|
|
350
483
|
}
|
|
@@ -352,12 +485,35 @@ export function detectTooling(cwd = process.cwd()) {
|
|
|
352
485
|
return [...merged.entries()]
|
|
353
486
|
.map(([name, entry]) => ({
|
|
354
487
|
name,
|
|
355
|
-
why: KNOWN_TOOLS[name]?.why ??
|
|
488
|
+
why: KNOWN_TOOLS[name]?.why ?? discoveredWhy(name, hooks.has(name)),
|
|
356
489
|
evidence: entry.evidence,
|
|
490
|
+
// Says which vocabulary the `why` came from, so a reader can weigh it.
|
|
491
|
+
// A curated entry has been thought about; a discovered one has only been
|
|
492
|
+
// observed.
|
|
493
|
+
source: Object.hasOwn(KNOWN_TOOLS, name) ? "curated" : "discovered",
|
|
357
494
|
}))
|
|
358
495
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
359
496
|
}
|
|
360
497
|
|
|
498
|
+
/**
|
|
499
|
+
* The `why` for a tool nobody wrote prose for.
|
|
500
|
+
*
|
|
501
|
+
* Every proposal must carry one — a proposal without a reason is an assertion,
|
|
502
|
+
* and this program's whole boundary is that it asserts nothing. So a discovered
|
|
503
|
+
* tool states the observation plainly rather than borrowing confidence it has
|
|
504
|
+
* not earned.
|
|
505
|
+
* @param {string} name Tool name.
|
|
506
|
+
* @param {boolean} fromHook Whether a git hook runs it.
|
|
507
|
+
* @returns {string} A reason a reader can check.
|
|
508
|
+
*/
|
|
509
|
+
export function discoveredWhy(name, fromHook) {
|
|
510
|
+
return fromHook
|
|
511
|
+
? `A git hook runs \`${name}\` on every commit or push, and nothing ` +
|
|
512
|
+
`declares it — a machine without it fails at commit time, or skips the ` +
|
|
513
|
+
`check silently if the hook guards it.`
|
|
514
|
+
: `An npm script invokes \`${name}\`, and nothing puts it on PATH.`;
|
|
515
|
+
}
|
|
516
|
+
|
|
361
517
|
/**
|
|
362
518
|
* The manifest entries an operator can paste, with the parts only they supply.
|
|
363
519
|
*
|
|
@@ -438,8 +594,18 @@ if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) {
|
|
|
438
594
|
console.log(
|
|
439
595
|
`${proposals.length} tool(s) look required but are not declared:\n`
|
|
440
596
|
);
|
|
597
|
+
if (!dependenciesInstalled(process.cwd())) {
|
|
598
|
+
// Without node_modules there is no way to tell a tool that needs PATH
|
|
599
|
+
// from one npm already provides, so every dev dependency looks missing.
|
|
600
|
+
// Said out loud rather than silently over-proposing.
|
|
601
|
+
console.log(
|
|
602
|
+
"note: dependencies are not installed, so binaries npm would provide " +
|
|
603
|
+
"cannot be\n excluded. Run the install first for a shorter, " +
|
|
604
|
+
"more accurate list.\n"
|
|
605
|
+
);
|
|
606
|
+
}
|
|
441
607
|
for (const proposal of proposals) {
|
|
442
|
-
console.log(` ${proposal.name}`);
|
|
608
|
+
console.log(` ${proposal.name} [${proposal.source}]`);
|
|
443
609
|
console.log(` why: ${proposal.why}`);
|
|
444
610
|
for (const evidence of proposal.evidence) {
|
|
445
611
|
console.log(` evidence: ${evidence}`);
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lisa-setup-workstation
|
|
3
|
+
description: "Prepare a machine — a fresh…"
|
|
4
|
+
allowed-tools: ["Bash", "Read", "Skill"]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Setup Workstation: $ARGUMENTS
|
|
8
|
+
|
|
9
|
+
Get a machine ready to run agents. No checkout required.
|
|
10
|
+
|
|
11
|
+
## Why this exists separately
|
|
12
|
+
|
|
13
|
+
`lisa-setup-local-env` reads `remoteEnv.tools` — a **project's** declared toolchain — so it
|
|
14
|
+
needs a checkout to exist before it can answer anything. That leaves a gap at the start:
|
|
15
|
+
a bare laptop or an empty container has no repository yet, and therefore nothing to ask.
|
|
16
|
+
|
|
17
|
+
This skill fills that gap and stops there. It describes a **machine**, never a project:
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
workstation -> git clone -> lisa-detect-tooling -> lisa-setup-local-env
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Project-scoped tools stay in `remoteEnv.tools` where they belong. Nothing here reads a
|
|
24
|
+
repository, and nothing here duplicates that manifest.
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
node scripts/cli.mjs # report what is present and missing
|
|
30
|
+
node scripts/cli.mjs --install # install what is missing
|
|
31
|
+
node scripts/cli.mjs --install --agents=claude,codex # only these agents
|
|
32
|
+
node scripts/cli.mjs --provider=bitwarden # credential manager (asked if a TTY)
|
|
33
|
+
node scripts/cli.mjs --json # machine-readable plan
|
|
34
|
+
node scripts/cli.mjs --print-dockerfile # an image that runs this same script
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Nothing is installed or written without `--install`. Exit status is non-zero when a required
|
|
38
|
+
tool is absent or an install fails, so this is usable as a gate.
|
|
39
|
+
|
|
40
|
+
## Ephemerality is not a mode
|
|
41
|
+
|
|
42
|
+
There is no Docker branch anywhere in this skill. A container gets a throwaway `$HOME` and
|
|
43
|
+
dies with it; a laptop keeps its own. Same code path either way — so the rarely-run one
|
|
44
|
+
cannot rot, which is the failure mode a second install path eventually always has.
|
|
45
|
+
|
|
46
|
+
`--print-dockerfile` emits an image rather than shipping a committed one, so the image cannot
|
|
47
|
+
drift from the catalogue it provisions. It deliberately bakes in **no repository**: clone
|
|
48
|
+
inside the container, then run `lisa-detect-tooling` and `lisa-setup-local-env` for that
|
|
49
|
+
project's own tools.
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
node scripts/cli.mjs --print-dockerfile --agents=claude,codex --provider=bitwarden > Dockerfile
|
|
53
|
+
docker build -t lisa-workstation . && docker run --rm -it lisa-workstation
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Already-installed wins, however it got there
|
|
57
|
+
|
|
58
|
+
A tool present by **any** means is reported and left alone. That dissolves the "Homebrew or
|
|
59
|
+
`~/.local/bin`" question entirely: we never contend for ownership of a tool someone else's
|
|
60
|
+
package manager installed, and never shadow it with an earlier PATH entry that would silently
|
|
61
|
+
win over their upgrades. The report names where each tool came from so the operator can see
|
|
62
|
+
why it was skipped.
|
|
63
|
+
|
|
64
|
+
Running twice installs nothing the second time, and says so rather than going quiet — silence
|
|
65
|
+
reads as "did nothing because it broke".
|
|
66
|
+
|
|
67
|
+
## Install method is a per-tool property, not a global policy
|
|
68
|
+
|
|
69
|
+
The obvious design — put every binary in `~/.local/bin` so one code path serves a laptop and a
|
|
70
|
+
container alike — is wrong, and the agents show why:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
claude -> ~/.local/share/claude/versions/2.1.221
|
|
74
|
+
cursor-agent -> ~/.local/share/cursor-agent/versions/2026.07.16-899851b
|
|
75
|
+
codex -> ~/.codex/packages/standalone/current/bin/codex
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Each vendor manages its own version directory, and `~/.local/bin` holds only a **symlink**.
|
|
79
|
+
Writing a raw binary there breaks the vendor's self-updater, which expects to swap a link
|
|
80
|
+
target and would instead find a real file in its way.
|
|
81
|
+
|
|
82
|
+
So each catalogue entry declares its own method, and the honest cost is recorded with it:
|
|
83
|
+
|
|
84
|
+
| kind | pinned | checksummed | notes |
|
|
85
|
+
| --- | --- | --- | --- |
|
|
86
|
+
| `release-zip` / `release-tar` | yes | yes | reuses the remote-env installer |
|
|
87
|
+
| `npm-global` | no | no | vendor registry |
|
|
88
|
+
| `vendor-script` | no | no | pipes a fetched script to a shell |
|
|
89
|
+
| `manual` | n/a | n/a | no headless installer published |
|
|
90
|
+
| `required` | n/a | n/a | expected from the OS or base image |
|
|
91
|
+
|
|
92
|
+
`vendor-script` is a real weakening of the guarantee `assertPinned` exists to provide, so the
|
|
93
|
+
report **names every unchecksummed install** rather than blending them in. Installing them
|
|
94
|
+
trusts the vendor's script at fetch time. The alternative — repackaging vendor binaries
|
|
95
|
+
ourselves — would break the self-updaters that own those directories.
|
|
96
|
+
|
|
97
|
+
## The credential manager is asked, never assumed
|
|
98
|
+
|
|
99
|
+
`lisa-secrets-access` already treats the provider as an axis: Bitwarden, 1Password, Doppler,
|
|
100
|
+
Vault, AWS, or plain environment variables. A bootstrap that installed `bws` unconditionally
|
|
101
|
+
would contradict that — pushing every workstation onto one vendor and quietly making the
|
|
102
|
+
others second-class.
|
|
103
|
+
|
|
104
|
+
So the provider is a question, `none` is a first-class answer, and only the selected one is
|
|
105
|
+
installed. Installing all of them would leave four unused credential CLIs on the machine,
|
|
106
|
+
each an extra thing to keep patched for no benefit.
|
|
107
|
+
|
|
108
|
+
Headless is the primary mode, so with no TTY and no `--provider`, the answer is `none` — not
|
|
109
|
+
a prompt that would hang a container build forever. A misspelled provider stops the run
|
|
110
|
+
instead of falling back, because quietly provisioning the wrong credential manager is worse
|
|
111
|
+
than stopping.
|
|
112
|
+
|
|
113
|
+
## What it will not do
|
|
114
|
+
|
|
115
|
+
- **Read a repository.** That is the next step, not this one.
|
|
116
|
+
- **Install a credential manager you did not pick.**
|
|
117
|
+
- **Reinstall over a tool that is already there**, whatever installed it.
|
|
118
|
+
- **Install anything without `--install`.**
|
|
119
|
+
- **Install `agy`.** Antigravity publishes no headless installer; it is reported as manual
|
|
120
|
+
with a pointer, because "not installed" and "cannot be installed by this tool" are
|
|
121
|
+
different answers to the operator.
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a workstation can hold, and how each piece is installed.
|
|
3
|
+
*
|
|
4
|
+
* This is the layer beneath `remoteEnv.tools`: it describes a MACHINE, not a
|
|
5
|
+
* project, so nothing here is read from a checkout and no repository needs to
|
|
6
|
+
* exist. The project-scoped manifest still owns project-scoped tools.
|
|
7
|
+
*
|
|
8
|
+
* ## Install method is a per-tool property, not a global policy
|
|
9
|
+
*
|
|
10
|
+
* The obvious design — put every binary in `~/.local/bin` so one code path
|
|
11
|
+
* serves a laptop and a container alike — is wrong, and the agents show why:
|
|
12
|
+
*
|
|
13
|
+
* claude -> ~/.local/share/claude/versions/2.1.221
|
|
14
|
+
* cursor-agent -> ~/.local/share/cursor-agent/versions/2026.07.16-899851b
|
|
15
|
+
* codex -> ~/.codex/packages/standalone/current/bin/codex
|
|
16
|
+
*
|
|
17
|
+
* Each vendor manages its own version directory and `~/.local/bin` holds only a
|
|
18
|
+
* SYMLINK. Writing a raw binary there breaks the vendor's self-updater, which
|
|
19
|
+
* expects to swap a link target and would instead find a real file in its way.
|
|
20
|
+
*
|
|
21
|
+
* So each entry declares its own method, and the honest cost is recorded with
|
|
22
|
+
* it: a `vendor-script` entry is NOT pinned and NOT checksummed. That is a real
|
|
23
|
+
* weakening of the guarantee `assertPinned` exists to provide, so the kind is
|
|
24
|
+
* visible in the catalogue and in the report rather than blended in with
|
|
25
|
+
* checksummed entries. A reader can see which tools are trusted to a vendor.
|
|
26
|
+
* @module catalogue
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** Lisa's supported coding agents. Kept in sync with the harness list. */
|
|
30
|
+
export const AGENTS = [
|
|
31
|
+
{
|
|
32
|
+
name: "claude",
|
|
33
|
+
label: "Claude Code",
|
|
34
|
+
kind: "vendor-script",
|
|
35
|
+
// The vendor script installs into ~/.local/share/claude/versions/<v> and
|
|
36
|
+
// links ~/.local/bin/claude at it, which is why we must not write there.
|
|
37
|
+
script: "https://claude.ai/install.sh",
|
|
38
|
+
selfUpdates: true,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "codex",
|
|
42
|
+
label: "Codex",
|
|
43
|
+
kind: "npm-global",
|
|
44
|
+
package: "@openai/codex",
|
|
45
|
+
selfUpdates: true,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "cursor-agent",
|
|
49
|
+
label: "Cursor",
|
|
50
|
+
kind: "vendor-script",
|
|
51
|
+
script: "https://cursor.com/install",
|
|
52
|
+
selfUpdates: true,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "opencode",
|
|
56
|
+
label: "OpenCode",
|
|
57
|
+
kind: "vendor-script",
|
|
58
|
+
script: "https://opencode.ai/install",
|
|
59
|
+
// Installs to its own directory and appends a PATH line to the user's
|
|
60
|
+
// shell rc — which a non-interactive container never sources. Declared
|
|
61
|
+
// here so the run puts it on PATH itself rather than reporting a binary
|
|
62
|
+
// that is on disk as missing.
|
|
63
|
+
binDir: "~/.opencode/bin",
|
|
64
|
+
selfUpdates: true,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "agy",
|
|
68
|
+
label: "Antigravity",
|
|
69
|
+
kind: "manual",
|
|
70
|
+
// No headless installer published. Reported as absent with a pointer
|
|
71
|
+
// rather than silently skipped, because "not installed" and "cannot be
|
|
72
|
+
// installed by this tool" are different answers to the operator.
|
|
73
|
+
note: "no headless installer published; install from the vendor",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "copilot",
|
|
77
|
+
label: "GitHub Copilot CLI",
|
|
78
|
+
kind: "npm-global",
|
|
79
|
+
package: "@github/copilot",
|
|
80
|
+
selfUpdates: false,
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Credential managers, one of which a workstation may use.
|
|
86
|
+
*
|
|
87
|
+
* A CHOICE, not a fixed tool. `lisa-secrets-access` already treats the provider
|
|
88
|
+
* as an axis — bitwarden, 1password, doppler, vault, aws, or plain `env` — and
|
|
89
|
+
* a bootstrap that installed `bws` unconditionally would contradict that,
|
|
90
|
+
* pushing every workstation onto one vendor and quietly making the others
|
|
91
|
+
* second-class.
|
|
92
|
+
*
|
|
93
|
+
* `none` is a first-class answer. A machine using plain environment variables
|
|
94
|
+
* is a supported configuration, not a degraded one, so it must be selectable
|
|
95
|
+
* rather than merely what happens when a question is skipped.
|
|
96
|
+
*
|
|
97
|
+
* Only the SELECTED provider is installed. Installing all of them would put
|
|
98
|
+
* four unused credential CLIs on a machine, each an extra thing to keep patched
|
|
99
|
+
* for no benefit.
|
|
100
|
+
*/
|
|
101
|
+
export const PROVIDERS = [
|
|
102
|
+
{
|
|
103
|
+
name: "bitwarden",
|
|
104
|
+
binary: "bws",
|
|
105
|
+
label: "Bitwarden Secrets Manager",
|
|
106
|
+
// The one genuinely pinned credential CLI: Bitwarden publishes per-platform
|
|
107
|
+
// archives that can carry a checksum, and a credential bootstrap is exactly
|
|
108
|
+
// where an unverified download matters most.
|
|
109
|
+
kind: "release-zip",
|
|
110
|
+
version: "2.1.0",
|
|
111
|
+
platforms: {
|
|
112
|
+
"linux-x64": {
|
|
113
|
+
url: "https://github.com/bitwarden/sdk-sm/releases/download/bws-v2.1.0/bws-x86_64-unknown-linux-gnu-2.1.0.zip",
|
|
114
|
+
sha256:
|
|
115
|
+
"ba8233c3a4aee5d43e3c73bbd04d99e9bc5aba13bbbfd06d89b073abe732b860",
|
|
116
|
+
},
|
|
117
|
+
"linux-arm64": {
|
|
118
|
+
url: "https://github.com/bitwarden/sdk-sm/releases/download/bws-v2.1.0/bws-aarch64-unknown-linux-gnu-2.1.0.zip",
|
|
119
|
+
sha256:
|
|
120
|
+
"18253757286e119d450133a87eb463bf8c1ce418ce24c834f4f250d60cba6f9e",
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "1password",
|
|
126
|
+
binary: "op",
|
|
127
|
+
label: "1Password CLI",
|
|
128
|
+
kind: "vendor-script",
|
|
129
|
+
note: "install from 1password.com/downloads/command-line",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: "doppler",
|
|
133
|
+
binary: "doppler",
|
|
134
|
+
label: "Doppler CLI",
|
|
135
|
+
kind: "vendor-script",
|
|
136
|
+
script: "https://cli.doppler.com/install.sh",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: "vault",
|
|
140
|
+
binary: "vault",
|
|
141
|
+
label: "HashiCorp Vault CLI",
|
|
142
|
+
kind: "vendor-script",
|
|
143
|
+
note: "install from developer.hashicorp.com/vault/install",
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: "aws",
|
|
147
|
+
binary: "aws",
|
|
148
|
+
label: "AWS Secrets Manager (via AWS CLI)",
|
|
149
|
+
kind: "vendor-script",
|
|
150
|
+
// Deliberately the same entry as the general AWS CLI below: a provider of
|
|
151
|
+
// `aws` needs exactly that binary and nothing extra.
|
|
152
|
+
note: "uses the AWS CLI, which is also a general tool",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "none",
|
|
156
|
+
binary: null,
|
|
157
|
+
label: "No credential manager (plain environment variables)",
|
|
158
|
+
kind: "none",
|
|
159
|
+
note: "supported configuration; nothing is installed",
|
|
160
|
+
},
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Tools every workstation needs regardless of project or provider.
|
|
165
|
+
*/
|
|
166
|
+
export const TOOLS = [
|
|
167
|
+
{
|
|
168
|
+
name: "git",
|
|
169
|
+
label: "git",
|
|
170
|
+
kind: "required",
|
|
171
|
+
note: "expected from the OS or base image",
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: "node",
|
|
175
|
+
label: "Node.js",
|
|
176
|
+
kind: "required",
|
|
177
|
+
minVersion: "22",
|
|
178
|
+
note: "expected from the OS or base image; the installers run on it",
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: "gh",
|
|
182
|
+
label: "GitHub CLI",
|
|
183
|
+
kind: "release-tar",
|
|
184
|
+
version: "2.83.0",
|
|
185
|
+
platforms: {
|
|
186
|
+
"linux-x64": {
|
|
187
|
+
url: "https://github.com/cli/cli/releases/download/v2.83.0/gh_2.83.0_linux_amd64.tar.gz",
|
|
188
|
+
sha256:
|
|
189
|
+
"a5cf6cdb40fc67751adf561126b3314044779cea81ba4f254fbe8e9a69f1676f",
|
|
190
|
+
binary: "gh_2.83.0_linux_amd64/bin/gh",
|
|
191
|
+
},
|
|
192
|
+
"linux-arm64": {
|
|
193
|
+
url: "https://github.com/cli/cli/releases/download/v2.83.0/gh_2.83.0_linux_arm64.tar.gz",
|
|
194
|
+
sha256:
|
|
195
|
+
"12311e320d4cfdb54d7fa2d58cd1e3a2ccb4c12e1c3abb32b0a2e48bd0f991bf",
|
|
196
|
+
binary: "gh_2.83.0_linux_arm64/bin/gh",
|
|
197
|
+
},
|
|
198
|
+
"darwin-arm64": {
|
|
199
|
+
url: "https://github.com/cli/cli/releases/download/v2.83.0/gh_2.83.0_macOS_arm64.zip",
|
|
200
|
+
sha256:
|
|
201
|
+
"fecba907bc361d5e33620dbf1145f11432c39fb2b388a839463cfbb89a84820b",
|
|
202
|
+
binary: "gh_2.83.0_macOS_arm64/bin/gh",
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: "aws",
|
|
208
|
+
label: "AWS CLI v2",
|
|
209
|
+
kind: "aws-cli",
|
|
210
|
+
// Not expressible as release-zip: the artifact is a ~73 MB archive
|
|
211
|
+
// containing an installer, not a single binary on PATH. It gets its own
|
|
212
|
+
// kind rather than a vendor-script URL because the steps in between —
|
|
213
|
+
// unzip, then `./aws/install` with an explicit prefix — are the whole job.
|
|
214
|
+
note: "archive wraps an installer rather than a binary",
|
|
215
|
+
selfUpdates: false,
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: "sonar",
|
|
219
|
+
label: "SonarQube CLI",
|
|
220
|
+
kind: "vendor-script",
|
|
221
|
+
// The vendor's real installer. The plausible-looking `sonarsource.com/install`
|
|
222
|
+
// that was here first 404s, and because a 404 piped to a shell exits 0, it
|
|
223
|
+
// was reported as installed on a machine that had no `sonar` at all.
|
|
224
|
+
script:
|
|
225
|
+
"https://raw.githubusercontent.com/SonarSource/sonarqube-cli/refs/heads/master/user-scripts/install.sh",
|
|
226
|
+
// Same shell-rc assumption as OpenCode.
|
|
227
|
+
binDir: "~/.local/share/sonarqube-cli/bin",
|
|
228
|
+
selfUpdates: true,
|
|
229
|
+
},
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
/** Install kinds this tool can actually perform, versus report only. */
|
|
233
|
+
export const INSTALLABLE = new Set([
|
|
234
|
+
"release-zip",
|
|
235
|
+
"release-tar",
|
|
236
|
+
"npm-global",
|
|
237
|
+
"vendor-script",
|
|
238
|
+
"aws-cli",
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
/** Kinds whose artifact is verified against a published checksum. */
|
|
242
|
+
export const CHECKSUMMED = new Set(["release-zip", "release-tar"]);
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Every directory a workstation must have on PATH.
|
|
246
|
+
*
|
|
247
|
+
* Vendors that manage their own version directory append a PATH line to the
|
|
248
|
+
* user's shell rc, which a non-interactive run — a container build, a cron, an
|
|
249
|
+
* agent — never sources. Collecting them here means one place decides what PATH
|
|
250
|
+
* must contain, used identically by the runtime and by the emitted Dockerfile.
|
|
251
|
+
* @param {string} home The home directory to expand `~` against.
|
|
252
|
+
* @returns {string[]} Absolute directories, most specific last.
|
|
253
|
+
*/
|
|
254
|
+
export function pathDirs(home) {
|
|
255
|
+
const declared = [...AGENTS, ...PROVIDERS, ...TOOLS]
|
|
256
|
+
.map(entry => entry.binDir)
|
|
257
|
+
.filter(Boolean)
|
|
258
|
+
.map(dir => dir.replace(/^~/, home));
|
|
259
|
+
// ~/.local/bin first: it is where the pinned, checksummed binaries land, and
|
|
260
|
+
// those must win over whatever an image happens to ship under the same name.
|
|
261
|
+
return [`${home}/.local/bin`, ...declared];
|
|
262
|
+
}
|