@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.
Files changed (85) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +39 -0
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/package.json +1 -1
  5. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  6. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  7. package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/SKILL.md +121 -0
  8. package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/agents/openai.yaml +4 -0
  9. package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/scripts/catalogue.mjs +262 -0
  10. package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/scripts/cli.mjs +315 -0
  11. package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/scripts/workstation.mjs +403 -0
  12. package/plugins/lisa/commands/setup/workstation.md +7 -0
  13. package/plugins/lisa/skills/lisa-setup-workstation/SKILL.md +121 -0
  14. package/plugins/lisa/skills/lisa-setup-workstation/agents/openai.yaml +4 -0
  15. package/plugins/lisa/skills/lisa-setup-workstation/scripts/catalogue.mjs +262 -0
  16. package/plugins/lisa/skills/lisa-setup-workstation/scripts/cli.mjs +315 -0
  17. package/plugins/lisa/skills/lisa-setup-workstation/scripts/workstation.mjs +403 -0
  18. package/plugins/lisa-agy/commands/lisa/setup/workstation.md +7 -0
  19. package/plugins/lisa-agy/plugin.json +1 -1
  20. package/plugins/lisa-agy/skills/lisa-setup-workstation/SKILL.md +121 -0
  21. package/plugins/lisa-agy/skills/lisa-setup-workstation/scripts/catalogue.mjs +262 -0
  22. package/plugins/lisa-agy/skills/lisa-setup-workstation/scripts/cli.mjs +315 -0
  23. package/plugins/lisa-agy/skills/lisa-setup-workstation/scripts/workstation.mjs +403 -0
  24. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  26. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  27. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-copilot/commands/lisa/setup/workstation.md +7 -0
  31. package/plugins/lisa-copilot/skills/lisa-setup-workstation/SKILL.md +121 -0
  32. package/plugins/lisa-copilot/skills/lisa-setup-workstation/scripts/catalogue.mjs +262 -0
  33. package/plugins/lisa-copilot/skills/lisa-setup-workstation/scripts/cli.mjs +315 -0
  34. package/plugins/lisa-copilot/skills/lisa-setup-workstation/scripts/workstation.mjs +403 -0
  35. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-cursor/commands/lisa/setup/workstation.md +7 -0
  37. package/plugins/lisa-cursor/skills/lisa-setup-workstation/SKILL.md +121 -0
  38. package/plugins/lisa-cursor/skills/lisa-setup-workstation/scripts/catalogue.mjs +262 -0
  39. package/plugins/lisa-cursor/skills/lisa-setup-workstation/scripts/cli.mjs +315 -0
  40. package/plugins/lisa-cursor/skills/lisa-setup-workstation/scripts/workstation.mjs +403 -0
  41. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  43. package/plugins/lisa-expo-agy/plugin.json +1 -1
  44. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  48. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  49. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  54. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  59. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  64. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-rails-agy/plugin.json +1 -1
  69. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  73. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  74. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  78. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  79. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  81. package/plugins/src/base/commands/setup/workstation.md +7 -0
  82. package/plugins/src/base/skills/lisa-setup-workstation/SKILL.md +121 -0
  83. package/plugins/src/base/skills/lisa-setup-workstation/scripts/catalogue.mjs +262 -0
  84. package/plugins/src/base/skills/lisa-setup-workstation/scripts/cli.mjs +315 -0
  85. package/plugins/src/base/skills/lisa-setup-workstation/scripts/workstation.mjs +403 -0
