@codyswann/lisa 2.227.0 → 2.229.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 (71) hide show
  1. package/all/copy-contents/gitignore +2 -0
  2. package/dist/cli/ui-cmd.d.ts +30 -2
  3. package/dist/cli/ui-cmd.d.ts.map +1 -1
  4. package/dist/cli/ui-cmd.js +191 -14
  5. package/dist/cli/ui-cmd.js.map +1 -1
  6. package/dist/cli/ui-status-json.d.ts +29 -0
  7. package/dist/cli/ui-status-json.d.ts.map +1 -0
  8. package/dist/cli/ui-status-json.js +162 -0
  9. package/dist/cli/ui-status-json.js.map +1 -0
  10. package/dist/cli/ui-status.d.ts +42 -0
  11. package/dist/cli/ui-status.d.ts.map +1 -0
  12. package/dist/cli/ui-status.js +221 -0
  13. package/dist/cli/ui-status.js.map +1 -0
  14. package/dist/core/lisa.d.ts +74 -1
  15. package/dist/core/lisa.d.ts.map +1 -1
  16. package/dist/core/lisa.js +165 -25
  17. package/dist/core/lisa.js.map +1 -1
  18. package/package.json +2 -1
  19. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  21. package/plugins/lisa-agy/plugin.json +1 -1
  22. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  24. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  25. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  31. package/plugins/lisa-expo-agy/plugin.json +1 -1
  32. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  36. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  37. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  41. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  42. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  46. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  47. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  51. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  52. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  56. package/plugins/lisa-rails-agy/plugin.json +1 -1
  57. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  61. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  62. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  66. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  67. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  69. package/scripts/install-claude-plugins.sh +89 -9
  70. package/ui/README.md +16 -0
  71. package/ui/index.html +299 -8
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Live-status probe contract and bounded execution for `lisa ui`.
3
+ * @module cli/ui-status
4
+ */
5
+ import { execFile } from "node:child_process";
6
+ import { normalizeProbeResult } from "./ui-status-json.js";
7
+ /** Error used internally to distinguish a bounded probe timeout. */
8
+ class ProbeTimeoutError extends Error {
9
+ /**
10
+ * Create a probe timeout error.
11
+ * @param timeoutMs - Configured timeout in milliseconds
12
+ */
13
+ constructor(timeoutMs) {
14
+ super(`Probe timed out after ${timeoutMs}ms`);
15
+ this.name = "ProbeTimeoutError";
16
+ }
17
+ }
18
+ /**
19
+ * Convert arbitrary thrown data into a stable failure classification.
20
+ * @param error - Value thrown by a probe
21
+ * @returns Timeout classification and non-empty human-readable message
22
+ */
23
+ function classifyProbeError(error) {
24
+ try {
25
+ const timedOut = error instanceof ProbeTimeoutError;
26
+ const rawMessage = error instanceof Error ? error.message : String(error);
27
+ return {
28
+ timedOut,
29
+ message: rawMessage.trim().length > 0
30
+ ? rawMessage
31
+ : "Probe failed without an error message",
32
+ };
33
+ }
34
+ catch {
35
+ return {
36
+ timedOut: false,
37
+ message: "Probe failed with an unreadable error",
38
+ };
39
+ }
40
+ }
41
+ /**
42
+ * Run one probe without allowing it to block siblings or fabricate data.
43
+ * @param probe - Probe to execute
44
+ * @returns Explicit result, or unknown for invalid input, failure, or timeout
45
+ */
46
+ export async function runProbe(probe) {
47
+ if (!Number.isFinite(probe.timeoutMs) || probe.timeoutMs < 0) {
48
+ return {
49
+ state: "unknown",
50
+ reason: "invalid-timeout",
51
+ message: `Probe timeout must be a finite non-negative number; received ${String(probe.timeoutMs)}`,
52
+ };
53
+ }
54
+ const controller = new AbortController();
55
+ const timeout = new Promise((_resolve, reject) => {
56
+ controller.signal.addEventListener("abort", () => reject(controller.signal.reason), { once: true });
57
+ });
58
+ const timer = setTimeout(() => controller.abort(new ProbeTimeoutError(probe.timeoutMs)), probe.timeoutMs);
59
+ try {
60
+ const result = await Promise.race([probe.run(controller.signal), timeout]);
61
+ return normalizeProbeResult(result);
62
+ }
63
+ catch (error) {
64
+ const { timedOut, message } = classifyProbeError(error);
65
+ return {
66
+ state: "unknown",
67
+ reason: timedOut ? "timeout" : "probe-failed",
68
+ message,
69
+ };
70
+ }
71
+ finally {
72
+ clearTimeout(timer);
73
+ }
74
+ }
75
+ /**
76
+ * Execute `gh auth status` without invoking a shell.
77
+ * @param cwd - Project root used as the command working directory
78
+ * @param timeoutMs - Child-process timeout in milliseconds
79
+ * @param signal - Cancellation signal controlled by the probe runner
80
+ * @param hostname - Project origin hostname whose active account is checked
81
+ * @returns Authentication state when the command runs
82
+ */
83
+ const runGithubAuthStatus = async (cwd, timeoutMs, signal, hostname) => await new Promise((resolve, reject) => {
84
+ const ghExecutable = "gh";
85
+ execFile(
86
+ // eslint-disable-next-line sonarjs/no-os-command-from-path -- fixed user-installed gh executable
87
+ ghExecutable, ["auth", "status", "--active", "--hostname", hostname, "--json", "hosts"], { cwd, signal, timeout: timeoutMs, encoding: "utf8" }, (error, stdout) => {
88
+ if (error !== null) {
89
+ reject(error);
90
+ return;
91
+ }
92
+ try {
93
+ const parsed = JSON.parse(stdout);
94
+ if (typeof parsed !== "object" || parsed === null) {
95
+ throw new TypeError("GitHub auth status did not return an object");
96
+ }
97
+ const hosts = Reflect.get(parsed, "hosts");
98
+ if (typeof hosts !== "object" || hosts === null) {
99
+ throw new TypeError("GitHub auth status omitted its hosts map");
100
+ }
101
+ const accounts = Reflect.get(hosts, hostname);
102
+ const authenticated = Array.isArray(accounts) &&
103
+ accounts.some(account => typeof account === "object" &&
104
+ account !== null &&
105
+ Reflect.get(account, "active") === true &&
106
+ Reflect.get(account, "state") === "success");
107
+ resolve(authenticated ? "authenticated" : "not-authenticated");
108
+ }
109
+ catch (parseError) {
110
+ reject(parseError);
111
+ }
112
+ });
113
+ });
114
+ /**
115
+ * Read the effective origin URL through git without invoking a shell.
116
+ * @param cwd - Project root
117
+ * @param timeoutMs - Child-process timeout
118
+ * @param signal - Probe cancellation signal
119
+ * @returns Origin remote URL
120
+ */
121
+ async function readOriginRemote(cwd, timeoutMs, signal) {
122
+ return await new Promise((resolve, reject) => {
123
+ const gitExecutable = "git";
124
+ execFile(
125
+ // eslint-disable-next-line sonarjs/no-os-command-from-path -- fixed user-installed git executable
126
+ gitExecutable, ["-C", cwd, "remote", "get-url", "origin"], { cwd, signal, timeout: timeoutMs, encoding: "utf8" }, (error, stdout) => {
127
+ if (error !== null) {
128
+ reject(error);
129
+ return;
130
+ }
131
+ const remote = stdout.trim();
132
+ if (remote.length === 0) {
133
+ reject(new Error("Project origin remote is empty"));
134
+ return;
135
+ }
136
+ resolve(remote);
137
+ });
138
+ });
139
+ }
140
+ /**
141
+ * Extract a hostname from URL-form and scp-form git remotes.
142
+ * @param remote - Git remote URL
143
+ * @returns Hostname used by `gh auth status`
144
+ */
145
+ function remoteHostname(remote) {
146
+ const normalize = (hostname) => {
147
+ const normalized = hostname.toLowerCase();
148
+ return normalized.endsWith(".") ? normalized.slice(0, -1) : normalized;
149
+ };
150
+ if (!remote.includes("://")) {
151
+ const separator = remote.indexOf(":");
152
+ const authority = separator > 0 ? remote.slice(0, separator) : "";
153
+ const host = authority.slice(authority.lastIndexOf("@") + 1);
154
+ const looksLikeWindowsPath = authority.length === 1 && /[A-Za-z]/u.test(authority);
155
+ if (host.length > 0 &&
156
+ !looksLikeWindowsPath &&
157
+ !Array.from(host).some(character => /\s/u.test(character))) {
158
+ return normalize(host);
159
+ }
160
+ }
161
+ try {
162
+ const hostname = new URL(remote).hostname;
163
+ if (hostname.length > 0) {
164
+ return normalize(hostname);
165
+ }
166
+ }
167
+ catch {
168
+ // Fall through to the explicit error below.
169
+ }
170
+ throw new Error(`Unable to determine GitHub hostname from origin: ${remote}`);
171
+ }
172
+ /**
173
+ * Create the reference GitHub authentication probe.
174
+ * @param cwd - Project root used as the command working directory
175
+ * @param check - Injectable command check used by focused tests
176
+ * @param readRemote - Injectable effective-origin reader used by focused tests
177
+ * @returns Reference probe for the live-status transport
178
+ */
179
+ export function createGithubAuthProbe(cwd, check = runGithubAuthStatus, readRemote = readOriginRemote) {
180
+ const timeoutMs = 5_000;
181
+ return {
182
+ id: "github-auth",
183
+ timeoutMs,
184
+ run: async (signal) => {
185
+ const remote = await readRemote(cwd, timeoutMs + 250, signal);
186
+ const hostname = remoteHostname(remote);
187
+ const state = await check(cwd, timeoutMs + 250, signal, hostname);
188
+ return state === "authenticated"
189
+ ? { state: "value", value: true }
190
+ : {
191
+ state: "unknown",
192
+ reason: "not-authenticated",
193
+ message: "GitHub CLI is not authenticated",
194
+ };
195
+ },
196
+ };
197
+ }
198
+ /**
199
+ * Validate probe keys before binding a server so status snapshots cannot lose data.
200
+ * @param probes - Registered status probes
201
+ */
202
+ export function validateStatusProbes(probes) {
203
+ for (const [index, probe] of probes.entries()) {
204
+ if (probe.id.trim().length === 0) {
205
+ throw new Error("Probe id must contain a non-whitespace character");
206
+ }
207
+ if (probes.slice(0, index).some(candidate => candidate.id === probe.id)) {
208
+ throw new Error(`Duplicate probe id: ${probe.id}`);
209
+ }
210
+ }
211
+ }
212
+ /**
213
+ * Execute all probes concurrently, with failures isolated per probe.
214
+ * @param probes - Probes to execute
215
+ * @returns Results keyed by stable probe identifier
216
+ */
217
+ export async function readStatusSnapshot(probes) {
218
+ const entries = await Promise.all(probes.map(async (probe) => [probe.id, await runProbe(probe)]));
219
+ return Object.fromEntries(entries);
220
+ }
221
+ //# sourceMappingURL=ui-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui-status.js","sourceRoot":"","sources":["../../src/cli/ui-status.ts"],"names":[],"mappings":"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"}
@@ -226,11 +226,76 @@ export declare class Lisa {
226
226
  */
227
227
  private registerPlugins;
228
228
  /**
229
- * Install each plugin and refresh the Lisa marketplace cache
229
+ * Install each plugin and refresh the Lisa marketplace cache.
230
+ *
231
+ * Version-gated (perf): `claude plugin marketplace update` is a network git
232
+ * pull of the Lisa repo and every `claude plugin install` spawns the Claude
233
+ * CLI (seconds each); re-running all of it on every apply made a single
234
+ * `bun add` cost minutes across the repeated apply passes. A full sync
235
+ * (marketplace refresh + install every plugin) still runs whenever the
236
+ * installed Lisa version differs from the `.claude/.lisa-plugins-synced`
237
+ * marker; between version changes only plugins missing for this project are
238
+ * installed, so the self-heal property (a removed plugin comes back on the
239
+ * next install) survives at the cost of one `claude plugin list` call.
230
240
  * @param execAsync - Promisified exec function for running shell commands
231
241
  * @param plugins - Plugin identifiers from enabledPlugins (e.g. "lisa@lisa")
232
242
  */
233
243
  private installPluginsAndUpdateMarketplace;
244
+ /**
245
+ * Decide whether this apply needs a full plugin sync and which plugins to
246
+ * install.
247
+ * @param execAsync - Promisified exec function for running shell commands
248
+ * @param plugins - Plugin identifiers from enabledPlugins
249
+ * @returns Sync plan: fullSync (version changed), plugins to install, and
250
+ * the current Lisa version
251
+ */
252
+ private computePluginSyncPlan;
253
+ /**
254
+ * Refresh the cached Lisa marketplace via the Claude CLI.
255
+ * @param execAsync - Promisified exec function for running shell commands
256
+ * @param announce - Whether to log the outcome (silent for the pre-install
257
+ * best-effort refresh)
258
+ * @returns Promise that resolves when the refresh attempt completes
259
+ */
260
+ private updateLisaMarketplace;
261
+ /**
262
+ * Install a single plugin at project scope via the Claude CLI.
263
+ * @param execAsync - Promisified exec function for running shell commands
264
+ * @param plugin - Plugin identifier (already validated)
265
+ * @returns Promise that resolves when the install attempt completes
266
+ */
267
+ private installPlugin;
268
+ /**
269
+ * Exec options shared by all Claude CLI plugin commands.
270
+ * @returns Options object with the project cwd and POSIX shell
271
+ */
272
+ private pluginExecOptions;
273
+ /**
274
+ * Absolute path of the plugin sync marker recording the last fully-synced
275
+ * Lisa version for this project. Shared with install-claude-plugins.sh.
276
+ * @returns Marker path under the project's .claude directory
277
+ */
278
+ private pluginSyncMarkerPath;
279
+ /**
280
+ * Absolute path of the host project's package.json.
281
+ * @returns package.json path under the destination directory
282
+ */
283
+ private hostPackageJsonPath;
284
+ /**
285
+ * Read the plugin ids already installed for this project via
286
+ * `claude plugin list --json`.
287
+ * @param execAsync - Promisified exec function for running shell commands
288
+ * @returns Set of installed plugin ids, or null when the list cannot be
289
+ * obtained (older CLI, parse failure) so callers fall back to installing
290
+ * everything — the pre-existing behavior
291
+ */
292
+ private readInstalledPluginIds;
293
+ /**
294
+ * Record the Lisa version whose plugin set was last fully synced.
295
+ * @param version - Lisa package version to record
296
+ * @returns Promise that resolves when the marker is written (best-effort)
297
+ */
298
+ private writePluginSyncMarker;
234
299
  /**
235
300
  * Finalize operation
236
301
  * @returns Promise that resolves when finalization is complete
@@ -387,6 +452,14 @@ export declare class Lisa {
387
452
  * The trampoline is always fully detached so the package manager can exit
388
453
  * normally; the child then detects the exit and re-runs Lisa. See
389
454
  * utils/postinstall-trampoline.ts for details.
455
+ *
456
+ * Skipped when this apply did not mutate package.json: the trampoline exists
457
+ * solely to redo package.json changes that the parent package manager's
458
+ * end-of-command rewrite clobbers (#383). With no mutation there is nothing
459
+ * to clobber — the rewrite restores identical content — and scheduling a
460
+ * detached re-apply (plus its lockfile regen) is pure waste. File-based
461
+ * template writes are not clobbered and need no reconciliation.
462
+ * @param prePackageJsonHash - Hash of package.json captured before apply mutated anything (null if the file did not exist)
390
463
  * @returns Promise that resolves immediately after the detached child is spawned
391
464
  */
392
465
  private schedulePostinstallReconciliation;
@@ -1 +1 @@
1
- {"version":3,"file":"lisa.d.ts","sourceRoot":"","sources":["../../src/core/lisa.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAyBnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAOzD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EACL,iBAAiB,EAGlB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,gBAAgB,EAAwB,MAAM,wBAAwB,CAAC;AAChF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAc9D,OAAO,KAAK,EAIV,UAAU,EACV,UAAU,EAGX,MAAM,aAAa,CAAC;AAWrB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAkBpD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,cAAc,CAAC;IACvC,QAAQ,CAAC,gBAAgB,EAAE,gBAAgB,CAAC;IAC5C,QAAQ,CAAC,gBAAgB,EAAE,gBAAgB,CAAC;IAC5C,QAAQ,CAAC,UAAU,EAAE,WAAW,CAAC;IACjC,QAAQ,CAAC,iBAAiB,EAAE,iBAAiB,CAAC;CAC/C;AAED;;GAEG;AACH,qBAAa,IAAI;IA4Eb,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,IAAI;IA5EvB,OAAO,CAAC,QAAQ,CAA8C;IAC9D,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,cAAc,CAGpB;IACF;;;;;;OAMG;IACH,OAAO,CAAC,gBAAgB,CAA0B;IAClD;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,mBAAmB,CAAkC;IAC7D;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,OAAO,CAAC,sBAAsB,CAAkC;IAChE,2EAA2E;IAC3E,OAAO,CAAC,oBAAoB,CAAmC;IAC/D,4EAA4E;IAC5E,OAAO,CAAC,kCAAkC,CAAS;IACnD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA8C;IACxE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAmB;IAEpD;;;;OAIG;IACH,OAAO,CAAC,SAAS,CAAS;IAE1B;;;;OAIG;gBAEgB,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,gBAAgB;IAGzC;;;;;;;;OAQG;IACH,OAAO,CAAC,oCAAoC;IAI5C;;OAEG;YACW,YAAY;IAQ1B;;OAEG;YACW,WAAW;IAUzB;;;;;;OAMG;YACW,eAAe;IAa7B;;OAEG;YACW,qBAAqB;IAyBnC;;;;;;;;;;;;;;;OAeG;YACW,oBAAoB;IAWlC;;;;OAIG;YACW,wBAAwB;IAYtC;;;;;;;;;;;;;;;;;;;;;;OAsBG;YACW,uBAAuB;IAsCrC;;;;;;;;;;;OAWG;YACW,0BAA0B;IAqBxC;;;;;;;OAOG;YACW,2BAA2B;IAyBzC;;OAEG;YACW,gBAAgB;IAoB9B;;;OAGG;YACW,oBAAoB;IA2BlC;;;OAGG;YACW,qBAAqB;IAoDnC;;;OAGG;YACW,6BAA6B;IAc3C;;OAEG;YACW,iBAAiB;IAgB/B;;;OAGG;IACH,OAAO,CAAC,uBAAuB;IAW/B;;OAEG;YACW,eAAe;IA+C7B;;;;OAIG;YACW,kCAAkC;IAgDhD;;;OAGG;YACW,QAAQ;IAQtB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAWxB;;;;OAIG;YACW,gBAAgB;IAqB9B;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC;IA6ClC;;;;;;;;;;;;;;;;;;;OAmBG;YACW,2BAA2B;IAqBzC;;;;;;;;;OASG;YACW,gBAAgB;IAoC9B;;;;;;;;OAQG;YACW,iBAAiB;IA0B/B;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;YACW,cAAc;IA+E5B;;;;;;;OAOG;IACH,OAAO,CAAC,qBAAqB;IAM7B;;;;;;;;;OASG;YACW,kBAAkB;IAoDhC;;;;;;;;;;;;;;OAcG;YACW,mBAAmB;IAsGjC;;;;;;;;;;;;;;;;OAgBG;YACW,gCAAgC;IAiB9C;;;;;;;;;;;OAWG;YACW,iCAAiC;IAmB/C;;;OAGG;IACG,QAAQ,IAAI,OAAO,CAAC,UAAU,CAAC;IAKrC;;;OAGG;YACW,kBAAkB;IAuBhC;;;;;;;OAOG;YACW,gBAAgB;IAuD9B;;;;;;;OAOG;IACH,OAAO,CAAC,0BAA0B;IAelC;;;;;OAKG;IACH,OAAO,CAAC,sBAAsB;IAO9B;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,OAAO,CAAC,iBAAiB;IA4CzB;;;;;OAKG;IACH,OAAO,CAAC,0BAA0B;IAyBlC;;;;;;;OAOG;IACH,OAAO,CAAC,kBAAkB;IAK1B;;;;;;OAMG;IACH,OAAO,CAAC,OAAO;IAaf;;;;;;OAMG;YACW,qBAAqB;IAyBnC;;;;OAIG;YACW,QAAQ;IAatB;;;OAGG;IACH,OAAO,CAAC,cAAc;IAqBtB;;;;OAIG;IACH,OAAO,CAAC,UAAU;IAKlB;;;OAGG;IACH,OAAO,CAAC,SAAS;IAqCjB;;OAEG;YACW,mBAAmB;IAajC;;OAEG;YACW,gBAAgB;IA+B9B;;OAEG;IACH,OAAO,CAAC,WAAW;IA4BnB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAgB1B;;;;;;OAMG;IACH,OAAO,CAAC,aAAa;IAYrB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAmB1B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IA6BxB;;OAEG;IACH,OAAO,CAAC,eAAe;IA6BvB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAezB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAazB;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAgB7B;;OAEG;IACH,OAAO,CAAC,YAAY;IAOpB;;;;;OAKG;YACW,WAAW;IAQzB;;;;;OAKG;YACW,qBAAqB;CAqDpC"}
1
+ {"version":3,"file":"lisa.d.ts","sourceRoot":"","sources":["../../src/core/lisa.ts"],"names":[],"mappings":"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"}
package/dist/core/lisa.js CHANGED
@@ -4,6 +4,7 @@ import { existsSync } from "node:fs";
4
4
  import { readFile, stat } from "node:fs/promises";
5
5
  import * as path from "node:path";
6
6
  import pc from "picocolors";
7
+ import { getPackageVersion } from "../cli/version.js";
7
8
  import { installAgentsMd } from "../codex/agents-md-installer.js";
8
9
  import { installCodexProjectOverlay } from "../codex/project-overlay.js";
9
10
  import { installAgyPlugin } from "../agy/plugin-installer.js";
@@ -491,49 +492,171 @@ export class Lisa {
491
492
  await this.installPluginsAndUpdateMarketplace(execAsync, plugins);
492
493
  }
493
494
  /**
494
- * Install each plugin and refresh the Lisa marketplace cache
495
+ * Install each plugin and refresh the Lisa marketplace cache.
496
+ *
497
+ * Version-gated (perf): `claude plugin marketplace update` is a network git
498
+ * pull of the Lisa repo and every `claude plugin install` spawns the Claude
499
+ * CLI (seconds each); re-running all of it on every apply made a single
500
+ * `bun add` cost minutes across the repeated apply passes. A full sync
501
+ * (marketplace refresh + install every plugin) still runs whenever the
502
+ * installed Lisa version differs from the `.claude/.lisa-plugins-synced`
503
+ * marker; between version changes only plugins missing for this project are
504
+ * installed, so the self-heal property (a removed plugin comes back on the
505
+ * next install) survives at the cost of one `claude plugin list` call.
495
506
  * @param execAsync - Promisified exec function for running shell commands
496
507
  * @param plugins - Plugin identifiers from enabledPlugins (e.g. "lisa@lisa")
497
508
  */
498
509
  async installPluginsAndUpdateMarketplace(execAsync, plugins) {
499
510
  const { logger } = this.deps;
500
511
  const validPluginName = /^[\w@./-]+$/;
501
- logger.info("Registering plugins with Claude Code (project scope)...");
502
- try {
503
- await execAsync("claude plugin marketplace update lisa", {
504
- cwd: this.config.destDir,
505
- shell: "/bin/sh",
506
- });
512
+ const { fullSync, toInstall, version } = await this.computePluginSyncPlan(execAsync, plugins);
513
+ if (!fullSync && toInstall.length === 0) {
514
+ logger.info(pc.gray(`Plugins already in sync for Lisa ${version}; skipping`));
515
+ return;
507
516
  }
508
- catch {
517
+ logger.info("Registering plugins with Claude Code (project scope)...");
518
+ if (fullSync) {
509
519
  // Best-effort cache refresh. Individual plugin installs below will still
510
520
  // report whether the marketplace entry is available.
521
+ await this.updateLisaMarketplace(execAsync, false);
511
522
  }
512
- for (const plugin of plugins) {
523
+ for (const plugin of toInstall) {
513
524
  if (!validPluginName.test(plugin)) {
514
525
  logger.warn(`Skipping invalid plugin name: ${plugin}`);
515
526
  continue;
516
527
  }
517
- try {
518
- await execAsync(`claude plugin install ${plugin} --scope project`, {
519
- cwd: this.config.destDir,
520
- shell: "/bin/sh",
521
- });
522
- logger.success(`Registered plugin: ${plugin}`);
528
+ await this.installPlugin(execAsync, plugin);
529
+ }
530
+ // Post-install refresh must run whenever installs happened: installing can
531
+ // register the marketplace for the first time, and without a refresh newly
532
+ // added skills are not discovered ("Unknown skill" in nightly CI — #320).
533
+ await this.updateLisaMarketplace(execAsync, true);
534
+ if (fullSync) {
535
+ await this.writePluginSyncMarker(version);
536
+ }
537
+ }
538
+ /**
539
+ * Decide whether this apply needs a full plugin sync and which plugins to
540
+ * install.
541
+ * @param execAsync - Promisified exec function for running shell commands
542
+ * @param plugins - Plugin identifiers from enabledPlugins
543
+ * @returns Sync plan: fullSync (version changed), plugins to install, and
544
+ * the current Lisa version
545
+ */
546
+ async computePluginSyncPlan(execAsync, plugins) {
547
+ const version = getPackageVersion();
548
+ const syncedVersion = await readFile(this.pluginSyncMarkerPath(), "utf-8")
549
+ .then(content => content.trim())
550
+ .catch(() => null);
551
+ const fullSync = syncedVersion !== version;
552
+ const installed = fullSync
553
+ ? null
554
+ : await this.readInstalledPluginIds(execAsync);
555
+ const toInstall = installed === null
556
+ ? plugins
557
+ : plugins.filter(plugin => !installed.has(plugin));
558
+ return { fullSync, toInstall, version };
559
+ }
560
+ /**
561
+ * Refresh the cached Lisa marketplace via the Claude CLI.
562
+ * @param execAsync - Promisified exec function for running shell commands
563
+ * @param announce - Whether to log the outcome (silent for the pre-install
564
+ * best-effort refresh)
565
+ * @returns Promise that resolves when the refresh attempt completes
566
+ */
567
+ async updateLisaMarketplace(execAsync, announce) {
568
+ const { logger } = this.deps;
569
+ try {
570
+ await execAsync("claude plugin marketplace update lisa", this.pluginExecOptions());
571
+ if (announce) {
572
+ logger.success("Updated marketplace: lisa");
523
573
  }
524
- catch {
525
- logger.warn(`Could not register plugin: ${plugin}`);
574
+ }
575
+ catch {
576
+ if (announce) {
577
+ logger.warn("Could not update marketplace: lisa");
526
578
  }
527
579
  }
580
+ }
581
+ /**
582
+ * Install a single plugin at project scope via the Claude CLI.
583
+ * @param execAsync - Promisified exec function for running shell commands
584
+ * @param plugin - Plugin identifier (already validated)
585
+ * @returns Promise that resolves when the install attempt completes
586
+ */
587
+ async installPlugin(execAsync, plugin) {
588
+ const { logger } = this.deps;
589
+ try {
590
+ await execAsync(`claude plugin install ${plugin} --scope project`, this.pluginExecOptions());
591
+ logger.success(`Registered plugin: ${plugin}`);
592
+ }
593
+ catch {
594
+ logger.warn(`Could not register plugin: ${plugin}`);
595
+ }
596
+ }
597
+ /**
598
+ * Exec options shared by all Claude CLI plugin commands.
599
+ * @returns Options object with the project cwd and POSIX shell
600
+ */
601
+ pluginExecOptions() {
602
+ return { cwd: this.config.destDir, shell: "/bin/sh" };
603
+ }
604
+ /**
605
+ * Absolute path of the plugin sync marker recording the last fully-synced
606
+ * Lisa version for this project. Shared with install-claude-plugins.sh.
607
+ * @returns Marker path under the project's .claude directory
608
+ */
609
+ pluginSyncMarkerPath() {
610
+ return path.join(this.config.destDir, ".claude", ".lisa-plugins-synced");
611
+ }
612
+ /**
613
+ * Absolute path of the host project's package.json.
614
+ * @returns package.json path under the destination directory
615
+ */
616
+ hostPackageJsonPath() {
617
+ return path.join(this.config.destDir, "package.json");
618
+ }
619
+ /**
620
+ * Read the plugin ids already installed for this project via
621
+ * `claude plugin list --json`.
622
+ * @param execAsync - Promisified exec function for running shell commands
623
+ * @returns Set of installed plugin ids, or null when the list cannot be
624
+ * obtained (older CLI, parse failure) so callers fall back to installing
625
+ * everything — the pre-existing behavior
626
+ */
627
+ async readInstalledPluginIds(execAsync) {
528
628
  try {
529
- await execAsync("claude plugin marketplace update lisa", {
629
+ const result = (await execAsync("claude plugin list --json", {
530
630
  cwd: this.config.destDir,
531
631
  shell: "/bin/sh",
532
- });
533
- logger.success("Updated marketplace: lisa");
632
+ }));
633
+ const parsed = JSON.parse(String(result.stdout ?? ""));
634
+ if (!Array.isArray(parsed)) {
635
+ return null;
636
+ }
637
+ const destPath = path.resolve(this.config.destDir);
638
+ const ids = parsed
639
+ .filter((entry) => typeof entry.id === "string" &&
640
+ entry.projectPath === destPath)
641
+ .map(entry => entry.id);
642
+ return new Set(ids);
534
643
  }
535
644
  catch {
536
- logger.warn("Could not update marketplace: lisa");
645
+ return null;
646
+ }
647
+ }
648
+ /**
649
+ * Record the Lisa version whose plugin set was last fully synced.
650
+ * @param version - Lisa package version to record
651
+ * @returns Promise that resolves when the marker is written (best-effort)
652
+ */
653
+ async writePluginSyncMarker(version) {
654
+ try {
655
+ await fse.ensureDir(path.join(this.config.destDir, ".claude"));
656
+ await fse.writeFile(this.pluginSyncMarkerPath(), `${version}\n`, "utf-8");
657
+ }
658
+ catch {
659
+ // Best-effort: a missing marker only costs a redundant full sync next run.
537
660
  }
538
661
  }
539
662
  /* v8 ignore stop */
@@ -602,8 +725,12 @@ export class Lisa {
602
725
  await this.loadPendingDeletions();
603
726
  await this.loadCreateOnlyOwnership();
604
727
  await this.loadCopyOverwriteOwnership();
728
+ // Captured BEFORE any mutation (migrations included): the hash gates both
729
+ // the detached trampoline and the in-process lockfile reconcile, and
730
+ // before-strategies migrations can wire scripts.postinstall — a change the
731
+ // package manager's end-of-command rewrite would clobber (#383).
732
+ const prePackageJsonHash = hashFile(this.hostPackageJsonPath());
605
733
  await this.runMigrationsBeforeStrategies();
606
- const prePackageJsonHash = hashFile(path.join(this.config.destDir, "package.json"));
607
734
  await this.processConfigurations();
608
735
  if (this.selfApply) {
609
736
  this.deps.logger.info(pc.gray("Self-apply (Lisa source repo): skipped deletions.json processing"));
@@ -622,7 +749,7 @@ export class Lisa {
622
749
  await this.finalize();
623
750
  this.printSummary();
624
751
  await this.printMigrationNotices(this.config.destDir);
625
- await this.schedulePostinstallReconciliation();
752
+ await this.schedulePostinstallReconciliation(prePackageJsonHash);
626
753
  await this.reconcileLockfilesInProcess(prePackageJsonHash);
627
754
  return this.getSuccessResult();
628
755
  }
@@ -657,7 +784,7 @@ export class Lisa {
657
784
  return;
658
785
  if (shouldSchedulePostinstallReconciliation(this.config.dryRun))
659
786
  return;
660
- const pkgPath = path.join(this.config.destDir, "package.json");
787
+ const pkgPath = this.hostPackageJsonPath();
661
788
  const postHash = hashFile(pkgPath);
662
789
  if (prePackageJsonHash === null ||
663
790
  postHash === null ||
@@ -987,9 +1114,22 @@ export class Lisa {
987
1114
  * The trampoline is always fully detached so the package manager can exit
988
1115
  * normally; the child then detects the exit and re-runs Lisa. See
989
1116
  * utils/postinstall-trampoline.ts for details.
1117
+ *
1118
+ * Skipped when this apply did not mutate package.json: the trampoline exists
1119
+ * solely to redo package.json changes that the parent package manager's
1120
+ * end-of-command rewrite clobbers (#383). With no mutation there is nothing
1121
+ * to clobber — the rewrite restores identical content — and scheduling a
1122
+ * detached re-apply (plus its lockfile regen) is pure waste. File-based
1123
+ * template writes are not clobbered and need no reconciliation.
1124
+ * @param prePackageJsonHash - Hash of package.json captured before apply mutated anything (null if the file did not exist)
990
1125
  * @returns Promise that resolves immediately after the detached child is spawned
991
1126
  */
992
- async schedulePostinstallReconciliation() {
1127
+ async schedulePostinstallReconciliation(prePackageJsonHash) {
1128
+ const postHash = hashFile(this.hostPackageJsonPath());
1129
+ if (prePackageJsonHash === postHash) {
1130
+ this.deps.logger.info(pc.gray("Postinstall reconciliation skipped: package.json unchanged by this apply"));
1131
+ return;
1132
+ }
993
1133
  if (!shouldSchedulePostinstallReconciliation(this.config.dryRun)) {
994
1134
  return;
995
1135
  }