projectstore-claude 0.30.4 → 0.30.6

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 (50) hide show
  1. package/node_modules/projectstore/.claude-plugin/marketplace.json +2 -2
  2. package/node_modules/projectstore/.claude-plugin/plugin.json +6 -2
  3. package/node_modules/projectstore/.mcp.json +1 -2
  4. package/node_modules/projectstore/README.md +65 -16
  5. package/node_modules/projectstore/agents/archaeologist.md +1 -1
  6. package/node_modules/projectstore/agents/librarian.md +1 -1
  7. package/node_modules/projectstore/agents/planner.md +1 -1
  8. package/node_modules/projectstore/commands/doctor.md +14 -11
  9. package/node_modules/projectstore/commands/reconcile.md +1 -1
  10. package/node_modules/projectstore/commands/review.md +2 -2
  11. package/node_modules/projectstore/commands/story.md +1 -1
  12. package/node_modules/projectstore/docs/getting-started.md +3 -4
  13. package/node_modules/projectstore/docs/harnesses.md +1 -1
  14. package/node_modules/projectstore/docs/how-it-works.md +7 -0
  15. package/node_modules/projectstore/docs/images/icon.png +0 -0
  16. package/node_modules/projectstore/harnesses/claude-code.json +61 -11
  17. package/node_modules/projectstore/harnesses/codex.json +45 -15
  18. package/node_modules/projectstore/hooks/band.tsx +19 -8
  19. package/node_modules/projectstore/hooks/hooks.json +6 -6
  20. package/node_modules/projectstore/hooks/session-start.mjs +55 -24
  21. package/node_modules/projectstore/hooks/session-stop.mjs +43 -20
  22. package/node_modules/projectstore/package.json +11 -12
  23. package/node_modules/projectstore/scripts/band-shared.mjs +17 -2
  24. package/node_modules/projectstore/scripts/band.mjs +20 -2
  25. package/node_modules/projectstore/scripts/binding.mjs +1 -1
  26. package/node_modules/projectstore/scripts/build-adapters.mjs +45 -4
  27. package/node_modules/projectstore/scripts/cli.mjs +74 -1373
  28. package/node_modules/projectstore/scripts/convergence.mjs +59 -16
  29. package/node_modules/projectstore/scripts/doctor.mjs +199 -36
  30. package/node_modules/projectstore/scripts/harness.mjs +88 -6
  31. package/node_modules/projectstore/scripts/install-handlers.mjs +925 -0
  32. package/node_modules/projectstore/scripts/install-harness.mjs +129 -2081
  33. package/node_modules/projectstore/scripts/install-layout.mjs +453 -0
  34. package/node_modules/projectstore/scripts/install-preview.mjs +747 -0
  35. package/node_modules/projectstore/scripts/lib.mjs +387 -107
  36. package/node_modules/projectstore/scripts/mcp-server.mjs +33 -0
  37. package/node_modules/projectstore/scripts/mcp.mjs +60 -60
  38. package/node_modules/projectstore/scripts/model-spec.mjs +24 -0
  39. package/node_modules/projectstore/scripts/models.mjs +6 -10
  40. package/node_modules/projectstore/scripts/query.mjs +7 -6
  41. package/node_modules/projectstore/scripts/read-verbs.mjs +277 -0
  42. package/node_modules/projectstore/scripts/setup.mjs +62 -12
  43. package/node_modules/projectstore/scripts/statusline-launcher.mjs +36 -3
  44. package/node_modules/projectstore/scripts/surfaces.mjs +18 -5
  45. package/node_modules/projectstore/scripts/touch-session.mjs +30 -15
  46. package/node_modules/projectstore/scripts/verbs.mjs +1202 -0
  47. package/node_modules/projectstore/scripts/version-guard.mjs +57 -7
  48. package/package.json +3 -3
  49. package/node_modules/projectstore/docs/images/loop-light.svg +0 -94
  50. package/node_modules/projectstore/docs/images/team-light.svg +0 -79
