@codyswann/lisa 2.328.5 → 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 +39 -0
- 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-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-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-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-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-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-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
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Prepare a machine — or a throwaway container — to run coding agents.
|
|
4
|
+
*
|
|
5
|
+
* This is the layer beneath `lisa-setup-local-env`, which reads a project's
|
|
6
|
+
* `remoteEnv.tools` and therefore needs a checkout to exist first. That left
|
|
7
|
+
* the machine most likely to be missing a tool — a fresh laptop, or an empty
|
|
8
|
+
* container — with nothing to ask.
|
|
9
|
+
*
|
|
10
|
+
* Nothing here reads a repository. The composition is:
|
|
11
|
+
*
|
|
12
|
+
* workstation -> git clone -> lisa-detect-tooling -> lisa-setup-local-env
|
|
13
|
+
*
|
|
14
|
+
* ## Ephemerality is not a mode
|
|
15
|
+
*
|
|
16
|
+
* There is no Docker branch. A container gets a throwaway `$HOME` and dies with
|
|
17
|
+
* it; a laptop keeps its own. Same code path, so the rarely-run one cannot rot
|
|
18
|
+
* — the failure mode a second install path always eventually has.
|
|
19
|
+
*
|
|
20
|
+
* ## Already-installed wins, however it got there
|
|
21
|
+
*
|
|
22
|
+
* A tool present by ANY means is reported and left alone. That dissolves the
|
|
23
|
+
* "Homebrew or ~/.local/bin" question: we never contend for ownership of a tool
|
|
24
|
+
* someone else's package manager installed, and never shadow it with an earlier
|
|
25
|
+
* PATH entry that would silently win over their upgrades.
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* workstation.mjs report what is present and missing
|
|
29
|
+
* workstation.mjs --install install everything missing
|
|
30
|
+
* workstation.mjs --install --agents=claude,codex only these agents
|
|
31
|
+
* workstation.mjs --json machine-readable plan
|
|
32
|
+
* workstation.mjs --print-dockerfile an image that runs this same script
|
|
33
|
+
* @module workstation
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { execFileSync } from "node:child_process";
|
|
37
|
+
|
|
38
|
+
import {
|
|
39
|
+
AGENTS,
|
|
40
|
+
CHECKSUMMED,
|
|
41
|
+
INSTALLABLE,
|
|
42
|
+
PROVIDERS,
|
|
43
|
+
TOOLS,
|
|
44
|
+
} from "./catalogue.mjs";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Locate a command without running it.
|
|
48
|
+
*
|
|
49
|
+
* Presence and version are different questions, and conflating them has bitten
|
|
50
|
+
* this codebase before: probing with `--version` reported `unzip` missing on
|
|
51
|
+
* machines that had it, because Info-ZIP exits 10 on an unknown flag. So
|
|
52
|
+
* presence is `command -v` and nothing else.
|
|
53
|
+
* @param {string} name Executable name.
|
|
54
|
+
* @param {Function} [exec] Runner, injectable for tests.
|
|
55
|
+
* @returns {string|null} Absolute path, or null when absent.
|
|
56
|
+
*/
|
|
57
|
+
export function locate(name, exec = execFileSync) {
|
|
58
|
+
try {
|
|
59
|
+
return String(
|
|
60
|
+
exec("command", ["-v", name], { encoding: "utf8", shell: true })
|
|
61
|
+
).trim();
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Read a tool's version, tolerating tools that dislike `--version`.
|
|
69
|
+
*
|
|
70
|
+
* A non-zero exit does NOT mean absent — only a failure to spawn does. Version
|
|
71
|
+
* text is still recovered from a failed invocation where there is any, because
|
|
72
|
+
* a tool that refuses the flag often prints its banner anyway.
|
|
73
|
+
* @param {string} name Executable name.
|
|
74
|
+
* @param {Function} [exec] Runner, injectable for tests.
|
|
75
|
+
* @returns {string|null} A dotted version, or null.
|
|
76
|
+
*/
|
|
77
|
+
export function version(name, exec = execFileSync) {
|
|
78
|
+
const read = out => {
|
|
79
|
+
const match = /(\d+(?:\.\d+)+)/.exec(String(out ?? ""));
|
|
80
|
+
return match ? match[1] : null;
|
|
81
|
+
};
|
|
82
|
+
try {
|
|
83
|
+
return read(
|
|
84
|
+
exec(name, ["--version"], {
|
|
85
|
+
encoding: "utf8",
|
|
86
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error.code === "ENOENT") return null;
|
|
91
|
+
return read(`${error.stdout ?? ""}${error.stderr ?? ""}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Where a tool came from, so the report can say why it is being left alone.
|
|
97
|
+
* @param {string|null} path Absolute path to the executable.
|
|
98
|
+
* @returns {string} A short provenance label.
|
|
99
|
+
*/
|
|
100
|
+
export function provenance(path) {
|
|
101
|
+
if (!path) return "absent";
|
|
102
|
+
if (path.includes("/homebrew/") || path.startsWith("/usr/local/Cellar"))
|
|
103
|
+
return "homebrew";
|
|
104
|
+
if (path.includes("/.local/bin/")) return "~/.local/bin";
|
|
105
|
+
if (path.includes("/node_modules/")) return "node_modules";
|
|
106
|
+
if (path.startsWith("/usr/bin") || path.startsWith("/bin")) return "system";
|
|
107
|
+
return "other";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Decide what to do about one catalogue entry.
|
|
112
|
+
* @param {object} entry Catalogue entry.
|
|
113
|
+
* @param {object} [deps] Injected probes, for tests.
|
|
114
|
+
* @returns {object} A plan row.
|
|
115
|
+
*/
|
|
116
|
+
export function planEntry(entry, deps = {}) {
|
|
117
|
+
const find = deps.locate ?? locate;
|
|
118
|
+
const read = deps.version ?? version;
|
|
119
|
+
|
|
120
|
+
const path = find(entry.name);
|
|
121
|
+
const found = path ? read(entry.name) : null;
|
|
122
|
+
const base = {
|
|
123
|
+
name: entry.name,
|
|
124
|
+
label: entry.label,
|
|
125
|
+
kind: entry.kind,
|
|
126
|
+
path,
|
|
127
|
+
version: found,
|
|
128
|
+
from: provenance(path),
|
|
129
|
+
checksummed: CHECKSUMMED.has(entry.kind),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
if (path) {
|
|
133
|
+
// Present by any means: report and leave alone. Re-installing over a
|
|
134
|
+
// vendor's own layout is how a self-updater gets broken.
|
|
135
|
+
return {
|
|
136
|
+
...base,
|
|
137
|
+
action: "present",
|
|
138
|
+
reason: `already installed (${base.from})`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (entry.kind === "required") {
|
|
142
|
+
return {
|
|
143
|
+
...base,
|
|
144
|
+
action: "blocked",
|
|
145
|
+
reason: `${entry.name} is required but absent — ${entry.note}`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (!INSTALLABLE.has(entry.kind)) {
|
|
149
|
+
return {
|
|
150
|
+
...base,
|
|
151
|
+
action: "manual",
|
|
152
|
+
reason: entry.note ?? "no headless installer available",
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return { ...base, action: "install", reason: "absent" };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Build the full plan across agents and tools.
|
|
160
|
+
* @param {object} [options] Selection and injected probes.
|
|
161
|
+
* @returns {object[]} Plan rows.
|
|
162
|
+
*/
|
|
163
|
+
export function planWorkstation(options = {}) {
|
|
164
|
+
const wanted = options.agents ?? null;
|
|
165
|
+
const agents = AGENTS.filter(a => !wanted || wanted.includes(a.name)).map(
|
|
166
|
+
a => ({
|
|
167
|
+
...planEntry(a, options),
|
|
168
|
+
group: "agent",
|
|
169
|
+
})
|
|
170
|
+
);
|
|
171
|
+
const tools = TOOLS.map(t => ({ ...planEntry(t, options), group: "tool" }));
|
|
172
|
+
|
|
173
|
+
// Only the SELECTED provider is planned. Installing every credential CLI
|
|
174
|
+
// would leave four unused ones on the machine, each needing patching for no
|
|
175
|
+
// benefit — and would contradict the secrets contract, where the provider is
|
|
176
|
+
// a per-project axis rather than a fixed vendor.
|
|
177
|
+
const provider = resolveProvider(options.provider);
|
|
178
|
+
const credential = provider
|
|
179
|
+
? [{ ...planProvider(provider, options), group: "provider" }]
|
|
180
|
+
: [];
|
|
181
|
+
|
|
182
|
+
return [...agents, ...credential, ...tools];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Resolve a requested provider name to its catalogue entry.
|
|
187
|
+
*
|
|
188
|
+
* An unknown name is an error rather than a silent fallback to Bitwarden: a
|
|
189
|
+
* typo that quietly provisions the wrong credential manager is worse than one
|
|
190
|
+
* that stops.
|
|
191
|
+
* @param {string|null|undefined} name Requested provider.
|
|
192
|
+
* @returns {object|null} Entry, or null when none was requested.
|
|
193
|
+
*/
|
|
194
|
+
export function resolveProvider(name) {
|
|
195
|
+
if (name === null || name === undefined) return null;
|
|
196
|
+
const entry = PROVIDERS.find(p => p.name === name);
|
|
197
|
+
if (!entry) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`unknown credential manager "${name}". Supported: ` +
|
|
200
|
+
`${PROVIDERS.map(p => p.name).join(", ")}`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
return entry;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Plan the selected credential manager.
|
|
208
|
+
* @param {object} provider Catalogue entry.
|
|
209
|
+
* @param {object} [deps] Injected probes, for tests.
|
|
210
|
+
* @returns {object} A plan row.
|
|
211
|
+
*/
|
|
212
|
+
export function planProvider(provider, deps = {}) {
|
|
213
|
+
if (provider.kind === "none") {
|
|
214
|
+
// A first-class answer, not an absence. Reported so the operator can see
|
|
215
|
+
// the machine is deliberately on plain environment variables.
|
|
216
|
+
return {
|
|
217
|
+
name: "none",
|
|
218
|
+
label: provider.label,
|
|
219
|
+
kind: "none",
|
|
220
|
+
path: null,
|
|
221
|
+
version: null,
|
|
222
|
+
from: "n/a",
|
|
223
|
+
checksummed: false,
|
|
224
|
+
action: "present",
|
|
225
|
+
reason: "no credential manager selected",
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return planEntry({ ...provider, name: provider.binary }, deps);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Render the plan for a human.
|
|
233
|
+
* @param {object[]} plan Plan rows.
|
|
234
|
+
* @returns {string} Report text.
|
|
235
|
+
*/
|
|
236
|
+
export function renderPlan(plan) {
|
|
237
|
+
const lines = [];
|
|
238
|
+
const heading = {
|
|
239
|
+
agent: "Coding agents:",
|
|
240
|
+
provider: "\nCredential manager:",
|
|
241
|
+
tool: "\nTools:",
|
|
242
|
+
};
|
|
243
|
+
for (const group of ["agent", "provider", "tool"]) {
|
|
244
|
+
const rows = plan.filter(r => r.group === group);
|
|
245
|
+
if (!rows.length) continue;
|
|
246
|
+
lines.push(heading[group]);
|
|
247
|
+
for (const row of rows) {
|
|
248
|
+
const mark = {
|
|
249
|
+
present: " ok ",
|
|
250
|
+
install: " install",
|
|
251
|
+
manual: " manual ",
|
|
252
|
+
blocked: " BLOCKED",
|
|
253
|
+
}[row.action];
|
|
254
|
+
// `none` is present-but-not-a-binary, so it has no version to show; a
|
|
255
|
+
// bare "?" there reads as a failed probe rather than a chosen answer.
|
|
256
|
+
const detail =
|
|
257
|
+
row.action !== "present"
|
|
258
|
+
? row.reason
|
|
259
|
+
: row.path
|
|
260
|
+
? `${row.version ?? "unknown version"} (${row.from})`
|
|
261
|
+
: row.reason;
|
|
262
|
+
lines.push(`${mark} ${row.label.padEnd(22)} ${detail}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Say plainly which installs carry a checksum and which trust a vendor.
|
|
267
|
+
const unverified = plan.filter(r => r.action === "install" && !r.checksummed);
|
|
268
|
+
if (unverified.length) {
|
|
269
|
+
lines.push(
|
|
270
|
+
`\nNot checksummed: ${unverified.map(r => r.name).join(", ")} — these run a` +
|
|
271
|
+
` vendor installer, which is weaker than the pinned entries. Installing` +
|
|
272
|
+
` them trusts the vendor's script at fetch time.`
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
return lines.join("\n");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Install one entry by its own preferred method.
|
|
280
|
+
*
|
|
281
|
+
* Each vendor owns its layout; we invoke their installer rather than placing a
|
|
282
|
+
* binary ourselves. `release-zip`/`release-tar` are the exception and the only
|
|
283
|
+
* checksummed path — they are reused from the remote-env installer rather than
|
|
284
|
+
* reimplemented, because a second installer is a second thing to keep honest.
|
|
285
|
+
* @param {object} entry Catalogue entry.
|
|
286
|
+
* @param {object} row Plan row for it.
|
|
287
|
+
* @param {object} [deps] Injected runner, for tests.
|
|
288
|
+
* @returns {object} Result row.
|
|
289
|
+
*/
|
|
290
|
+
export function installEntry(entry, row, deps = {}) {
|
|
291
|
+
const run =
|
|
292
|
+
deps.run ??
|
|
293
|
+
((cmd, args, opts) =>
|
|
294
|
+
execFileSync(cmd, args, { stdio: "inherit", ...opts }));
|
|
295
|
+
const find = deps.locate ?? locate;
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
if (entry.kind === "npm-global") {
|
|
299
|
+
run("npm", ["install", "-g", entry.package]);
|
|
300
|
+
} else if (entry.kind === "vendor-script") {
|
|
301
|
+
if (!entry.script) {
|
|
302
|
+
return {
|
|
303
|
+
...row,
|
|
304
|
+
action: "manual",
|
|
305
|
+
reason: entry.note ?? "no installer",
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
// Piping a fetched script to a shell is exactly the unverified path the
|
|
309
|
+
// report warns about. It is the vendor's supported route for these
|
|
310
|
+
// tools, and the alternative — repackaging their binaries ourselves —
|
|
311
|
+
// would break the self-updaters that own those directories.
|
|
312
|
+
//
|
|
313
|
+
// bash, NOT sh. On Ubuntu `/bin/sh` is dash, and every one of these
|
|
314
|
+
// vendor scripts is a bash script: piping them to sh in a container gave
|
|
315
|
+
// `Syntax error: "(" unexpected` and `set: Illegal option -o pipefail`
|
|
316
|
+
// while the same command worked on a Mac, where sh is bash.
|
|
317
|
+
//
|
|
318
|
+
// `set -o pipefail` matters as much as the shell does: without it the
|
|
319
|
+
// exit status is the SHELL's, not curl's, so a 404 feeds an empty script
|
|
320
|
+
// to bash, bash exits 0, and a tool that was never installed is reported
|
|
321
|
+
// as installed. That is exactly what `sonarsource.com/install` did.
|
|
322
|
+
run("bash", [
|
|
323
|
+
"-o",
|
|
324
|
+
"pipefail",
|
|
325
|
+
"-c",
|
|
326
|
+
`curl -fsSL ${entry.script} | bash`,
|
|
327
|
+
]);
|
|
328
|
+
} else if (entry.kind === "aws-cli") {
|
|
329
|
+
// macOS ships a .pkg that wants an administrator, which a headless run
|
|
330
|
+
// has no business prompting for. Reported as manual with the one-line
|
|
331
|
+
// fix rather than attempted and failed halfway.
|
|
332
|
+
if ((deps.platform ?? process.platform) === "darwin") {
|
|
333
|
+
return {
|
|
334
|
+
...row,
|
|
335
|
+
action: "manual",
|
|
336
|
+
reason:
|
|
337
|
+
"macOS installs from a .pkg needing admin — `brew install awscli`",
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
// Unprivileged by construction: the vendor installer defaults to /usr/local,
|
|
341
|
+
// which needs root and would put a container and a laptop on different
|
|
342
|
+
// paths. An explicit prefix under $HOME keeps one code path for both and
|
|
343
|
+
// survives a throwaway container, which is where $HOME dies anyway.
|
|
344
|
+
const home = deps.home ?? process.env.HOME;
|
|
345
|
+
const url = `https://awscli.amazonaws.com/awscli-exe-linux-${
|
|
346
|
+
process.arch === "arm64" ? "aarch64" : "x86_64"
|
|
347
|
+
}.zip`;
|
|
348
|
+
run("bash", [
|
|
349
|
+
"-o",
|
|
350
|
+
"pipefail",
|
|
351
|
+
"-c",
|
|
352
|
+
[
|
|
353
|
+
"set -e",
|
|
354
|
+
'tmp="$(mktemp -d)"',
|
|
355
|
+
`curl -fsSL "${url}" -o "$tmp/awscli.zip"`,
|
|
356
|
+
'unzip -q "$tmp/awscli.zip" -d "$tmp"',
|
|
357
|
+
`"$tmp/aws/install" --install-dir "${home}/.local/aws-cli" ` +
|
|
358
|
+
`--bin-dir "${home}/.local/bin" --update`,
|
|
359
|
+
'rm -rf "$tmp"',
|
|
360
|
+
].join("\n"),
|
|
361
|
+
]);
|
|
362
|
+
} else if (CHECKSUMMED.has(entry.kind)) {
|
|
363
|
+
// The pinned, checksummed path belongs to the remote-env installer. It is
|
|
364
|
+
// borrowed rather than reimplemented so the pins and checksums cannot
|
|
365
|
+
// drift between the two — a second installer is a second thing to keep
|
|
366
|
+
// honest, and the one run least often is the one that rots.
|
|
367
|
+
if (!deps.installPinned) {
|
|
368
|
+
return {
|
|
369
|
+
...row,
|
|
370
|
+
action: "failed",
|
|
371
|
+
reason:
|
|
372
|
+
"pinned installer unavailable — cannot verify a checksum here",
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
deps.installPinned(entry);
|
|
376
|
+
} else {
|
|
377
|
+
return {
|
|
378
|
+
...row,
|
|
379
|
+
action: "manual",
|
|
380
|
+
reason: entry.note ?? "no headless installer",
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
} catch (error) {
|
|
384
|
+
return {
|
|
385
|
+
...row,
|
|
386
|
+
action: "failed",
|
|
387
|
+
reason: String(error.message).slice(0, 160),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Verify by looking, not by exit status. Every installer above can exit 0
|
|
392
|
+
// having installed nothing, so a claim of success is only as good as a fresh
|
|
393
|
+
// probe — reporting a tool as installed when it is absent is worse than
|
|
394
|
+
// reporting the failure, because it moves the error to the moment of use.
|
|
395
|
+
if (!find(entry.name)) {
|
|
396
|
+
return {
|
|
397
|
+
...row,
|
|
398
|
+
action: "failed",
|
|
399
|
+
reason: `installer exited 0 but ${entry.name} is still not on PATH`,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
return { ...row, action: "installed", reason: "ok" };
|
|
403
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Prepare this machine — or a throwaway container — to run coding agents, before any repository exists. Detects which of Lisa's supported agents are already installed, asks which credential manager the machine uses (or none), and installs only what is missing, each by its vendor's own preferred method. Idempotent and headless; --print-dockerfile emits a spin-up/spin-down environment. Nothing is installed without --install."
|
|
3
|
+
allowed-tools: ["Skill"]
|
|
4
|
+
argument-hint: "[--install] [--agents=claude,codex] [--provider=bitwarden] [--json] [--print-dockerfile]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Use the /lisa-setup-workstation skill to report and optionally install the coding agents, credential manager, and base tools this machine needs. $ARGUMENTS
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lisa-setup-workstation
|
|
3
|
+
description: "Prepare a machine — a fresh laptop or a throwaway container — to run coding agents, before any repository exists. Detects which of Lisa's supported agents (Claude Code, Codex, Cursor, OpenCode, Antigravity, Copilot) are already installed, asks which credential manager the machine uses (Bitwarden, 1Password, Doppler, Vault, AWS, or none), and installs only what is missing, each by its vendor's own preferred method. Idempotent, headless by default, and emits a Dockerfile for a spin-up/spin-down environment. Run it on a new machine, in a container, or before cloning anything."
|
|
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,4 @@
|
|
|
1
|
+
display_name: "Setup Workstation"
|
|
2
|
+
short_description: "Prepare a machine — a fresh laptop or a throwaway container — to run coding agents, before any repository exists"
|
|
3
|
+
default_prompt:
|
|
4
|
+
- "Use $lisa-setup-workstation: Prepare a machine — a fresh laptop or a throwaway container — to run coding agents, before any repository exists."
|