@@ -0,0 +1,315 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Command-line entry for the workstation bootstrap.
4
+ *
5
+ * Kept separate from `workstation.mjs` so the planning logic stays importable
6
+ * and testable without a process boundary — a test should be able to ask "what
7
+ * would this do" without any risk of it doing it.
8
+ *
9
+ * Usage:
10
+ * cli.mjs report present vs missing
11
+ * cli.mjs --install provision what is missing
12
+ * cli.mjs --install --agents=claude,codex only these agents
13
+ * cli.mjs --provider=bitwarden credential manager (asked if a TTY)
14
+ * cli.mjs --json machine-readable plan
15
+ * cli.mjs --print-dockerfile an image that runs this same script
16
+ * @module cli
17
+ */
18
+
19
+ import { closeSync, existsSync, openSync, readSync, writeSync } from "node:fs";
20
+ import { dirname, resolve } from "node:path";
21
+ import { fileURLToPath, pathToFileURL } from "node:url";
22
+
23
+ import { AGENTS, PROVIDERS, TOOLS, pathDirs } from "./catalogue.mjs";
24
+ import { installEntry, planWorkstation, renderPlan } from "./workstation.mjs";
25
+
26
+ const HERE = dirname(fileURLToPath(import.meta.url));
27
+
28
+ /** Where pinned binaries land, and what must therefore be on PATH. */
29
+ export const BIN_DIR = `${process.env.HOME}/.local/bin`;
30
+
31
+ /**
32
+ * Borrow the pinned, checksummed installer the remote-env skill owns.
33
+ *
34
+ * Composed rather than reimplemented: the pins and checksums must be the same
35
+ * ones the remote surfaces use, or the two drift and only one gets fixed.
36
+ * Returns null when the sibling is absent, so a machine can still install the
37
+ * unpinned entries instead of failing wholesale — the report says which ones
38
+ * were skipped rather than passing them off as done.
39
+ * @returns {Promise<Function|null>} An installer, or null.
40
+ */
41
+ export async function loadPinnedInstaller() {
42
+ const sibling = name =>
43
+ resolve(HERE, "..", "..", "lisa-setup-remote-env", "scripts", name);
44
+ const setup = sibling("setup-remote-env.mjs");
45
+ const toolchain = sibling("toolchain.mjs");
46
+ if (!existsSync(setup) || !existsSync(toolchain)) return null;
47
+
48
+ const { installTool, ensureBinDir } = await import(pathToFileURL(setup).href);
49
+ const { resolvePlatform } = await import(pathToFileURL(toolchain).href);
50
+ if (typeof installTool !== "function") return null;
51
+
52
+ return entry => {
53
+ // resolvePlatform must run first: installTool calls assertPinned, which
54
+ // refuses an entry that still carries a `platforms` map. Skipping it failed
55
+ // bws and gh with "platform-specific entry was not resolved before install"
56
+ // — the guard doing exactly its job.
57
+ const resolved = resolvePlatform(toManifestEntry(entry));
58
+ installTool(resolved, ensureBinDir ? ensureBinDir() : BIN_DIR);
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Translate a catalogue entry into the manifest shape the installer expects.
64
+ *
65
+ * The two vocabularies differ — the catalogue describes a machine, the manifest
66
+ * describes a project — so the seam is made explicit here rather than by giving
67
+ * the catalogue a shape it does not otherwise need. `kind` becomes `install`;
68
+ * everything else the manifest reads is already named the same.
69
+ * @param {object} entry Catalogue entry.
70
+ * @returns {object} A `remoteEnv.tools` style entry.
71
+ */
72
+ export function toManifestEntry(entry) {
73
+ const { kind, label, binDir, note, selfUpdates, ...rest } = entry;
74
+ return { ...rest, install: kind };
75
+ }
76
+
77
+ /**
78
+ * Decide which credential manager to provision.
79
+ *
80
+ * Asked, never assumed. Defaulting to Bitwarden because that is what this repo
81
+ * happens to use would install a vendor CLI on a machine whose project resolves
82
+ * secrets some other way — and would make `none`, a supported configuration,
83
+ * reachable only by accident.
84
+ *
85
+ * Headless is the primary mode, so a missing answer without a TTY is `none`
86
+ * rather than a prompt that would hang a container build forever.
87
+ * @param {string|null} requested Value of --provider, if given.
88
+ * @param {object} io Injected ask/tty, for tests.
89
+ * @returns {string} A provider name from the catalogue.
90
+ */
91
+ export function chooseProvider(requested, io = {}) {
92
+ if (requested) return requested;
93
+
94
+ const ask = io.ask ?? null;
95
+ const interactive = io.interactive ?? Boolean(process.stdin.isTTY);
96
+ if (!ask || !interactive) return "none";
97
+
98
+ const names = PROVIDERS.map(p => p.name);
99
+ const menu = PROVIDERS.map((p, i) => ` ${i + 1}) ${p.label}`).join("\n");
100
+ const answer = String(
101
+ ask(`Which credential manager does this machine use?\n${menu}\nChoice: `) ??
102
+ ""
103
+ ).trim();
104
+
105
+ // Accept either the menu number or the name, because an operator reading the
106
+ // list will reach for whichever is in front of them.
107
+ const index = Number.parseInt(answer, 10);
108
+ if (Number.isInteger(index) && index >= 1 && index <= names.length) {
109
+ return names[index - 1];
110
+ }
111
+ return names.includes(answer) ? answer : "none";
112
+ }
113
+
114
+ /**
115
+ * A Dockerfile that runs this same script, for a throwaway environment.
116
+ *
117
+ * Emitted rather than committed so it cannot drift from the catalogue it
118
+ * provisions. There is no Docker-specific code path anywhere in this skill —
119
+ * the image simply gives the script a `$HOME` that dies with the container.
120
+ *
121
+ * Deliberately bakes in NO repository. Clone inside the container, then run
122
+ * lisa-detect-tooling and lisa-setup-local-env for that project's own tools.
123
+ * @param {string[]} agents Agent names to install.
124
+ * @param {string} [provider] Credential manager to provision.
125
+ * @returns {string} Dockerfile text.
126
+ */
127
+ export function renderDockerfile(agents, provider = "none") {
128
+ const list = agents.length ? agents.join(",") : "claude,codex";
129
+ const clean = "apt-get clean";
130
+ return [
131
+ "# Ephemeral coding environment. Build, use, discard.",
132
+ "# docker build -t lisa-workstation .",
133
+ "# docker run --rm -it lisa-workstation",
134
+ "FROM ubuntu:24.04",
135
+ "ENV DEBIAN_FRONTEND=noninteractive",
136
+ "RUN apt-get update -qq \\",
137
+ " && apt-get install -y -qq curl git unzip ca-certificates python3 \\",
138
+ " && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \\",
139
+ ` && apt-get install -y -qq nodejs && ${clean}`,
140
+ // The whole skills/ tree, not just this skill: the checksummed installs are
141
+ // borrowed from lisa-setup-remote-env, and copying only this directory left
142
+ // bws and gh silently unprovisioned.
143
+ "COPY skills/ /opt/lisa/skills/",
144
+ // Set BEFORE the install, not after. Installers write into ~/.local/bin and
145
+ // the post-install check looks for the binary by bare name, so a PATH set
146
+ // afterwards would fail every verification in the image while passing on a
147
+ // laptop that already exports the directory.
148
+ `ENV PATH="${pathDirs("/root").join(":")}:\${PATH}"`,
149
+ // --provider is passed explicitly: the build has no TTY, so an omitted flag
150
+ // would resolve to `none` and produce an image with no way to reach secrets.
151
+ `RUN node /opt/lisa/skills/lisa-setup-workstation/scripts/cli.mjs \\`,
152
+ ` --install --agents=${list} --provider=${provider}`,
153
+ 'CMD ["bash"]',
154
+ ].join("\n");
155
+ }
156
+
157
+ /**
158
+ * Run the CLI.
159
+ * @param {string[]} argv Arguments after the script name.
160
+ * @param {object} [io] Injected output sinks, for tests.
161
+ * @returns {number} Process exit code.
162
+ */
163
+ export async function run(argv, io = {}) {
164
+ const out = io.log ?? console.log;
165
+ const err = io.error ?? console.error;
166
+ const flag = name => argv.includes(`--${name}`);
167
+ const value = name => {
168
+ const hit = argv.find(a => a.startsWith(`--${name}=`));
169
+ return hit ? hit.slice(name.length + 3) : null;
170
+ };
171
+
172
+ const selected = value("agents");
173
+ const agents = selected
174
+ ? selected
175
+ .split(",")
176
+ .map(s => s.trim())
177
+ .filter(Boolean)
178
+ : null;
179
+
180
+ const provider = chooseProvider(value("provider"), io);
181
+
182
+ if (flag("print-dockerfile")) {
183
+ out(renderDockerfile(agents ?? [], provider));
184
+ return 0;
185
+ }
186
+
187
+ let plan;
188
+ try {
189
+ plan = planWorkstation({ agents, provider, ...(io.probes ?? {}) });
190
+ } catch (error) {
191
+ // A misspelled provider stops here rather than silently provisioning the
192
+ // wrong credential manager.
193
+ err(String(error.message));
194
+ return 1;
195
+ }
196
+
197
+ if (flag("json")) {
198
+ out(JSON.stringify({ plan }, null, 2));
199
+ return 0;
200
+ }
201
+
202
+ out(renderPlan(plan));
203
+
204
+ const blocked = plan.filter(r => r.action === "blocked");
205
+ if (blocked.length) {
206
+ err(
207
+ `\n${blocked.length} required tool(s) absent. These come from the OS or ` +
208
+ `base image and cannot be installed here.`
209
+ );
210
+ return 1;
211
+ }
212
+
213
+ const pending = plan.filter(r => r.action === "install");
214
+ if (!pending.length) {
215
+ // Idempotence made visible. A second run states it rather than going quiet,
216
+ // because silence reads as "did nothing because it broke".
217
+ out("\nNothing to install — this workstation is already prepared.");
218
+ return 0;
219
+ }
220
+
221
+ if (!flag("install")) {
222
+ out(
223
+ `\n${pending.length} missing. Re-run with --install to provision them. ` +
224
+ `Nothing has been installed or written.`
225
+ );
226
+ return 0;
227
+ }
228
+
229
+ out("");
230
+ // Provider entries are keyed by vendor name but planned under their BINARY
231
+ // name (`bitwarden` -> `bws`), so they are re-keyed here or the lookup below
232
+ // silently finds nothing and the install is skipped without a word.
233
+ const catalogue = [
234
+ ...AGENTS,
235
+ ...TOOLS,
236
+ ...PROVIDERS.filter(p => p.binary).map(p => ({ ...p, name: p.binary })),
237
+ ];
238
+ const probes = io.probes ?? {};
239
+ const installPinned =
240
+ probes.installPinned ?? (await loadPinnedInstaller()) ?? undefined;
241
+
242
+ const results = [];
243
+ for (const row of pending) {
244
+ results.push(
245
+ installEntry(
246
+ catalogue.find(e => e.name === row.name),
247
+ row,
248
+ { ...probes, installPinned }
249
+ )
250
+ );
251
+ }
252
+ for (const result of results) {
253
+ out(` ${result.action.padEnd(10)} ${result.label} ${result.reason}`);
254
+ }
255
+
256
+ const failed = results.filter(r => r.action === "failed");
257
+ if (failed.length) {
258
+ err(
259
+ `\n${failed.length} install(s) failed: ${failed
260
+ .map(r => r.name)
261
+ .join(", ")}. Nothing above was reported as installed unless the ` +
262
+ `binary was found on PATH afterwards.`
263
+ );
264
+ }
265
+ return failed.length ? 1 : 0;
266
+ }
267
+
268
+ /**
269
+ * Prompt on the terminal, synchronously.
270
+ *
271
+ * Reads `/dev/tty` rather than stdin so the question still works when the
272
+ * script is being piped — the ordinary way a bootstrap gets run.
273
+ * @param {string} prompt Text to display.
274
+ * @returns {string|null} The answer, or null if no terminal is attached.
275
+ */
276
+ export function askTty(prompt) {
277
+ let fd;
278
+ try {
279
+ fd = openSync("/dev/tty", "r+");
280
+ } catch {
281
+ return null;
282
+ }
283
+ try {
284
+ writeSync(fd, prompt);
285
+ const buffer = Buffer.alloc(256);
286
+ const read = readSync(fd, buffer, 0, buffer.length, null);
287
+ return buffer.toString("utf8", 0, read).trim();
288
+ } catch {
289
+ return null;
290
+ } finally {
291
+ closeSync(fd);
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Put the install directory on PATH before anything probes it.
297
+ *
298
+ * Installers write into `~/.local/bin`, and the post-install check then looks
299
+ * for the binary by bare name. That directory is on PATH by default on a
300
+ * developer workstation and is NOT on a minimal container, so without this
301
+ * every install in a container would report "exited 0 but still not on PATH"
302
+ * while every laptop run passed — the environment doing the verifying hiding
303
+ * the bug. Prepended, and compared entry-by-entry rather than by substring.
304
+ * @param {string} dir Directory to ensure is present.
305
+ * @param {object} [env] Environment to mutate.
306
+ */
307
+ export function ensureOnPath(dir, env = process.env) {
308
+ const entries = (env.PATH ?? "").split(":").filter(Boolean);
309
+ if (!entries.includes(dir)) env.PATH = [dir, ...entries].join(":");
310
+ }
311
+
312
+ if (process.argv[1] && process.argv[1].endsWith("cli.mjs")) {
313
+ for (const dir of pathDirs(process.env.HOME ?? "")) ensureOnPath(dir);
314
+ process.exitCode = await run(process.argv.slice(2), { ask: askTty });
315
+ }