@@ -0,0 +1,453 @@
1
+ // projectstore — install-layout.mjs: the layout migration, planned and applied
2
+ // (the layout ADR; layout spec contract 6), and the uninstall item that
3
+ // empties a harness's state directory.
4
+ //
5
+ // Moved out of install-harness.mjs as it was (the story "The installer is
6
+ // split into modules under 200 KiB each, with every export and every test
7
+ // unchanged"). plan() calls planLayout and planHarnessState; apply() hands a
8
+ // `layout` item to applyLayout, which writes files only through lib.mjs.
9
+ // stepResult is what one step left behind for its APPLY line; the
10
+ // registration's apply reads it too.
11
+ //
12
+ // A leaf: it imports neither the installer nor any module that reads the
13
+ // registry, fetches a shell or runs a host command.
14
+
15
+ import { mkdirSync, rmdirSync, readdirSync, existsSync, readFileSync, statSync } from "node:fs";
16
+ import { join, resolve, dirname, isAbsolute, basename } from "node:path";
17
+ import { homedir } from "node:os";
18
+ import { loadHarness, insideHostSession } from "./harness.mjs";
19
+ import { analyseLayout, analyseHarnessState, isOurFile } from "./surfaces.mjs";
20
+ import {
21
+ writeFileAtomic,
22
+ ensureStateDir,
23
+ ensureRuntimeDir,
24
+ statusLineIsOurWiring,
25
+ claudeHome,
26
+ moveStateDir,
27
+ mergeEntryLog,
28
+ movePath,
29
+ removeInside,
30
+ statusLineScriptPath,
31
+ layoutPaths,
32
+ } from "./lib.mjs";
33
+
34
+ // ─── the layout migration (the layout ADR; layout spec contract 6) ──────
35
+ //
36
+ // Two items, kind `layout`. The FIRST moves what only we read — the legacy
37
+ // state directory (per-file collision policy), the entry log (merged), the
38
+ // two legacy markers — and, last of its steps, the binding, whose `agents`
39
+ // block becomes the harness's overlay. It never touches the launcher: the
40
+ // settings entry names it, and the entry is re-pointed by the statusline item
41
+ // only after the launcher item has written the new one. The LAST item, planned
42
+ // after every surface, removes the legacy launcher once nothing names it and
43
+ // prunes the emptied legacy runtime directory. Two config files is the one
44
+ // refusal (a merge is the user's); uninstall is never blocked by it.
45
+ export function planLayout(ctx) {
46
+ const { projectDir, mode, env, harness } = ctx;
47
+ const a = analyseLayout(projectDir, { harness });
48
+ const P = a.paths;
49
+ const none = { first: [], last: [] };
50
+ if (!a.pending) return none;
51
+ const base = { surface: "layout", kind: "layout", path: P.legacy.binding, entry: null, state: a.state, reason: null };
52
+ if (mode === "uninstall") {
53
+ // Disowning: the legacy runtime directory goes with the new one when it is
54
+ // ours (its header); the legacy binding is bind's, never uninstall's. A
55
+ // narrowed uninstall leaves it alone: the way back from an npm switch is
56
+ // `uninstall --surface plugin`, and it deleted a not-yet-moved project's
57
+ // sessions, log and the launcher its status line still ran (the critic's
58
+ // fourth pass, 2026-10-04). `surfaces` is null when nothing was named.
59
+ if (ctx.surfaces) return none;
60
+ if (!a.legacy.runtime || !a.legacy.runtimeOurs) return none;
61
+ return {
62
+ first: [],
63
+ last: [
64
+ {
65
+ ...base,
66
+ surface: "layout_cleanup",
67
+ path: P.legacy.runtime,
68
+ state: "legacy",
69
+ action: "remove",
70
+ reason: "the legacy runtime directory is ours (its .gitignore header) and goes with the state",
71
+ steps: [
72
+ {
73
+ kind: "remove-legacy-runtime",
74
+ path: P.legacy.runtime,
75
+ why: "the pre-0.28 state directory, removed whole",
76
+ },
77
+ {
78
+ kind: "note",
79
+ why: ".projectstore/state/ (sessions, the entry log) and the binding stay: the hooks' records and bind's file, not install's — delete .projectstore/ by hand to disown fully",
80
+ },
81
+ ],
82
+ },
83
+ ],
84
+ };
85
+ }
86
+ if (a.twoConfigs && !a.resumable)
87
+ return {
88
+ first: [
89
+ {
90
+ ...base,
91
+ action: "refuse",
92
+ reason: `two bindings: ${P.legacy.binding} (legacy) and ${P.binding} — keep one and delete the other (usually the legacy one), then run install again; nothing is written while both exist`,
93
+ },
94
+ ],
95
+ last: [],
96
+ };
97
+ if (insideHostSession(env, harness)) {
98
+ ctx.incomplete = true;
99
+ return {
100
+ first: [
101
+ {
102
+ ...base,
103
+ action: "skip",
104
+ deferred: true,
105
+ reason: `the layout migration moves files this ${harness.display_name} session reads and writes — run it from a terminal outside the session`,
106
+ },
107
+ ],
108
+ last: [],
109
+ };
110
+ }
111
+ const steps = [
112
+ {
113
+ kind: "note",
114
+ why: `close other ${harness.display_name} sessions in this project first and restart afterwards: the migration moves files a running session reads and writes, and the status line does not reload mid-session`,
115
+ },
116
+ { kind: "ensure", path: P.root, why: ".projectstore/ with its .gitignore (projectstore.json, state/)" },
117
+ ];
118
+ if (a.legacy.state)
119
+ steps.push({
120
+ kind: "move-state",
121
+ from: P.legacy.state,
122
+ to: P.sessions,
123
+ files: a.legacy.stateFiles.length,
124
+ why: "session files move into state/sessions/; one present on both sides keeps the newer, a per-session directory keeps the new side",
125
+ });
126
+ if (a.legacy.entryLog)
127
+ steps.push({
128
+ kind: "merge-log",
129
+ from: P.legacy.entryLog,
130
+ to: P.entryLog,
131
+ why: "the legacy entry log's lines go before the new log's",
132
+ });
133
+ if (a.legacy.welcomed)
134
+ steps.push({
135
+ kind: "move-marker",
136
+ from: P.legacy.welcomed,
137
+ to: P.welcomed(harness.id),
138
+ why: "the welcome marker moves under state/<harness>/ — a migrated project is not welcomed twice",
139
+ });
140
+ if (a.legacy.sessionId) steps.push({ kind: "delete", path: P.legacy.sessionId, why: "the 0.6-era session-id file" });
141
+ if (a.legacy.binding && a.resumable)
142
+ steps.push({
143
+ kind: "delete",
144
+ path: P.legacy.binding,
145
+ why: "the binding was already moved (an interrupted run left the legacy copy, byte-equal but for its agents block)",
146
+ });
147
+ else if (a.legacy.binding)
148
+ steps.push({
149
+ kind: "move-binding",
150
+ from: P.legacy.binding,
151
+ to: P.binding,
152
+ overlay: P.overlay(harness.id),
153
+ why: `the binding moves; its agents block becomes harness/${harness.id}.json`,
154
+ });
155
+ const first = [
156
+ {
157
+ ...base,
158
+ action: "migrate",
159
+ steps,
160
+ reason: `${a.legacy.binding ? "binding" : "state"} in the pre-0.28 layout under ${a.legacy.dir}/`,
161
+ },
162
+ ];
163
+ const last = [];
164
+ if (a.legacy.launcher || a.legacy.runtime) {
165
+ const cleanup = [];
166
+ if (a.legacy.launcher)
167
+ cleanup.push({
168
+ kind: "remove-legacy-launcher",
169
+ path: P.legacy.launcher,
170
+ why: "removed once the new launcher is written and the settings entry names it — or at once when the status-line slot is not ours (foreign, or empty because the status line is off); kept while a settings file the host reads (this project's local or committed settings, or the user's) still runs it as the status line — a script that calls it indirectly is not seen",
171
+ });
172
+ if (a.legacy.runtime)
173
+ cleanup.push({ kind: "rmdir-legacy", path: P.legacy.runtime, why: "the emptied pre-0.28 runtime directory" });
174
+ last.push({
175
+ ...base,
176
+ surface: "layout_cleanup",
177
+ path: P.legacy.runtime,
178
+ state: "legacy",
179
+ action: "cleanup",
180
+ reason: null,
181
+ steps: cleanup,
182
+ });
183
+ }
184
+ return { first, last };
185
+ }
186
+
187
+ // A project uninstall's last item per harness (the shell-fetch story's rule
188
+ // 5; install spec contract 13 as amended): .projectstore/state/<id>/ is
189
+ // emptied of the welcome marker — recognised by analyseHarnessState — and
190
+ // removed once empty, so the next bare upgrade stops selecting the harness
191
+ // it disowned. Planned for a directory holding the marker, or nothing; a
192
+ // directory holding only what is not ours is left without a row. It never
193
+ // touches a launcher: that is its surface item's to remove, when it is ours.
194
+ // What stays — a launcher this plan does not remove, a file of the user's —
195
+ // is named in the preview, and keeps the directory. Not a guarantee: a
196
+ // harness whose plugin stays installed for the machine fires its hooks here
197
+ // again, its welcome comes back, and so does the signal — accurately.
198
+ export function planHarnessState(projectDir, harness, planned) {
199
+ const a = analyseHarnessState(projectDir, harness.id);
200
+ if (!a.present || (!a.marker && a.names.length)) return null;
201
+ const removing = new Set(
202
+ planned
203
+ .filter((i) => i.kind === "exclusive" && i.action === "remove" && dirname(i.path) === a.dir)
204
+ .map((i) => basename(i.path)),
205
+ );
206
+ const left = a.others.filter((n) => !removing.has(n));
207
+ const steps = [];
208
+ if (a.marker)
209
+ steps.push({
210
+ kind: "delete",
211
+ path: a.marker,
212
+ why: "the welcome marker the SessionStart hook wrote; a bare upgrade selects the harness by it",
213
+ });
214
+ steps.push({
215
+ kind: "rmdir-state",
216
+ path: a.dir,
217
+ left,
218
+ why: left.length
219
+ ? `${left.join(", ")} ${left.length === 1 ? "is" : "are"} not this step's to remove, and keep${left.length === 1 ? "s" : ""} the directory`
220
+ : "removed once empty",
221
+ });
222
+ return {
223
+ surface: "harness_state",
224
+ kind: "layout",
225
+ path: a.dir,
226
+ entry: null,
227
+ state: a.marker ? "ours" : "empty",
228
+ action: "remove",
229
+ reason: a.marker
230
+ ? `${harness.display_name}'s welcome marker — the state directory selects ${harness.display_name} for a bare upgrade`
231
+ : `an empty state directory of ${harness.display_name}`,
232
+ steps,
233
+ };
234
+ }
235
+
236
+ export function applyLayout(p, i, { failed, home = homedir(), onStep = null }) {
237
+ const out = { path: i.path, action: i.action, surface: i.surface, steps: [] };
238
+ const within = p.projectDir;
239
+ if (i.action === "cleanup" && failed) {
240
+ out.action = "skipped";
241
+ out.reason = "an earlier item failed; the legacy files stay until the next run";
242
+ return out;
243
+ }
244
+ const fail = (step, message) => {
245
+ out.failed = { step, status: null, stderr: message };
246
+ return out;
247
+ };
248
+ for (const st of i.steps || []) {
249
+ const seen = out.steps.length;
250
+ if (onStep) onStep(st, "start");
251
+ try {
252
+ if (st.kind === "ensure") {
253
+ ensureRuntimeDir(within);
254
+ out.steps.push({ kind: st.kind, ok: true });
255
+ } else if (st.kind === "move-state") {
256
+ const r = moveStateDir(st.from, st.to, within);
257
+ ensureStateDir(within);
258
+ out.steps.push({ kind: st.kind, ok: true, ...r });
259
+ } else if (st.kind === "merge-log") {
260
+ const r = mergeEntryLog(st.from, st.to, within);
261
+ out.steps.push({ kind: st.kind, ok: true, result: r });
262
+ } else if (st.kind === "delete") {
263
+ out.steps.push({ kind: st.kind, path: st.path, ok: true, removed: removeInside(st.path, within) });
264
+ } else if (st.kind === "move-marker") {
265
+ const r = movePath(st.from, st.to, within);
266
+ if (r === "target-exists") removeInside(st.from, within);
267
+ out.steps.push({ kind: st.kind, ok: true, result: r });
268
+ } else if (st.kind === "move-binding") {
269
+ let cfg;
270
+ try {
271
+ cfg = JSON.parse(readFileSync(st.from, "utf8"));
272
+ } catch {
273
+ return fail(st.kind, `${st.from} is not valid JSON; the binding was not moved`);
274
+ }
275
+ if (existsSync(st.to)) return fail(st.kind, `${st.to} appeared under the plan; two bindings are a refusal`);
276
+ const { agents, ...binding } = cfg && typeof cfg === "object" ? cfg : {};
277
+ if (agents && typeof agents === "object") {
278
+ let overlay = {};
279
+ try {
280
+ overlay = JSON.parse(readFileSync(st.overlay, "utf8"));
281
+ } catch {}
282
+ mkdirSync(dirname(st.overlay), { recursive: true });
283
+ writeFileAtomic(st.overlay, JSON.stringify({ ...overlay, agents }, null, 2) + "\n", { sweep: false });
284
+ }
285
+ mkdirSync(dirname(st.to), { recursive: true });
286
+ writeFileAtomic(st.to, JSON.stringify(binding, null, 2) + "\n", { sweep: false });
287
+ removeInside(st.from, within);
288
+ out.steps.push({ kind: st.kind, ok: true, overlay: Boolean(agents) });
289
+ } else if (st.kind === "remove-legacy-launcher") {
290
+ // Only ours, and only when no settings entry names it any more.
291
+ let text = null;
292
+ try {
293
+ text = readFileSync(st.path, "utf8");
294
+ } catch {}
295
+ // "Still in use" is decided by the FILE, not the spelling: an entry
296
+ // written through a symlinked or differently-cased path, or living in
297
+ // the committed or the user's settings, still runs it (the third review
298
+ // of the 2026-10-03 fixes — a string comparison of the local file alone
299
+ // deleted a launcher the status line was running).
300
+ const named =
301
+ entryNames(p.projectDir, i.harness, st.path) || settingsRunFile(p.projectDir, i.harness, st.path, home);
302
+ // Moved, never just deleted: the legacy launcher goes only once the new
303
+ // one exists (a dev root produces none — contract 7 leaves it in place)
304
+ // — unless the status-line slot is not ours (any wiring of ours counts).
305
+ // Under a foreign or empty slot the new launcher is never made, and
306
+ // keeping the old one kept layout-legacy alive forever (the critic of
307
+ // the layout spec's 2026-10-03 amendment, case S1).
308
+ const moved = existsSync(layoutPaths(p.projectDir).launcher(i.harness));
309
+ const slotOurs = slotIsOurs(p.projectDir, i.harness, { home, root: p.root });
310
+ if (text === null) out.steps.push({ kind: st.kind, ok: true, removed: false });
311
+ else if (!moved && slotOurs)
312
+ out.steps.push({
313
+ kind: st.kind,
314
+ ok: true,
315
+ removed: false,
316
+ reason: "no launcher at the new path yet (this root does not produce one) — left in place",
317
+ });
318
+ else if (!isOurFile(text))
319
+ out.steps.push({ kind: st.kind, ok: true, removed: false, reason: "not ours — left in place" });
320
+ else if (named)
321
+ out.steps.push({
322
+ kind: st.kind,
323
+ ok: true,
324
+ removed: false,
325
+ reason: "a settings file the host reads still runs it as the status line — left in place",
326
+ });
327
+ else out.steps.push({ kind: st.kind, ok: true, removed: removeInside(st.path, within) });
328
+ } else if (st.kind === "rmdir-legacy") {
329
+ // Prune the legacy runtime dir when only its own .gitignore (and an empty state/) remain.
330
+ let left = [];
331
+ try {
332
+ left = readdirSync(st.path).filter((n) => n !== ".gitignore");
333
+ } catch {
334
+ out.steps.push({ kind: st.kind, ok: true, removed: false });
335
+ continue;
336
+ }
337
+ if (left.length === 1 && left[0] === "state") {
338
+ try {
339
+ if (readdirSync(join(st.path, "state")).length === 0) {
340
+ rmdirSync(join(st.path, "state"));
341
+ left = [];
342
+ }
343
+ } catch {}
344
+ }
345
+ if (left.length) {
346
+ out.steps.push({ kind: st.kind, ok: true, removed: false, reason: `${left.join(", ")} remain` });
347
+ continue;
348
+ }
349
+ removeInside(st.path, within, { recursive: true });
350
+ out.steps.push({ kind: st.kind, ok: true, removed: true });
351
+ } else if (st.kind === "remove-legacy-runtime") {
352
+ removeInside(st.path, within, { recursive: true });
353
+ out.steps.push({ kind: st.kind, ok: true, removed: true });
354
+ } else if (st.kind === "rmdir-state") {
355
+ // rmdirSync on an empty directory only: whatever is left — read now,
356
+ // after the items before it ran — keeps it, and is named.
357
+ let left = [];
358
+ try {
359
+ left = readdirSync(st.path).sort();
360
+ } catch {
361
+ out.steps.push({ kind: st.kind, ok: true, removed: false });
362
+ continue;
363
+ }
364
+ if (left.length) {
365
+ out.steps.push({ kind: st.kind, ok: true, removed: false, reason: `${left.join(", ")} remain` });
366
+ continue;
367
+ }
368
+ rmdirSync(st.path);
369
+ out.steps.push({ kind: st.kind, ok: true, removed: true });
370
+ }
371
+ } catch (e) {
372
+ return fail(st.kind, e && e.message ? e.message : String(e));
373
+ } finally {
374
+ if (onStep) onStep(st, "end", stepResult(out, seen));
375
+ }
376
+ }
377
+ return out;
378
+ }
379
+
380
+ // What one step left behind, for the APPLY line: its record when it pushed
381
+ // one, and a failure when the item failed under it.
382
+ export function stepResult(out, seen) {
383
+ const rec = out.steps.length > seen ? out.steps[out.steps.length - 1] : null;
384
+ return { ok: !out.failed && (!rec || rec.ok !== false), kept: Boolean(rec && rec.removed === false && rec.reason) };
385
+ }
386
+
387
+ // Does any settings file the host reads run this file as its status line?
388
+ // Compared by identity (device and inode), so a path spelled through a symlink
389
+ // or in another case still counts; the project-directory variable a command
390
+ // may carry is substituted first.
391
+ function settingsRunFile(projectDir, harnessId, file, home) {
392
+ const id = (f) => {
393
+ try {
394
+ const s = statSync(f);
395
+ return `${s.dev}:${s.ino}`;
396
+ } catch {
397
+ return null;
398
+ }
399
+ };
400
+ const want = id(file);
401
+ if (!want) return false;
402
+ const h = loadHarness(harnessId);
403
+ const dir = h.runtime?.harness_dir || ".claude",
404
+ v = h.runtime?.project_dir_env;
405
+ const files = [
406
+ join(projectDir, h.surfaces?.statusline?.file || join(dir, "settings.local.json")),
407
+ join(projectDir, dir, "settings.json"),
408
+ join(claudeHome(home), "settings.json"),
409
+ ];
410
+ return files.some((f) => {
411
+ try {
412
+ let cmd = JSON.parse(readFileSync(f, "utf8"))?.statusLine?.command;
413
+ if (typeof cmd !== "string") return false;
414
+ if (v)
415
+ cmd = cmd
416
+ .split("${" + v + "}")
417
+ .join(projectDir)
418
+ .split("$" + v)
419
+ .join(projectDir);
420
+ const sp = statusLineScriptPath(cmd);
421
+ return Boolean(sp) && id(isAbsolute(sp) ? sp : join(projectDir, sp)) === want;
422
+ } catch {
423
+ return false;
424
+ }
425
+ });
426
+ }
427
+
428
+ // Is the harness's status-line slot wired to anything of ours (any wiring)?
429
+ function slotIsOurs(projectDir, harnessId, { home, root }) {
430
+ try {
431
+ const s = loadHarness(harnessId).surfaces?.statusline;
432
+ if (!s || !s.file) return false;
433
+ const cmd = JSON.parse(readFileSync(join(projectDir, s.file), "utf8"))?.statusLine?.command;
434
+ return typeof cmd === "string" && statusLineIsOurWiring(cmd, projectDir, home, root);
435
+ } catch {
436
+ return false;
437
+ }
438
+ }
439
+
440
+ // Does the harness's settings entry still name this launcher path?
441
+ function entryNames(projectDir, harnessId, launcherPath) {
442
+ try {
443
+ const h = loadHarness(harnessId);
444
+ const s = h.surfaces?.statusline;
445
+ if (!s || !s.file) return false;
446
+ const settings = JSON.parse(readFileSync(join(projectDir, s.file), "utf8"));
447
+ const cmd = settings?.statusLine?.command;
448
+ const named = statusLineScriptPath(typeof cmd === "string" ? cmd : null);
449
+ return Boolean(named) && resolve(named) === resolve(launcherPath);
450
+ } catch {
451
+ return false;
452
+ }
453
+ }